Repository: aws/chalice Branch: master Commit: a33863e29d24 Files: 357 Total size: 3.4 MB Directory structure: gitextract_8gh5n9v8/ ├── .changes/ │ ├── 0.1.0.json │ ├── 0.10.0.json │ ├── 0.10.1.json │ ├── 0.2.0.json │ ├── 0.3.0.json │ ├── 0.4.0.json │ ├── 0.5.0.json │ ├── 0.5.1.json │ ├── 0.6.0.json │ ├── 0.7.0.json │ ├── 0.8.0.json │ ├── 0.8.1.json │ ├── 0.8.2.json │ ├── 0.9.0.json │ ├── 1.0.0.json │ ├── 1.0.0b1.json │ ├── 1.0.0b2.json │ ├── 1.0.1.json │ ├── 1.0.2.json │ ├── 1.0.3.json │ ├── 1.0.4.json │ ├── 1.1.0.json │ ├── 1.1.1.json │ ├── 1.10.0.json │ ├── 1.11.0.json │ ├── 1.11.1.json │ ├── 1.12.0.json │ ├── 1.13.0.json │ ├── 1.13.1.json │ ├── 1.14.0.json │ ├── 1.14.1.json │ ├── 1.15.0.json │ ├── 1.15.1.json │ ├── 1.16.0.json │ ├── 1.17.0.json │ ├── 1.18.0.json │ ├── 1.18.1.json │ ├── 1.19.0.json │ ├── 1.2.0.json │ ├── 1.2.1.json │ ├── 1.2.2.json │ ├── 1.2.3.json │ ├── 1.20.0.json │ ├── 1.20.1.json │ ├── 1.21.0.json │ ├── 1.21.1.json │ ├── 1.21.2.json │ ├── 1.21.3.json │ ├── 1.21.4.json │ ├── 1.21.5.json │ ├── 1.21.6.json │ ├── 1.21.7.json │ ├── 1.21.8.json │ ├── 1.21.9.json │ ├── 1.22.0.json │ ├── 1.22.1.json │ ├── 1.22.2.json │ ├── 1.22.3.json │ ├── 1.22.4.json │ ├── 1.23.0.json │ ├── 1.24.0.json │ ├── 1.24.1.json │ ├── 1.24.2.json │ ├── 1.25.0.json │ ├── 1.26.0.json │ ├── 1.26.1.json │ ├── 1.26.2.json │ ├── 1.26.3.json │ ├── 1.26.4.json │ ├── 1.26.5.json │ ├── 1.26.6.json │ ├── 1.27.0.json │ ├── 1.27.1.json │ ├── 1.27.2.json │ ├── 1.27.3.json │ ├── 1.28.0.json │ ├── 1.29.0.json │ ├── 1.3.0.json │ ├── 1.30.0.json │ ├── 1.31.0.json │ ├── 1.31.1.json │ ├── 1.31.2.json │ ├── 1.31.3.json │ ├── 1.31.4.json │ ├── 1.32.0.json │ ├── 1.4.0.json │ ├── 1.5.0.json │ ├── 1.6.0.json │ ├── 1.6.1.json │ ├── 1.6.2.json │ ├── 1.7.0.json │ ├── 1.8.0.json │ ├── 1.9.0.json │ ├── 1.9.1.json │ └── templates/ │ └── changelog ├── .coveragerc ├── .github/ │ ├── PULL_REQUEST_TEMPLATE.md │ ├── dependabot.yml │ └── workflows/ │ ├── run-tests.yml │ └── stale-issue.yml ├── .gitignore ├── .pylintrc ├── .python-version ├── CHANGELOG.md ├── CODE_OF_CONDUCT.rst ├── CONTRIBUTING.rst ├── LICENSE ├── MANIFEST.in ├── Makefile ├── NOTICE ├── README.rst ├── chalice/ │ ├── __init__.py │ ├── analyzer.py │ ├── api/ │ │ └── __init__.py │ ├── app.py │ ├── awsclient.py │ ├── cdk/ │ │ ├── __init__.py │ │ └── construct.py │ ├── cli/ │ │ ├── __init__.py │ │ ├── factory.py │ │ ├── filewatch/ │ │ │ ├── __init__.py │ │ │ ├── eventbased.py │ │ │ └── stat.py │ │ ├── newproj.py │ │ └── reloader.py │ ├── compat.py │ ├── config.py │ ├── constants.py │ ├── deploy/ │ │ ├── __init__.py │ │ ├── appgraph.py │ │ ├── deployer.py │ │ ├── executor.py │ │ ├── models.py │ │ ├── packager.py │ │ ├── planner.py │ │ ├── swagger.py │ │ ├── sweeper.py │ │ └── validate.py │ ├── invoke.py │ ├── local.py │ ├── logs.py │ ├── package.py │ ├── pipeline.py │ ├── policies-extra.json │ ├── policies.json │ ├── policy.py │ ├── py.typed │ ├── templates/ │ │ ├── 0000-rest-api/ │ │ │ ├── .chalice/ │ │ │ │ └── config.json │ │ │ ├── .gitignore │ │ │ ├── app.py │ │ │ ├── chalicelib/ │ │ │ │ └── __init__.py │ │ │ ├── metadata.json │ │ │ ├── requirements-dev.txt │ │ │ ├── requirements.txt │ │ │ └── tests/ │ │ │ ├── __init__.py │ │ │ └── test_app.py │ │ ├── 0002-s3-event-handler/ │ │ │ ├── .chalice/ │ │ │ │ └── config.json │ │ │ ├── .gitignore │ │ │ ├── app.py │ │ │ ├── chalicelib/ │ │ │ │ └── __init__.py │ │ │ ├── metadata.json │ │ │ ├── requirements-dev.txt │ │ │ ├── requirements.txt │ │ │ └── tests/ │ │ │ ├── __init__.py │ │ │ └── test_app.py │ │ ├── 0007-lambda-only/ │ │ │ ├── .chalice/ │ │ │ │ └── config.json │ │ │ ├── .gitignore │ │ │ ├── app.py │ │ │ ├── chalicelib/ │ │ │ │ └── __init__.py │ │ │ ├── metadata.json │ │ │ ├── requirements-dev.txt │ │ │ ├── requirements.txt │ │ │ └── tests/ │ │ │ ├── __init__.py │ │ │ └── test_app.py │ │ ├── 0009-legacy/ │ │ │ ├── .chalice/ │ │ │ │ └── config.json │ │ │ ├── .gitignore │ │ │ ├── app.py │ │ │ ├── metadata.json │ │ │ └── requirements.txt │ │ └── 6001-cdk-ddb/ │ │ ├── README.rst │ │ ├── infrastructure/ │ │ │ ├── app.py │ │ │ ├── cdk.json │ │ │ ├── requirements.txt │ │ │ └── stacks/ │ │ │ ├── __init__.py │ │ │ └── chaliceapp.py │ │ ├── metadata.json │ │ ├── requirements.txt │ │ └── runtime/ │ │ ├── .chalice/ │ │ │ └── config.json │ │ ├── .gitignore │ │ ├── app.py │ │ └── requirements.txt │ ├── test.py │ ├── utils.py │ └── vendored/ │ ├── __init__.py │ └── botocore/ │ ├── __init__.py │ └── regions.py ├── docs/ │ ├── Makefile │ └── source/ │ ├── _static/ │ │ ├── custom.css │ │ └── fonts/ │ │ └── open-sans/ │ │ └── stylesheet.css │ ├── _templates/ │ │ └── layout.html │ ├── api.rst │ ├── chalicedocs.py │ ├── conf.py │ ├── faq.rst │ ├── index.rst │ ├── main.rst │ ├── quickstart.rst │ ├── samples/ │ │ ├── index.rst │ │ ├── media-query/ │ │ │ ├── code/ │ │ │ │ ├── .chalice/ │ │ │ │ │ ├── config.json │ │ │ │ │ └── policy-dev.json │ │ │ │ ├── app.py │ │ │ │ ├── chalicelib/ │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── db.py │ │ │ │ │ └── rekognition.py │ │ │ │ ├── recordresources.py │ │ │ │ ├── requirements.txt │ │ │ │ └── resources.json │ │ │ └── index.rst │ │ └── todo-app/ │ │ ├── code/ │ │ │ ├── .chalice/ │ │ │ │ ├── config.json │ │ │ │ └── policy-dev.json │ │ │ ├── .gitignore │ │ │ ├── app.py │ │ │ ├── chalicelib/ │ │ │ │ ├── __init__.py │ │ │ │ ├── auth.py │ │ │ │ └── db.py │ │ │ ├── create-resources.py │ │ │ ├── requirements-dev.txt │ │ │ ├── requirements.txt │ │ │ ├── tests/ │ │ │ │ ├── __init__.py │ │ │ │ └── test_db.py │ │ │ └── users.py │ │ └── index.rst │ ├── theme/ │ │ └── smithy/ │ │ ├── globaltoc.html │ │ ├── landing.html │ │ ├── layout.html │ │ ├── static/ │ │ │ ├── asciinema-player.css │ │ │ ├── asciinema-player.js │ │ │ ├── bootstrap-reboot.css │ │ │ ├── casts/ │ │ │ │ ├── chalice-quickstart-old.cast │ │ │ │ └── chalice-quickstart.cast │ │ │ ├── custom-tabs.css │ │ │ └── default.css_t │ │ └── theme.conf │ ├── topics/ │ │ ├── authorizers.rst │ │ ├── blueprints.rst │ │ ├── cd.rst │ │ ├── cfn.rst │ │ ├── configfile.rst │ │ ├── domainname.rst │ │ ├── events.rst │ │ ├── experimental.rst │ │ ├── index.rst │ │ ├── logging.rst │ │ ├── middleware.rst │ │ ├── multifile.rst │ │ ├── packaging.rst │ │ ├── purelambda.rst │ │ ├── pyversion.rst │ │ ├── routing.rst │ │ ├── sdks.rst │ │ ├── stages.rst │ │ ├── testing.rst │ │ ├── tf.rst │ │ ├── views.rst │ │ └── websockets.rst │ ├── tutorials/ │ │ ├── basicrestapi.rst │ │ ├── cdk.rst │ │ ├── customdomain.rst │ │ ├── events.rst │ │ ├── index.rst │ │ ├── wschat.rst │ │ └── wsecho.rst │ └── upgrading.rst ├── requirements-dev.in ├── requirements-dev.txt ├── requirements-test.in ├── requirements-test.txt ├── scripts/ │ ├── gh-page-docs │ └── release ├── setup.cfg ├── setup.py └── tests/ ├── __init__.py ├── aws/ │ ├── __init__.py │ ├── conftest.py │ ├── test_features.py │ ├── test_websockets.py │ ├── testapp/ │ │ ├── .chalice/ │ │ │ └── config.json │ │ ├── app-redeploy.py │ │ ├── app.py │ │ ├── chalicelib/ │ │ │ └── __init__.py │ │ └── requirements.txt │ └── testwebsocketapp/ │ ├── .chalice/ │ │ └── config.json │ ├── .gitignore │ ├── app-redeploy.py │ ├── app.py │ └── requirements.txt ├── conftest.py ├── functional/ │ ├── __init__.py │ ├── api/ │ │ ├── __init__.py │ │ └── test_package.py │ ├── basicapp/ │ │ ├── .chalice/ │ │ │ └── config.json │ │ ├── .gitignore │ │ ├── app.py │ │ └── requirements.txt │ ├── cdk/ │ │ ├── __init__.py │ │ └── test_construct.py │ ├── cli/ │ │ ├── __init__.py │ │ ├── test_cli.py │ │ ├── test_factory.py │ │ └── test_reloader.py │ ├── conftest.py │ ├── envapp/ │ │ ├── .chalice/ │ │ │ └── config.json │ │ ├── .gitignore │ │ ├── app.py │ │ └── requirements.txt │ ├── test_awsclient.py │ ├── test_deployer.py │ ├── test_local.py │ ├── test_package.py │ └── test_utils.py ├── integration/ │ ├── __init__.py │ ├── conftest.py │ ├── test_cli.py │ └── test_package.py ├── plugins/ │ ├── codelinter.py │ └── testlinter.py └── unit/ ├── __init__.py ├── cli/ │ ├── __init__.py │ ├── filewatch/ │ │ ├── test_eventbased.py │ │ └── test_stat.py │ ├── test_cli.py │ └── test_newproj.py ├── conftest.py ├── deploy/ │ ├── __init__.py │ ├── test_appgraph.py │ ├── test_deployer.py │ ├── test_executor.py │ ├── test_models.py │ ├── test_packager.py │ ├── test_planner.py │ ├── test_swagger.py │ └── test_validate.py ├── test_analyzer.py ├── test_app.py ├── test_awsclient.py ├── test_config.py ├── test_invoke.py ├── test_local.py ├── test_logs.py ├── test_package.py ├── test_pipeline.py ├── test_policy.py ├── test_test.py ├── test_utils.py └── vendored/ ├── __init__.py └── botocore/ ├── __init__.py └── test_regions.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .changes/0.1.0.json ================================================ { "schema-version": "0.2", "changes": [ { "category": "packaging", "description": "Require ``virtualenv`` as a package dependency. (#33)", "type": "enhancement" }, { "category": "CLI", "description": "Add ``--profile`` option when creating a new project (#28)", "type": "enhancement" }, { "category": "rest-api", "description": "Add support for more error codes exceptions (#34)", "type": "enhancement" }, { "category": "rest-api", "description": "Improve error validation when routes containing a\ntrailing ``/`` char (#65)", "type": "enhancement" }, { "category": "rest-api", "description": "Validate duplicate route entries (#79)", "type": "enhancement" }, { "category": "policy", "description": "Ignore lambda expressions in policy analyzer (#74)", "type": "enhancement" }, { "category": "rest-api", "description": "Print original error traceback in debug mode (#50)", "type": "enhancement" }, { "category": "rest-api", "description": "Add support for authenticate routes (#14)", "type": "feature" }, { "category": "policy", "description": "Add ability to disable IAM role management (#61)", "type": "feature" } ] } ================================================ FILE: .changes/0.10.0.json ================================================ { "schema-version": "0.2", "changes": [ { "category": "deployment", "description": "Fix issue where provided ``iam_role_arn`` was not respected on\nredeployments of chalice applications and in the CloudFormation template\ngenerated by ``chalice package`` (#339)", "type": "bugfix" }, { "category": "config", "description": "Fix ``autogen_policy`` in config being ignored (#367)", "type": "bugfix" }, { "category": "rest-api", "description": "Add support for view functions that share the same view url but\ndiffer by HTTP method (#81)", "type": "feature" }, { "category": "deployment", "description": "Improve deployment error messages for deployment packages that are\ntoo large (#246, #330, #380)", "type": "enhancement" }, { "category": "rest-api", "description": "Add support for built-in authorizers (#356)", "type": "feature" } ] } ================================================ FILE: .changes/0.10.1.json ================================================ { "schema-version": "0.2", "changes": [ { "category": "deployment", "description": "Fix deployment issue for projects deployed with versions\nprior to 0.10.0 (#387)", "type": "bugfix" }, { "category": "policy", "description": "Fix crash in analyzer when encountering genexprs and listcomps (#263)", "type": "bugfix" } ] } ================================================ FILE: .changes/0.2.0.json ================================================ { "schema-version": "0.2", "changes": [ { "category": "rest-api", "description": "Add support for input content types besides ``application/json`` (#96)", "type": "enhancement" }, { "category": "rest-api", "description": "Allow ``ChaliceViewErrors`` to propagate, so that API Gateway\ncan properly map HTTP status codes in non debug mode (#113)", "type": "enhancement" }, { "category": "deployment", "description": "Add windows compatibility (#31)", "type": "enhancement" } ] } ================================================ FILE: .changes/0.3.0.json ================================================ { "schema-version": "0.2", "changes": [ { "category": "rest-api", "description": "Fix bug with case insensitive headers (#129)", "type": "bugfix" }, { "category": "CORS", "description": "Add initial support for CORS (#133)", "type": "feature" }, { "category": "deployment", "description": "Only add API gateway permissions if needed (#48)", "type": "enhancement" }, { "category": "policy", "description": "Fix error when dict comprehension is encountered during policy generation (#131)", "type": "bugfix" }, { "category": "CLI", "description": "Add ``--version`` and ``--debug`` options to the chalice CLI", "type": "enhancement" } ] } ================================================ FILE: .changes/0.4.0.json ================================================ { "schema-version": "0.2", "changes": [ { "category": "deployment", "description": "Fix issue where role name to arn lookup was failing due to lack of pagination (#139)", "type": "bugfix" }, { "category": "rest-api", "description": "Raise errors when unknown kwargs are provided to ``app.route(...)`` (#144)", "type": "enhancement" }, { "category": "config", "description": "Raise validation error when configuring CORS and an OPTIONS method (#142)", "type": "enhancement" }, { "category": "rest-api", "description": "Add support for multi-file applications (#21)", "type": "feature" }, { "category": "local", "description": "Add support for ``chalice local``, which runs a local HTTP server for testing (#22)", "type": "feature" } ] } ================================================ FILE: .changes/0.5.0.json ================================================ { "schema-version": "0.2", "changes": [ { "category": "logging", "description": "Add default application logger (#149)", "type": "enhancement" }, { "category": "local", "description": "Return 405 when method is not supported when running\n``chalice local`` (#159)", "type": "enhancement" }, { "category": "SDK", "description": "Add path params as requestParameters so they can be used\nin generated SDKs as well as cache keys (#163)", "type": "enhancement" }, { "category": "rest-api", "description": "Map cognito user pool claims as part of request context (#165)", "type": "enhancement" }, { "category": "CLI", "description": "Add ``chalice url`` command to print the deployed URL (#169)", "type": "feature" }, { "category": "deployment", "description": "Bump up retry limit on initial function creation to 30 seconds (#172)", "type": "enhancement" }, { "category": "local", "description": "Add support for ``DELETE`` and ``PATCH`` in ``chalice local`` (#167)", "type": "feature" }, { "category": "CLI", "description": "Add ``chalice generate-sdk`` command (#178)", "type": "feature" } ] } ================================================ FILE: .changes/0.5.1.json ================================================ { "schema-version": "0.2", "changes": [ { "category": "local", "description": "Add support for serializing decimals in ``chalice local`` (#187)", "type": "enhancement" }, { "category": "local", "description": "Add stdout handler for root logger when using ``chalice local`` (#186)", "type": "enhancement" }, { "category": "local", "description": "Map query string parameters when using ``chalice local`` (#184)", "type": "enhancement" }, { "category": "rest-api", "description": "Support Content-Type with a charset (#180)", "type": "enhancement" }, { "category": "deployment", "description": "Fix not all resources being retrieved due to pagination (#188)", "type": "bugfix" }, { "category": "deployment", "description": "Fix issue where root resource was not being correctly retrieved (#205)", "type": "bugfix" }, { "category": "deployment", "description": "Handle case where local policy does not exist\n(`29 `__)", "type": "bugfix" } ] } ================================================ FILE: .changes/0.6.0.json ================================================ { "schema-version": "0.2", "summary": "Check out the `upgrade notes for 0.6.0\n`__\nfor more detailed information about changes in this release.\n\n", "changes": [ { "category": "local", "description": "Add port parameter to local command (#220)", "type": "feature" }, { "category": "packaging", "description": "Add support for binary vendored packages (#182, #106, #42)", "type": "feature" }, { "category": "rest-api", "description": "Add support for customizing the returned HTTP response (#240, #218, #110, #30, #226)", "type": "feature" }, { "category": "packaging", "description": "Always inject latest runtime to allow for chalice upgrades (#245)", "type": "enhancement" } ] } ================================================ FILE: .changes/0.7.0.json ================================================ { "schema-version": "0.2", "changes": [ { "category": "CLI", "description": "Add ``chalice package`` command. This will\ncreate a SAM template and Lambda deployment package that\ncan be subsequently deployed by AWS CloudFormation. (#258)", "type": "feature" }, { "category": "CLI", "description": "Add a ``--stage-name`` argument for creating chalice stages.\nA chalice stage is a completely separate set of AWS resources.\nAs a result, most configuration values can also be specified\nper chalice stage. (#264, #270)", "type": "feature" }, { "category": "policy", "description": "Add support for ``iam_role_file``, which allows you to\nspecify the file location of an IAM policy to use for your app (#272)", "type": "feature" }, { "category": "config", "description": "Add support for setting environment variables in your app (#273)", "type": "feature" }, { "category": "CI-CD", "description": "Add a ``generate-pipeline`` command (#277)", "type": "feature" } ] } ================================================ FILE: .changes/0.8.0.json ================================================ { "schema-version": "0.2", "changes": [ { "category": "python", "description": "Add support for python3! (#296)", "type": "feature" }, { "category": "packaging", "description": "Fix swagger generation when using ``api_key_required=True`` (#279)", "type": "bugfix" }, { "category": "CI-CD", "description": "Fix ``generate-pipeline`` to install requirements file before packaging (#295)", "type": "bugfix" } ] } ================================================ FILE: .changes/0.8.1.json ================================================ { "schema-version": "0.2", "changes": [ { "category": "deployment", "description": "Alway overwrite existing API Gateway Rest API on updates (#305)", "type": "enhancement" }, { "category": "CORS", "description": "Added more granular support for CORS (#311)", "type": "enhancement" }, { "category": "local", "description": "Fix duplicate content type header in local model (#311)", "type": "bugfix" }, { "category": "rest-api", "description": "Fix content type validation when charset is provided (#306)", "type": "bugfix" }, { "category": "rest-api", "description": "Add back custom authorizer support (#322)", "type": "enhancement" } ] } ================================================ FILE: .changes/0.8.2.json ================================================ { "schema-version": "0.2", "changes": [ { "category": "CLI", "description": "Fix issue where ``--api-gateway-stage`` was being\nignored (#325)", "type": "bugfix" }, { "category": "CLI", "description": "Add ``chalice delete`` command (#40)", "type": "feature" } ] } ================================================ FILE: .changes/0.9.0.json ================================================ { "schema-version": "0.2", "changes": [ { "category": "rest-api", "description": "Add support for ``IAM`` authorizer (#334)", "type": "feature" }, { "category": "config", "description": "Add support for configuring ``lambda_timeout``, ``lambda_memory_size``,\nand ``tags`` in your AWS Lambda function (#347)", "type": "feature" }, { "category": "packaging", "description": "Fix vendor directory contents not being importable locally (#350)", "type": "bugfix" }, { "category": "rest-api", "description": "Add support for binary payloads (#348)", "type": "feature" } ] } ================================================ FILE: .changes/1.0.0.json ================================================ { "schema-version": "0.2", "changes": [ { "category": "rest-api", "description": "Change default API Gateway stage name to ``api`` (#431)", "type": "enhancement" }, { "category": "local", "description": "Add support for ``CORSConfig`` in ``chalice local`` (#436)", "type": "enhancement" }, { "category": "logging", "description": "Propagate ``DEBUG`` log level when setting ``app.debug`` (#386)", "type": "enhancement" }, { "category": "rest-api", "description": "Add support for wildcard routes and HTTP methods in ``AuthResponse`` (#403)", "type": "feature" }, { "category": "policy", "description": "Fix bug when analyzing list comprehensions (#412)", "type": "bugfix" }, { "category": "local", "description": "Update ``chalice local`` to use HTTP 1.1 (#448)", "type": "enhancement" } ] } ================================================ FILE: .changes/1.0.0b1.json ================================================ { "schema-version": "0.2", "summary": "Please read the `upgrade notes for 1.0.0b1\n`__\nfor more detailed information about upgrading to this release.\n\nNote: to install this beta version of chalice you must specify\n``pip install 'chalice>=1.0.0b1,<2.0.0'`` or\nuse the ``--pre`` flag for pip: ``pip install --pre chalice``.\n\n", "changes": [ { "category": "rest-api", "description": "Fix unicode responses being quoted in python 2.7 (#262)", "type": "bugfix" }, { "category": "event-source", "description": "Add support for scheduled events (#390)", "type": "feature" }, { "category": "event-source", "description": "Add support for pure lambda functions (#390)", "type": "feature" }, { "category": "packaging", "description": "Add support for wheel packaging. (#249)", "type": "feature" } ] } ================================================ FILE: .changes/1.0.0b2.json ================================================ { "schema-version": "0.2", "summary": "Please read the `upgrade notes for 1.0.0b2\n`__\nfor more detailed information about upgrading to this release.\n\nNote: to install this beta version of chalice you must specify\n``pip install 'chalice>=1.0.0b2,<2.0.0'`` or\nuse the ``--pre`` flag for pip: ``pip install --pre chalice``.\n", "changes": [ { "category": "local", "description": "Set env vars from config in ``chalice local`` (#396)", "type": "enhancement" }, { "category": "packaging", "description": "Fix edge case when building packages with optional c extensions (#421)", "type": "bugfix" }, { "category": "policy", "description": "Remove legacy ``policy.json`` file support. Policy files must\nuse the stage name, e.g. ``policy-dev.json`` (#430)", "type": "enhancement" }, { "category": "deployment", "description": "Fix issue where IAM role policies were updated twice on redeploys (#428)", "type": "bugfix" }, { "category": "rest-api", "description": "Validate route path is not an empty string (#432)", "type": "enhancement" }, { "category": "rest-api", "description": "Change route code to invoke view function with kwargs instead of\npositional args (#429)", "type": "enhancement" } ] } ================================================ FILE: .changes/1.0.1.json ================================================ { "schema-version": "0.2", "changes": [ { "category": "packaging", "description": "Only use alphanumeric characters for event names in SAM template (#450)", "type": "bugfix" }, { "category": "config", "description": "Print useful error message when config.json is invalid (#458)", "type": "enhancement" }, { "category": "rest-api", "description": "Fix api gateway stage being set incorrectly in non-default chalice stage\n(`#$70 `__)", "type": "bugfix" } ] } ================================================ FILE: .changes/1.0.2.json ================================================ { "schema-version": "0.2", "changes": [ { "category": "rest-api", "description": "Fix issue where requestParameters were not being mapped\ncorrectly resulting in invalid generated javascript SDKs (#498)", "type": "bugfix" }, { "category": "rest-api", "description": "Fix issue where ``api_gateway_stage`` was being\nignored when set in the ``config.json`` file (#495)", "type": "bugfix" }, { "category": "rest-api", "description": "Fix bug where ``raw_body`` would raise an exception if no HTTP\nbody was provided (#503)", "type": "bugfix" }, { "category": "CLI", "description": "Fix bug where exit codes were not properly being propagated during packaging (#500)", "type": "bugfix" }, { "category": "local", "description": "Add support for Builtin Authorizers in local mode (#404)", "type": "feature" }, { "category": "packaging", "description": "Fix environment variables being passed to subprocess while packaging (#501)", "type": "bugfix" }, { "category": "rest-api", "description": "Allow view to require API keys as well as authorization (#473)", "type": "enhancement" } ] } ================================================ FILE: .changes/1.0.3.json ================================================ { "schema-version": "0.2", "changes": [ { "category": "packaging", "description": "Fix issue with some packages with `-` or `.` in their distribution name (#555)", "type": "bugfix" }, { "category": "rest-api", "description": "Fix issue where chalice local returned a 403 for successful OPTIONS requests (#554)", "type": "bugfix" }, { "category": "local", "description": "Fix issue with chalice local mode causing http clients to hang on responses\nwith no body (#525)", "type": "bugfix" }, { "category": "local", "description": "Add ``--stage`` parameter to ``chalice local`` (#545)", "type": "enhancement" }, { "category": "policy", "description": "Fix issue with analyzer that followed recursive functions infinitely (#531)", "type": "bugfix" } ] } ================================================ FILE: .changes/1.0.4.json ================================================ { "schema-version": "0.2", "changes": [ { "category": "packaging", "description": "Fix issue deploying some packages in Windows with utf-8 characters (#560)", "type": "bugfix" }, { "category": "packaging", "description": "Add support for custom authorizers with ``chalice package`` (#580)", "type": "feature" } ] } ================================================ FILE: .changes/1.1.0.json ================================================ { "schema-version": "0.2", "changes": [ { "category": "rest-api", "description": "Default to ``None`` in local mode when no query parameters\nare provided (#593)", "type": "enhancement" }, { "category": "local", "description": "Add support for binding a custom address for local dev server (#596)", "type": "enhancement" }, { "category": "rest-api", "description": "Fix local mode handling of routes with trailing slashes (#582)", "type": "bugfix" }, { "category": "config", "description": "Scale ``lambda_timeout`` parameter correctly in local mode (#579)", "type": "bugfix" }, { "category": "CI-CD", "description": "Add ``--codebuild-image`` to the ``generate-pipeline`` command (#609)", "type": "feature" }, { "category": "CI-CD", "description": "Add ``--source`` and ``--buildspec-file`` to the\n``generate-pipeline`` command (#609)", "type": "feature" } ] } ================================================ FILE: .changes/1.1.1.json ================================================ { "schema-version": "0.2", "changes": [ { "category": "CLI", "description": "Add ``--connection-timeout`` to the ``deploy`` command (#344)", "type": "feature" }, { "category": "policy", "description": "Fix IAM role creation issue (#565)", "type": "bugfix" }, { "category": "local", "description": "Fix `chalice local` handling of browser requests (#565)", "type": "bugfix" }, { "category": "policy", "description": "Support async/await syntax in automatic policy generation (#565)", "type": "enhancement" }, { "category": "packaging", "description": "Support additional PyPi package formats (.tar.bz2) (#720)", "type": "enhancement" } ] } ================================================ FILE: .changes/1.10.0.json ================================================ { "schema-version": "0.2", "changes": [ { "category": "websocket", "description": "Add experimental support for websockets (#1017)", "type": "feature" }, { "category": "rest-api", "description": "API Gateway Endpoint Type Configuration (#1160)", "type": "feature" }, { "category": "rest-api", "description": "API Gateway Resource Policy Configuration (#1160)", "type": "feature" }, { "category": "packaging", "description": "Add --merge-template option to package command (#1195)", "type": "feature" }, { "category": "packaging", "description": "Add support for packaging via terraform (#1129)", "type": "feature" } ] } ================================================ FILE: .changes/1.11.0.json ================================================ { "schema-version": "0.2", "changes": [ { "category": "config", "description": "Add support for stage independent lambda configuration (#1162)", "type": "feature" }, { "category": "event-source", "description": "Add support for subscribing to CloudWatch Events (#1126)", "type": "feature" }, { "category": "event-source", "description": "Add a ``description`` argument to CloudWatch schedule events (#1155)", "type": "feature" }, { "category": "rest-api", "description": "Fix deployment of API Gateway resource policies (#1220)", "type": "bugfix" } ] } ================================================ FILE: .changes/1.11.1.json ================================================ { "schema-version": "0.2", "changes": [ { "category": "blueprint", "description": "Fix mouting blueprints with root routes (#1230)", "type": "bugfix" }, { "category": "rest-api", "description": "Add support for multi-value headers responses (#1205)", "type": "feature" } ] } ================================================ FILE: .changes/1.12.0.json ================================================ { "schema-version": "0.2", "changes": [ { "category": "CLI", "description": "Add ``generate-models`` command (#1245)", "type": "feature" }, { "category": "websocket", "description": "Add ``close`` and ``info`` commands to websocket api (#1259)", "type": "enhancement" }, { "category": "dependencies", "description": "Bump upper bound on PIP to ``<19.4`` (#1273, #1272)", "type": "enhancement" } ] } ================================================ FILE: .changes/1.13.0.json ================================================ { "schema-version": "0.2", "changes": [ { "category": "logs", "description": "Fix error for ``chalice logs`` when a Lambda function\nhas not been invoked (#1252)", "type": "bugfix" }, { "category": "CORS", "description": "Add global CORS configuration (#70)", "type": "feature" }, { "category": "packaging", "description": "Fix packaging simplejson (#1304)", "type": "bugfix" }, { "category": "python", "description": "Add support for Python 3.8 (#1315)", "type": "feature" }, { "category": "authorizer", "description": "Add support for invocation role in custom authorizer (#1303)", "type": "feature" }, { "category": "packaging", "description": "Fix packaging on case-sensitive filesystems (#1356)", "type": "bugfix" } ] } ================================================ FILE: .changes/1.13.1.json ================================================ { "schema-version": "1.0", "changes": [ { "type": "enhancement", "category": "local", "description": "Add support for multiValueHeaders in local mode (#1381)." }, { "type": "bugfix", "category": "local", "description": "Make ``current_request`` thread safe in local mode (#759)" }, { "type": "enhancement", "category": "local", "description": "Add support for cognito in local mode (#1377)." }, { "type": "bugfix", "category": "packaging", "description": "Fix terraform generation when injecting custom domains (#1237)" }, { "type": "enhancement", "category": "packaging", "description": "Ensure repeatable zip file generation (#1114)." }, { "type": "bugfix", "category": "CORS", "description": "Fix CORS request when returning compressed binary types (#1336)" } ] } ================================================ FILE: .changes/1.14.0.json ================================================ { "schema-version": "1.0", "changes": [ { "type": "bugfix", "category": "packaging", "description": "Fix pandas packaging regression (#1398)" }, { "type": "feature", "category": "CLI", "description": "Add ``dev plan/appgraph`` commands (#1396)" }, { "type": "enhancement", "category": "SQS", "description": "Validate queue name is used and not queue URL or ARN (#1388)" } ] } ================================================ FILE: .changes/1.14.1.json ================================================ { "schema-version": "1.0", "changes": [ { "type": "enhancement", "category": "pip", "description": "Update pip version range to 20.1." } ] } ================================================ FILE: .changes/1.15.0.json ================================================ { "schema-version": "1.0", "changes": [ { "type": "feature", "category": "blueprints", "description": "Mark blueprints as an accepted API (#1250)" }, { "type": "feature", "category": "package", "description": "Add ability to generate and merge yaml CloudFormation templates (#1425)" }, { "type": "enhancement", "category": "terraform", "description": "Allow generated terraform template to be used as a terraform module (#1300)" }, { "type": "feature", "category": "logs", "description": "Add support for tailing logs (#4)." } ] } ================================================ FILE: .changes/1.15.1.json ================================================ { "schema-version": "1.0", "changes": [ { "type": "bugfix", "category": "packaging", "description": "Fix setup.py dependencies where the wheel package was not being installed (#1435)" } ] } ================================================ FILE: .changes/1.16.0.json ================================================ { "schema-version": "1.0", "changes": [ { "type": "enhancement", "category": "local", "description": "Avoid error from cognito client credentials in local authorizer (#1447)" }, { "type": "bugfix", "category": "package", "description": "Traverse symlinks to directories when packaging the vendor directory (#583)." }, { "type": "feature", "category": "DomainName", "description": "Add support for custom domain names to REST/WebSocket APIs (#1194)" }, { "type": "feature", "category": "auth", "description": "Add support for oauth scopes on routes (#1444)." } ] } ================================================ FILE: .changes/1.17.0.json ================================================ { "schema-version": "1.0", "changes": [ { "type": "feature", "category": "Testing", "description": "Add Chalice test client (#1468)" }, { "type": "enhancement", "category": "regions", "description": "Add support for non `aws` partitions including aws-cn and aws-us-gov (#792)." }, { "type": "bugfix", "category": "dependencies", "description": "Fix error when using old versions of click by requiring >=7" }, { "type": "bugfix", "category": "local", "description": "Fix local mode builtin authorizer not stripping query string from URL (#1470)" } ] } ================================================ FILE: .changes/1.18.0.json ================================================ { "schema-version": "1.0", "changes": [ { "type": "feature", "category": "Packaging", "description": "Add support for automatic layer creation (#1485, #1001)" } ] } ================================================ FILE: .changes/1.18.1.json ================================================ { "schema-version": "1.0", "changes": [ { "type": "bugfix", "category": "Packaging", "description": "Add fallback to retrieve name/version from sdist (#1486)" }, { "type": "bugfix", "category": "Analyzer", "description": "Handle symbols with multiple (shadowed) namespaces (#1494)" } ] } ================================================ FILE: .changes/1.19.0.json ================================================ { "schema-version": "1.0", "changes": [ { "type": "feature", "category": "Pipeline", "description": "Add a new v2 template for the deployment pipeline CloudFormation template (#1506)" } ] } ================================================ FILE: .changes/1.2.0.json ================================================ { "schema-version": "0.2", "summary": "This release features a rewrite of the core deployment\ncode used in Chalice. This is a backwards compatible change\nfor users, but you may see changes to the autogenerated\nfiles Chalice creates.\nPlease read the `upgrade notes for 1.2.0\n`__\nfor more detailed information about upgrading to this release.\n\n", "changes": [ { "category": "rest-api", "description": "Print out full stack trace when an error occurs (#711)", "type": "enhancement" }, { "category": "rest-api", "description": "Add ``image/jpeg`` as a default binary content type (#707)", "type": "enhancement" }, { "category": "event-source", "description": "Add support for AWS Lambda only projects (#162, #640)", "type": "feature" }, { "category": "policy", "description": "Fix inconsistent IAM role generation with pure lambdas (#685)", "type": "bugfix" }, { "category": "deployment", "description": "Rewrite Chalice deployer to more easily support additional AWS resources (#604)", "type": "enhancement" }, { "category": "packaging", "description": "Update the ``chalice package`` command to support\npure lambda functions and scheduled events. (#772)", "type": "feature" }, { "category": "packaging", "description": "Fix packager edge case normalizing sdist names (#778)", "type": "bugfix" }, { "category": "packaging", "description": "Fix SQLAlchemy packaging (#778)", "type": "bugfix" }, { "category": "packaging", "description": "Fix packaging abi3, wheels this fixes cryptography 2.2.x packaging (#764)", "type": "bugfix" } ] } ================================================ FILE: .changes/1.2.1.json ================================================ { "schema-version": "0.2", "changes": [ { "category": "rest-api", "description": "Add CORS headers to error response (#715)", "type": "enhancement" }, { "category": "local", "description": "Fix parsing empty query strings in local mode (#767)", "type": "bugfix" }, { "category": "packaging", "description": "Fix regression in ``chalice package`` when using role arns (#793)", "type": "bugfix" } ] } ================================================ FILE: .changes/1.2.2.json ================================================ { "schema-version": "0.2", "changes": [ { "category": "packaging", "description": "Fix package command not correctly setting environment variables (#795)", "type": "bugfix" } ] } ================================================ FILE: .changes/1.2.3.json ================================================ { "schema-version": "0.2", "changes": [ { "category": "dependency", "description": "Add support for pip 10 (#808)", "type": "enhancement" }, { "category": "policy", "description": "Update ``policies.json`` file (#817)", "type": "enhancement" } ] } ================================================ FILE: .changes/1.20.0.json ================================================ { "schema-version": "1.0", "changes": [ { "type": "enhancement", "category": "Blueprints", "description": "Add `current_app` property to Blueprints (#1094)" }, { "type": "enhancement", "category": "CLI", "description": "Set `AWS_CHALICE_CLI_MODE` env var whenever a Chalice CLI command is run (#1200)" }, { "type": "feature", "category": "Middleware", "description": "Add support for middleware (#1509)" }, { "type": "feature", "category": "X-Ray", "description": "Add support for AWS X-Ray (#464)" } ] } ================================================ FILE: .changes/1.20.1.json ================================================ { "schema-version": "1.0", "changes": [ { "type": "bugfix", "category": "Blueprints", "description": "Preserve docstring in blueprints (#1525)" }, { "type": "enhancement", "category": "Binary", "description": "Support returning native python types when using `*/*` for binary types (#1501)" } ] } ================================================ FILE: .changes/1.21.0.json ================================================ { "schema-version": "1.0", "changes": [ { "type": "bugfix", "category": "Blueprints", "description": "Fix regression when invoking Lambda functions from blueprints (#1535)" }, { "type": "feature", "category": "Events", "description": "Add support for Kinesis and DynamoDB event handlers (#987)" } ] } ================================================ FILE: .changes/1.21.1.json ================================================ { "schema-version": "1.0", "changes": [ { "type": "bugfix", "category": "Websockets", "description": "Fix custom domain name configuration for websockets (#1531)" }, { "type": "bugfix", "category": "Local", "description": "Add support for multiple actions in builtin auth in local mode (#1527)" }, { "type": "bugfix", "category": "Websocket", "description": "Fix websocket client configuration when using a custom domain (#1503)" }, { "type": "bugfix", "category": "Local", "description": "Fix CORs handling in local mode (#761)" } ] } ================================================ FILE: .changes/1.21.2.json ================================================ { "schema-version": "1.0", "changes": [ { "type": "bugfix", "category": "Terraform", "description": "Fix issue with wildcard partition names in s3 event handlers (#1508)" }, { "type": "bugfix", "category": "Auth", "description": "Fix special case processing for root URL auth (#1271)" }, { "type": "enhancement", "category": "Middleware", "description": "Add support for HTTP middleware catching exceptions (#1541)" } ] } ================================================ FILE: .changes/1.21.3.json ================================================ { "schema-version": "1.0", "changes": [ { "type": "enhancement", "category": "Test", "description": "Add test client methods for generating sample kinesis events" }, { "type": "enhancement", "category": "Config", "description": "Validate env var values are strings (#1543)" } ] } ================================================ FILE: .changes/1.21.4.json ================================================ { "schema-version": "1.0", "changes": [ { "type": "enhancement", "category": "Local", "description": "Allow custom Chalice class in local mode (#1502)" }, { "type": "bugfix", "category": "Layers", "description": "Ensure single reference to managed layer (#1563)" } ] } ================================================ FILE: .changes/1.21.5.json ================================================ { "schema-version": "1.0", "changes": [ { "type": "bugfix", "category": "Config", "description": "Fix config validation for env vars on py27 (#1573)" }, { "type": "bugfix", "category": "Pip", "description": "Bump pip version contraint (#1590)" }, { "type": "bugfix", "category": "REST", "description": "Add Allow header with list of allowed methods when returning 405 error (#1583)" } ] } ================================================ FILE: .changes/1.21.6.json ================================================ { "schema-version": "1.0", "changes": [ { "type": "enhancement", "category": "Packaging", "description": "Increase upper bound for AWS provider in Terraform to 3.x (#1596)" }, { "type": "enhancement", "category": "Packaging", "description": "Add support for manylinux2014 wheels (#1551)" } ] } ================================================ FILE: .changes/1.21.7.json ================================================ { "schema-version": "1.0", "changes": [ { "type": "enhancement", "category": "Terraform", "description": "Map custom domain outputs in Terraform packaging (#1601)" } ] } ================================================ FILE: .changes/1.21.8.json ================================================ { "schema-version": "1.0", "changes": [ { "type": "enhancement", "category": "Authorizers", "description": "Add support for custom headers in built-in authorizers (#1613)" } ] } ================================================ FILE: .changes/1.21.9.json ================================================ { "schema-version": "1.0", "changes": [ { "type": "enhancement", "category": "Dependencies", "description": "Bump attr version constraint (#1620)" } ] } ================================================ FILE: .changes/1.22.0.json ================================================ { "schema-version": "1.0", "changes": [ { "type": "feature", "category": "CDK", "description": "Add built-in support for the AWS CDK (#1622)" } ] } ================================================ FILE: .changes/1.22.1.json ================================================ { "schema-version": "1.0", "changes": [ { "type": "enhancement", "category": "Pip", "description": "Bump pip version range to latest version 21.x (#1630)" }, { "type": "enhancement", "category": "IAM", "description": "Improve client call collection when generation policies (#692)" } ] } ================================================ FILE: .changes/1.22.2.json ================================================ { "schema-version": "1.0", "changes": [ { "type": "enhancement", "category": "Blueprint", "description": "Add log property to blueprint" }, { "type": "bugfix", "category": "Pipeline", "description": "Fix build command in pipeline generation (#1653)" }, { "type": "enhancement", "category": "Dependencies", "description": "Change enum-compat dependency to enum34 with version restrictions (#1667)" } ] } ================================================ FILE: .changes/1.22.3.json ================================================ { "schema-version": "1.0", "changes": [ { "type": "enhancement", "category": "Terraform", "description": "Bump Terraform version to include 0.14" }, { "type": "bugfix", "category": "Typing", "description": "Fix type definitions in app.pyi (#1676)" }, { "type": "bugfix", "category": "Terraform", "description": "Use references instead of function names in Terraform packaging (#1558)" } ] } ================================================ FILE: .changes/1.22.4.json ================================================ { "schema-version": "1.0", "changes": [ { "type": "enhancement", "category": "Types", "description": "Add missing types to app.pyi stub file (#1701)" }, { "type": "bugfix", "category": "Custom Domain", "description": "Fix custom domain generation when using the CDK (#1640)" }, { "type": "bugfix", "category": "Packaging", "description": "Special cases pyrsistent packaging (#1696)" } ] } ================================================ FILE: .changes/1.23.0.json ================================================ { "schema-version": "1.0", "changes": [ { "type": "enhancement", "category": "Deploy", "description": "Wait for function state to be active when deploying" }, { "type": "feature", "category": "SQS", "description": "Add queue_arn parameter to enable CDK integration with SQS event handler (#1681)" } ] } ================================================ FILE: .changes/1.24.0.json ================================================ { "schema-version": "1.0", "changes": [ { "type": "feature", "category": "Python2.7", "description": "Remove support for Python 2.7 (#1766)" }, { "type": "enhancement", "category": "Terraform", "description": "Update Terraform packaging to support version 1.0 (#1757)" }, { "type": "enhancement", "category": "Typing", "description": "Add missing WebsocketEvent type information (#1746)" }, { "type": "enhancement", "category": "S3 events", "description": "Add source account to Lambda permissions when configuring S3 events (#1635)" }, { "type": "enhancement", "category": "Packaging", "description": "Add support for Terraform v0.15 (#1725)" } ] } ================================================ FILE: .changes/1.24.1.json ================================================ { "schema-version": "1.0", "changes": [ { "type": "bugfix", "category": "GovCloud", "description": "Fix partition error when updating API Gateway in GovCloud region (#1770)" } ] } ================================================ FILE: .changes/1.24.2.json ================================================ { "schema-version": "1.0", "changes": [ { "type": "enhancement", "category": "Dependencies", "description": "Bump attrs dependency to latest version (#1786)" }, { "type": "bugfix", "category": "Auth", "description": "Fix ARN parsing when generating a builtin AuthResponse (#1775)" }, { "type": "enhancement", "category": "CLI", "description": "Upgrade Click dependency to support v8.0.0 (#1729)" } ] } ================================================ FILE: .changes/1.25.0.json ================================================ { "schema-version": "1.0", "changes": [ { "type": "feature", "category": "Python", "description": "Add support for Python 3.9 (#1787)" } ] } ================================================ FILE: .changes/1.26.0.json ================================================ { "schema-version": "1.0", "changes": [ { "type": "feature", "category": "Websockets", "description": "Add support for setting the Websocket protocol from the connect handler (#1768)" }, { "type": "feature", "category": "SQS", "description": "Added MaximumBatchingWindowInSeconds to SQS event handler (#1778)" } ] } ================================================ FILE: .changes/1.26.1.json ================================================ { "schema-version": "1.0", "changes": [ { "type": "enhancement", "category": "Dependencies", "description": "Bump pip dependency to latest released version (#1817)" }, { "type": "enhancement", "category": "Tests", "description": "Don't include tests package in .whl file (#1814)" } ] } ================================================ FILE: .changes/1.26.2.json ================================================ { "schema-version": "1.0", "changes": [ { "type": "enhancement", "category": "Dependencies", "description": "Update pyyaml to 6.x (#1830)" }, { "type": "bugfix", "category": "Websocket", "description": "Correctly configure websocket endpoint in the aws-cn partition (#1820)" } ] } ================================================ FILE: .changes/1.26.3.json ================================================ { "schema-version": "1.0", "changes": [ { "type": "enhancement", "category": "Errors", "description": "Remove redundant error code in error message string (#1339)" }, { "type": "enhancement", "category": "VPC", "description": "Associate VPC endpoint with Rest API (#1449)" } ] } ================================================ FILE: .changes/1.26.4.json ================================================ { "schema-version": "1.0", "changes": [ { "type": "bugfix", "category": "Terraform", "description": "Use updated keywords for providing provider version constraints (#1717)" } ] } ================================================ FILE: .changes/1.26.5.json ================================================ { "schema-version": "1.0", "changes": [ { "type": "enhancement", "category": "Terraform", "description": "Remove template provider in favor of locals (#1869)" }, { "type": "enhancement", "category": "Terraform", "description": "Bump Terraform version to suppose 1.1.x (#1868)" } ] } ================================================ FILE: .changes/1.26.6.json ================================================ { "schema-version": "1.0", "changes": [ { "type": "bugfix", "category": "pip", "description": "Fix RuntimeError with pip v22.x (#1887)" } ] } ================================================ FILE: .changes/1.27.0.json ================================================ { "schema-version": "1.0", "changes": [ { "type": "bugfix", "category": "Local", "description": "Set a default timeout when creating the local LambdaContext instance (#1896)" }, { "type": "feature", "category": "CDK", "description": "Add support for CDK v2 (#1742)" } ] } ================================================ FILE: .changes/1.27.1.json ================================================ { "schema-version": "1.0", "changes": [ { "type": "enhancement", "category": "Pip", "description": "Bump pip version range to latest version <22.2 (#1924)" }, { "type": "enhancement", "category": "Websockets", "description": "Add support for WebSockets API Terraform packaging (#1670)" } ] } ================================================ FILE: .changes/1.27.2.json ================================================ { "schema-version": "1.0", "changes": [ { "type": "enhancement", "category": "Terraform", "description": "Update aws provider constraint to allow versions 4.x (#1951)" }, { "type": "enhancement", "category": "event-source", "description": "Add attribute for message attributes in SNSEvent and generated test events (#1934)" } ] } ================================================ FILE: .changes/1.27.3.json ================================================ { "schema-version": "1.0", "changes": [ { "type": "bugfix", "category": "Versioning", "description": "Fix version string updates used in the release process (#1971)" } ] } ================================================ FILE: .changes/1.28.0.json ================================================ { "schema-version": "1.0", "changes": [ { "type": "enhancement", "category": "Terraform", "description": "Update required terraform version to support 1.3 (#2014)" }, { "type": "enhancement", "category": "Pip", "description": "Bump pip version range to latest version <22.3 (#2016)" }, { "type": "feature", "category": "Config", "description": "Add support for `log_retention_in_days` (#943)" } ] } ================================================ FILE: .changes/1.29.0.json ================================================ { "schema-version": "1.0", "changes": [ { "type": "feature", "category": "Python", "description": "Add support for Python 3.10 (#2037)" }, { "type": "enhancement", "category": "Pip", "description": "Bump pip version range to latest version <23.2 (#2034)" } ] } ================================================ FILE: .changes/1.3.0.json ================================================ { "schema-version": "0.2", "changes": [ { "category": "config", "description": "Add support for Lambdas in a VPC (#413, #837, #673)", "type": "feature" }, { "category": "packaging", "description": "Add support for packaging local directories (#653)", "type": "feature" }, { "category": "local", "description": "Add support for automatically reloading the local\ndev server when files are modified (#316, #846, #706)", "type": "enhancement" }, { "category": "logging", "description": "Add support for viewing cloudwatch logs of all\nlambda functions (#841, #849)", "type": "enhancement" } ] } ================================================ FILE: .changes/1.30.0.json ================================================ { "schema-version": "1.0", "changes": [ { "type": "feature", "category": "Python", "description": "Add support for Python 3.11 (#2053)" }, { "type": "enhancement", "category": "Pip", "description": "Update version dependency on pip (#2080)" } ] } ================================================ FILE: .changes/1.31.0.json ================================================ { "schema-version": "1.0", "changes": [ { "type": "feature", "category": "Python", "description": "Add support for Python 3.12 (#2086)" }, { "type": "enhancement", "category": "Python", "description": "Drop support for Python 3.7 (#2095)" } ] } ================================================ FILE: .changes/1.31.1.json ================================================ { "schema-version": "0.2", "changes": [ { "type": "enhancement", "category": "pip", "description": "Update pip version to allow 24.0 (#2092)" }, { "type": "bugfix", "category": "tar", "description": "Validate tar extraction does not escape destination dir (#1990)" } ] } ================================================ FILE: .changes/1.31.2.json ================================================ { "schema-version": "0.2", "changes": [ { "type": "enhancement", "category": "SQS", "description": "Add configuration option for MaximumConcurrency for SQS event source (#2104)" } ] } ================================================ FILE: .changes/1.31.3.json ================================================ { "schema-version": "0.2", "changes": [ { "type": "enhancement", "category": "Pip", "description": "Update pip to the latest version (<24.4)" }, { "type": "enhancement", "category": "CLI", "description": "Remove distutils warning when packaging/deploying apps (#2123)" } ] } ================================================ FILE: .changes/1.31.4.json ================================================ { "schema-version": "0.2", "changes": [ { "type": "enhancement", "category": "Pip", "description": "Update pip to the latest version (<25.1)" } ] } ================================================ FILE: .changes/1.32.0.json ================================================ { "schema-version": "0.2", "changes": [ { "type": "feature", "category": "Python", "description": "Add support for Python 3.13 (#2137)" }, { "type": "feature", "category": "Python", "description": "Drop support for Python 3.8 (#2138)" } ] } ================================================ FILE: .changes/1.4.0.json ================================================ { "schema-version": "0.2", "changes": [ { "category": "CI-CD", "description": "Add support for generating python 3.6 pipelines (#858)", "type": "enhancement" }, { "category": "event-source", "description": "Add support for connecting lambda functions to S3 events (#855)", "type": "feature" }, { "category": "event-source", "description": "Add support for connecting lambda functions to SNS message (#488)", "type": "feature" }, { "category": "local", "description": "Make ``watchdog`` an optional dependency and add a built in\n``stat()`` based file poller (#867)", "type": "enhancement" }, { "category": "event-source", "description": "Add support for connecting lambda functions to an SQS queue (#884)", "type": "feature" } ] } ================================================ FILE: .changes/1.5.0.json ================================================ { "schema-version": "0.2", "changes": [ { "category": "policy", "description": "Add support for S3 upload_file/download_file in\npolicy generator (#889)", "type": "feature" } ] } ================================================ FILE: .changes/1.6.0.json ================================================ { "schema-version": "0.2", "changes": [ { "category": "CLI", "description": "Add ``chalice invoke`` command (#900)", "type": "feature" } ] } ================================================ FILE: .changes/1.6.1.json ================================================ { "schema-version": "0.2", "changes": [ { "category": "local", "description": "Fix local mode issue with unicode responses and Content-Length (#910)", "type": "bugfix" }, { "category": "dev", "description": "Fix issue with ``requirements-dev.txt`` not setting up a working\ndev environment (#920)", "type": "enhancement" }, { "category": "dependencies", "description": "Add support for pip 18 (#910)", "type": "enhancement" } ] } ================================================ FILE: .changes/1.6.2.json ================================================ { "schema-version": "0.2", "changes": [ { "category": "dependencies", "description": "Add support for pip 18.2 (#991)", "type": "enhancement" }, { "category": "logging", "description": "Add more detailed debug logs to the packager. (#934)", "type": "enhancement" }, { "category": "python", "description": "Add support for python3.7 (#992)", "type": "feature" }, { "category": "rest-api", "description": "Support bytes for the application/json binary type (#988)", "type": "feature" }, { "category": "rest-api", "description": "Use more compact JSON representation by default for dicts (#958)", "type": "enhancement" }, { "category": "logging", "description": "Log internal exceptions as errors (#254)", "type": "enhancement" }, { "category": "rest-api", "description": "Generate swagger documentation from docstrings (#574)", "type": "feature" } ] } ================================================ FILE: .changes/1.7.0.json ================================================ { "schema-version": "0.2", "changes": [ { "category": "packaging", "description": "Fix packaging multiple local directories as dependencies (#1047)", "type": "bugfix" }, { "category": "event-source", "description": "Add support for passing SNS ARNs to ``on_sns_message`` (#1048)", "type": "feature" }, { "category": "blueprint", "description": "Add support for Blueprints (#1023)", "type": "feature" }, { "category": "config", "description": "Add support for opting-in to experimental features (#1053)", "type": "feature" }, { "category": "event-source", "description": "Provide Lambda context in event object (#856)", "type": "feature" } ] } ================================================ FILE: .changes/1.8.0.json ================================================ { "schema-version": "0.2", "changes": [ { "category": "packaging", "description": "Fall back to pure python version of yaml parser\nwhen unable to compile C bindings for PyYAML (#1074)", "type": "bugfix" }, { "category": "packaging", "description": "Add support for Lambda layers. (#1001)", "type": "feature" } ] } ================================================ FILE: .changes/1.9.0.json ================================================ { "schema-version": "0.2", "changes": [ { "category": "dependencies", "description": "Update PIP to support up to 19.1.x (#1104)", "type": "enhancement" }, { "category": "rest-api", "description": "Fix handling of more complex Accept headers for binary\ncontent types (#1078)", "type": "bugfix" }, { "category": "rest-api", "description": "Raise TypeError when trying to serialize an unserializable\ntype (#1100)", "type": "enhancement" }, { "category": "policy", "description": "Update ``policies.json`` file (#1110)", "type": "enhancement" }, { "category": "rest-api", "description": "Support repeating values in the query string (#1131)", "type": "feature" }, { "category": "packaging", "description": "Add layer support to chalice package (#1130)", "type": "feature" }, { "category": "rest-api", "description": "Fix bug with route ``name`` kwarg raising a ``TypeError`` (#1112)", "type": "bugfix" }, { "category": "logging", "description": "Change exceptions to always be logged at the ERROR level (#969)", "type": "enhancement" }, { "category": "CLI", "description": "Fix bug handling exceptions during ``chalice invoke`` on\nPython 3.7 (#1139)", "type": "bugfix" }, { "category": "rest-api", "description": "Add support for API Gateway compression (#672)", "type": "bugfix" }, { "category": "packaging", "description": "Add support for both relative and absolute paths for\n``--package-dir`` (#940)", "type": "enhancement" } ] } ================================================ FILE: .changes/1.9.1.json ================================================ { "schema-version": "0.2", "changes": [ { "category": "rest-api", "description": "Make MultiDict mutable (#1158)", "type": "enhancement" } ] } ================================================ FILE: .changes/templates/changelog ================================================ # CHANGELOG {% for release, changes in releases %} ## {{ release }} {% if changes.summary %} {{ changes.summary -}} {% endif %} {% for change in changes.changes %} * {{ change.type }}:{{ change.category }}:{{ change.description -}} {% endfor %} {% endfor %} ================================================ FILE: .coveragerc ================================================ [run] branch = True [report] exclude_lines = raise NotImplementedError.* ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ *Issue #, if available:* *Description of changes:* By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: pip directory: "/." schedule: interval: daily time: "13:00" open-pull-requests-limit: 10 allow: - dependency-name: pip - dependency-name: click ================================================ FILE: .github/workflows/run-tests.yml ================================================ name: Run PR Checks on: push: branches: - master - "feature/**" pull_request: branches: - master - "feature/**" jobs: prcheck: runs-on: ${{ matrix.os }} env: HYPOTHESIS_PROFILE: ci CHALICE_TEST_EXTENDED_PACKAGING: true strategy: matrix: os: [ubuntu-latest, macos-latest] python-version: [3.9, '3.10', 3.11, 3.12, 3.13] steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 name: Set up Python ${{ matrix.python-version }} with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | make install-dev-deps - name: Run PRCheck run: make prcheck cdktests: runs-on: ubuntu-latest strategy: matrix: python-version: [3.9, '3.10', 3.11, 3.12, 3.13] steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v2 with: node-version: '14' - uses: actions/setup-python@v2 name: Set up Python ${{ matrix.python-version }} with: python-version: ${{ matrix.python-version }} - name: Install CDK run: npm install -g aws-cdk - name: Install dependencies run: | pip install -r requirements-test.txt --upgrade --upgrade-strategy eager -e .[cdkv2] - name: Run CDK tests run: python -m pytest tests/functional/cdk # Chalice works on windows, but there's some differences between # the GitHub actions windows environment and our windows dev # laptops that are causing certain tests to fail. Once these # are fixed we can also test on windows but for now we have to # disable these. # tests-windows: # runs-on: windows-latest # strategy: # matrix: # # In windows where you have to explicitly install # # python, it's unlikely users are going to install # # python 2.7 which is no longer supported so we're # # only testing python3 on windows. # python-version: [3.6, 3.7, 3.8, 3.9] # steps: # - uses: actions/checkout@v2 # - uses: actions/setup-python@v2 # name: Set up Python ${{ matrix.python-version }} # with: # python-version: ${{ matrix.python-version }} # - name: Install dependencies # run: | # pip install -r requirements-dev.txt # pip install -e . # - name: Run PRCheck # run: python -m pytest tests/unit tests/functional tests/integration ================================================ FILE: .github/workflows/stale-issue.yml ================================================ name: "Close stale issues" on: schedule: - cron: "*/60 * * * *" jobs: cleanup: runs-on: ubuntu-latest name: Stale issue job steps: - uses: aws-actions/stale-issue-cleanup@v4 with: issue-types: issues ancient-issue-message: "" stale-issue-message: > Greetings! It looks like this issue hasn’t been active in longer than five days. We encourage you to check if this is still an issue in the latest release. In the absence of more information, we will be closing this issue soon. If you find that this is still a problem, please feel free to provide a comment or upvote with a reaction on the initial post to prevent automatic closure. If the issue is already closed, please feel free to open a new one. # These labels are required stale-issue-label: closing-soon exempt-issue-labels: bug, feature-request response-requested-label: response-requested # Don't set closed-for-staleness label to skip closing very old issues # regardless of label closed-for-staleness-label: closed-for-staleness # Issue timing days-before-stale: 10 days-before-close: 4 days-before-ancient: 2190 # If you don't want to mark a issue as being ancient based on a # threshold of "upvotes", you can set this here. An "upvote" is # the total number of +1, heart, hooray, and rocket reactions # on an issue. minimum-upvotes-to-exempt: 2 repo-token: ${{ secrets.GITHUB_TOKEN }} loglevel: DEBUG # Set dry-run to true to not perform label or close actions. dry-run: false ================================================ FILE: .gitignore ================================================ sample/ .cache venv docs/build/ .idea __pycache__/ .coverage chalice.egg-info/ .hypothesis/ .pytest_cache/ .mypy_cache/ *.pyc .vscode ================================================ FILE: .pylintrc ================================================ [MASTER] # Specify a configuration file. #rcfile= # Python code to execute, usually for sys.path manipulation such as # pygtk.require(). init-hook="import sys, os; sys.path.append(os.path.join('tests', 'plugins'))" # Add files or directories to the blacklist. They should be base names, not # paths. ignore=compat.py,regions.py # Pickle collected data for later comparisons. persistent=yes # List of plugins (as comma separated values of python modules names) to load, # usually to register additional checkers. load-plugins=codelinter,testlinter # Use multiple processes to speed up Pylint. jobs=1 # Allow loading of arbitrary C extensions. Extensions are imported into the # active Python interpreter and may run arbitrary code. unsafe-load-any-extension=no # A comma-separated list of package or module names from where C extensions may # be loaded. Extensions are loading into the active Python interpreter and may # run arbitrary code extension-pkg-whitelist= [MESSAGES CONTROL] # Only show warnings with the listed confidence levels. Leave empty to show # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED confidence= # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option # multiple time. See also the "--disable" option for examples. #enable= # Disable the message, report, category or checker with the given id(s). You # can either give multiple identifiers separated by comma (,) or put this # option multiple times (only on the command line, not in the configuration # file where it should appear only once).You can also use "--disable=all" to # disable everything first and then reenable specific checks. For example, if # you want to run only the similarities checker, you can use "--disable=all # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use"--disable=all --enable=classes # --disable=W" disable=W0613,I0021,I0020,C0111,R0902,R0903,W0231,W0611,R0913,W0703,I0011,R0904,R0205,R1705,R1710,C0415,R1725,W0707,R1732,W1512,C0209,W1514,C0412,C0411,R1735,R0917 [REPORTS] # Set the output format. Available formats are text, parseable, colorized, msvs # (visual studio) and html. You can also give a reporter class, eg # mypackage.mymodule.MyReporterClass. output-format=text # Tells whether to display a full report or only the messages reports=no # Python expression which should return a note less than 10 (10 is the highest # note). You have access to the variables errors warning, statement which # respectively contain the number of errors / warnings messages and the total # number of statements analyzed. This is used by the global evaluation report # (RP0004). evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) # Template used to display messages. This is a python new-style format string # used to format the message information. See doc for all details #msg-template= [BASIC] # Good variable names which should always be accepted, separated by a comma good-names=e,i,j,k,n,ex,Run,_ # Bad variable names which should always be refused, separated by a comma bad-names=foo,bar,baz,toto,tutu,tata # Colon-delimited sets of names that determine each other's naming style when # the name regexes allow several styles. name-group= # Include a hint for the correct naming format with invalid-name include-naming-hint=no # Regular expression matching correct function names function-rgx=[a-z_][a-z0-9_]{2,50}$ # Regular expression matching correct variable names variable-rgx=[a-z_][a-z0-9_]{0,50}$ # Regular expression matching correct constant names const-rgx=(([a-zA-Z_][a-zA-Z0-9_]*)|(__.*__))$ # Regular expression matching correct attribute names attr-rgx=[a-z_][a-z0-9_]{1,50}$ # Regular expression matching correct argument names argument-rgx=[a-z_][a-z0-9_]{0,50}$ # Regular expression matching correct class attribute names class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,40}|(__.*__))$ # Regular expression matching correct inline iteration names inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ # Regular expression matching correct class names class-rgx=[A-Z_][a-zA-Z0-9]+$ # Regular expression matching correct module names module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ # Regular expression matching correct method names # Allow for the ast visitor method names of visit_CamelCase. # Allow for do_HTTPMETHOD used in chalice local. method-rgx=([a-z_][a-z0-9_]{2,50}|visit_[a-zA-Z]+|do_[A-Z]+)$ # Regular expression which should only match function or class names that do # not require a docstring. no-docstring-rgx=.* # Minimum line length for functions/classes that require docstrings, shorter # ones are exempt. docstring-min-length=-1 typevar-rgx=(([a-zA-Z_][a-zA-Z0-9_]*)|(__.*__))$ typealias-rgx=(([a-zA-Z_][a-zA-Z0-9_]*)|(__.*__))$ [FORMAT] # Maximum number of characters on a single line. max-line-length=80 # Regexp for a line that is allowed to be longer than the limit. ignore-long-lines=^\s*(# )??$ # Allow the body of an if to be on the same line as the test if there is no # else. single-line-if-stmt=no # Maximum number of lines in a module max-module-lines=1000 # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 # tab). indent-string=' ' # Number of spaces of indent required inside a hanging or continued line. indent-after-paren=4 # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. expected-line-ending-format= [LOGGING] # Logging modules to check that the string format arguments are in logging # function parameter format logging-modules=logging [MISCELLANEOUS] # List of note tags to take in consideration, separated by a comma. notes=FIXME,XXX [SIMILARITIES] # Minimum lines number of a similarity. min-similarity-lines=5 # Ignore comments when computing similarities. ignore-comments=no # Ignore docstrings when computing similarities. ignore-docstrings=no # Ignore imports when computing similarities. ignore-imports=no [SPELLING] # Spelling dictionary name. Available dictionaries: none. To make it working # install python-enchant package. spelling-dict= # List of comma separated words that should not be checked. spelling-ignore-words= # A path to a file that contains private dictionary; one word per line. spelling-private-dict-file= # Tells whether to store unknown words to indicated private dictionary in # --spelling-private-dict-file option instead of raising a message. spelling-store-unknown-words=no [TYPECHECK] # Tells whether missing members accessed in mixin class should be ignored. A # mixin class is detected if its name ends with "mixin" (case insensitive). ignore-mixin-members=yes # List of module names for which member attributes should not be checked # (useful for modules/projects where namespaces are manipulated during runtime # and thus existing member attributes cannot be deduced by static analysis ignored-modules=six.moves,aws_cdk # List of classes names for which member attributes should not be checked # (useful for classes with attributes dynamically set). ignored-classes=SQLObject # List of members which are set dynamically and missed by pylint inference # system, and so shouldn't trigger E0201 when accessed. Python regular # expressions are accepted. generated-members=REQUEST,acl_users,aq_parent,objects,DoesNotExist,md5,sha1,sha224,sha256,sha384,sha512 [VARIABLES] # Tells whether we should check for unused import in __init__ files. init-import=no # A regular expression matching the name of dummy variables (i.e. expectedly # not used). dummy-variables-rgx=_|dummy|ignore # List of additional names supposed to be defined in builtins. Remember that # you should avoid to define new builtins when possible. additional-builtins= # List of strings which can identify a callback function by name. A callback # name must start or end with one of those strings. callbacks=cb_,_cb [CLASSES] # List of method names used to declare (i.e. assign) instance attributes. defining-attr-methods=__init__,__new__,setUp # List of valid names for the first argument in a class method. valid-classmethod-first-arg=cls # List of valid names for the first argument in a metaclass class method. valid-metaclass-classmethod-first-arg=mcs # List of member names, which should be excluded from the protected access # warning. exclude-protected=_asdict,_fields,_replace,_source,_make [DESIGN] # Maximum number of arguments for function / method max-args=5 # Argument names that match this expression will be ignored. Default to name # with leading underscore ignored-argument-names=_.* # Maximum number of locals for function / method body max-locals=15 # Maximum number of return / yield for function / method body max-returns=6 # Maximum number of branch for function / method body max-branches=12 # Maximum number of statements in function / method body max-statements=30 # Maximum number of parents for a class (see R0901). max-parents=6 # Maximum number of attributes for a class (see R0902). max-attributes=7 # Minimum number of public methods for a class (see R0903). min-public-methods=0 # Maximum number of public methods for a class (see R0904). max-public-methods=20 [IMPORTS] # Deprecated modules which should not be used, separated by a comma deprecated-modules=regsub,string,TERMIOS,Bastion,rexec,UserDict # Create a graph of every (i.e. internal and external) dependencies in the # given file (report RP0402 must not be disabled) import-graph= # Create a graph of external dependencies in the given file (report RP0402 must # not be disabled) ext-import-graph= # Create a graph of internal dependencies in the given file (report RP0402 must # not be disabled) int-import-graph= # Typing is a third party package in python 2, but built-in to the standard # library for Python 3. This causes linting issues when typing get grouped # with the third party package imports. Since chalice was originally written # just for Python 2, the code standard is to treat as a third party library # and thus mark it as so when linting. known-third-party=typing [EXCEPTIONS] # Exceptions that will emit a warning when being caught. Defaults to # "Exception" overgeneral-exceptions=builtins.Exception ================================================ FILE: .python-version ================================================ 3.7 ================================================ FILE: CHANGELOG.md ================================================ # CHANGELOG ## 1.32.0 * feature:Python:Add support for Python 3.13 (#2137) * feature:Python:Drop support for Python 3.8 (#2138) ## 1.31.4 * enhancement:Pip:Update pip to the latest version (<25.1) ## 1.31.3 * enhancement:Pip:Update pip to the latest version (<24.4) * enhancement:CLI:Remove distutils warning when packaging/deploying apps (#2123) ## 1.31.2 * enhancement:SQS:Add configuration option for MaximumConcurrency for SQS event source (#2104) ## 1.31.1 * enhancement:pip:Update pip version to allow 24.0 (#2092) * bugfix:tar:Validate tar extraction does not escape destination dir (#1990) ## 1.31.0 * feature:Python:Add support for Python 3.12 (#2086) * enhancement:Python:Drop support for Python 3.7 (#2095) ## 1.30.0 * feature:Python:Add support for Python 3.11 (#2053) * enhancement:Pip:Update version dependency on pip (#2080) ## 1.29.0 * feature:Python:Add support for Python 3.10 (#2037) * enhancement:Pip:Bump pip version range to latest version <23.2 (#2034) ## 1.28.0 * enhancement:Terraform:Update required terraform version to support 1.3 (#2014) * enhancement:Pip:Bump pip version range to latest version <22.3 (#2016) * feature:Config:Add support for `log_retention_in_days` (#943) ## 1.27.3 * bugfix:Versioning:Fix version string updates used in the release process (#1971) ## 1.27.2 * enhancement:Terraform:Update aws provider constraint to allow versions 4.x (#1951) * enhancement:event-source:Add attribute for message attributes in SNSEvent and generated test events (#1934) ## 1.27.1 * enhancement:Pip:Bump pip version range to latest version <22.2 (#1924) * enhancement:Websockets:Add support for WebSockets API Terraform packaging (#1670) ## 1.27.0 * bugfix:Local:Set a default timeout when creating the local LambdaContext instance (#1896) * feature:CDK:Add support for CDK v2 (#1742) ## 1.26.6 * bugfix:pip:Fix RuntimeError with pip v22.x (#1887) ## 1.26.5 * enhancement:Terraform:Remove template provider in favor of locals (#1869) * enhancement:Terraform:Bump Terraform version to suppose 1.1.x (#1868) ## 1.26.4 * bugfix:Terraform:Use updated keywords for providing provider version constraints (#1717) ## 1.26.3 * enhancement:Errors:Remove redundant error code in error message string (#1339) * enhancement:VPC:Associate VPC endpoint with Rest API (#1449) ## 1.26.2 * enhancement:Dependencies:Update pyyaml to 6.x (#1830) * bugfix:Websocket:Correctly configure websocket endpoint in the aws-cn partition (#1820) ## 1.26.1 * enhancement:Dependencies:Bump pip dependency to latest released version (#1817) * enhancement:Tests:Don't include tests package in .whl file (#1814) ## 1.26.0 * feature:Websockets:Add support for setting the Websocket protocol from the connect handler (#1768) * feature:SQS:Added MaximumBatchingWindowInSeconds to SQS event handler (#1778) ## 1.25.0 * feature:Python:Add support for Python 3.9 (#1787) ## 1.24.2 * enhancement:Dependencies:Bump attrs dependency to latest version (#1786) * bugfix:Auth:Fix ARN parsing when generating a builtin AuthResponse (#1775) * enhancement:CLI:Upgrade Click dependency to support v8.0.0 (#1729) ## 1.24.1 * bugfix:GovCloud:Fix partition error when updating API Gateway in GovCloud region (#1770) ## 1.24.0 * feature:Python2.7:Remove support for Python 2.7 (#1766) * enhancement:Terraform:Update Terraform packaging to support version 1.0 (#1757) * enhancement:Typing:Add missing WebsocketEvent type information (#1746) * enhancement:S3 events:Add source account to Lambda permissions when configuring S3 events (#1635) * enhancement:Packaging:Add support for Terraform v0.15 (#1725) ## 1.23.0 * enhancement:Deploy:Wait for function state to be active when deploying * feature:SQS:Add queue_arn parameter to enable CDK integration with SQS event handler (#1681) ## 1.22.4 * enhancement:Types:Add missing types to app.pyi stub file (#1701) * bugfix:Custom Domain:Fix custom domain generation when using the CDK (#1640) * bugfix:Packaging:Special cases pyrsistent packaging (#1696) ## 1.22.3 * enhancement:Terraform:Bump Terraform version to include 0.14 * bugfix:Typing:Fix type definitions in app.pyi (#1676) * bugfix:Terraform:Use references instead of function names in Terraform packaging (#1558) ## 1.22.2 * enhancement:Blueprint:Add log property to blueprint * bugfix:Pipeline:Fix build command in pipeline generation (#1653) * enhancement:Dependencies:Change enum-compat dependency to enum34 with version restrictions (#1667) ## 1.22.1 * enhancement:Pip:Bump pip version range to latest version 21.x (#1630) * enhancement:IAM:Improve client call collection when generation policies (#692) ## 1.22.0 * feature:CDK:Add built-in support for the AWS CDK (#1622) ## 1.21.9 * enhancement:Dependencies:Bump attr version constraint (#1620) ## 1.21.8 * enhancement:Authorizers:Add support for custom headers in built-in authorizers (#1613) ## 1.21.7 * enhancement:Terraform:Map custom domain outputs in Terraform packaging (#1601) ## 1.21.6 * enhancement:Packaging:Increase upper bound for AWS provider in Terraform to 3.x (#1596) * enhancement:Packaging:Add support for manylinux2014 wheels (#1551) ## 1.21.5 * bugfix:Config:Fix config validation for env vars on py27 (#1573) * bugfix:Pip:Bump pip version contraint (#1590) * bugfix:REST:Add Allow header with list of allowed methods when returning 405 error (#1583) ## 1.21.4 * enhancement:Local:Allow custom Chalice class in local mode (#1502) * bugfix:Layers:Ensure single reference to managed layer (#1563) ## 1.21.3 * enhancement:Test:Add test client methods for generating sample kinesis events * enhancement:Config:Validate env var values are strings (#1543) ## 1.21.2 * bugfix:Terraform:Fix issue with wildcard partition names in s3 event handlers (#1508) * bugfix:Auth:Fix special case processing for root URL auth (#1271) * enhancement:Middleware:Add support for HTTP middleware catching exceptions (#1541) ## 1.21.1 * bugfix:Websockets:Fix custom domain name configuration for websockets (#1531) * bugfix:Local:Add support for multiple actions in builtin auth in local mode (#1527) * bugfix:Websocket:Fix websocket client configuration when using a custom domain (#1503) * bugfix:Local:Fix CORs handling in local mode (#761) ## 1.21.0 * bugfix:Blueprints:Fix regression when invoking Lambda functions from blueprints (#1535) * feature:Events:Add support for Kinesis and DynamoDB event handlers (#987) ## 1.20.1 * bugfix:Blueprints:Preserve docstring in blueprints (#1525) * enhancement:Binary:Support returning native python types when using `*/*` for binary types (#1501) ## 1.20.0 * enhancement:Blueprints:Add `current_app` property to Blueprints (#1094) * enhancement:CLI:Set `AWS_CHALICE_CLI_MODE` env var whenever a Chalice CLI command is run (#1200) * feature:Middleware:Add support for middleware (#1509) * feature:X-Ray:Add support for AWS X-Ray (#464) ## 1.19.0 * feature:Pipeline:Add a new v2 template for the deployment pipeline CloudFormation template (#1506) ## 1.18.1 * bugfix:Packaging:Add fallback to retrieve name/version from sdist (#1486) * bugfix:Analyzer:Handle symbols with multiple (shadowed) namespaces (#1494) ## 1.18.0 * feature:Packaging:Add support for automatic layer creation (#1485, #1001) ## 1.17.0 * feature:Testing:Add Chalice test client (#1468) * enhancement:regions:Add support for non `aws` partitions including aws-cn and aws-us-gov (#792). * bugfix:dependencies:Fix error when using old versions of click by requiring >=7 * bugfix:local:Fix local mode builtin authorizer not stripping query string from URL (#1470) ## 1.16.0 * enhancement:local:Avoid error from cognito client credentials in local authorizer (#1447) * bugfix:package:Traverse symlinks to directories when packaging the vendor directory (#583). * feature:DomainName:Add support for custom domain names to REST/WebSocket APIs (#1194) * feature:auth:Add support for oauth scopes on routes (#1444). ## 1.15.1 * bugfix:packaging:Fix setup.py dependencies where the wheel package was not being installed (#1435) ## 1.15.0 * feature:blueprints:Mark blueprints as an accepted API (#1250) * feature:package:Add ability to generate and merge yaml CloudFormation templates (#1425) * enhancement:terraform:Allow generated terraform template to be used as a terraform module (#1300) * feature:logs:Add support for tailing logs (#4). ## 1.14.1 * enhancement:pip:Update pip version range to 20.1. ## 1.14.0 * bugfix:packaging:Fix pandas packaging regression (#1398) * feature:CLI:Add ``dev plan/appgraph`` commands (#1396) * enhancement:SQS:Validate queue name is used and not queue URL or ARN (#1388) ## 1.13.1 * enhancement:local:Add support for multiValueHeaders in local mode (#1381). * bugfix:local:Make ``current_request`` thread safe in local mode (#759) * enhancement:local:Add support for cognito in local mode (#1377). * bugfix:packaging:Fix terraform generation when injecting custom domains (#1237) * enhancement:packaging:Ensure repeatable zip file generation (#1114). * bugfix:CORS:Fix CORS request when returning compressed binary types (#1336) ## 1.13.0 * bugfix:logs:Fix error for ``chalice logs`` when a Lambda function has not been invoked (#1252) * feature:CORS:Add global CORS configuration (#70) * bugfix:packaging:Fix packaging simplejson (#1304) * feature:python:Add support for Python 3.8 (#1315) * feature:authorizer:Add support for invocation role in custom authorizer (#1303) * bugfix:packaging:Fix packaging on case-sensitive filesystems (#1356) ## 1.12.0 * feature:CLI:Add ``generate-models`` command (#1245) * enhancement:websocket:Add ``close`` and ``info`` commands to websocket api (#1259) * enhancement:dependencies:Bump upper bound on PIP to ``<19.4`` (#1273, #1272) ## 1.11.1 * bugfix:blueprint:Fix mouting blueprints with root routes (#1230) * feature:rest-api:Add support for multi-value headers responses (#1205) ## 1.11.0 * feature:config:Add support for stage independent lambda configuration (#1162) * feature:event-source:Add support for subscribing to CloudWatch Events (#1126) * feature:event-source:Add a ``description`` argument to CloudWatch schedule events (#1155) * bugfix:rest-api:Fix deployment of API Gateway resource policies (#1220) ## 1.10.0 * feature:websocket:Add experimental support for websockets (#1017) * feature:rest-api:API Gateway Endpoint Type Configuration (#1160) * feature:rest-api:API Gateway Resource Policy Configuration (#1160) * feature:packaging:Add --merge-template option to package command (#1195) * feature:packaging:Add support for packaging via terraform (#1129) ## 1.9.1 * enhancement:rest-api:Make MultiDict mutable (#1158) ## 1.9.0 * enhancement:dependencies:Update PIP to support up to 19.1.x (#1104) * bugfix:rest-api:Fix handling of more complex Accept headers for binary content types (#1078) * enhancement:rest-api:Raise TypeError when trying to serialize an unserializable type (#1100) * enhancement:policy:Update ``policies.json`` file (#1110) * feature:rest-api:Support repeating values in the query string (#1131) * feature:packaging:Add layer support to chalice package (#1130) * bugfix:rest-api:Fix bug with route ``name`` kwarg raising a ``TypeError`` (#1112) * enhancement:logging:Change exceptions to always be logged at the ERROR level (#969) * bugfix:CLI:Fix bug handling exceptions during ``chalice invoke`` on Python 3.7 (#1139) * bugfix:rest-api:Add support for API Gateway compression (#672) * enhancement:packaging:Add support for both relative and absolute paths for ``--package-dir`` (#940) ## 1.8.0 * bugfix:packaging:Fall back to pure python version of yaml parser when unable to compile C bindings for PyYAML (#1074) * feature:packaging:Add support for Lambda layers. (#1001) ## 1.7.0 * bugfix:packaging:Fix packaging multiple local directories as dependencies (#1047) * feature:event-source:Add support for passing SNS ARNs to ``on_sns_message`` (#1048) * feature:blueprint:Add support for Blueprints (#1023) * feature:config:Add support for opting-in to experimental features (#1053) * feature:event-source:Provide Lambda context in event object (#856) ## 1.6.2 * enhancement:dependencies:Add support for pip 18.2 (#991) * enhancement:logging:Add more detailed debug logs to the packager. (#934) * feature:python:Add support for python3.7 (#992) * feature:rest-api:Support bytes for the application/json binary type (#988) * enhancement:rest-api:Use more compact JSON representation by default for dicts (#958) * enhancement:logging:Log internal exceptions as errors (#254) * feature:rest-api:Generate swagger documentation from docstrings (#574) ## 1.6.1 * bugfix:local:Fix local mode issue with unicode responses and Content-Length (#910) * enhancement:dev:Fix issue with ``requirements-dev.txt`` not setting up a working dev environment (#920) * enhancement:dependencies:Add support for pip 18 (#910) ## 1.6.0 * feature:CLI:Add ``chalice invoke`` command (#900) ## 1.5.0 * feature:policy:Add support for S3 upload_file/download_file in policy generator (#889) ## 1.4.0 * enhancement:CI-CD:Add support for generating python 3.6 pipelines (#858) * feature:event-source:Add support for connecting lambda functions to S3 events (#855) * feature:event-source:Add support for connecting lambda functions to SNS message (#488) * enhancement:local:Make ``watchdog`` an optional dependency and add a built in ``stat()`` based file poller (#867) * feature:event-source:Add support for connecting lambda functions to an SQS queue (#884) ## 1.3.0 * feature:config:Add support for Lambdas in a VPC (#413, #837, #673) * feature:packaging:Add support for packaging local directories (#653) * enhancement:local:Add support for automatically reloading the local dev server when files are modified (#316, #846, #706) * enhancement:logging:Add support for viewing cloudwatch logs of all lambda functions (#841, #849) ## 1.2.3 * enhancement:dependency:Add support for pip 10 (#808) * enhancement:policy:Update ``policies.json`` file (#817) ## 1.2.2 * bugfix:packaging:Fix package command not correctly setting environment variables (#795) ## 1.2.1 * enhancement:rest-api:Add CORS headers to error response (#715) * bugfix:local:Fix parsing empty query strings in local mode (#767) * bugfix:packaging:Fix regression in ``chalice package`` when using role arns (#793) ## 1.2.0 This release features a rewrite of the core deployment code used in Chalice. This is a backwards compatible change for users, but you may see changes to the autogenerated files Chalice creates. Please read the `upgrade notes for 1.2.0 `__ for more detailed information about upgrading to this release. * enhancement:rest-api:Print out full stack trace when an error occurs (#711) * enhancement:rest-api:Add ``image/jpeg`` as a default binary content type (#707) * feature:event-source:Add support for AWS Lambda only projects (#162, #640) * bugfix:policy:Fix inconsistent IAM role generation with pure lambdas (#685) * enhancement:deployment:Rewrite Chalice deployer to more easily support additional AWS resources (#604) * feature:packaging:Update the ``chalice package`` command to support pure lambda functions and scheduled events. (#772) * bugfix:packaging:Fix packager edge case normalizing sdist names (#778) * bugfix:packaging:Fix SQLAlchemy packaging (#778) * bugfix:packaging:Fix packaging abi3, wheels this fixes cryptography 2.2.x packaging (#764) ## 1.1.1 * feature:CLI:Add ``--connection-timeout`` to the ``deploy`` command (#344) * bugfix:policy:Fix IAM role creation issue (#565) * bugfix:local:Fix `chalice local` handling of browser requests (#565) * enhancement:policy:Support async/await syntax in automatic policy generation (#565) * enhancement:packaging:Support additional PyPi package formats (.tar.bz2) (#720) ## 1.1.0 * enhancement:rest-api:Default to ``None`` in local mode when no query parameters are provided (#593) * enhancement:local:Add support for binding a custom address for local dev server (#596) * bugfix:rest-api:Fix local mode handling of routes with trailing slashes (#582) * bugfix:config:Scale ``lambda_timeout`` parameter correctly in local mode (#579) * feature:CI-CD:Add ``--codebuild-image`` to the ``generate-pipeline`` command (#609) * feature:CI-CD:Add ``--source`` and ``--buildspec-file`` to the ``generate-pipeline`` command (#609) ## 1.0.4 * bugfix:packaging:Fix issue deploying some packages in Windows with utf-8 characters (#560) * feature:packaging:Add support for custom authorizers with ``chalice package`` (#580) ## 1.0.3 * bugfix:packaging:Fix issue with some packages with `-` or `.` in their distribution name (#555) * bugfix:rest-api:Fix issue where chalice local returned a 403 for successful OPTIONS requests (#554) * bugfix:local:Fix issue with chalice local mode causing http clients to hang on responses with no body (#525) * enhancement:local:Add ``--stage`` parameter to ``chalice local`` (#545) * bugfix:policy:Fix issue with analyzer that followed recursive functions infinitely (#531) ## 1.0.2 * bugfix:rest-api:Fix issue where requestParameters were not being mapped correctly resulting in invalid generated javascript SDKs (#498) * bugfix:rest-api:Fix issue where ``api_gateway_stage`` was being ignored when set in the ``config.json`` file (#495) * bugfix:rest-api:Fix bug where ``raw_body`` would raise an exception if no HTTP body was provided (#503) * bugfix:CLI:Fix bug where exit codes were not properly being propagated during packaging (#500) * feature:local:Add support for Builtin Authorizers in local mode (#404) * bugfix:packaging:Fix environment variables being passed to subprocess while packaging (#501) * enhancement:rest-api:Allow view to require API keys as well as authorization (#473) ## 1.0.1 * bugfix:packaging:Only use alphanumeric characters for event names in SAM template (#450) * enhancement:config:Print useful error message when config.json is invalid (#458) * bugfix:rest-api:Fix api gateway stage being set incorrectly in non-default chalice stage (`#$70 `__) ## 1.0.0 * enhancement:rest-api:Change default API Gateway stage name to ``api`` (#431) * enhancement:local:Add support for ``CORSConfig`` in ``chalice local`` (#436) * enhancement:logging:Propagate ``DEBUG`` log level when setting ``app.debug`` (#386) * feature:rest-api:Add support for wildcard routes and HTTP methods in ``AuthResponse`` (#403) * bugfix:policy:Fix bug when analyzing list comprehensions (#412) * enhancement:local:Update ``chalice local`` to use HTTP 1.1 (#448) ## 1.0.0b2 Please read the `upgrade notes for 1.0.0b2 `__ for more detailed information about upgrading to this release. Note: to install this beta version of chalice you must specify ``pip install 'chalice>=1.0.0b2,<2.0.0'`` or use the ``--pre`` flag for pip: ``pip install --pre chalice``. * enhancement:local:Set env vars from config in ``chalice local`` (#396) * bugfix:packaging:Fix edge case when building packages with optional c extensions (#421) * enhancement:policy:Remove legacy ``policy.json`` file support. Policy files must use the stage name, e.g. ``policy-dev.json`` (#430) * bugfix:deployment:Fix issue where IAM role policies were updated twice on redeploys (#428) * enhancement:rest-api:Validate route path is not an empty string (#432) * enhancement:rest-api:Change route code to invoke view function with kwargs instead of positional args (#429) ## 1.0.0b1 Please read the `upgrade notes for 1.0.0b1 `__ for more detailed information about upgrading to this release. Note: to install this beta version of chalice you must specify ``pip install 'chalice>=1.0.0b1,<2.0.0'`` or use the ``--pre`` flag for pip: ``pip install --pre chalice``. * bugfix:rest-api:Fix unicode responses being quoted in python 2.7 (#262) * feature:event-source:Add support for scheduled events (#390) * feature:event-source:Add support for pure lambda functions (#390) * feature:packaging:Add support for wheel packaging. (#249) ## 0.10.1 * bugfix:deployment:Fix deployment issue for projects deployed with versions prior to 0.10.0 (#387) * bugfix:policy:Fix crash in analyzer when encountering genexprs and listcomps (#263) ## 0.10.0 * bugfix:deployment:Fix issue where provided ``iam_role_arn`` was not respected on redeployments of chalice applications and in the CloudFormation template generated by ``chalice package`` (#339) * bugfix:config:Fix ``autogen_policy`` in config being ignored (#367) * feature:rest-api:Add support for view functions that share the same view url but differ by HTTP method (#81) * enhancement:deployment:Improve deployment error messages for deployment packages that are too large (#246, #330, #380) * feature:rest-api:Add support for built-in authorizers (#356) ## 0.9.0 * feature:rest-api:Add support for ``IAM`` authorizer (#334) * feature:config:Add support for configuring ``lambda_timeout``, ``lambda_memory_size``, and ``tags`` in your AWS Lambda function (#347) * bugfix:packaging:Fix vendor directory contents not being importable locally (#350) * feature:rest-api:Add support for binary payloads (#348) ## 0.8.2 * bugfix:CLI:Fix issue where ``--api-gateway-stage`` was being ignored (#325) * feature:CLI:Add ``chalice delete`` command (#40) ## 0.8.1 * enhancement:deployment:Alway overwrite existing API Gateway Rest API on updates (#305) * enhancement:CORS:Added more granular support for CORS (#311) * bugfix:local:Fix duplicate content type header in local model (#311) * bugfix:rest-api:Fix content type validation when charset is provided (#306) * enhancement:rest-api:Add back custom authorizer support (#322) ## 0.8.0 * feature:python:Add support for python3! (#296) * bugfix:packaging:Fix swagger generation when using ``api_key_required=True`` (#279) * bugfix:CI-CD:Fix ``generate-pipeline`` to install requirements file before packaging (#295) ## 0.7.0 * feature:CLI:Add ``chalice package`` command. This will create a SAM template and Lambda deployment package that can be subsequently deployed by AWS CloudFormation. (#258) * feature:CLI:Add a ``--stage-name`` argument for creating chalice stages. A chalice stage is a completely separate set of AWS resources. As a result, most configuration values can also be specified per chalice stage. (#264, #270) * feature:policy:Add support for ``iam_role_file``, which allows you to specify the file location of an IAM policy to use for your app (#272) * feature:config:Add support for setting environment variables in your app (#273) * feature:CI-CD:Add a ``generate-pipeline`` command (#277) ## 0.6.0 Check out the `upgrade notes for 0.6.0 `__ for more detailed information about changes in this release. * feature:local:Add port parameter to local command (#220) * feature:packaging:Add support for binary vendored packages (#182, #106, #42) * feature:rest-api:Add support for customizing the returned HTTP response (#240, #218, #110, #30, #226) * enhancement:packaging:Always inject latest runtime to allow for chalice upgrades (#245) ## 0.5.1 * enhancement:local:Add support for serializing decimals in ``chalice local`` (#187) * enhancement:local:Add stdout handler for root logger when using ``chalice local`` (#186) * enhancement:local:Map query string parameters when using ``chalice local`` (#184) * enhancement:rest-api:Support Content-Type with a charset (#180) * bugfix:deployment:Fix not all resources being retrieved due to pagination (#188) * bugfix:deployment:Fix issue where root resource was not being correctly retrieved (#205) * bugfix:deployment:Handle case where local policy does not exist (`29 `__) ## 0.5.0 * enhancement:logging:Add default application logger (#149) * enhancement:local:Return 405 when method is not supported when running ``chalice local`` (#159) * enhancement:SDK:Add path params as requestParameters so they can be used in generated SDKs as well as cache keys (#163) * enhancement:rest-api:Map cognito user pool claims as part of request context (#165) * feature:CLI:Add ``chalice url`` command to print the deployed URL (#169) * enhancement:deployment:Bump up retry limit on initial function creation to 30 seconds (#172) * feature:local:Add support for ``DELETE`` and ``PATCH`` in ``chalice local`` (#167) * feature:CLI:Add ``chalice generate-sdk`` command (#178) ## 0.4.0 * bugfix:deployment:Fix issue where role name to arn lookup was failing due to lack of pagination (#139) * enhancement:rest-api:Raise errors when unknown kwargs are provided to ``app.route(...)`` (#144) * enhancement:config:Raise validation error when configuring CORS and an OPTIONS method (#142) * feature:rest-api:Add support for multi-file applications (#21) * feature:local:Add support for ``chalice local``, which runs a local HTTP server for testing (#22) ## 0.3.0 * bugfix:rest-api:Fix bug with case insensitive headers (#129) * feature:CORS:Add initial support for CORS (#133) * enhancement:deployment:Only add API gateway permissions if needed (#48) * bugfix:policy:Fix error when dict comprehension is encountered during policy generation (#131) * enhancement:CLI:Add ``--version`` and ``--debug`` options to the chalice CLI ## 0.2.0 * enhancement:rest-api:Add support for input content types besides ``application/json`` (#96) * enhancement:rest-api:Allow ``ChaliceViewErrors`` to propagate, so that API Gateway can properly map HTTP status codes in non debug mode (#113) * enhancement:deployment:Add windows compatibility (#31) ## 0.1.0 * enhancement:packaging:Require ``virtualenv`` as a package dependency. (#33) * enhancement:CLI:Add ``--profile`` option when creating a new project (#28) * enhancement:rest-api:Add support for more error codes exceptions (#34) * enhancement:rest-api:Improve error validation when routes containing a trailing ``/`` char (#65) * enhancement:rest-api:Validate duplicate route entries (#79) * enhancement:policy:Ignore lambda expressions in policy analyzer (#74) * enhancement:rest-api:Print original error traceback in debug mode (#50) * feature:rest-api:Add support for authenticate routes (#14) * feature:policy:Add ability to disable IAM role management (#61) ================================================ FILE: CODE_OF_CONDUCT.rst ================================================ =============== Code of Conduct =============== This project has adopted the `Amazon Open Source Code of Conduct `__. For more information see the `Code of Conduct FAQ `__ or contact opensource-codeofconduct@amazon.com with any additional questions or comments. ================================================ FILE: CONTRIBUTING.rst ================================================ ============ Contributing ============ We work hard to provide a high-quality and useful framework, and we greatly value feedback and contributions from our community. Whether it's a new feature, correction, or additional documentation, we welcome your pull requests. Please submit any `issues `__ or `pull requests `__ through GitHub. This document contains guidelines for contributing code and filing issues. Contributing Code ================= This list below are guidelines to use when submitting pull requests. These are the same set of guidelines that the core contributors use when submitting changes, and we ask the same of all community contributions as well: * Chalice is released under the `Apache license `__. Any code you submit will be released under that license. * We maintain a high percentage of code coverage in our tests. As a general rule of thumb, code changes should not lower the overall code coverage percentage for the project. To help with this, we use `codecov `__, which will comment on changes in code coverage for every pull request. In practice, this means that every bug fix and feature addition should include unit tests, and optionally functional and integration tests. * All PRs must run cleanly through ``make prcheck``. This is described in more detail in the sections below. * All new features must include documentation before they can be merged. Feature Development =================== Any significant feature development for chalice should have a corresponding github issue for discussion. This gives several benefits: * Helps avoid wasted work by discussing the proposed API changes before significant dev work is started. * Gives a single place to capture discussion about the rationale for a feature. This applies to: * Any feature that proposes modifying the public API for chalice * Additions to the chalice config file * Any new CLI commands If you'd like to implement a significant feature for chalice, please file an `issue `__ to start the design discussion. All of the existing proposals are tagged with `proposals `__. Development Environment Setup ============================= First, create a virtual environment for chalice:: $ virtualenv venv $ source venv/bin/activate Keep in mind that chalice is designed to work with AWS Lambda. Make sure to create your virtual environment using Python 3.9 to 3.12, as these are versions currently supported by both AWS Lambda and chalice. Next, you'll need to install chalice. The easiest way to configure this is to use:: $ pip install -e ".[event-file-poller]" Run this command in the root directory of the chalice repo. Next, you have a few options. There are various requirements files depending on what you'd like to do. For example, if you'd like to work on chalice, either fixing bugs or adding new features, install ``requirements-dev.txt``:: $ pip install -r requirements-dev.txt Running Tests ------------- Chalice uses `pytest `__ to run tests. The tests are categorized into 3 categories: * ``unit`` - Fast tests that don't make any IO calls (including file system access). Object dependencies are usually mocked out. * ``functional`` - These tests will test multiple components together, typically through an interface that's close to what an end user would be using. For example, there are CLI functional tests that will invoke the same functions that would correspond to a ``chalice deploy`` command. In the functional tests, AWS calls are stubbed, but they'll go through the `botocore stubber `__. * ``integration`` - These tests require an AWS account and will actually create real AWS resources. The integration tests in chalice usually involve deploying a sample app and making assertions about the deployed app by making HTTP/AWS requests to external endpoints. During development, you'll generally run the unit tests, and less frequently you'll run the functional tests (the functional tests take an order of magnitude longer than the unit tests). To run the unit tests, you can run:: $ py.test tests/unit/ To run the functional tests you can run:: $ py.test tests/functional/ There's also a ``Makefile`` in the repo and you can run ``make test`` to run both the unit and functional tests. Code Analysis ------------- Chalice uses several python linters to help ensure high code quality. This also helps to cut down on the noise for pull request reviews because many issues are caught locally during development. To run all the linters, you can run ``make check``. This will run: * `flake8 `__, a tool for checking pep8 as well as common lint checks * `doc8 `__, a style checker for sphinx docs * `pydocstyle `__, a docstring checker * `pylint `__, a much more exhaustive linter that can catch additional issues compared to ``flake8``. Type Checking ------------- Chalice leverages the type hints introduced in python 3.5 from `pep 484 `__ and `pep 526 `__. `mypy `__ is used to check types. All chalice code must have type hints added or else the CI build will fail. To check types you can run ``make typecheck``. Chalice supports python2 as well as python3. Because of the requirement of supporting python2, function annotations are not allowed for specifying type hints, you must use type comments as outlined in pep 484. Keep in mind that ``mypy`` only runs in python3, so you'll need to either use python3 when developing features or have mypy globally installed. PRCheck ------- Before submitting a PR, ensure that ``make prcheck`` runs without any errors. This command will run the linters, the typecheckers and the unit and functional tests. ``make prcheck`` is also run as part of the travis CI build. Pull requests must pass ``make prcheck`` before they can be merged. ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: MANIFEST.in ================================================ include CONTRIBUTING.rst include CHANGELOG.md include LICENSE include NOTICE include README.rst recursive-include chalice *.json recursive-exclude * __pycache__ recursive-exclude * *.py[co] ================================================ FILE: Makefile ================================================ # Eventually I'll add: # py.test --cov chalice --cov-report term-missing --cov-fail-under 95 tests/ # which will fail if tests are under 95% TESTS=tests/unit tests/functional tests/integration check: ###### FLAKE8 ##### # No unused imports, no undefined vars, flake8 --ignore=E731,W503,W504 --exclude chalice/__init__.py,chalice/compat.py,chalice/vendored/botocore/regions.py --max-complexity 10 chalice/ flake8 --ignore=E731,W503,W504,F401 --max-complexity 10 chalice/compat.py flake8 tests/unit/ tests/functional/ tests/integration tests/aws # # Proper docstring conventions according to pep257 # # pydocstyle --add-ignore=D100,D101,D102,D103,D104,D105,D107,D204,D301 --match='(?!(test_|regions)).*\.py' chalice/ pylint: ###### PYLINT ###### pylint --rcfile .pylintrc chalice # Run our custom linter on test code. pylint --disable=I,E,W,R,C,F --enable C9999,C9998 tests/ test: py.test -v $(TESTS) typecheck: mypy --ignore-missing-imports --follow-imports=skip -p chalice --disallow-untyped-defs --strict-optional --warn-no-return coverage: py.test --cov chalice --cov-report term-missing $(TESTS) coverage-unit: py.test --cov chalice --cov-report term-missing tests/unit htmlcov: py.test --cov chalice --cov-report html $(TESTS) rm -rf /tmp/htmlcov && mv htmlcov /tmp/ open /tmp/htmlcov/index.html doccheck: ##### DOC8 ###### # Correct rst formatting for documentation # # TODO: Remove doc8 ## doc8 docs/source --ignore-path docs/source/topics/multifile.rst # # # Verify we have no broken external links # as well as no undefined internal references. $(MAKE) -C docs linkcheck # Verify we can build the docs. The # treat warnings as errors flag is enabled # so any sphinx-build warnings will fail the build. $(MAKE) -C docs html prcheck: check pylint coverage doccheck typecheck install-dev-deps: pip install -r requirements-dev.txt --upgrade --upgrade-strategy eager -e . ================================================ FILE: NOTICE ================================================ chalice Copyright 2015 James Saryerwinnie. All Rights Reserved. ================================================ FILE: README.rst ================================================ =========== AWS Chalice =========== .. image:: https://badges.gitter.im/awslabs/chalice.svg :target: https://gitter.im/awslabs/chalice?utm_source=badge&utm_medium=badge :alt: Gitter .. image:: https://readthedocs.org/projects/chalice/badge/?version=latest :target: http://aws.github.io/chalice/?badge=latest :alt: Documentation Status .. image:: https://aws.github.io/chalice/_images/chalice-logo-whitespace.png :target: https://aws.github.io/chalice/ :alt: Chalice Logo Chalice is a framework for writing serverless apps in python. It allows you to quickly create and deploy applications that use AWS Lambda. It provides: * A command line tool for creating, deploying, and managing your app * A decorator based API for integrating with Amazon API Gateway, Amazon S3, Amazon SNS, Amazon SQS, and other AWS services. * Automatic IAM policy generation You can create Rest APIs: .. code-block:: python from chalice import Chalice app = Chalice(app_name="helloworld") @app.route("/") def index(): return {"hello": "world"} Tasks that run on a periodic basis: .. code-block:: python from chalice import Chalice, Rate app = Chalice(app_name="helloworld") # Automatically runs every 5 minutes @app.schedule(Rate(5, unit=Rate.MINUTES)) def periodic_task(event): return {"hello": "world"} You can connect a lambda function to an S3 event: .. code-block:: python from chalice import Chalice app = Chalice(app_name="helloworld") # Whenever an object is uploaded to 'mybucket' # this lambda function will be invoked. @app.on_s3_event(bucket='mybucket') def handler(event): print("Object uploaded for bucket: %s, key: %s" % (event.bucket, event.key)) As well as an SQS queue: .. code-block:: python from chalice import Chalice app = Chalice(app_name="helloworld") # Invoke this lambda function whenever a message # is sent to the ``my-queue-name`` SQS queue. @app.on_sqs_message(queue='my-queue-name') def handler(event): for record in event: print("Message body: %s" % record.body) And several other AWS resources. Once you've written your code, you just run ``chalice deploy`` and Chalice takes care of deploying your app. :: $ chalice deploy ... https://endpoint/dev $ curl https://endpoint/api {"hello": "world"} Up and running in less than 30 seconds. Give this project a try and share your feedback with us here on Github. The documentation is available `here `__. Quickstart ========== .. quick-start-begin In this tutorial, you'll use the ``chalice`` command line utility to create and deploy a basic REST API. This quickstart uses Python 3.9, but AWS Chalice supports all versions of python supported by AWS Lambda, which includes Python 3.9 through python 3.13. To install Chalice, we'll first create and activate a virtual environment in python3.9:: $ python3 --version Python 3.9.22 $ python3 -m venv venv39 $ . venv39/bin/activate Next we'll install Chalice using ``pip``:: $ python3 -m pip install chalice You can verify you have chalice installed by running:: $ chalice --help Usage: chalice [OPTIONS] COMMAND [ARGS]... ... Credentials ----------- Before you can deploy an application, be sure you have credentials configured. If you have previously configured your machine to run boto3 (the AWS SDK for Python) or the AWS CLI then you can skip this section. If this is your first time configuring credentials for AWS you can follow these steps to quickly get started:: $ mkdir ~/.aws $ cat >> ~/.aws/config [default] aws_access_key_id=YOUR_ACCESS_KEY_HERE aws_secret_access_key=YOUR_SECRET_ACCESS_KEY region=YOUR_REGION (such as us-west-2, us-west-1, etc) If you want more information on all the supported methods for configuring credentials, see the `boto3 docs `__. Creating Your Project --------------------- The next thing we'll do is use the ``chalice`` command to create a new project:: $ chalice new-project helloworld This will create a ``helloworld`` directory. Cd into this directory. You'll see several files have been created for you:: $ cd helloworld $ ls -la drwxr-xr-x .chalice -rw-r--r-- app.py -rw-r--r-- requirements.txt You can ignore the ``.chalice`` directory for now, the two main files we'll focus on is ``app.py`` and ``requirements.txt``. Let's take a look at the ``app.py`` file: .. code-block:: python from chalice import Chalice app = Chalice(app_name='helloworld') @app.route('/') def index(): return {'hello': 'world'} The ``new-project`` command created a sample app that defines a single view, ``/``, that when called will return the JSON body ``{"hello": "world"}``. Deploying --------- Let's deploy this app. Make sure you're in the ``helloworld`` directory and run ``chalice deploy``:: $ chalice deploy Creating deployment package. Creating IAM role: helloworld-dev Creating lambda function: helloworld-dev Creating Rest API Resources deployed: - Lambda ARN: arn:aws:lambda:us-west-2:12345:function:helloworld-dev - Rest API URL: https://abcd.execute-api.us-west-2.amazonaws.com/api/ You now have an API up and running using API Gateway and Lambda:: $ curl https://qxea58oupc.execute-api.us-west-2.amazonaws.com/api/ {"hello": "world"} Try making a change to the returned dictionary from the ``index()`` function. You can then redeploy your changes by running ``chalice deploy``. .. quick-start-end Next Steps ---------- You've now created your first app using ``chalice``. You can make modifications to your ``app.py`` file and rerun ``chalice deploy`` to redeploy your changes. At this point, there are several next steps you can take. * `Tutorials `__ - Choose from among several guided tutorials that will give you step-by-step examples of various features of Chalice. * `Topics `__ - Deep dive into documentation on specific areas of Chalice. This contains more detailed documentation than the tutorials. * `API Reference `__ - Low level reference documentation on all the classes and methods that are part of the public API of Chalice. If you're done experimenting with Chalice and you'd like to cleanup, you can use the ``chalice delete`` command, and Chalice will delete all the resources it created when running the ``chalice deploy`` command. :: $ chalice delete Deleting Rest API: abcd4kwyl4 Deleting function aws:arn:lambda:region:123456789:helloworld-dev Deleting IAM Role helloworld-dev Feedback ======== We'd also love to hear from you. Please create any Github issues for additional features you'd like to see over at https://github.com/aws/chalice/issues. You can also chat with us on gitter: https://gitter.im/awslabs/chalice ================================================ FILE: chalice/__init__.py ================================================ from chalice.app import Chalice, Blueprint from chalice.app import ( ChaliceViewError, BadRequestError, UnauthorizedError, ForbiddenError, NotFoundError, ConflictError, TooManyRequestsError, Response, CORSConfig, CustomAuthorizer, CognitoUserPoolAuthorizer, IAMAuthorizer, UnprocessableEntityError, WebsocketDisconnectedError, AuthResponse, AuthRoute, Cron, Rate, __version__ as chalice_version, ConvertToMiddleware, ChaliceUnhandledError ) # We're reassigning version here to keep mypy happy. __version__ = chalice_version ================================================ FILE: chalice/analyzer.py ================================================ """Source code analyzer for chalice app. The main point of this module is to analyze your source code and track which AWS API calls you make. We can then use this information to create IAM policies automatically for you. How it Works ============ This is basically a simplified abstract interpreter. The type inference is greatly simplified because we're only interested in boto3 client types. In a nutshell: * Create an AST and symbol table from the source code. * Interpret the AST and track boto3 types. This is governed by a few simple rules. * Propagate inferred boto3 types as much as possible. Most of the basic stuff is handled, for example: * ``x = y`` if y is a boto3 type, so is x. * ``a :: (x -> y), where y is a boto3 type, then given ``b = a()``, b is of type y. * Map inferred types across function params and return types. At the end of the analysis, a final walk is performed to collect any node of type ``Boto3ClientMethodCallType``. This represents an API call being made. This also lets you be selective about which API calls you care about. For example, if you want only want to see which API calls happen in a particular function, only walk that particular ``FunctionDef`` node. """ import ast import symtable from typing import Dict, Set, Any, Optional, List, Union, cast # noqa APICallT = Dict[str, Set[str]] OptASTSet = Optional[Set[ast.AST]] ComprehensionNode = Union[ast.DictComp, ast.GeneratorExp, ast.ListComp] def get_client_calls(source_code): # type: (str) -> APICallT """Return all clients calls made in provided source code. :returns: A dict of service_name -> set([client calls]). Example: {"s3": set(["list_objects", "create_bucket"]), "dynamodb": set(["describe_table"])} """ parsed = parse_code(source_code) t = SymbolTableTypeInfer(parsed) binder = t.bind_types() collector = APICallCollector(binder) api_calls = collector.collect_api_calls(parsed.parsed_ast) return api_calls def get_client_calls_for_app(source_code): # type: (str) -> APICallT """Return client calls for a chalice app. This is similar to ``get_client_calls`` except it will automatically traverse into chalice views with the assumption that they will be called. """ parsed = parse_code(source_code) t = AppViewTypeInfer(parsed) binder = t.bind_types() collector = APICallCollector(binder) api_calls = collector.collect_api_calls(parsed.parsed_ast) return api_calls def parse_code(source_code, filename='app.py'): # type: (str, str) -> ParsedCode parsed = ast.parse(source_code, filename) table = symtable.symtable(source_code, filename, 'exec') return ParsedCode(parsed, ChainedSymbolTable(table, table)) class BaseType(object): def __repr__(self): # type: () -> str return "%s()" % self.__class__.__name__ def __eq__(self, other): # type: (Any) -> bool return isinstance(other, self.__class__) # The next 5 classes are used to track the # components needed to create a boto3 client. # While we really only care about boto3 clients we need # to track all the types it takes to get there: # # import boto3 <--- bind "boto3" as the boto3 module type # c = boto.client <--- bind "c" as the boto3 create client type # s3 = c('s3') <--- bind 's3' as the boto3 client type, subtype 's3'. # m = s3.list_objects <--- bind as API call 's3', 'list_objects' # r = m() <--- bind as API call invoked (what we care about). # # That way we can handle (in addition to the case above) things like: # import boto3; boto3.client('s3').list_objects() # import boto3; s3 = boto3.client('s3'); s3.list_objects() class Boto3ModuleType(BaseType): pass class Boto3CreateClientType(BaseType): pass class Boto3ClientType(BaseType): def __init__(self, service_name): # type: (str) -> None #: The name of the AWS service, e.g. 's3'. self.service_name = service_name def __eq__(self, other): # type: (Any) -> bool # NOTE: We can't use self.__class__ because of a mypy bug: # https://github.com/python/mypy/issues/3061 # We can change this back once that bug is fixed. if not isinstance(other, Boto3ClientType): return False return self.service_name == other.service_name def __repr__(self): # type: () -> str return "%s(%s)" % (self.__class__.__name__, self.service_name) class Boto3ClientMethodType(BaseType): def __init__(self, service_name, method_name): # type: (str, str) -> None self.service_name = service_name self.method_name = method_name def __eq__(self, other): # type: (Any) -> bool if self.__class__ != other.__class__: return False return ( self.service_name == other.service_name and self.method_name == other.method_name) def __repr__(self): # type: () -> str return "%s(%s, %s)" % ( self.__class__.__name__, self.service_name, self.method_name ) class Boto3ClientMethodCallType(Boto3ClientMethodType): pass class TypedSymbol(symtable.Symbol): inferred_type = None # type: Any ast_node = None # type: ast.AST class FunctionType(BaseType): def __init__(self, return_type): # type: (Any) -> None self.return_type = return_type def __eq__(self, other): # type: (Any) -> bool if self.__class__ != other.__class__: return False return self.return_type == other.return_type def __repr__(self): # type: () -> str return "%s(%s)" % ( self.__class__.__name__, self.return_type, ) class StringLiteral(object): def __init__(self, value): # type: (str) -> None self.value = value class ParsedCode(object): def __init__(self, parsed_ast, symbol_table): # type: (ast.AST, ChainedSymbolTable) -> None self.parsed_ast = parsed_ast self.symbol_table = symbol_table class APICallCollector(ast.NodeVisitor): """Traverse a given AST and look for any inferred API call types. This visitor assumes you've ran type inference on the AST. It will search through the AST and collect any API calls. """ def __init__(self, binder): # type: (TypeBinder) -> None self.api_calls = {} # type: APICallT self._binder = binder def collect_api_calls(self, node): # type: (ast.AST) -> APICallT self.visit(node) return self.api_calls def visit(self, node): # type: (ast.AST) -> None inferred_type = self._binder.get_type_for_node(node) if isinstance(inferred_type, Boto3ClientMethodCallType): self.api_calls.setdefault(inferred_type.service_name, set()).add( inferred_type.method_name) ast.NodeVisitor.visit(self, node) class ChainedSymbolTable(object): def __init__(self, local_table, global_table): # type: (symtable.SymbolTable, symtable.SymbolTable) -> None # If you're in the module scope, then pass in # the same symbol table for local and global. self._local_table = local_table self._global_table = global_table def new_sub_table(self, local_table): # type: (symtable.SymbolTable) -> ChainedSymbolTable # Create a new symbol table using this instances # local table as the new global table and the passed # in local table as the new local table. return self.__class__(local_table, self._local_table) def get_inferred_type(self, name): # type: (str) -> Any # Given a symbol name, check whether a type # has been inferred. # The stdlib symtable will already fall back to # global scope if necessary. symbol = self._local_table.lookup(name) if symbol.is_global(): try: global_symbol = self._global_table.lookup(name) except KeyError: # It's not an error if a symbol.is_global() # but is not in our "_global_table", because # we're not considering the builtin scope. # In this case we just say that there is no # type we've inferred. return None return getattr(global_symbol, 'inferred_type', None) return getattr(symbol, 'inferred_type', None) def set_inferred_type(self, name, inferred_type): # type: (str, Any) -> None symbol = cast(TypedSymbol, self._local_table.lookup(name)) symbol.inferred_type = inferred_type def lookup_sub_namespace(self, name, lineno=None): # type: (str, Optional[int]) -> ChainedSymbolTable for child in self._local_table.get_children(): if child.get_name() == name: if lineno is not None: if child.get_lineno() == lineno: return self.__class__(child, self._local_table) else: return self.__class__(child, self._local_table) for child in self._global_table.get_children(): if child.get_name() == name: return self.__class__(child, self._global_table) raise ValueError("Unknown symbol name: %s" % name) def get_sub_namespaces(self): # type: () -> List[symtable.SymbolTable] return self._local_table.get_children() def get_name(self): # type: () -> str return self._local_table.get_name() def get_symbols(self): # type: () -> List[symtable.Symbol] return self._local_table.get_symbols() def register_ast_node_for_symbol(self, name, node): # type: (str, ast.AST) -> None symbol = cast(TypedSymbol, self._local_table.lookup(name)) symbol.ast_node = node def lookup_ast_node_for_symbol(self, name): # type: (str) -> ast.AST symbol = self._local_table.lookup(name) if symbol.is_global(): symbol = self._global_table.lookup(name) try: return cast(TypedSymbol, symbol).ast_node except AttributeError: raise ValueError( "No AST node registered for symbol: %s" % name) def has_ast_node_for_symbol(self, name): # type: (str) -> bool try: self.lookup_ast_node_for_symbol(name) return True except (ValueError, KeyError): return False class TypeBinder(object): def __init__(self): # type: () -> None self._node_to_type = {} # type: Dict[ast.AST, Any] def get_type_for_node(self, node): # type: (Any) -> Any return self._node_to_type.get(node) def set_type_for_node(self, node, inferred_type): # type: (Any, Any) -> None self._node_to_type[node] = inferred_type class SymbolTableTypeInfer(ast.NodeVisitor): _SDK_PACKAGE = 'boto3' _CREATE_CLIENT = 'client' def __init__(self, parsed_code, binder=None, visited=None): # type: (ParsedCode, Optional[TypeBinder], OptASTSet) -> None self._symbol_table = parsed_code.symbol_table self._current_ast_namespace = parsed_code.parsed_ast self._node_inference = {} # type: Dict[ast.AST, Any] if binder is None: binder = TypeBinder() if visited is None: visited = set() self._binder = binder self._visited = visited def bind_types(self): # type: () -> TypeBinder self.visit(self._current_ast_namespace) return self._binder def known_types(self, scope_name=None): # type: (Optional[str]) -> Dict[str, Any] table = None if scope_name is None: table = self._symbol_table else: table = self._symbol_table.lookup_sub_namespace(scope_name) return { s.get_name(): cast(TypedSymbol, s).inferred_type for s in table.get_symbols() if hasattr(s, 'inferred_type') and cast(TypedSymbol, s).inferred_type is not None and s.is_local() } def _set_inferred_type_for_name(self, name, inferred_type): # type: (str, Any) -> None self._symbol_table.set_inferred_type(name, inferred_type) def _set_inferred_type_for_node(self, node, inferred_type): # type: (Any, Any) -> None self._binder.set_type_for_node(node, inferred_type) def _get_inferred_type_for_node(self, node): # type: (Any) -> Any return self._binder.get_type_for_node(node) def _new_inference_scope(self, parsed_code, binder, visited): # type: (ParsedCode, TypeBinder, Set[ast.AST]) -> SymbolTableTypeInfer instance = self.__class__(parsed_code, binder, visited) return instance def visit_Import(self, node): # type: (ast.Import) -> None for child in node.names: if isinstance(child, ast.alias): import_name = child.name if import_name == self._SDK_PACKAGE: self._set_inferred_type_for_name( import_name, Boto3ModuleType()) self.generic_visit(node) def visit_Name(self, node): # type: (ast.Name) -> None self._set_inferred_type_for_node( node, self._symbol_table.get_inferred_type(node.id) ) self.generic_visit(node) def visit_Assign(self, node): # type: (ast.Assign) -> None # The LHS gets the inferred type of the RHS. # We do this post-traversal to let the type inference # run on the children first. self.generic_visit(node) rhs_inferred_type = self._get_inferred_type_for_node(node.value) if rhs_inferred_type is None: # Special casing assignment to a string literal. if isinstance(node.value, ast.Str): rhs_inferred_type = StringLiteral(node.value.s) self._set_inferred_type_for_node(node.value, rhs_inferred_type) for t in node.targets: if isinstance(t, ast.Name): self._symbol_table.set_inferred_type(t.id, rhs_inferred_type) self._set_inferred_type_for_node(node, rhs_inferred_type) def visit_Attribute(self, node): # type: (ast.Attribute) -> None self.generic_visit(node) lhs_inferred_type = self._get_inferred_type_for_node(node.value) if lhs_inferred_type is None: return elif lhs_inferred_type == Boto3ModuleType(): # Check for attributes such as boto3.client. if node.attr == self._CREATE_CLIENT: # This is a "boto3.client" attribute. self._set_inferred_type_for_node(node, Boto3CreateClientType()) elif isinstance(lhs_inferred_type, Boto3ClientType): self._set_inferred_type_for_node( node, Boto3ClientMethodType( lhs_inferred_type.service_name, node.attr ) ) def visit_Call(self, node): # type: (ast.Call) -> None self.generic_visit(node) # func -> Node that's being called # args -> Arguments being passed. inferred_func_type = self._get_inferred_type_for_node(node.func) if inferred_func_type == Boto3CreateClientType(): # e_0 : B3CCT -> B3CT[S] # e_1 : S str which is a service name # e_0(e_1) : B3CT[e_1] if len(node.args) >= 1: service_arg = node.args[0] if isinstance(service_arg, ast.Str): self._set_inferred_type_for_node( node, Boto3ClientType(service_arg.s)) elif isinstance(self._get_inferred_type_for_node(service_arg), StringLiteral): sub_type = self._get_inferred_type_for_node(service_arg) inferred_type = Boto3ClientType(sub_type.value) self._set_inferred_type_for_node(node, inferred_type) elif isinstance(inferred_func_type, Boto3ClientMethodType): self._set_inferred_type_for_node( node, Boto3ClientMethodCallType( inferred_func_type.service_name, inferred_func_type.method_name ) ) elif isinstance(inferred_func_type, FunctionType): self._set_inferred_type_for_node( node, inferred_func_type.return_type) elif isinstance(node.func, ast.Name) and \ self._symbol_table.has_ast_node_for_symbol(node.func.id): if node not in self._visited: self._visited.add(node) self._infer_function_call(node) def visit_Lambda(self, node): # type: (ast.Lambda) -> None # Lambda is going to be a bit tricky because # there's a new child namespace (via .get_children()), # but it's not something that will show up in the # current symbol table via .lookup(). # For now, we're going to ignore lambda expressions. pass def _infer_function_call(self, node): # type: (Any) -> None # Here we're calling a function we haven't analyzed # yet. We're first going to analyze the function. # This will set the inferred_type on the FunctionDef # node. # If we get a FunctionType as the inferred type of the # function, then we know that the inferred type for # calling the function is the .return_type type. function_name = node.func.id sub_table = self._symbol_table.lookup_sub_namespace( function_name, node.lineno) ast_node = self._symbol_table.lookup_ast_node_for_symbol( function_name) self._map_function_params(sub_table, node, ast_node) child_infer = self._new_inference_scope( ParsedCode(ast_node, sub_table), self._binder, self._visited) child_infer.bind_types() inferred_func_type = self._get_inferred_type_for_node(ast_node) self._symbol_table.set_inferred_type(function_name, inferred_func_type) # And finally the result of this Call() node will be # the return type from the function we just analyzed. if isinstance(inferred_func_type, FunctionType): self._set_inferred_type_for_node( node, inferred_func_type.return_type) def _map_function_params(self, sub_table, node, def_node): # type: (ChainedSymbolTable, Any, Any) -> None # TODO: Handle the full calling syntax, kwargs, stargs, etc. # Right now we just handle positional args. defined_args = def_node.args for arg, defined in zip(node.args, defined_args.args): inferred_type = self._get_inferred_type_for_node(arg) if inferred_type is not None: name = self._get_name(defined) sub_table.set_inferred_type(name, inferred_type) def _get_name(self, node): # type: (Any) -> str try: return getattr(node, 'id') except AttributeError: return getattr(node, 'arg') def visit_FunctionDef(self, node): # type: (ast.FunctionDef) -> None if node.name == self._symbol_table.get_name(): # Not using generic_visit() because we don't want to # visit the decorator_list attr. for child in node.body: self.visit(child) else: self._symbol_table.register_ast_node_for_symbol(node.name, node) def visit_AsyncFunctionDef(self, node): # type: (ast.AsyncFunctionDef) -> None # this type is actually wrong but we can't use the actual type as it's # not available in python 2 converted = cast(ast.FunctionDef, node) self.visit_FunctionDef(converted) def visit_ClassDef(self, node): # type: (ast.ClassDef) -> None # Not implemented yet. We want to ensure we don't # traverse into the class body for now. return def visit_DictComp(self, node): # type: (ast.DictComp) -> None self._handle_comprehension(node, 'dictcomp') def visit_Return(self, node): # type: (Any) -> None self.generic_visit(node) inferred_type = self._get_inferred_type_for_node(node.value) if inferred_type is not None: self._set_inferred_type_for_node(node, inferred_type) # We're making a pretty big assumption there's one return # type per function. Will likely need to come back to this. inferred_func_type = FunctionType(inferred_type) self._set_inferred_type_for_node(self._current_ast_namespace, inferred_func_type) def visit_ListComp(self, node): # type: (ast.ListComp) -> None # 'listcomp' is the string literal used by python # to creating the SymbolTable for the corresponding # list comp function. self._handle_comprehension(node, 'listcomp') def visit_GeneratorExp(self, node): # type: (ast.GeneratorExp) -> None # Generator expressions are an interesting case. # They create a new sub scope, but they're not # explicitly named. Python just creates a table # with the name "genexpr". self._handle_comprehension(node, 'genexpr') def _visit_first_comprehension_generator(self, node): # type: (ComprehensionNode) -> None if node.generators: # first generator's iterator is visited in the current scope first_generator = node.generators[0] self.visit(first_generator.iter) def _collect_comprehension_children(self, node): # type: (ComprehensionNode) -> List[ast.expr] if isinstance(node, ast.DictComp): # dict comprehensions have two values to be checked child_nodes = [node.key, node.value] else: child_nodes = [node.elt] if node.generators: first_generator = node.generators[0] child_nodes.append(first_generator.target) for if_expr in first_generator.ifs: child_nodes.append(if_expr) for generator in node.generators[1:]: # rest need to be visited in the child scope child_nodes.append(generator.iter) child_nodes.append(generator.target) for if_expr in generator.ifs: child_nodes.append(if_expr) return child_nodes def _visit_comprehension_children(self, node, comprehension_type): # type: (ComprehensionNode, str) -> None child_nodes = self._collect_comprehension_children(node) child_scope = self._get_matching_sub_namespace(comprehension_type, node.lineno) if child_scope is None: # In Python 2 there's no child scope for list comp # Or we failed to locate the child scope, this happens in Python 2 # when there are multiple comprehensions of the same type in the # same scope. The line number trick doesn't work as Python 2 always # passes line number 0, make a best effort for child_node in child_nodes: try: self.visit(child_node) except KeyError: pass return for child_node in child_nodes: # visit sub expressions in the child scope child_table = self._symbol_table.new_sub_table(child_scope) child_infer = self._new_inference_scope( ParsedCode(child_node, child_table), self._binder, self._visited) child_infer.bind_types() def _handle_comprehension(self, node, comprehension_type): # type: (ComprehensionNode, str) -> None self._visit_first_comprehension_generator(node) self._visit_comprehension_children(node, comprehension_type) def _get_matching_sub_namespace(self, name, lineno): # type: (str, int) -> Optional[symtable.SymbolTable] namespaces = [t for t in self._symbol_table.get_sub_namespaces() if t.get_name() == name] if len(namespaces) == 1: # if there's only one match for the name, return it return namespaces[0] for namespace in namespaces: # otherwise disambiguate by using the line number if namespace.get_lineno() == lineno: return namespace return None def visit(self, node): # type: (Any) -> None return ast.NodeVisitor.visit(self, node) class AppViewTypeInfer(ast.NodeVisitor): _CHALICE_DECORATORS = [ 'route', 'authorizer', 'lambda_function', 'schedule', 'on_s3_event', 'on_sns_message', 'on_sqs_message', 'on_ws_connect', 'on_ws_message', 'on_ws_disconnect', ] def __init__(self, parsed_code): # type: (ParsedCode) -> None self._binder = TypeBinder() self._visited = set() # type: Set[ast.AST] self._parsed_code = parsed_code self._type_infer = SymbolTableTypeInfer( self._parsed_code, self._binder, self._visited) def bind_types(self): # type: () -> TypeBinder self._type_infer.bind_types() self.visit(self._parsed_code.parsed_ast) return self._binder def visit_FunctionDef(self, node): # type: (ast.FunctionDef) -> None if self._is_chalice_view(node): sub_table = self._parsed_code.symbol_table.lookup_sub_namespace( node.name, node.lineno) child_infer = SymbolTableTypeInfer( ParsedCode(node, sub_table), self._binder, self._visited) child_infer.bind_types() def _is_chalice_view(self, node): # type: (ast.FunctionDef) -> bool # We can certainly improve on this, but this check is more # of a heuristic for the time being. The ideal way to do this # is to infer the Chalice type and ensure the function is # decorated with the Chalice type's route() method. decorator_list = node.decorator_list if not decorator_list: return False for decorator in decorator_list: if isinstance(decorator, ast.Call) and \ isinstance(decorator.func, ast.Attribute): if decorator.func.attr in self._CHALICE_DECORATORS: return True return False ================================================ FILE: chalice/api/__init__.py ================================================ """Control plane APIs for programatically building/deploying Chalice apps. The eventual goal is to expose this as a public API that other tools can use in their own integrations with Chalice, but this will need time for the APIs to mature so for the time being this is an internal-only API. """ import os from typing import Optional, Dict, Any from chalice.cli.factory import CLIFactory def package_app(project_dir: str, output_dir: str, stage: str, chalice_config: Optional[Dict[str, Any]] = None, package_format: str = 'cloudformation', template_format: str = 'json') -> None: factory = CLIFactory(project_dir, environ=os.environ) if chalice_config is None: chalice_config = {} config = factory.create_config_obj( stage, user_provided_params=chalice_config) options = factory.create_package_options() packager = factory.create_app_packager(config, options, package_format=package_format, template_format=template_format) packager.package_app(config, output_dir, stage) ================================================ FILE: chalice/app.py ================================================ """Chalice app and routing code.""" # pylint: disable=too-many-lines,ungrouped-imports import re import sys import os import logging import json import traceback import decimal import base64 import copy import functools import datetime from collections import defaultdict # Implementation note: This file is intended to be a standalone file # that gets copied into the lambda deployment package. It has no dependencies # on other parts of chalice, so it can stay small and lightweight, with minimal # startup overhead. from urllib.parse import unquote_plus from collections.abc import Mapping from collections.abc import MutableMapping __version__: str = '1.32.0' from typing import List, Dict, Any, Optional, Sequence, Union, Callable, Set, \ Iterator, TYPE_CHECKING, Tuple if TYPE_CHECKING: from chalice.local import LambdaContext _PARAMS = re.compile(r'{\w+}') MiddlewareFuncType = Callable[[Any, Callable[[Any], Any]], Any] UserHandlerFuncType = Callable[..., Any] HeadersType = Dict[str, Union[str, List[str]]] # In python 3 string and bytes are different so we explicitly check # for both. _ANY_STRING = (str, bytes) def handle_extra_types( obj: Union[decimal.Decimal, 'MultiDict'] ) -> Union[float, Dict]: # Lambda will automatically serialize decimals so we need # to support that as well. if isinstance(obj, decimal.Decimal): return float(obj) # This is added for backwards compatibility. # It will keep only the last value for every key as it used to. if isinstance(obj, MultiDict): return dict(obj) raise TypeError('Object of type %s is not JSON serializable' % obj.__class__.__name__) def error_response( message: str, error_code: str, http_status_code: int, headers: Optional[HeadersType] = None ) -> 'Response': body = {'Code': error_code, 'Message': message} response = Response(body=body, status_code=http_status_code, headers=headers) return response def _matches_content_type(content_type: str, valid_content_types: List[str]) -> bool: # If '*/*' is in the Accept header or the valid types, # then all content_types match. Otherwise see of there are any common types content_type = content_type.lower() valid_content_types = [x.lower() for x in valid_content_types] return '*/*' in content_type or \ '*/*' in valid_content_types or \ _content_type_header_contains(content_type, valid_content_types) def _content_type_header_contains( content_type_header: str, valid_content_types: List[str] ) -> bool: content_type_header_parts = [ p.strip() for p in re.split('[,;]', content_type_header) ] valid_parts = set(valid_content_types).intersection( content_type_header_parts ) return len(valid_parts) > 0 class ChaliceError(Exception): pass class WebsocketDisconnectedError(ChaliceError): def __init__(self, connection_id: str): self.connection_id: str = connection_id class ChaliceViewError(ChaliceError): STATUS_CODE: int = 500 class ChaliceUnhandledError(ChaliceError): """This error is not caught from a Chalice view function. This exception is allowed to propagate from a view function so that middleware handlers can process the exception. """ class BadRequestError(ChaliceViewError): STATUS_CODE: int = 400 class UnauthorizedError(ChaliceViewError): STATUS_CODE: int = 401 class ForbiddenError(ChaliceViewError): STATUS_CODE: int = 403 class NotFoundError(ChaliceViewError): STATUS_CODE: int = 404 class MethodNotAllowedError(ChaliceViewError): STATUS_CODE: int = 405 class RequestTimeoutError(ChaliceViewError): STATUS_CODE: int = 408 class ConflictError(ChaliceViewError): STATUS_CODE: int = 409 class UnprocessableEntityError(ChaliceViewError): STATUS_CODE: int = 422 class TooManyRequestsError(ChaliceViewError): STATUS_CODE: int = 429 ALL_ERRORS = [ ChaliceViewError, BadRequestError, NotFoundError, UnauthorizedError, ForbiddenError, MethodNotAllowedError, RequestTimeoutError, ConflictError, UnprocessableEntityError, TooManyRequestsError, ] class MultiDict(MutableMapping): # pylint: disable=too-many-ancestors """A mapping of key to list of values. Accessing it in the usual way will return the last value in the list. Calling getlist will return a list of all the values associated with the same key. """ def __init__(self, mapping: Optional[Dict]): if mapping is None: mapping = {} self._dict = mapping def __getitem__(self, k: Any) -> Any: try: return self._dict[k][-1] except IndexError: raise KeyError(k) def __setitem__(self, k: Any, v: Any) -> None: self._dict[k] = [v] def __delitem__(self, k: Any) -> None: del self._dict[k] def getlist(self, k: Any) -> List: return list(self._dict[k]) def __len__(self) -> int: return len(self._dict) def __iter__(self) -> Iterator: return iter(self._dict) def __repr__(self) -> str: return 'MultiDict(%s)' % self._dict def __str__(self) -> str: return repr(self) class CaseInsensitiveMapping(Mapping): """Case insensitive and read-only mapping.""" def __init__(self, mapping: Union[Dict[str, Any], MultiDict]) -> None: mapping = mapping or {} self._dict = {k.lower(): v for k, v in mapping.items()} def __getitem__(self, key: str) -> Any: return self._dict[key.lower()] def __iter__(self) -> Iterator: return iter(self._dict) def __len__(self) -> int: return len(self._dict) def __repr__(self) -> str: return 'CaseInsensitiveMapping(%s)' % repr(self._dict) class Authorizer(object): name: str = '' scopes: List[str] = [] def to_swagger(self) -> Dict[str, Any]: raise NotImplementedError("to_swagger") def with_scopes(self, scopes: List[str]) -> 'Authorizer': raise NotImplementedError("with_scopes") class IAMAuthorizer(Authorizer): _AUTH_TYPE: str = 'aws_iam' def __init__(self) -> None: self.name: str = 'sigv4' self.scopes: List[str] = [] def to_swagger(self) -> Dict[str, str]: return { 'in': 'header', 'type': 'apiKey', 'name': 'Authorization', 'x-amazon-apigateway-authtype': 'awsSigv4', } def with_scopes(self, scopes: List[str]) -> 'Authorizer': raise NotImplementedError("with_scopes") class CognitoUserPoolAuthorizer(Authorizer): _AUTH_TYPE: str = 'cognito_user_pools' def __init__(self, name: str, provider_arns: List[str], header: Optional[str] = 'Authorization', scopes: Optional[List] = None) -> None: self.name = name self._header = header if not isinstance(provider_arns, list): # This class is used directly by users so we're # adding some validation to help them troubleshoot # potential issues. raise TypeError( "provider_arns should be a list of ARNs, received: %s" % provider_arns) self._provider_arns = provider_arns self.scopes = scopes or [] def to_swagger(self) -> Dict[str, Any]: return { 'in': 'header', 'type': 'apiKey', 'name': self._header, 'x-amazon-apigateway-authtype': self._AUTH_TYPE, 'x-amazon-apigateway-authorizer': { 'type': self._AUTH_TYPE, 'providerARNs': self._provider_arns, } } def with_scopes(self, scopes: List[str]) -> 'Authorizer': authorizer_with_scopes = copy.deepcopy(self) authorizer_with_scopes.scopes = scopes return authorizer_with_scopes class CustomAuthorizer(Authorizer): _AUTH_TYPE = 'custom' def __init__(self, name: str, authorizer_uri: str, ttl_seconds: int = 300, header: str = 'Authorization', invoke_role_arn: Optional[str] = None, scopes: Optional[List[str]] = None) -> None: self.name = name self._header = header self._authorizer_uri = authorizer_uri self._ttl_seconds = ttl_seconds self._invoke_role_arn = invoke_role_arn self.scopes = scopes or [] def to_swagger(self) -> Dict[str, Any]: swagger: Dict[str, Any] = { 'in': 'header', 'type': 'apiKey', 'name': self._header, 'x-amazon-apigateway-authtype': self._AUTH_TYPE, 'x-amazon-apigateway-authorizer': { 'type': 'token', 'authorizerUri': self._authorizer_uri, 'authorizerResultTtlInSeconds': self._ttl_seconds, } } if self._invoke_role_arn is not None: swagger['x-amazon-apigateway-authorizer'][ 'authorizerCredentials'] = self._invoke_role_arn return swagger def with_scopes(self, scopes: List[str]) -> 'Authorizer': authorizer_with_scopes = copy.deepcopy(self) authorizer_with_scopes.scopes = scopes return authorizer_with_scopes class CORSConfig(object): """A cors configuration to attach to a route.""" _REQUIRED_HEADERS: List[str] = ['Content-Type', 'X-Amz-Date', 'Authorization', 'X-Api-Key', 'X-Amz-Security-Token'] def __init__(self, allow_origin: str = '*', allow_headers: Optional[Sequence[str]] = None, expose_headers: Optional[Sequence[str]] = None, max_age: Optional[int] = None, allow_credentials: Optional[bool] = None): self.allow_origin = allow_origin if allow_headers is None: self._allow_headers = set(self._REQUIRED_HEADERS) else: self._allow_headers = set( list(allow_headers) + self._REQUIRED_HEADERS ) if expose_headers is None: expose_headers = [] self._expose_headers = expose_headers self._max_age = max_age self._allow_credentials = allow_credentials @property def allow_headers(self) -> str: return ','.join(sorted(self._allow_headers)) def get_access_control_headers(self) -> Dict[str, str]: headers = { 'Access-Control-Allow-Origin': self.allow_origin, 'Access-Control-Allow-Headers': self.allow_headers } if self._expose_headers: headers.update({ 'Access-Control-Expose-Headers': ','.join(self._expose_headers) }) if self._max_age is not None: headers.update({ 'Access-Control-Max-Age': str(self._max_age) }) if self._allow_credentials is True: headers.update({ 'Access-Control-Allow-Credentials': 'true' }) return headers def __eq__(self, other: object) -> bool: if isinstance(other, self.__class__): return self.get_access_control_headers() == \ other.get_access_control_headers() return False class Request(object): """The current request from API gateway.""" _NON_SERIALIZED_ATTRS: List[str] = ['lambda_context'] body: Any base64_body: str def __init__(self, event_dict: Dict[str, Any], lambda_context: Optional[Any] = None) -> None: query_params = event_dict['multiValueQueryStringParameters'] self.query_params: Optional[MultiDict] = None \ if query_params is None else MultiDict(query_params) self.headers: CaseInsensitiveMapping = \ CaseInsensitiveMapping(event_dict['headers']) self.uri_params: Optional[Dict[str, str]] \ = event_dict['pathParameters'] self.method: str = event_dict['requestContext']['httpMethod'] self._is_base64_encoded = event_dict.get('isBase64Encoded', False) self._body: Any = event_dict['body'] #: The parsed JSON from the body. This value should #: only be set if the Content-Type header is application/json, #: which is the default content type value in chalice. self._json_body: Optional[Any] = None self._raw_body = b'' self.context: Dict[str, Any] = event_dict['requestContext'] self.stage_vars: Optional[Dict[str, str]] \ = event_dict['stageVariables'] self.path: str = event_dict['requestContext']['resourcePath'] self.lambda_context = lambda_context self._event_dict = event_dict def _base64decode(self, encoded: Union[bytes, str]) -> bytes: if not isinstance(encoded, bytes): encoded = encoded.encode('ascii') output = base64.b64decode(encoded) return output @property def raw_body(self) -> Union[str, bytes]: if not self._raw_body and self._body is not None: if self._is_base64_encoded: self._raw_body = self._base64decode(self._body) elif not isinstance(self._body, bytes): self._raw_body = self._body.encode('utf-8') else: self._raw_body = self._body return self._raw_body @property def json_body(self) -> Any: if self.headers.get('content-type', '').startswith('application/json'): if self._json_body is None: try: self._json_body = json.loads(self.raw_body) except ValueError: raise BadRequestError('Error Parsing JSON') return self._json_body def to_dict(self) -> Dict[Any, Any]: # Don't copy internal attributes. copied = { k: v for k, v in self.__dict__.items() if not k.startswith('_') and k not in self._NON_SERIALIZED_ATTRS } # We want the output of `to_dict()` to be # JSON serializable, so we need to remove the CaseInsensitive dict. copied['headers'] = dict(copied['headers']) if copied['query_params'] is not None: copied['query_params'] = dict(copied['query_params']) return copied def to_original_event(self) -> Dict[str, Any]: # To bring consistency with the BaseLambdaEvents, every # input event should have access to the original event # dictionary as an escape hatch to the underlying data # in case something gets added and we haven't mapped it yet. # We unfortunately already have a `to_dict()` method which is # what other events use so we have to use a different method name. return self._event_dict class Response(object): def __init__( self, body: Any, headers: Optional[HeadersType] = None, status_code: int = 200 ): self.body: Any = body if headers is None: headers = {} self.headers: HeadersType = headers self.status_code = status_code def to_dict( self, binary_types: Optional[List[str]] = None ) -> Dict[str, Any]: body = self.body if not isinstance(body, _ANY_STRING): body = json.dumps(body, separators=(',', ':'), default=handle_extra_types) single_headers, multi_headers = self._sort_headers(self.headers) response = { 'headers': single_headers, 'multiValueHeaders': multi_headers, 'statusCode': self.status_code, 'body': body } if binary_types is not None: self._b64encode_body_if_needed(response, binary_types) return response def _sort_headers( self, all_headers: HeadersType ) -> Tuple[Dict[str, Any], Dict[str, List]]: multi_headers: Dict[str, List] = {} single_headers: Dict[str, Any] = {} for name, value in all_headers.items(): if isinstance(value, list): multi_headers[name] = value else: single_headers[name] = value return single_headers, multi_headers def _b64encode_body_if_needed( self, response_dict: Dict[str, Any], binary_types: List[str] ) -> None: response_headers = CaseInsensitiveMapping(response_dict['headers']) content_type = response_headers.get('content-type', '') body = response_dict['body'] if _matches_content_type(content_type, binary_types): if _matches_content_type(content_type, ['application/json']) or \ not content_type: # There's a special case when a user configures # ``application/json`` as a binary type. The default # json serialization results in a string type, but for binary # content types we need a type bytes(). So we need to special # case this scenario and encode the JSON body to bytes(). # # If a user does not provide a content type header, which can # happen if they return a python type instead of a ``Response`` # type, then we assume the content is application/json. body = body if isinstance(body, bytes) \ else body.encode('utf-8') body = self._base64encode(body) response_dict['isBase64Encoded'] = True response_dict['body'] = body def _base64encode(self, data: bytes) -> str: if not isinstance(data, bytes): raise ValueError('Expected bytes type for body with binary ' 'Content-Type. Got %s type body instead.' % type(data)) data = base64.b64encode(data) return data.decode('ascii') class RouteEntry(object): def __init__(self, view_function: Callable[..., Any], view_name: str, path: str, method: str, api_key_required: Optional[bool] = None, content_types: Optional[List[str]] = None, cors: Optional[Union[bool, CORSConfig]] = False, authorizer: Optional[Authorizer] = None): self.view_function: Callable[..., Any] = view_function self.view_name: str = view_name self.uri_pattern: str = path self.method: str = method self.api_key_required: Optional[bool] = api_key_required #: A list of names to extract from path: #: e.g, '/foo/{bar}/{baz}/qux -> ['bar', 'baz'] self.view_args: List[str] = self._parse_view_args() self.content_types: List[str] = content_types or [] # cors is passed as either a boolean or a CORSConfig object. If it is a # boolean it needs to be replaced with a real CORSConfig object to # pass the typechecker. None in this context will not inject any cors # headers, otherwise the CORSConfig object will determine which # headers are injected. if cors is True: cors = CORSConfig() elif cors is False: cors = None self.cors: CORSConfig = cors # type: ignore self.authorizer: Optional[Authorizer] = authorizer def _parse_view_args(self) -> List[str]: if '{' not in self.uri_pattern: return [] # The [1:-1] slice is to remove the braces # e.g {foobar} -> foobar results = [r[1:-1] for r in _PARAMS.findall(self.uri_pattern)] return results def __eq__(self, other: object) -> bool: return self.__dict__ == other.__dict__ class APIGateway(object): _DEFAULT_BINARY_TYPES = [ 'application/octet-stream', 'application/x-tar', 'application/zip', 'audio/basic', 'audio/ogg', 'audio/mp4', 'audio/mpeg', 'audio/wav', 'audio/webm', 'image/png', 'image/jpg', 'image/jpeg', 'image/gif', 'video/ogg', 'video/mpeg', 'video/webm', ] def __init__(self) -> None: self.binary_types: List[str] = self.default_binary_types self.cors: Union[bool, CORSConfig] = False @property def default_binary_types(self) -> List[str]: return list(self._DEFAULT_BINARY_TYPES) class WebsocketAPI(object): _WEBSOCKET_ENDPOINT_TEMPLATE = 'https://{domain_name}/{stage}' _REGION_ENV_VARS = ['AWS_REGION', 'AWS_DEFAULT_REGION'] def __init__(self, env: Optional[MutableMapping] = None) -> None: self.session: Optional[Any] = None self._endpoint: Optional[str] = None self._client = None if env is None: self._env: MutableMapping = os.environ else: self._env = env def configure(self, domain_name: str, stage: str) -> None: if self._endpoint is not None: return self._endpoint = self._WEBSOCKET_ENDPOINT_TEMPLATE.format( domain_name=domain_name, stage=stage, ) def configure_from_api_id(self, api_id: str, stage: str) -> None: if self._endpoint is not None: return region_name = self._get_region() if region_name.startswith("cn-"): domain_name_template = ( '{api_id}.execute-api.{region}.amazonaws.com.cn' ) else: domain_name_template = ( '{api_id}.execute-api.{region}.amazonaws.com' ) domain_name = domain_name_template.format( api_id=api_id, region=region_name) self.configure(domain_name, stage) def _get_region(self) -> str: # Attempt to get the region so we can configure the # apigatewaymanagementapi client. We'll first try # retrieving this value from env vars because these should # always be set in the Lambda runtime environment. for varname in self._REGION_ENV_VARS: if varname in self._env: return self._env[varname] # As a last attempt we'll try to retrieve the region # from the currently configured region. If the session # isn't configured or we can't get the region, we have # no choice but to error out. if self.session is not None: region_name = self.session.region_name if region_name is not None: return region_name raise ValueError( "Unable to retrieve the region name when configuring the " "websocket client. Either set the 'AWS_REGION' environment " "variable or assign 'app.websocket_api.session' to a boto3 " "session." ) def _get_client(self) -> Any: if self.session is None: raise ValueError( 'Assign app.websocket_api.session to a boto3 session before ' 'using the WebsocketAPI' ) if self._endpoint is None: raise ValueError( 'WebsocketAPI.configure must be called before using the ' 'WebsocketAPI' ) if self._client is None: self._client = self.session.client( 'apigatewaymanagementapi', endpoint_url=self._endpoint, ) return self._client def send(self, connection_id: str, message: str) -> None: client = self._get_client() try: client.post_to_connection( ConnectionId=connection_id, Data=message, ) except client.exceptions.GoneException: raise WebsocketDisconnectedError(connection_id) def close(self, connection_id: str) -> None: client = self._get_client() try: client.delete_connection( ConnectionId=connection_id, ) except client.exceptions.GoneException: raise WebsocketDisconnectedError(connection_id) def info(self, connection_id: str) -> Any: client = self._get_client() try: return client.get_connection( ConnectionId=connection_id, ) except client.exceptions.GoneException: raise WebsocketDisconnectedError(connection_id) class DecoratorAPI(object): websocket_api: Optional[WebsocketAPI] = None def middleware( self, event_type: str = 'all' ) -> Callable[[Callable[..., Any]], Any]: def _middleware_wrapper( func: Callable[..., Any] ) -> Callable[..., Any]: self.register_middleware(func, event_type) return func return _middleware_wrapper def authorizer(self, ttl_seconds: Optional[int] = None, execution_role: Optional[str] = None, name: Optional[str] = None, header: Optional[str] = 'Authorization' ) -> Callable[..., Any]: return self._create_registration_function( handler_type='authorizer', name=name, registration_kwargs={ 'ttl_seconds': ttl_seconds, 'execution_role': execution_role, 'header': header } ) def on_s3_event(self, bucket: str, events: Optional[List[str]] = None, prefix: Optional[str] = None, suffix: Optional[str] = None, name: Optional[str] = None) -> Callable[..., Any]: return self._create_registration_function( handler_type='on_s3_event', name=name, registration_kwargs={ 'bucket': bucket, 'events': events, 'prefix': prefix, 'suffix': suffix, } ) def on_sns_message(self, topic: str, name: Optional[str] = None) -> Callable[..., Any]: return self._create_registration_function( handler_type='on_sns_message', name=name, registration_kwargs={'topic': topic} ) def on_sqs_message(self, queue: Optional[str] = None, batch_size: int = 1, name: Optional[str] = None, queue_arn: Optional[str] = None, maximum_batching_window_in_seconds: int = 0, maximum_concurrency: Optional[int] = None, ) -> Callable[..., Any]: return self._create_registration_function( handler_type='on_sqs_message', name=name, registration_kwargs={ 'queue': queue, 'queue_arn': queue_arn, 'batch_size': batch_size, 'maximum_batching_window_in_seconds': maximum_batching_window_in_seconds, 'maximum_concurrency': maximum_concurrency, } ) def on_cw_event(self, event_pattern: Dict[str, Any], name: Optional[str] = None) -> Callable[..., Any]: return self._create_registration_function( handler_type='on_cw_event', name=name, registration_kwargs={'event_pattern': event_pattern} ) def schedule(self, expression: Union[str, 'ScheduleExpression'], name: Optional[str] = None, description: str = '') -> Callable[..., Any]: return self._create_registration_function( handler_type='schedule', name=name, registration_kwargs={'expression': expression, 'description': description}, ) def on_kinesis_record(self, stream: str, batch_size: int = 100, starting_position: str = 'LATEST', name: Optional[str] = None, maximum_batching_window_in_seconds: int = 0 ) -> Callable[..., Any]: return self._create_registration_function( handler_type='on_kinesis_record', name=name, registration_kwargs={ 'stream': stream, 'batch_size': batch_size, 'starting_position': starting_position, 'maximum_batching_window_in_seconds': maximum_batching_window_in_seconds}, ) def on_dynamodb_record( self, stream_arn: str, batch_size: int = 100, starting_position: str = 'LATEST', name: Optional[str] = None, maximum_batching_window_in_seconds: int = 0 ) -> Callable[..., Any]: return self._create_registration_function( handler_type='on_dynamodb_record', name=name, registration_kwargs={ 'stream_arn': stream_arn, 'batch_size': batch_size, 'starting_position': starting_position, 'maximum_batching_window_in_seconds': maximum_batching_window_in_seconds}, ) def route(self, path: str, **kwargs: Any) -> Callable[..., Any]: return self._create_registration_function( handler_type='route', name=kwargs.pop('name', None), # This looks a little weird taking kwargs as a key, # but we want to preserve keep the **kwargs signature # in the route decorator. registration_kwargs={'path': path, 'kwargs': kwargs}, ) def lambda_function(self, name: Optional[str] = None) -> Callable[..., Any]: return self._create_registration_function( handler_type='lambda_function', name=name) def on_ws_connect(self, name: Optional[str] = None) -> Callable[..., Any]: return self._create_registration_function( handler_type='on_ws_connect', name=name, registration_kwargs={'route_key': '$connect'}, ) def on_ws_disconnect(self, name: Optional[str] = None) -> Callable[..., Any]: return self._create_registration_function( handler_type='on_ws_disconnect', name=name, registration_kwargs={'route_key': '$disconnect'}, ) def on_ws_message(self, name: Optional[str] = None) -> Callable[..., Any]: return self._create_registration_function( handler_type='on_ws_message', name=name, registration_kwargs={'route_key': '$default'}, ) def _create_registration_function(self, handler_type: str, name: Optional[str] = None, registration_kwargs: Optional[Any] = None ) -> Callable[..., Any]: def _register_handler( user_handler: UserHandlerFuncType ) -> Callable[..., Any]: handler_name = name if handler_name is None: handler_name = user_handler.__name__ if registration_kwargs is not None: kwargs = registration_kwargs else: kwargs = {} wrapped = self._wrap_handler(handler_type, handler_name, user_handler) self._register_handler(handler_type, handler_name, user_handler, wrapped, kwargs) return wrapped return _register_handler def _wrap_handler(self, handler_type: str, handler_name: str, user_handler: UserHandlerFuncType ) -> UserHandlerFuncType: if handler_type in _EVENT_CLASSES: if handler_type == 'lambda_function': # We have to wrap existing @app.lambda_function() # handlers for backwards compat reasons so we can # preserve the `def handler(event, context): ...` # interface. However we need a consistent interface # for middleware so we have to wrap the event # here. user_handler = PureLambdaWrapper(user_handler) return EventSourceHandler( user_handler, _EVENT_CLASSES[handler_type], middleware_handlers=self._get_middleware_handlers( event_type=_MIDDLEWARE_MAPPING[handler_type], ) ) websocket_event_classes = [ 'on_ws_connect', 'on_ws_message', 'on_ws_disconnect', ] if self.websocket_api and handler_type in websocket_event_classes: return WebsocketEventSourceHandler( user_handler, WebsocketEvent, self.websocket_api, middleware_handlers=self._get_middleware_handlers( event_type='websocket') ) if handler_type == 'authorizer': # Authorizer is special cased and doesn't quite fit the # EventSourceHandler pattern. return ChaliceAuthorizer(handler_name, user_handler) return user_handler def _get_middleware_handlers(self, event_type: str) -> List: raise NotImplementedError("_get_middleware_handlers") def _register_handler(self, handler_type: str, name: str, user_handler: UserHandlerFuncType, wrapped_handler: Callable[..., Any], kwargs: Dict[str, Any], options: Optional[Dict[Any, Any]] = None) -> None: raise NotImplementedError("_register_handler") def register_middleware(self, func: MiddlewareFuncType, event_type: str = 'all') -> None: raise NotImplementedError("register_middleware") class _HandlerRegistration(object): def __init__(self) -> None: self.routes: Dict[str, Dict[str, RouteEntry]] = defaultdict(dict) self.websocket_handlers: Dict[str, Any] = {} self.builtin_auth_handlers: List['BuiltinAuthConfig'] = [] self.event_sources: List['BaseEventSourceConfig'] = [] self.pure_lambda_functions: List['LambdaFunction'] = [] self.api: APIGateway = APIGateway() self.handler_map: Dict[str, Callable[..., Any]] = {} self.middleware_handlers: List[Tuple[MiddlewareFuncType, str]] = [] def register_middleware(self, func: MiddlewareFuncType, event_type: str = 'all') -> None: self.middleware_handlers.append((func, event_type)) def _do_register_handler(self, handler_type: str, name: str, user_handler: UserHandlerFuncType, wrapped_handler: Callable[..., Any], kwargs: Any, options: Optional[Dict[Any, Any]] = None) -> None: module_name = 'app' if options is not None: name_prefix = options.get('name_prefix') if name_prefix is not None: name = name_prefix + name url_prefix = options.get('url_prefix') if url_prefix is not None and handler_type == 'route': # Move url_prefix into kwargs so only the # route() handler gets a url_prefix kwarg. kwargs['url_prefix'] = url_prefix # module_name is always provided if options is not None. module_name = options['module_name'] handler_string = '%s.%s' % (module_name, user_handler.__name__) getattr(self, '_register_%s' % handler_type)( name=name, user_handler=user_handler, handler_string=handler_string, wrapped_handler=wrapped_handler, kwargs=kwargs, ) self.handler_map[name] = wrapped_handler def _attach_websocket_handler(self, handler: Union[ 'WebsocketConnectConfig', 'WebsocketMessageConfig', 'WebsocketDisconnectConfig' ]) -> None: route_key = handler.route_key_handled decorator_name = { '$default': 'on_ws_message', '$connect': 'on_ws_connect', '$disconnect': 'on_ws_disconnect', }.get(route_key) if route_key in self.websocket_handlers: raise ValueError( "Duplicate websocket handler: '%s'. There can only be one " "handler for each websocket decorator." % decorator_name ) self.websocket_handlers[route_key] = handler def _register_on_ws_connect(self, name: str, user_handler: UserHandlerFuncType, handler_string: str, kwargs: Any, **unused: Dict[str, Any]) -> None: wrapper = WebsocketConnectConfig( name=name, handler_string=handler_string, user_handler=user_handler, ) self._attach_websocket_handler(wrapper) def _register_on_ws_message(self, name: str, user_handler: UserHandlerFuncType, handler_string: str, kwargs: Any, **unused: Dict[str, Any]) -> None: route_key = kwargs['route_key'] wrapper = WebsocketMessageConfig( name=name, route_key_handled=route_key, handler_string=handler_string, user_handler=user_handler, ) self._attach_websocket_handler(wrapper) self.websocket_handlers[route_key] = wrapper def _register_on_ws_disconnect(self, name: str, user_handler: UserHandlerFuncType, handler_string: str, kwargs: Any, **unused: Dict[str, Any]) -> None: wrapper = WebsocketDisconnectConfig( name=name, handler_string=handler_string, user_handler=user_handler, ) self._attach_websocket_handler(wrapper) def _register_lambda_function(self, name: str, user_handler: UserHandlerFuncType, handler_string: str, **unused: Dict[str, Any]) -> None: wrapper = LambdaFunction( func=user_handler, name=name, handler_string=handler_string, ) self.pure_lambda_functions.append(wrapper) def _register_on_s3_event(self, name: str, handler_string: str, kwargs: Any, **unused: Dict[str, Any] ) -> None: events = kwargs['events'] if events is None: events = ['s3:ObjectCreated:*'] s3_event = S3EventConfig( name=name, bucket=kwargs['bucket'], events=events, prefix=kwargs['prefix'], suffix=kwargs['suffix'], handler_string=handler_string, ) self.event_sources.append(s3_event) def _register_on_sns_message(self, name: str, handler_string: str, kwargs: Any, **unused: Dict[str, Any] ) -> None: sns_config = SNSEventConfig( name=name, handler_string=handler_string, topic=kwargs['topic'], ) self.event_sources.append(sns_config) def _register_on_sqs_message(self, name: str, handler_string: str, kwargs: Any, **unused: Dict[str, Any] ) -> None: queue = kwargs.get('queue') queue_arn = kwargs.get('queue_arn') if not queue and not queue_arn: raise ValueError( "Must provide either `queue` or `queue_arn` to the " "`on_sqs_message` decorator." ) sqs_config = SQSEventConfig( name=name, handler_string=handler_string, queue=queue, queue_arn=queue_arn, batch_size=kwargs['batch_size'], maximum_batching_window_in_seconds=kwargs[ 'maximum_batching_window_in_seconds'], maximum_concurrency=kwargs[ 'maximum_concurrency'], ) self.event_sources.append(sqs_config) def _register_on_kinesis_record(self, name: str, handler_string: str, kwargs: Any, **unused: Dict[str, Any] ) -> None: kinesis_config = KinesisEventConfig( name=name, handler_string=handler_string, stream=kwargs['stream'], batch_size=kwargs['batch_size'], starting_position=kwargs['starting_position'], maximum_batching_window_in_seconds=kwargs[ 'maximum_batching_window_in_seconds'], ) self.event_sources.append(kinesis_config) def _register_on_dynamodb_record(self, name: str, handler_string: str, kwargs: Any, **unused: Dict[str, Any]) -> None: ddb_config = DynamoDBEventConfig( name=name, handler_string=handler_string, stream_arn=kwargs['stream_arn'], batch_size=kwargs['batch_size'], starting_position=kwargs['starting_position'], maximum_batching_window_in_seconds=kwargs[ 'maximum_batching_window_in_seconds'], ) self.event_sources.append(ddb_config) def _register_on_cw_event(self, name: str, handler_string: str, kwargs: Any, **unused: Dict[str, Any]) -> None: event_source = CloudWatchEventConfig( name=name, event_pattern=kwargs['event_pattern'], handler_string=handler_string ) self.event_sources.append(event_source) def _register_schedule(self, name: str, handler_string: str, kwargs: Any, **unused: Dict[str, Any]) -> None: event_source = ScheduledEventConfig( name=name, schedule_expression=kwargs['expression'], description=kwargs["description"], handler_string=handler_string, ) self.event_sources.append(event_source) def _register_authorizer(self, name: str, handler_string: str, wrapped_handler: 'ChaliceAuthorizer', kwargs: Any, **unused: Dict[str, Any]) -> None: actual_kwargs = kwargs.copy() ttl_seconds = actual_kwargs.pop('ttl_seconds', None) execution_role = actual_kwargs.pop('execution_role', None) header = actual_kwargs.pop('header', None) if actual_kwargs: raise TypeError( 'TypeError: authorizer() got unexpected keyword ' 'arguments: %s' % ', '.join(list(actual_kwargs))) auth_config = BuiltinAuthConfig( name=name, handler_string=handler_string, ttl_seconds=ttl_seconds, execution_role=execution_role, header=header, ) wrapped_handler.config = auth_config self.builtin_auth_handlers.append(auth_config) def _register_route(self, name: str, user_handler: UserHandlerFuncType, kwargs: Any, **unused: Dict[str, Any]) -> None: actual_kwargs = kwargs['kwargs'] path = kwargs['path'] url_prefix = kwargs.pop('url_prefix', None) if url_prefix is not None: path = '/'.join([url_prefix.rstrip('/'), path.strip('/')]).rstrip('/') methods = actual_kwargs.pop('methods', ['GET']) route_kwargs = { 'authorizer': actual_kwargs.pop('authorizer', None), 'api_key_required': actual_kwargs.pop('api_key_required', None), 'content_types': actual_kwargs.pop('content_types', ['application/json']), 'cors': actual_kwargs.pop('cors', self.api.cors), } if route_kwargs['cors'] is None: route_kwargs['cors'] = self.api.cors if not isinstance(route_kwargs['content_types'], list): raise ValueError( 'In view function "%s", the content_types ' 'value must be a list, not %s: %s' % ( name, type(route_kwargs['content_types']), route_kwargs['content_types'])) if actual_kwargs: raise TypeError('TypeError: route() got unexpected keyword ' 'arguments: %s' % ', '.join(list(actual_kwargs))) for method in methods: if method in self.routes[path]: raise ValueError( "Duplicate method: '%s' detected for route: '%s'\n" "between view functions: \"%s\" and \"%s\". A specific " "method may only be specified once for " "a particular path." % ( method, path, self.routes[path][method].view_name, name) ) entry = RouteEntry(user_handler, name, path, method, **route_kwargs) self.routes[path][method] = entry class Chalice(_HandlerRegistration, DecoratorAPI): FORMAT_STRING = '%(name)s - %(levelname)s - %(message)s' authorizers: Dict[str, Dict[str, Any]] lambda_context: 'LambdaContext' current_request: Optional[Request] def __init__(self, app_name: str, debug: bool = False, configure_logs: bool = True, env: Optional[MutableMapping] = None) -> None: super(Chalice, self).__init__() self.app_name: str = app_name self.websocket_api: WebsocketAPI = WebsocketAPI() self._debug: bool = debug self.configure_logs: bool = configure_logs self.log: logging.Logger = logging.getLogger(self.app_name) if env is None: env = os.environ self._initialize(env) self.experimental_feature_flags: Set[str] = set() # This is marked as internal but is intended to be used by # any code within Chalice. self._features_used: Set[str] = set() def _initialize(self, env: MutableMapping) -> None: if self.configure_logs: self._configure_logging() env['AWS_EXECUTION_ENV'] = '%s aws-chalice/%s' % ( env.get('AWS_EXECUTION_ENV', 'AWS_Lambda'), __version__, ) @property def debug(self) -> bool: return self._debug @debug.setter def debug(self, value: bool) -> None: self._debug = value self._configure_log_level() def _configure_logging(self) -> None: if self._already_configured(self.log): return handler = logging.StreamHandler(sys.stdout) # Timestamp is handled by lambda itself so the # default FORMAT_STRING doesn't need to include it. formatter = logging.Formatter(self.FORMAT_STRING) handler.setFormatter(formatter) self.log.propagate = False self._configure_log_level() self.log.addHandler(handler) def _already_configured(self, log: logging.Logger) -> bool: if not log.handlers: return False for handler in log.handlers: if isinstance(handler, logging.StreamHandler): if handler.stream == sys.stdout: return True return False def _configure_log_level(self) -> None: if self._debug: level = logging.DEBUG else: level = logging.ERROR self.log.setLevel(level) def register_blueprint(self, blueprint: 'Blueprint', name_prefix: Optional[str] = None, url_prefix: Optional[str] = None) -> None: blueprint.register(self, options={'name_prefix': name_prefix, 'url_prefix': url_prefix}) def _register_handler(self, handler_type: str, name: str, user_handler: UserHandlerFuncType, wrapped_handler: Callable[..., Any], kwargs: Any, options: Optional[Dict[Any, Any]] = None ) -> None: self._do_register_handler(handler_type, name, user_handler, wrapped_handler, kwargs, options) # These are defined here on the Chalice class because we want all the # feature flag tracking to live in Chalice and not the DecoratorAPI. def _register_on_ws_connect(self, name: str, user_handler: UserHandlerFuncType, handler_string: str, kwargs: Any, **unused: Dict[str, Any]) -> None: self._features_used.add('WEBSOCKETS') super(Chalice, self)._register_on_ws_connect( name, user_handler, handler_string, kwargs, **unused) def _register_on_ws_message(self, name: str, user_handler: UserHandlerFuncType, handler_string: str, kwargs: Any, **unused: Dict[str, Any]) -> None: self._features_used.add('WEBSOCKETS') super(Chalice, self)._register_on_ws_message( name, user_handler, handler_string, kwargs, **unused) def _register_on_ws_disconnect(self, name: str, user_handler: UserHandlerFuncType, handler_string: str, kwargs: Any, **unused: Dict[str, Any]) -> None: self._features_used.add('WEBSOCKETS') super(Chalice, self)._register_on_ws_disconnect( name, user_handler, handler_string, kwargs, **unused) def _get_middleware_handlers(self, event_type: str) -> Any: # We're returning a generator here because we want to defer the # collection of all middleware until as last as possible (when # then handler is actually invoked). This lets us pick up any # middleware that's registered after a handler has been defined, # which is the behavior you'd expect. return (func for func, filter_type in self.middleware_handlers if filter_type in [event_type, 'all']) def __call__(self, event: Any, context: Any) -> Dict[str, Any]: # For legacy reasons, we can't move the Rest API handler entry # point away from this Chalice.__call__ method . However, we can # try to extract as much as logic as possible to a separate handler # class we can call. That way it's still structured somewhat similar # to the other event handlers which makes it more manageable to # implement shared functionality (e.g. middleware). self.lambda_context: 'LambdaContext' = context handler = RestAPIEventHandler( self.routes, self.api, self.log, self.debug, middleware_handlers=self._get_middleware_handlers('http'), ) self.current_request: \ Optional[Request] = handler.create_request_object(event, context) return handler(event, context) class BuiltinAuthConfig(object): def __init__(self, name: str, handler_string: str, ttl_seconds: Optional[int] = None, execution_role: Optional[str] = None, header: str = 'Authorization'): # We'd also support all the misc config options you can set. self.name: str = name self.handler_string: str = handler_string self.ttl_seconds: Optional[int] = ttl_seconds self.execution_role: Optional[str] = execution_role self.header: str = header # ChaliceAuthorizer is unique in that the runtime component (the thing # that wraps the decorated function) also needs a reference to the config # object (the object the describes how to create the resource). In # most event sources these are separate and don't need to know about # each other, but ChaliceAuthorizer does. This is because the way # you associate a builtin authorizer with a view function is by passing # a direct reference: # # @app.authorizer(...) # def my_auth_function(...): pass # # @app.route('/', auth=my_auth_function) # # The 'route' part needs to know about the auth function for two reasons: # # 1. We use ``view.authorizer`` to figure out how to deploy the app # 2. We need a reference to the runtime handler for the auth in order # to support local mode testing. # I *think* we can refactor things to handle both of those issues but # we would need more research to know for sure. For now, this is a # special cased runtime class that knows about its config. class ChaliceAuthorizer(object): def __init__(self, name: str, func: Callable[..., Any], scopes: Optional[List[str]] = None) -> None: self.name: str = name self.func: Callable[ ['AuthRequest'], Union['AuthResponse', Dict[str, Any]] ] = func self.scopes: List[str] = scopes or [] # This is filled in during the @app.authorizer() # processing. self.config: BuiltinAuthConfig = None # type: ignore def __call__( self, event: Dict[str, Any], context: Dict[str, Any] ) -> Dict[str, Any]: auth_request = self._transform_event(event) result = self.func(auth_request) if isinstance(result, AuthResponse): return result.to_dict(auth_request) return result def _transform_event(self, event: Dict[str, Any]) -> 'AuthRequest': return AuthRequest(event['type'], event['authorizationToken'], event['methodArn']) def with_scopes(self, scopes: List[str]) -> 'ChaliceAuthorizer': authorizer_with_scopes = copy.deepcopy(self) authorizer_with_scopes.scopes = scopes return authorizer_with_scopes class AuthRequest(object): def __init__(self, auth_type: str, token: str, method_arn: str) -> None: self.auth_type: str = auth_type self.token: str = token self.method_arn: str = method_arn class AuthResponse(object): ALL_HTTP_METHODS: List[str] = ['DELETE', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'GET'] def __init__(self, routes: List[Union[str, 'AuthRoute']], principal_id: str, context: Optional[Dict[str, str]] = None): self.routes: List[Union[str, 'AuthRoute']] = routes self.principal_id: str = principal_id # The request is used to generate full qualified ARNs # that we need for the resource portion of the returned # policy. if context is None: context = {} self.context: Dict[str, str] = context def to_dict(self, request: AuthRequest) -> Dict[str, Any]: return { 'context': self.context, 'principalId': self.principal_id, 'policyDocument': self._generate_policy(request), } def _generate_policy(self, request: AuthRequest) -> Dict[str, Any]: allowed_resources = self._generate_allowed_resources(request) return { 'Version': '2012-10-17', 'Statement': [ { 'Action': 'execute-api:Invoke', 'Effect': 'Allow', 'Resource': allowed_resources, } ] } def _generate_allowed_resources(self, request: AuthRequest) -> List[str]: allowed_resources = [] for route in self.routes: if isinstance(route, AuthRoute): methods = route.methods path = route.path elif route == '*': # A string route of '*' means that all paths and # all HTTP methods are now allowed. methods = ['*'] path = '*' else: # If 'route' is just a string, then they've # opted not to use the AuthRoute(), so we'll # generate a policy that allows all HTTP methods. methods = ['*'] path = route for method in methods: allowed_resources.append( self._generate_arn(path, request, method)) return allowed_resources def _generate_arn( self, route: str, request: AuthRequest, method: str = '*' ) -> str: incoming_arn = request.method_arn # An incoming_arn would look like this: # "arn:aws:execute-api:us-west-2:123:rest-api-id/stage/GET/needs/auth" # Then we pull out the rest-api-id and stage, such that: # base = ['rest-api-id', 'stage'] # # We rely on the fact that the first part of the ARN format is fixed # as: arn::::: arn_parts = incoming_arn.split(':', 5) allowed_resource = arn_parts[-1].split('/')[:2] # Now we add in the path components and rejoin everything # back together to make a full arn. # We're also assuming all HTTP methods (via '*') for now. # To support per HTTP method routes the API will need to be updated. # We also need to strip off the leading ``/`` so it can be # '/'.join(...)'d properly. allowed_resource.extend([method, route[1:]]) last_arn_segment = '/'.join(allowed_resource) if route == '*': # We also have to handle the '*' case which matches # all routes. last_arn_segment += route arn_parts[-1] = last_arn_segment final_arn = ':'.join(arn_parts) return final_arn class AuthRoute(object): def __init__(self, path: str, methods: List[str]): self.path: str = path self.methods: List[str] = methods class LambdaFunction(object): def __init__(self, func: Callable[..., Any], name: str, handler_string: str): self.func: Callable[..., Any] = func self.name: str = name self.handler_string: str = handler_string def __call__(self, event: Dict[str, Any], context: Dict[str, Any] ) -> Callable[[Dict[str, Any], Dict[str, Any]], Any]: return self.func(event, context) class BaseEventSourceConfig(object): def __init__(self, name: str, handler_string: str) -> None: self.name: str = name self.handler_string: str = handler_string class ScheduledEventConfig(BaseEventSourceConfig): def __init__(self, name: str, handler_string: str, schedule_expression: Union[str, 'ScheduleExpression'], description: str): super(ScheduledEventConfig, self).__init__(name, handler_string) self.schedule_expression: \ Union[str, 'ScheduleExpression'] = schedule_expression self.description: str = description class CloudWatchEventConfig(BaseEventSourceConfig): def __init__(self, name: str, handler_string: str, event_pattern: Dict[str, Any]): super(CloudWatchEventConfig, self).__init__(name, handler_string) self.event_pattern: Dict[str, Any] = event_pattern class ScheduleExpression(object): def to_string(self) -> str: raise NotImplementedError("to_string") class Rate(ScheduleExpression): MINUTES: str = 'MINUTES' HOURS: str = 'HOURS' DAYS: str = 'DAYS' def __init__(self, value: int, unit: str) -> None: self.value: int = value self.unit: str = unit def to_string(self) -> str: unit = self.unit.lower() if self.value == 1: # Remove the 's' from the end if it's singular. # This is required by the cloudwatch events API. unit = unit[:-1] return 'rate(%s %s)' % (self.value, unit) class Cron(ScheduleExpression): def __init__(self, minutes: Union[str, int], hours: Union[str, int], day_of_month: Union[str, int], month: Union[str, int], day_of_week: Union[str, int], year: Union[str, int]): self.minutes: Union[str, int] = minutes self.hours: Union[str, int] = hours self.day_of_month: Union[str, int] = day_of_month self.month: Union[str, int] = month self.day_of_week: Union[str, int] = day_of_week self.year: Union[str, int] = year def to_string(self) -> str: return 'cron(%s %s %s %s %s %s)' % ( self.minutes, self.hours, self.day_of_month, self.month, self.day_of_week, self.year, ) class S3EventConfig(BaseEventSourceConfig): def __init__(self, name: str, bucket: str, events: List[str], prefix: str, suffix: str, handler_string: str): super(S3EventConfig, self).__init__(name, handler_string) self.bucket: str = bucket self.events: List[str] = events self.prefix: str = prefix self.suffix: str = suffix class SNSEventConfig(BaseEventSourceConfig): def __init__(self, name: str, handler_string: str, topic: str): super(SNSEventConfig, self).__init__(name, handler_string) self.topic: str = topic class SQSEventConfig(BaseEventSourceConfig): def __init__(self, name: str, handler_string: str, queue: Optional[str], queue_arn: Optional[str], batch_size: int, maximum_batching_window_in_seconds: int, maximum_concurrency: Optional[int]): super(SQSEventConfig, self).__init__(name, handler_string) self.queue: Optional[str] = queue self.queue_arn: Optional[str] = queue_arn self.batch_size: int = batch_size self.maximum_batching_window_in_seconds: int = \ maximum_batching_window_in_seconds self.maximum_concurrency: Optional[int] = maximum_concurrency class KinesisEventConfig(BaseEventSourceConfig): def __init__(self, name: str, handler_string: str, stream: str, batch_size: int, starting_position: str, maximum_batching_window_in_seconds: int) -> None: super(KinesisEventConfig, self).__init__(name, handler_string) self.stream: str = stream self.batch_size: int = batch_size self.starting_position: str = starting_position self.maximum_batching_window_in_seconds: int = \ maximum_batching_window_in_seconds class DynamoDBEventConfig(BaseEventSourceConfig): def __init__(self, name: str, handler_string: str, stream_arn: str, batch_size: int, starting_position: str, maximum_batching_window_in_seconds: int) -> None: super(DynamoDBEventConfig, self).__init__(name, handler_string) self.stream_arn: str = stream_arn self.batch_size: int = batch_size self.starting_position: str = starting_position self.maximum_batching_window_in_seconds: int = \ maximum_batching_window_in_seconds class WebsocketConnectConfig(BaseEventSourceConfig): CONNECT_ROUTE: str = '$connect' def __init__(self, name: str, handler_string: str, user_handler: UserHandlerFuncType): super(WebsocketConnectConfig, self).__init__(name, handler_string) self.route_key_handled = self.CONNECT_ROUTE self.handler_function = user_handler class WebsocketMessageConfig(BaseEventSourceConfig): def __init__(self, name: str, route_key_handled: str, handler_string: str, user_handler: UserHandlerFuncType) -> None: super(WebsocketMessageConfig, self).__init__(name, handler_string) self.route_key_handled: str = route_key_handled self.handler_function: Callable[..., Any] = user_handler class WebsocketDisconnectConfig(BaseEventSourceConfig): DISCONNECT_ROUTE: str = '$disconnect' def __init__(self, name: str, handler_string: str, user_handler: UserHandlerFuncType): super(WebsocketDisconnectConfig, self).__init__(name, handler_string) self.route_key_handled = self.DISCONNECT_ROUTE self.handler_function = user_handler class PureLambdaWrapper(object): def __init__(self, original_func: Callable[ [Dict[str, Any], Optional[Dict[str, Any]]], Any ] ): self._original_func = original_func def __call__(self, event: 'BaseLambdaEvent') -> Any: # The @app.lambda_function() expects an event dict # and a context argument so this class will is used to adapt # from the Chalice single-arg style function (which is used # in all the event handlers) to the low-level lambda api. return self._original_func(event.to_dict(), event.context) class MiddlewareHandler(object): def __init__(self, handler: Callable[..., Any], next_handler: Callable[..., Any]) -> None: self.handler: Callable[..., Any] = handler self.next_handler: Callable[..., Any] = next_handler def __call__(self, request: Any) -> Any: return self.handler(request, self.next_handler) class BaseLambdaHandler(object): def __call__(self, event: Any, context: Any) -> Any: pass def _build_middleware_handlers(self, handlers: List[Callable[..., Any]], original_handler: Callable[..., Any] ) -> Callable[..., Any]: current = original_handler for handler in reversed(list(handlers)): current = MiddlewareHandler(handler=handler, next_handler=current) return current class EventSourceHandler(BaseLambdaHandler): def __init__( self, func: Callable[..., Any], event_class: Any, middleware_handlers: Optional[List[Callable[..., Any]]] = None ) -> None: self.func: Callable[..., Any] = func self.event_class: Any = event_class if middleware_handlers is None: middleware_handlers = [] self._middleware_handlers: \ List[Callable[..., Any]] = middleware_handlers self.handler: Optional[Callable[..., Any]] = None @property def middleware_handlers(self) -> List[Callable[..., Any]]: return self._middleware_handlers @middleware_handlers.setter def middleware_handlers(self, value: List[Callable[..., Any]]) -> None: self._middleware_handlers = value def __call__(self, event: Any, context: Any) -> Any: event_obj = self.event_class(event, context) if self.handler is None: # Defer creating handlers so we have all middleware configured. self.handler = self._build_middleware_handlers( self._middleware_handlers, original_handler=self.func) return self.handler(event_obj) class WebsocketEventSourceHandler(EventSourceHandler): WEBSOCKET_API_RESPONSE = {'statusCode': 200} def __init__(self, func: Callable[..., Any], event_class: Any, websocket_api: WebsocketAPI, middleware_handlers: Optional[List[Callable[..., Any]]] = None ) -> None: super(WebsocketEventSourceHandler, self).__init__(func, event_class, middleware_handlers) self.websocket_api: WebsocketAPI = websocket_api def __call__(self, event: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, Any]: self.websocket_api.configure_from_api_id( event['requestContext']['apiId'], event['requestContext']['stage'], ) response = super( WebsocketEventSourceHandler, self).__call__(event, context) data = None if isinstance(response, Response): data = response.to_dict() elif isinstance(response, dict): data = response if "statusCode" not in data: data = {**self.WEBSOCKET_API_RESPONSE, **data} return data or self.WEBSOCKET_API_RESPONSE class RestAPIEventHandler(BaseLambdaHandler): def __init__(self, route_table: Dict[str, Dict[str, RouteEntry]], api: APIGateway, log: logging.Logger, debug: bool, middleware_handlers: Optional[List[Callable[..., Any]]] = None ) -> None: self.routes: Dict[str, Dict[str, RouteEntry]] = route_table self.api: APIGateway = api self.log: logging.Logger = log self.debug: bool = debug self.current_request: Optional[Request] = None self.lambda_context: Optional['LambdaContext'] = None if middleware_handlers is None: middleware_handlers = [] self._middleware_handlers: \ List[Callable[..., Any]] = middleware_handlers def _global_error_handler(self, event: Any, get_response: Callable[..., Any]) -> Response: try: return get_response(event) except Exception: return self._unhandled_exception_to_response() def create_request_object(self, event: Any, context: Any) -> Optional[Request]: # For legacy reasons, there's some initial validation that takes # place before we convert the input event to a python object. # We don't do this in event handlers we added later, so we *should* # be able to remove this code. To be safe, we're keeping it in for # now to minimize the potential for breaking changes. resource_path = event.get('requestContext', {}).get('resourcePath') if resource_path is not None: self.current_request = Request(event, context) return self.current_request return None def __call__(self, event: Any, context: Any) -> Any: def wrapped_event(request: Request) -> Response: return self._main_rest_api_handler(event, context) final_handler = self._build_middleware_handlers( [self._global_error_handler] + list(self._middleware_handlers), original_handler=wrapped_event, ) response = final_handler(self.current_request) return response.to_dict(self.api.binary_types) def _main_rest_api_handler(self, event: Any, context: Any) -> Response: resource_path = event.get('requestContext', {}).get('resourcePath') if resource_path is None: return error_response(error_code='InternalServerError', message='Unknown request.', http_status_code=500) http_method = event['requestContext']['httpMethod'] if http_method not in self.routes[resource_path]: allowed_methods = ', '.join(self.routes[resource_path].keys()) return error_response( error_code='MethodNotAllowedError', message='Unsupported method: %s' % http_method, http_status_code=405, headers={'Allow': allowed_methods}) route_entry = self.routes[resource_path][http_method] view_function = route_entry.view_function function_args = {name: event['pathParameters'][name] for name in route_entry.view_args} self.lambda_context = context # We're getting the CORS headers before validation to be able to # output desired headers with cors_headers = None if self._cors_enabled_for_route(route_entry): cors_headers = self._get_cors_headers(route_entry.cors) # We're doing the header validation after creating the request # so can leverage the case insensitive dict that the Request class # uses for headers. if self.current_request and route_entry.content_types: content_type = self.current_request.headers.get( 'content-type', 'application/json') if not _matches_content_type(content_type, route_entry.content_types): return error_response( error_code='UnsupportedMediaType', message='Unsupported media type: %s' % content_type, http_status_code=415, headers=cors_headers ) response = self._get_view_function_response(view_function, function_args) if cors_headers is not None: self._add_cors_headers(response, cors_headers) response_headers = CaseInsensitiveMapping(response.headers) if self.current_request and not self._validate_binary_response( self.current_request.headers, response_headers): content_type = response_headers.get('content-type', '') return error_response( error_code='BadRequest', message=('Request did not specify an Accept header with %s, ' 'The response has a Content-Type of %s. If a ' 'response has a binary Content-Type then the request ' 'must specify an Accept header that matches.' % (content_type, content_type)), http_status_code=400, headers=cors_headers ) return response def _validate_binary_response(self, request_headers: CaseInsensitiveMapping, response_headers: CaseInsensitiveMapping ) -> bool: # Validates that a response is valid given the request. If the response # content-type specifies a binary type, there must be an accept header # that is a binary type as well. request_accept_header = request_headers.get('accept') response_content_type = response_headers.get( 'content-type', 'application/json') response_is_binary = _matches_content_type(response_content_type, self.api.binary_types) expects_binary_response = False if request_accept_header is not None: expects_binary_response = _matches_content_type( request_accept_header, self.api.binary_types) if response_is_binary and not expects_binary_response: return False return True def _get_view_function_response(self, view_function: Callable[..., Any], function_args: Dict[str, Any]) -> Response: try: response = view_function(**function_args) if not isinstance(response, Response): response = Response(body=response) self._validate_response(response) except ChaliceUnhandledError: # Reraise this exception so that middleware has a chance # to handle the exception. raise except ChaliceViewError as e: # Any chalice view error should propagate. These # get mapped to various HTTP status codes in API Gateway. response = Response(body={'Code': e.__class__.__name__, 'Message': str(e)}, status_code=e.STATUS_CODE) except Exception: response = self._unhandled_exception_to_response() return response def _unhandled_exception_to_response(self) -> Response: headers: HeadersType = {} path = getattr(self.current_request, 'path', 'unknown') self.log.error("Caught exception for path %s", path, exc_info=True) if self.debug: # If the user has turned on debug mode, # we'll let the original exception propagate so # they get more information about what went wrong. stack_trace = ''.join(traceback.format_exc()) body: Any = stack_trace headers['Content-Type'] = 'text/plain' else: body = {'Code': 'InternalServerError', 'Message': 'An internal server error occurred.'} response = Response(body=body, headers=headers, status_code=500) return response def _validate_response(self, response: Response) -> None: for header, value in response.headers.items(): if '\n' in value: raise ChaliceError("Bad value for header '%s': %r" % (header, value)) def _cors_enabled_for_route(self, route_entry: RouteEntry) -> bool: return route_entry.cors is not None def _get_cors_headers(self, cors: CORSConfig) -> Dict[str, Any]: return cors.get_access_control_headers() def _add_cors_headers(self, response: Response, cors_headers: Dict[str, str]) -> None: for name, value in cors_headers.items(): if name not in response.headers: response.headers[name] = value # These classes contain all the event types that are passed # in as arguments in the lambda event handlers. These are # part of Chalice's public API and must be backwards compatible. class BaseLambdaEvent(object): def __init__(self, event_dict: Dict[str, Any], context: Optional[Dict[str, Any]]) -> None: self._event_dict: Dict[str, Any] = event_dict self.context: Optional[Dict[str, Any]] = context self._extract_attributes(event_dict) def _extract_attributes(self, event_dict: Dict[str, Any]) -> None: raise NotImplementedError("_extract_attributes") def to_dict(self) -> Dict[str, Any]: return self._event_dict # This class is only used for middleware handlers because # we can't change the existing interface for @app.lambda_function(). # This could be a Chalice 2.0 thing where we make all the decorators # have a consistent interface that takes a single event arg. class LambdaFunctionEvent(BaseLambdaEvent): def __init__(self, event_dict: Dict[str, Any], context: Any) -> None: self.event: Dict[str, Any] = event_dict self.context: Optional[Dict[str, Any]] = context def _extract_attributes(self, event_dict: Dict[str, Any]) -> None: pass def to_dict(self) -> Dict[str, Any]: return self.event class CloudWatchEvent(BaseLambdaEvent): def _extract_attributes(self, event_dict: Dict[str, Any]) -> None: self.version: str = event_dict['version'] self.account: str = event_dict['account'] self.region: str = event_dict['region'] self.detail: Dict[str, Any] = event_dict['detail'] self.detail_type: str = event_dict['detail-type'] self.source: str = event_dict['source'] self.time: str = event_dict['time'] self.event_id: str = event_dict['id'] self.resources: List[str] = event_dict['resources'] class WebsocketEvent(BaseLambdaEvent): def __init__(self, event_dict: Dict[str, Any], context: Any): super(WebsocketEvent, self).__init__(event_dict, context) self._json_body: Optional[Dict[str, Any]] = None def _extract_attributes(self, event_dict: Dict[str, Any]) -> None: request_context = event_dict['requestContext'] self.domain_name: str = request_context['domainName'] self.stage: str = request_context['stage'] self.connection_id: str = request_context['connectionId'] self.body: str = str(event_dict.get('body')) @property def json_body(self) -> Dict[str, Any]: if self._json_body is None: try: self._json_body = json.loads(self.body) except ValueError: raise BadRequestError('Error Parsing JSON') return self._json_body class SNSEvent(BaseLambdaEvent): def _extract_attributes(self, event_dict: Dict[str, Any]) -> None: first_record = event_dict['Records'][0] self.message: str = first_record['Sns']['Message'] self.subject: str = first_record['Sns']['Subject'] self.message_attributes: Dict[str, Any] = \ first_record['Sns']['MessageAttributes'] class S3Event(BaseLambdaEvent): def _extract_attributes(self, event_dict: Dict[str, Any]) -> None: s3 = event_dict['Records'][0]['s3'] self.bucket: str = s3['bucket']['name'] self.key: str = unquote_plus(s3['object']['key']) class SQSEvent(BaseLambdaEvent): def _extract_attributes(self, event_dict: Dict[str, Any]) -> None: # We don't extract anything off the top level # event. pass def __iter__(self) -> Iterator['SQSRecord']: for record in self._event_dict['Records']: yield SQSRecord(record, self.context) class SQSRecord(BaseLambdaEvent): def _extract_attributes(self, event_dict: Dict[str, Any]) -> None: self.body: str = event_dict['body'] self.receipt_handle: str = event_dict['receiptHandle'] class KinesisEvent(BaseLambdaEvent): def _extract_attributes(self, event_dict: Dict[str, Any]) -> None: pass def __iter__(self) -> Iterator['KinesisRecord']: for record in self._event_dict['Records']: yield KinesisRecord(record, self.context) class KinesisRecord(BaseLambdaEvent): def _extract_attributes(self, event_dict: Dict[str, Any]) -> None: kinesis = event_dict['kinesis'] encoded_payload = kinesis['data'] self.data: bytes = base64.b64decode(encoded_payload) self.sequence_number: str = kinesis['sequenceNumber'] self.partition_key: str = kinesis['partitionKey'] self.schema_version: str = kinesis['kinesisSchemaVersion'] self.timestamp: datetime.datetime = datetime.datetime.utcfromtimestamp( kinesis['approximateArrivalTimestamp']) class DynamoDBEvent(BaseLambdaEvent): def _extract_attributes(self, event_dict: Dict[str, Any]) -> None: pass def __iter__(self) -> Iterator['DynamoDBRecord']: for record in self._event_dict['Records']: yield DynamoDBRecord(record, self.context) class DynamoDBRecord(BaseLambdaEvent): def _extract_attributes(self, event_dict: Dict[str, Any]) -> None: dynamodb = event_dict['dynamodb'] self.timestamp: datetime.datetime = datetime.datetime.utcfromtimestamp( dynamodb['ApproximateCreationDateTime']) self.keys: Any = dynamodb.get('Keys') self.new_image: Any = dynamodb.get('NewImage') self.old_image: Any = dynamodb.get('OldImage') self.sequence_number: str = dynamodb['SequenceNumber'] self.size_bytes: int = dynamodb['SizeBytes'] self.stream_view_type: str = dynamodb['StreamViewType'] # These are from the top level keys in a record. self.aws_region: str = event_dict['awsRegion'] self.event_id: str = event_dict['eventID'] self.event_name: str = event_dict['eventName'] self.event_source_arn: str = event_dict['eventSourceARN'] @property def table_name(self) -> str: # Converts: # "arn:aws:dynamodb:us-west-2:12345:table/MyTable/" # "stream/2020-09-28T16:49:14.209" # # into: # "MyTable" parts = self.event_source_arn.split(':', 5) if not len(parts) == 6: return '' full_name = parts[-1] name_parts = full_name.split('/') if len(name_parts) >= 2: return name_parts[1] return '' class Blueprint(DecoratorAPI): def __init__(self, import_name: str) -> None: self._import_name = import_name self._deferred_registrations: \ List[Callable[[Chalice, Dict[str, Any]], None]] = [] self._current_app: Optional[Chalice] = None self._lambda_context = None @property def log(self) -> logging.Logger: if self._current_app is None: raise RuntimeError( "Can only access Blueprint.log if it's registered to an app." ) return self._current_app.log @property def current_request(self) -> Request: if self._current_app is None or \ self._current_app.current_request is None: raise RuntimeError( "Can only access Blueprint.current_request if it's registered " "to an app." ) return self._current_app.current_request @property def current_app(self) -> Chalice: if self._current_app is None: raise RuntimeError( "Can only access Blueprint.current_app if it's registered " "to an app." ) return self._current_app @property def lambda_context(self) -> 'LambdaContext': if self._current_app is None: raise RuntimeError( "Can only access Blueprint.lambda_context if it's registered " "to an app." ) return self._current_app.lambda_context def register(self, app: Chalice, options: Dict[str, Any]) -> None: self._current_app = app all_options = options.copy() all_options['module_name'] = self._import_name for function in self._deferred_registrations: function(app, all_options) # Note on blueprints implementation. One option we have for implementing # blueprints is to copy every decorator in our public API over to the # Blueprints class. Instead what we do is inherit from DecoratorAPI so we # get new decorators for free. The tradeoff is that need to add # implementations of the internal methods used to manage handler # registration that defer registration until we get an app object. While # these methods are not public in the sense that we don't want users to # call them, they're available for blueprints to use in order to avoid # boilerplate code. def register_middleware(self, func: Callable, event_type: str = 'all') -> None: self._deferred_registrations.append( lambda app, options: app.register_middleware( func, event_type ) ) def _register_handler(self, handler_type: str, name: str, user_handler: UserHandlerFuncType, wrapped_handler: Any, kwargs: Dict[str, Any], options: Optional[Dict[Any, Any]] = None ) -> None: # If we go through the public API (app.route, app.schedule, etc) then # we have to duplicate either the methods or the params in this # class. We're using _register_handler as a tradeoff for cutting # down on the duplication. def _register_blueprint_handler(app: Chalice, options: Dict[Any, Any] ) -> None: if handler_type in _EVENT_CLASSES: # pylint: disable=protected-access wrapped_handler.middleware_handlers = \ app._get_middleware_handlers( _MIDDLEWARE_MAPPING[handler_type]) # pylint: disable=protected-access app._register_handler( handler_type, name, user_handler, wrapped_handler, kwargs, options ) self._deferred_registrations.append(_register_blueprint_handler) def _get_middleware_handlers(self, event_type: str) -> List: # This will get filled in later during the registration process. return [] # This class is used to convert any existing/3rd party decorators # that work directly on lambda functions with the original signature # of (event, context). By using ConvertToMiddleware you can automatically # apply this decorator to every lambda function in a Chalice app. # Example: # # Before: # # @third_part.decorator # def some_lambda_function(event, context): pass # # Now: # # app.register_middleware(ConvertToMiddleware(third_party.decorator)) # # class ConvertToMiddleware(object): def __init__(self, lambda_wrapper: Callable[..., Any]) -> None: self._wrapper = lambda_wrapper def __call__(self, event: Any, get_response: Callable[..., Any]) -> Any: original_event, context = self._extract_original_param(event) @functools.wraps(self._wrapper) def wrapped(original_event: Any, context: Any) -> Any: return get_response(event) return self._wrapper(wrapped)(original_event, context) def _extract_original_param(self, event: Any) -> Tuple[Any, Optional[Any]]: if isinstance(event, Request): return event.to_original_event(), event.lambda_context return event.to_dict(), event.context _EVENT_CLASSES = { 'on_s3_event': S3Event, 'on_sns_message': SNSEvent, 'on_sqs_message': SQSEvent, 'on_cw_event': CloudWatchEvent, 'on_kinesis_record': KinesisEvent, 'on_dynamodb_record': DynamoDBEvent, 'schedule': CloudWatchEvent, 'lambda_function': LambdaFunctionEvent, } _MIDDLEWARE_MAPPING = { 'on_s3_event': 's3', 'on_sns_message': 'sns', 'on_sqs_message': 'sqs', 'on_cw_event': 'cloudwatch', 'on_kinesis_record': 'kinesis', 'on_dynamodb_record': 'dynamodb', 'schedule': 'scheduled', 'lambda_function': 'pure_lambda', } ================================================ FILE: chalice/awsclient.py ================================================ """Simplified AWS client. This module abstracts the botocore session and clients to provide a simpler interface. This interface only contains the API calls needed to work with AWS services used by chalice. The interface provided can range from a direct 1-1 mapping of a method to a method on a botocore client all the way up to combining API calls across multiple AWS services. As a side benefit, I can also add type annotations to this class to get improved type checking across chalice. """ from __future__ import annotations # pylint: disable=too-many-lines import os import time import tempfile from datetime import datetime import zipfile import shutil import json import re import uuid from collections import OrderedDict from typing import ( Any, Optional, Dict, Callable, List, Iterator, Iterable, Sequence, IO, Tuple, Union, ) # noqa import botocore.session # noqa from botocore.loaders import create_loader from botocore.exceptions import ClientError from botocore.utils import datetime2timestamp from botocore.vendored.requests import ( ConnectionError as RequestsConnectionError, ) from botocore.vendored.requests.exceptions import ( ReadTimeout as RequestsReadTimeout, ) from typing import TypedDict from chalice.constants import DEFAULT_STAGE_NAME from chalice.constants import MAX_LAMBDA_DEPLOYMENT_SIZE from chalice.vendored.botocore.regions import EndpointResolver StrMap = Optional[Dict[str, str]] StrAnyMap = Dict[str, Any] OptStr = Optional[str] OptInt = Optional[int] OptStrList = Optional[List[str]] ClientMethod = Callable[..., Dict[str, Any]] CWLogEvent = TypedDict( 'CWLogEvent', { 'eventId': str, 'ingestionTime': datetime, 'logStreamName': str, 'message': str, 'timestamp': datetime, 'logShortId': str, }, ) LogEventsResponse = TypedDict( 'LogEventsResponse', { 'events': List[CWLogEvent], 'nextToken': str, }, total=False, ) DomainNameResponse = TypedDict( 'DomainNameResponse', { 'domain_name': str, 'security_policy': str, 'hosted_zone_id': str, 'certificate_arn': str, 'alias_domain_name': str, }, ) _REMOTE_CALL_ERRORS = ( botocore.exceptions.ClientError, RequestsConnectionError, ) class AWSClientError(Exception): pass class ReadTimeout(AWSClientError): def __init__(self, message: str) -> None: self.message = message class ResourceDoesNotExistError(AWSClientError): pass class LambdaClientError(AWSClientError): def __init__( self, original_error: Exception, context: LambdaErrorContext ) -> None: self.original_error = original_error self.context = context super(LambdaClientError, self).__init__(str(original_error)) class DeploymentPackageTooLargeError(LambdaClientError): pass class LambdaErrorContext(object): def __init__( self, function_name: str, client_method_name: str, deployment_size: int, ) -> None: self.function_name = function_name self.client_method_name = client_method_name self.deployment_size = deployment_size class TypedAWSClient(object): # 30 * 5 == 150 seconds or 2.5 minutes for the initial lambda # creation + role propagation. LAMBDA_CREATE_ATTEMPTS = 30 DELAY_TIME = 5 def __init__( self, session: botocore.session.Session, sleep: Callable[[int], None] = time.sleep, ) -> None: self._session = session self._sleep = sleep self._client_cache: Dict[str, Any] = {} loader = create_loader('data_loader') endpoints = loader.load_data('endpoints') self._endpoint_resolver = EndpointResolver(endpoints) def resolve_endpoint( self, service: str, region: str ) -> Optional[OrderedDict[str, Any]]: """Find details of an endpoint based on the service and region. This utilizes the botocore EndpointResolver in order to find details on the given service and region combination. If the service and region combination is not found the None will be returned. """ return self._endpoint_resolver.construct_endpoint(service, region) def endpoint_from_arn(self, arn: str) -> Optional[OrderedDict[str, Any]]: """Find details for the endpoint associated with a resource ARN. This allows the an endpoint to be discerned based on an ARN. This is a convenience method due to the need to parse multiple ARNs throughout the project. If the service and region combination is not found the None will be returned. """ arn_split = arn.split(':') return self.resolve_endpoint(arn_split[2], arn_split[3]) def endpoint_dns_suffix(self, service: str, region: str) -> str: """Discover the dns suffix for a given service and region combination. This allows the service DNS suffix to be discoverable throughout the framework. If the ARN's service and region combination is not found then amazonaws.com is returned. """ endpoint = self.resolve_endpoint(service, region) return endpoint['dnsSuffix'] if endpoint else 'amazonaws.com' def endpoint_dns_suffix_from_arn(self, arn: str) -> str: """Discover the dns suffix for a given ARN. This allows the service DNS suffix to be discoverable throughout the framework based on the ARN. If the ARN's service and region combination is not found then amazonaws.com is returned. """ endpoint = self.endpoint_from_arn(arn) return endpoint['dnsSuffix'] if endpoint else 'amazonaws.com' def service_principal( self, service: str, region: str = 'us-east-1', url_suffix: str = 'amazonaws.com', ) -> str: # Disable too-many-return-statements due to ported code # pylint: disable=too-many-return-statements """Compute a "standard" AWS Service principal for given arguments. Attribution: This code was ported from https://github.com/aws/aws-cdk and more specifically, aws-cdk/region-info/lib/default.ts Computes a "standard" AWS Service principal for a given service, region and suffix. This is useful for example when you need to compute a service principal name, but you do not have a synthesize-time region literal available (so all you have is `{ "Ref": "AWS::Region" }`). This way you get the same defaulting behavior that is normally used for built-in data. :param service: the name of the service (s3, s3.amazonaws.com, ...) :param region: the region in which the service principal is needed. :param url_suffix: the URL suffix for the partition in which the region is located. :return: The service principal for the given combination of arguments """ matches = re.match( ( r'^([^.]+)' r'(?:(?:\.amazonaws\.com(?:\.cn)?)|' r'(?:\.c2s\.ic\.gov)|' r'(?:\.sc2s\.sgov\.gov))?$' ), service, ) if matches is None: # Return "service" if it does not look like any of the following: # - s3 # - s3.amazonaws.com # - s3.amazonaws.com.cn # - s3.c2s.ic.gov # - s3.sc2s.sgov.gov return service # Simplify the service name down to something like "s3" service_name = matches.group(1) # Exceptions for Service Principals in us-iso-* us_iso_exceptions = {'cloudhsm', 'config', 'states', 'workspaces'} # Exceptions for Service Principals in us-isob-* us_isob_exceptions = {'dms', 'states'} # Account for idiosyncratic Service Principals in `us-iso-*` regions if region.startswith('us-iso-') and service_name in us_iso_exceptions: if service_name == 'states': # Services with universal principal return '{}.amazonaws.com'.format(service_name) else: # Services with a partitional principal return '{}.{}'.format(service_name, url_suffix) # Account for idiosyncratic Service Principals in `us-isob-*` regions if ( region.startswith('us-isob-') and service_name in us_isob_exceptions ): if service_name == 'states': # Services with universal principal return '{}.amazonaws.com'.format(service_name) else: # Services with a partitional principal return '{}.{}'.format(service_name, url_suffix) if service_name in ['codedeploy', 'logs']: return '{}.{}.{}'.format(service_name, region, url_suffix) elif service_name == 'states': return '{}.{}.amazonaws.com'.format(service_name, region) elif service_name == 'ec2': return '{}.{}'.format(service_name, url_suffix) else: return '{}.amazonaws.com'.format(service_name) def lambda_function_exists(self, name: str) -> bool: client = self._client('lambda') try: client.get_function(FunctionName=name) return True except client.exceptions.ResourceNotFoundException: return False def api_mapping_exists(self, domain_name: str, api_map_key: str) -> bool: client = self._client('apigatewayv2') try: result = client.get_api_mappings(DomainName=domain_name) api_map = [ api_map for api_map in result['Items'] if api_map['ApiMappingKey'] == api_map_key ] if api_map: return True return False except client.exceptions.NotFoundException: return False def get_domain_name(self, domain_name: str) -> Dict[str, Any]: client = self._client('apigateway') try: domain = client.get_domain_name(domainName=domain_name) except client.exceptions.NotFoundException: err_msg = "No domain name found by %s name" % domain_name raise ResourceDoesNotExistError(err_msg) return domain def domain_name_exists(self, domain_name: str) -> bool: try: self.get_domain_name(domain_name) return True except ResourceDoesNotExistError: return False def domain_name_exists_v2(self, domain_name: str) -> bool: client = self._client('apigatewayv2') try: client.get_domain_name(DomainName=domain_name) return True except client.exceptions.NotFoundException: return False def get_function_configuration(self, name: str) -> Dict[str, Any]: response = self._client('lambda').get_function_configuration( FunctionName=name ) return response def _create_vpc_config( self, security_group_ids: OptStrList, subnet_ids: OptStrList ) -> Dict[str, List[str]]: # We always set the SubnetIds and SecurityGroupIds to an empty # list to ensure that we properly remove Vpc configuration # if you remove these values from your config.json. Omitting # the VpcConfig key or just setting to {} won't actually remove # the VPC configuration. vpc_config: Dict[str, List[str]] = { 'SubnetIds': [], 'SecurityGroupIds': [], } if security_group_ids is not None and subnet_ids is not None: vpc_config['SubnetIds'] = subnet_ids vpc_config['SecurityGroupIds'] = security_group_ids return vpc_config def publish_layer( self, layer_name: str, zip_contents: bytes, runtime: str ) -> str: try: return self._client('lambda').publish_layer_version( LayerName=layer_name, Content={'ZipFile': zip_contents}, CompatibleRuntimes=[runtime], )['LayerVersionArn'] except _REMOTE_CALL_ERRORS as e: context = LambdaErrorContext( layer_name, 'publish_layer_version', len(zip_contents) ) raise self._get_lambda_code_deployment_error(e, context) def delete_layer_version(self, layer_version_arn: str) -> None: client = self._client('lambda') _, layer_name, version_number = layer_version_arn.rsplit(":", 2) try: return client.delete_layer_version( LayerName=layer_name, VersionNumber=int(version_number) ) except client.exceptions.ResourceNotFoundException: pass def get_layer_version(self, layer_version_arn: str) -> Dict[str, Any]: client = self._client('lambda') try: return client.get_layer_version_by_arn(Arn=layer_version_arn) except client.exceptions.ResourceNotFoundException: pass return {} def create_function( self, function_name: str, role_arn: str, zip_contents: str, runtime: str, handler: str, environment_variables: Optional[StrMap] = None, tags: Optional[StrMap] = None, xray: Optional[bool] = None, timeout: OptInt = None, memory_size: OptInt = None, security_group_ids: OptStrList = None, subnet_ids: OptStrList = None, layers: OptStrList = None, ) -> str: # pylint: disable=too-many-locals kwargs: Dict[str, Any] = { 'FunctionName': function_name, 'Runtime': runtime, 'Code': {'ZipFile': zip_contents}, 'Handler': handler, 'Role': role_arn, } if environment_variables is not None: kwargs['Environment'] = {"Variables": environment_variables} if tags is not None: kwargs['Tags'] = tags if xray is True: kwargs['TracingConfig'] = {'Mode': 'Active'} if timeout is not None: kwargs['Timeout'] = timeout if memory_size is not None: kwargs['MemorySize'] = memory_size if security_group_ids is not None and subnet_ids is not None: kwargs['VpcConfig'] = self._create_vpc_config( security_group_ids=security_group_ids, subnet_ids=subnet_ids, ) if layers is not None: kwargs['Layers'] = layers arn, state = self._create_lambda_function(kwargs) # Avoid the GetFunctionConfiguration call unless # we're not immediately active. if state != 'Active': self._wait_for_active(function_name) return arn def _wait_for_active(self, function_name: str) -> None: client = self._client('lambda') waiter = client.get_waiter('function_active') waiter.wait(FunctionName=function_name) def create_api_mapping( self, domain_name: str, path_key: str, api_id: str, stage: str ) -> Dict[str, str]: kwargs = { 'DomainName': domain_name, 'ApiMappingKey': path_key, 'ApiId': api_id, 'Stage': stage, } return self._create_api_mapping(kwargs) def create_base_path_mapping( self, domain_name: str, path_key: str, api_id: str, stage: str ) -> Dict[str, str]: kwargs = { 'domainName': domain_name, 'basePath': path_key, 'restApiId': api_id, 'stage': stage, } return self._create_base_path_mapping(kwargs) def _create_base_path_mapping( self, base_path_args: Dict[str, Any] ) -> Dict[str, str]: result = self._client('apigateway').create_base_path_mapping( **base_path_args ) if result['basePath'] == '(none)': base_path = "/" else: base_path = "/%s" % result['basePath'] base_path_mapping = {'key': base_path} return base_path_mapping def _create_api_mapping(self, api_args: Dict[str, Any]) -> Dict[str, str]: result = self._client('apigatewayv2').create_api_mapping(**api_args) if result['ApiMappingKey'] == '(none)': map_key = "/" else: map_key = "/%s" % result['ApiMappingKey'] api_mapping = {'key': map_key} return api_mapping def create_domain_name( self, protocol: str, domain_name: str, endpoint_type: str, certificate_arn: str, security_policy: Optional[str] = None, tags: Optional[StrMap] = None, ) -> DomainNameResponse: if protocol == 'HTTP': kwargs = { 'domainName': domain_name, 'endpointConfiguration': { 'types': [endpoint_type], }, } if security_policy is not None: kwargs['securityPolicy'] = security_policy if endpoint_type == 'EDGE': kwargs['certificateArn'] = certificate_arn else: kwargs['regionalCertificateArn'] = certificate_arn if tags is not None: kwargs['tags'] = tags created_domain_name = self._create_domain_name(kwargs) elif protocol == 'WEBSOCKET': kwargs = self.get_custom_domain_params_v2( domain_name=domain_name, endpoint_type=endpoint_type, security_policy=security_policy, certificate_arn=certificate_arn, tags=tags, ) created_domain_name = self._create_domain_name_v2(kwargs) else: raise ValueError("Unsupported protocol value.") return created_domain_name def _create_domain_name( self, api_args: Dict[str, Any] ) -> DomainNameResponse: client = self._client('apigateway') exceptions = (client.exceptions.TooManyRequestsException,) result = self._call_client_method_with_retries( client.create_domain_name, api_args, max_attempts=6, should_retry=lambda x: True, retryable_exceptions=exceptions, ) if result.get('regionalHostedZoneId'): hosted_zone_id = result['regionalHostedZoneId'] else: hosted_zone_id = result['distributionHostedZoneId'] if result.get('regionalCertificateArn'): certificate_arn = result['regionalCertificateArn'] else: certificate_arn = result['certificateArn'] if result.get('regionalDomainName') is not None: alias_domain_name = result['regionalDomainName'] else: alias_domain_name = result['distributionDomainName'] domain_name: DomainNameResponse = { 'domain_name': result['domainName'], 'security_policy': result['securityPolicy'], 'hosted_zone_id': hosted_zone_id, 'certificate_arn': certificate_arn, 'alias_domain_name': alias_domain_name, } return domain_name def _create_domain_name_v2( self, api_args: Dict[str, Any] ) -> DomainNameResponse: client = self._client('apigatewayv2') exceptions = (client.exceptions.TooManyRequestsException,) result = self._call_client_method_with_retries( client.create_domain_name, api_args, max_attempts=6, should_retry=lambda x: True, retryable_exceptions=exceptions, ) result_data = result['DomainNameConfigurations'][0] domain_name: DomainNameResponse = { 'domain_name': result['DomainName'], 'alias_domain_name': result_data['ApiGatewayDomainName'], 'security_policy': result_data['SecurityPolicy'], 'hosted_zone_id': result_data['HostedZoneId'], 'certificate_arn': result_data['CertificateArn'], } return domain_name def _create_lambda_function( self, api_args: Dict[str, Any] ) -> Tuple[str, str]: try: result = self._call_client_method_with_retries( self._client('lambda').create_function, api_args, max_attempts=self.LAMBDA_CREATE_ATTEMPTS, ) return result['FunctionArn'], result['State'] except _REMOTE_CALL_ERRORS as e: context = LambdaErrorContext( api_args['FunctionName'], 'create_function', len(api_args['Code']['ZipFile']), ) raise self._get_lambda_code_deployment_error(e, context) def _is_settling_error( self, error: botocore.exceptions.ClientError ) -> bool: message = error.response['Error'].get('Message', '') if re.search('event source mapping.*is in use', message): return True return False def invoke_function( self, name: str, payload: Optional[bytes] = None ) -> Dict[str, Any]: kwargs: Dict[str, Union[str, bytes]] = { 'FunctionName': name, 'InvocationType': 'RequestResponse', } if payload is not None: kwargs['Payload'] = payload try: return self._client('lambda').invoke(**kwargs) except RequestsReadTimeout as e: raise ReadTimeout(str(e)) def _is_iam_role_related_error( self, error: botocore.exceptions.ClientError ) -> bool: message = error.response['Error'].get('Message', '') if re.search('role.*cannot be assumed', message): return True if re.search('role.*does not have permissions', message): return True # This message is also related to IAM roles, it happens when the grant # used for the KMS key for encrypting env vars doesn't think the # principal is valid yet. if re.search('InvalidArnException.*valid principal', message): return True return False def _get_lambda_code_deployment_error( self, error: Any, context: LambdaErrorContext ) -> LambdaClientError: error_cls = LambdaClientError if ( isinstance(error, RequestsConnectionError) and context.deployment_size > MAX_LAMBDA_DEPLOYMENT_SIZE ): # When the zip deployment package is too large and Lambda # aborts the connection as chalice is still sending it # data error_cls = DeploymentPackageTooLargeError elif isinstance(error, ClientError): code = error.response['Error'].get('Code', '') message = error.response['Error'].get('Message', '') if code == 'RequestEntityTooLargeException': # Happens when the zipped deployment package sent to lambda # is too large error_cls = DeploymentPackageTooLargeError elif ( code == 'InvalidParameterValueException' and 'Unzipped size must be smaller' in message ): # Happens when the contents of the unzipped deployment # package sent to lambda is too large error_cls = DeploymentPackageTooLargeError return error_cls(error, context) def delete_function(self, function_name: str) -> None: lambda_client = self._client('lambda') try: lambda_client.delete_function(FunctionName=function_name) except lambda_client.exceptions.ResourceNotFoundException: raise ResourceDoesNotExistError(function_name) def get_custom_domain_params_v2( self, domain_name: str, endpoint_type: str, certificate_arn: str, security_policy: Optional[str] = None, tags: Optional[StrMap] = None, ) -> Dict[str, Any]: kwargs: Dict[str, Any] = { 'DomainName': domain_name, 'DomainNameConfigurations': [ { 'ApiGatewayDomainName': domain_name, 'CertificateArn': certificate_arn, 'EndpointType': endpoint_type, 'SecurityPolicy': security_policy, 'DomainNameStatus': 'AVAILABLE', } ], } if tags: kwargs['Tags'] = tags return kwargs def get_custom_domain_patch_operations( self, certificate_arn: str, endpoint_type: Optional[str], security_policy: Optional[str] = None, ) -> List[Dict[str, str]]: patch_operations = [] if security_policy is not None: patch_operations.append( { 'op': 'replace', 'path': '/securityPolicy', 'value': security_policy, } ) if endpoint_type == 'EDGE': patch_operations.append( { 'op': 'replace', 'path': '/certificateArn', 'value': certificate_arn, } ) else: patch_operations.append( { 'op': 'replace', 'path': '/regionalCertificateArn', 'value': certificate_arn, } ) return patch_operations def update_domain_name( self, protocol: str, domain_name: str, endpoint_type: str, certificate_arn: str, security_policy: Optional[str] = None, tags: Optional[StrMap] = None, ) -> DomainNameResponse: if protocol == 'HTTP': patch_operations = self.get_custom_domain_patch_operations( certificate_arn, endpoint_type, security_policy, ) updated_domain_name = self._update_domain_name( domain_name, patch_operations ) elif protocol == 'WEBSOCKET': kwargs = self.get_custom_domain_params_v2( domain_name=domain_name, endpoint_type=endpoint_type, security_policy=security_policy, certificate_arn=certificate_arn, ) updated_domain_name = self._update_domain_name_v2(kwargs) else: raise ValueError('Unsupported protocol value.') resource_arn = ( 'arn:{partition}:apigateway:{region_name}:' ':/domainnames/{domain_name}'.format( partition=self.partition_name, region_name=self.region_name, domain_name=domain_name, ) ) self._update_resource_tags(resource_arn, tags) return updated_domain_name def _update_resource_tags( self, resource_arn: str, requested_tags: Optional[Dict[str, str]] ) -> None: if not requested_tags: requested_tags = {} remote_tags = self._client('apigatewayv2').get_tags( ResourceArn=resource_arn )['Tags'] self._remove_unrequested_resource_tags( resource_arn, requested_tags, remote_tags ) self._add_missing_or_differing_value_resource_tags( resource_arn, requested_tags, remote_tags ) def _remove_unrequested_resource_tags( self, resource_arn: str, requested_tags: Dict[Any, Any], remote_tags: Dict[Any, Any], ) -> None: tag_keys_to_remove = list(set(remote_tags) - set(requested_tags)) if tag_keys_to_remove: self._client('apigatewayv2').untag_resource( ResourceArn=resource_arn, TagKeys=tag_keys_to_remove ) def _add_missing_or_differing_value_resource_tags( self, resource_arn: str, requested_tags: Dict[Any, Any], remote_tags: Dict[Any, Any], ) -> None: tags_to_add = { k: v for k, v in requested_tags.items() if k not in remote_tags or v != remote_tags[k] } if tags_to_add: self._client('apigatewayv2').tag_resource( ResourceArn=resource_arn, Tags=tags_to_add ) def _update_domain_name( self, custom_domain_name: str, patch_operations: List[Dict[str, str]] ) -> DomainNameResponse: client = self._client('apigateway') exceptions = (client.exceptions.TooManyRequestsException,) result = {} for patch_operation in patch_operations: api_args = { 'domainName': custom_domain_name, 'patchOperations': [patch_operation], } response = self._call_client_method_with_retries( client.update_domain_name, api_args, max_attempts=6, should_retry=lambda x: True, retryable_exceptions=exceptions, ) result.update(response) if result.get('regionalCertificateArn'): certificate_arn = result['regionalCertificateArn'] else: certificate_arn = result['certificateArn'] if result.get('regionalHostedZoneId'): hosted_zone_id = result['regionalHostedZoneId'] else: hosted_zone_id = result['distributionHostedZoneId'] if result.get('regionalDomainName') is not None: alias_domain_name = result['regionalDomainName'] else: alias_domain_name = result['distributionDomainName'] domain_name: DomainNameResponse = { 'domain_name': result['domainName'], 'security_policy': result['securityPolicy'], 'certificate_arn': certificate_arn, 'hosted_zone_id': hosted_zone_id, 'alias_domain_name': alias_domain_name, } return domain_name def _update_domain_name_v2( self, api_args: Dict[str, Any] ) -> DomainNameResponse: client = self._client('apigatewayv2') exceptions = (client.exceptions.TooManyRequestsException,) result = self._call_client_method_with_retries( client.update_domain_name, api_args, max_attempts=6, should_retry=lambda x: True, retryable_exceptions=exceptions, ) result_data = result['DomainNameConfigurations'][0] domain_name: DomainNameResponse = { 'domain_name': result['DomainName'], 'alias_domain_name': result_data['ApiGatewayDomainName'], 'security_policy': result_data['SecurityPolicy'], 'hosted_zone_id': result_data['HostedZoneId'], 'certificate_arn': result_data['CertificateArn'], } return domain_name def delete_domain_name(self, domain_name: str) -> None: client = self._client('apigatewayv2') params = {'DomainName': domain_name} exceptions = (client.exceptions.TooManyRequestsException,) self._call_client_method_with_retries( client.delete_domain_name, params, max_attempts=6, should_retry=lambda x: True, retryable_exceptions=exceptions, ) def delete_api_mapping(self, domain_name: str, path_key: str) -> None: client = self._client('apigateway') params = {'domainName': domain_name, 'basePath': path_key} client.delete_base_path_mapping(**params) def update_function( self, function_name: str, zip_contents: str, environment_variables: Optional[StrMap] = None, runtime: OptStr = None, tags: Optional[StrMap] = None, xray: Optional[bool] = None, timeout: OptInt = None, memory_size: OptInt = None, role_arn: OptStr = None, subnet_ids: OptStrList = None, security_group_ids: OptStrList = None, layers: OptStrList = None, ) -> Dict[str, Any]: """Update a Lambda function's code and configuration. This method only updates the values provided to it. If a parameter is not provided, no changes will be made for that that parameter on the targeted lambda function. """ return_value = self._update_function_code( function_name=function_name, zip_contents=zip_contents ) self._update_function_config( environment_variables=environment_variables, runtime=runtime, timeout=timeout, memory_size=memory_size, role_arn=role_arn, xray=xray, subnet_ids=subnet_ids, security_group_ids=security_group_ids, function_name=function_name, layers=layers, ) if tags is not None: self._update_function_tags(return_value['FunctionArn'], tags) return return_value def _update_function_code( self, function_name: str, zip_contents: str ) -> Dict[str, Any]: lambda_client = self._client('lambda') try: result = lambda_client.update_function_code( FunctionName=function_name, ZipFile=zip_contents ) except _REMOTE_CALL_ERRORS as e: context = LambdaErrorContext( function_name, 'update_function_code', len(zip_contents) ) raise self._get_lambda_code_deployment_error(e, context) if result['LastUpdateStatus'] != 'Successful': self._wait_for_function_update(function_name) return result def _wait_for_function_update(self, function_name: str) -> None: client = self._client('lambda') waiter = client.get_waiter('function_updated') waiter.wait(FunctionName=function_name) def put_function_concurrency( self, function_name: str, reserved_concurrent_executions: int ) -> None: lambda_client = self._client('lambda') lambda_client.put_function_concurrency( FunctionName=function_name, ReservedConcurrentExecutions=reserved_concurrent_executions, ) def delete_function_concurrency(self, function_name: str) -> None: lambda_client = self._client('lambda') lambda_client.delete_function_concurrency(FunctionName=function_name) def _update_function_config( self, environment_variables: StrMap, runtime: OptStr, timeout: OptInt, memory_size: OptInt, role_arn: OptStr, subnet_ids: OptStrList, security_group_ids: OptStrList, function_name: str, layers: OptStrList, xray: Optional[bool], ) -> None: kwargs: Dict[str, Any] = {} if environment_variables is not None: kwargs['Environment'] = {'Variables': environment_variables} if runtime is not None: kwargs['Runtime'] = runtime if timeout is not None: kwargs['Timeout'] = timeout if memory_size is not None: kwargs['MemorySize'] = memory_size if role_arn is not None: kwargs['Role'] = role_arn if xray: kwargs['TracingConfig'] = {'Mode': 'Active'} if security_group_ids is not None and subnet_ids is not None: kwargs['VpcConfig'] = self._create_vpc_config( subnet_ids=subnet_ids, security_group_ids=security_group_ids ) if layers is not None: kwargs['Layers'] = layers if kwargs: self._do_update_function_config(function_name, kwargs) def _do_update_function_config( self, function_name: str, kwargs: Dict[str, Any] ) -> None: kwargs['FunctionName'] = function_name lambda_client = self._client('lambda') result = self._call_client_method_with_retries( lambda_client.update_function_configuration, kwargs, max_attempts=self.LAMBDA_CREATE_ATTEMPTS, ) if result['LastUpdateStatus'] != 'Successful': self._wait_for_function_update(function_name) def _update_function_tags( self, function_arn: str, requested_tags: Dict[str, str] ) -> None: remote_tags = self._client('lambda').list_tags(Resource=function_arn)[ 'Tags' ] self._remove_unrequested_remote_tags( function_arn, requested_tags, remote_tags ) self._add_missing_or_differing_value_requested_tags( function_arn, requested_tags, remote_tags ) def _remove_unrequested_remote_tags( self, function_arn: str, requested_tags: Dict[Any, Any], remote_tags: Dict[Any, Any], ) -> None: tag_keys_to_remove = list(set(remote_tags) - set(requested_tags)) if tag_keys_to_remove: self._client('lambda').untag_resource( Resource=function_arn, TagKeys=tag_keys_to_remove ) def _add_missing_or_differing_value_requested_tags( self, function_arn: str, requested_tags: Dict[Any, Any], remote_tags: Dict[Any, Any], ) -> None: tags_to_add = { k: v for k, v in requested_tags.items() if k not in remote_tags or v != remote_tags[k] } if tags_to_add: self._client('lambda').tag_resource( Resource=function_arn, Tags=tags_to_add ) def get_role_arn_for_name(self, name: str) -> str: role = self.get_role(name) return role['Arn'] def get_role(self, name: str) -> Dict[str, Any]: client = self._client('iam') try: role = client.get_role(RoleName=name) except client.exceptions.NoSuchEntityException: raise ResourceDoesNotExistError("No role ARN found for: %s" % name) return role['Role'] def delete_role_policy(self, role_name: str, policy_name: str) -> None: self._client('iam').delete_role_policy( RoleName=role_name, PolicyName=policy_name ) def put_role_policy( self, role_name: str, policy_name: str, policy_document: Dict[str, Any] ) -> None: # Note: policy_document is not JSON encoded. self._client('iam').put_role_policy( RoleName=role_name, PolicyName=policy_name, PolicyDocument=json.dumps(policy_document, indent=2), ) def create_role( self, name: str, trust_policy: Dict[str, Any], policy: Dict[str, Any] ) -> str: client = self._client('iam') response = client.create_role( RoleName=name, AssumeRolePolicyDocument=json.dumps(trust_policy) ) role_arn = response['Role']['Arn'] try: self.put_role_policy( role_name=name, policy_name=name, policy_document=policy ) except client.exceptions.MalformedPolicyDocumentException as e: self.delete_role(name=name) raise e return role_arn def delete_role(self, name: str) -> None: """Delete a role by first deleting all inline policies.""" client = self._client('iam') inline_policies = client.list_role_policies(RoleName=name)[ 'PolicyNames' ] for policy_name in inline_policies: self.delete_role_policy(name, policy_name) client.delete_role(RoleName=name) def log_group_exists(self, name: str) -> bool: """Check if an CloudWatch LOG GROUP exists.""" client = self._client('logs') result = client.describe_log_groups( logGroupNamePrefix=name ) if len(result['logGroups']) == 0: return False return True def create_log_group(self, log_group_name: str) -> None: self._client('logs').create_log_group( logGroupName=log_group_name, ) def delete_retention_policy(self, log_group_name: str) -> None: self._client('logs').delete_retention_policy( logGroupName=log_group_name, ) def delete_log_group(self, log_group_name: str) -> None: self._client('logs').delete_log_group( logGroupName=log_group_name, ) def put_retention_policy(self, name: str, retention_in_days: int) -> None: self._client('logs').put_retention_policy( logGroupName=name, retentionInDays=retention_in_days ) def get_rest_api_id(self, name: str) -> Optional[str]: """Get rest api id associated with an API name. :type name: str :param name: The name of the rest api. :rtype: str :return: If the rest api exists, then the restApiId is returned, otherwise None. """ rest_apis = self._client('apigateway').get_rest_apis()['items'] for api in rest_apis: if api['name'] == name: return api['id'] return None def get_rest_api(self, rest_api_id: str) -> Dict[str, Any]: """Check if an API Gateway REST API exists.""" client = self._client('apigateway') try: result = client.get_rest_api(restApiId=rest_api_id) result.pop('ResponseMetadata', None) return result except client.exceptions.NotFoundException: return {} def import_rest_api( self, swagger_document: Dict[str, Any], endpoint_type: str ) -> str: client = self._client('apigateway') response = client.import_rest_api( body=json.dumps(swagger_document, indent=2), parameters={'endpointConfigurationTypes': endpoint_type}, ) rest_api_id = response['id'] return rest_api_id def update_api_from_swagger( self, rest_api_id: str, swagger_document: Dict[str, Any] ) -> None: client = self._client('apigateway') client.put_rest_api( restApiId=rest_api_id, mode='overwrite', body=json.dumps(swagger_document, indent=2), ) def update_rest_api( self, rest_api_id: str, patch_operations: List[Dict] ) -> None: client = self._client('apigateway') client.update_rest_api( restApiId=rest_api_id, patchOperations=patch_operations ) def delete_rest_api(self, rest_api_id: str) -> None: client = self._client('apigateway') try: client.delete_rest_api(restApiId=rest_api_id) except client.exceptions.NotFoundException: raise ResourceDoesNotExistError(rest_api_id) def deploy_rest_api( self, rest_api_id: str, api_gateway_stage: str, xray: bool ) -> None: client = self._client('apigateway') client.create_deployment( restApiId=rest_api_id, stageName=api_gateway_stage, tracingEnabled=bool(xray), ) def add_permission_for_apigateway( self, function_name: str, region_name: str, account_id: str, rest_api_id: str, random_id: Optional[str] = None, ) -> None: """Authorize API gateway to invoke a lambda function is needed. This method will first check if API gateway has permission to call the lambda function, and only if necessary will it invoke ``self.add_permission_for_apigateway(...). """ source_arn = self._build_source_arn_str( region_name, account_id, rest_api_id ) self._add_lambda_permission_if_needed( source_arn=source_arn, function_arn=function_name, service_name='apigateway', ) def add_permission_for_apigateway_v2( self, function_name: str, region_name: str, account_id: str, api_id: str, random_id: Optional[str] = None, ) -> None: """Authorize API gateway v2 to invoke a lambda function.""" source_arn = self._build_source_arn_str( region_name, account_id, api_id ) self._add_lambda_permission_if_needed( source_arn=source_arn, function_arn=function_name, service_name='apigateway', ) def get_function_policy(self, function_name: str) -> Dict[str, Any]: """Return the function policy for a lambda function. This function will extract the policy string as a json document and return the json.loads(...) version of the policy. """ client = self._client('lambda') try: policy = client.get_policy(FunctionName=function_name) return json.loads(policy['Policy']) except client.exceptions.ResourceNotFoundException: return {'Statement': []} def download_sdk( self, rest_api_id: str, output_dir: str, api_gateway_stage: str = DEFAULT_STAGE_NAME, sdk_type: str = 'javascript', ) -> None: """Download an SDK to a directory. This will generate an SDK and download it to the provided ``output_dir``. If you're using ``get_sdk_download_stream()``, you have to handle downloading the stream and unzipping the contents yourself. This method handles that for you. """ zip_stream = self.get_sdk_download_stream( rest_api_id, api_gateway_stage=api_gateway_stage, sdk_type=sdk_type ) tmpdir = tempfile.mkdtemp() with open(os.path.join(tmpdir, 'sdk.zip'), 'wb') as f: f.write(zip_stream.read()) tmp_extract = os.path.join(tmpdir, 'extracted') with zipfile.ZipFile(os.path.join(tmpdir, 'sdk.zip')) as z: z.extractall(tmp_extract) # The extract zip dir will have a single directory: # ['apiGateway-js-sdk'] dirnames = os.listdir(tmp_extract) if len(dirnames) == 1: full_dirname = os.path.join(tmp_extract, dirnames[0]) if os.path.isdir(full_dirname): final_dirname = 'chalice-%s-sdk' % sdk_type full_renamed_name = os.path.join(tmp_extract, final_dirname) os.rename(full_dirname, full_renamed_name) shutil.move(full_renamed_name, output_dir) return raise RuntimeError( "The downloaded SDK had an unexpected directory structure: %s" % (', '.join(dirnames)) ) def get_sdk_download_stream( self, rest_api_id: str, api_gateway_stage: str = DEFAULT_STAGE_NAME, sdk_type: str = 'javascript', ) -> IO[bytes]: """Generate an SDK for a given SDK. Returns a file like object that streams a zip contents for the generated SDK. """ response = self._client('apigateway').get_sdk( restApiId=rest_api_id, stageName=api_gateway_stage, sdkType=sdk_type, ) return response['body'] def subscribe_function_to_topic( self, topic_arn: str, function_arn: str ) -> str: sns_client = self._client('sns') response = sns_client.subscribe( TopicArn=topic_arn, Protocol='lambda', Endpoint=function_arn ) return response['SubscriptionArn'] def unsubscribe_from_topic(self, subscription_arn: str) -> None: sns_client = self._client('sns') sns_client.unsubscribe(SubscriptionArn=subscription_arn) def verify_sns_subscription_current( self, subscription_arn: str, topic_name: str, function_arn: str ) -> bool: """Verify a subscription arn matches the topic and function name. Given a subscription arn, verify that the associated topic name and function arn match up to the parameters passed in. """ sns_client = self._client('sns') try: attributes = sns_client.get_subscription_attributes( SubscriptionArn=subscription_arn )['Attributes'] return ( # Splitting on ':' is safe because topic names can't have # a ':' char. attributes['TopicArn'].rsplit(':', 1)[1] == topic_name and attributes['Endpoint'] == function_arn ) except sns_client.exceptions.NotFoundException: return False def add_permission_for_sns_topic( self, topic_arn: str, function_arn: str ) -> None: self._add_lambda_permission_if_needed( source_arn=topic_arn, function_arn=function_arn, service_name='sns', ) def remove_permission_for_sns_topic( self, topic_arn: str, function_arn: str ) -> None: self._remove_lambda_permission_if_needed( source_arn=topic_arn, function_arn=function_arn, service_name='sns', ) def _build_source_arn_str( self, region_name: str, account_id: str, rest_api_id: str ) -> str: source_arn = ( 'arn:{partition}:execute-api:' '{region_name}:{account_id}:{rest_api_id}/*' ).format( partition=self.partition_name, region_name=region_name, # Assuming same account id for lambda function and API gateway. account_id=account_id, rest_api_id=rest_api_id, ) return source_arn @property def partition_name(self) -> str: return self._client('apigateway').meta.partition @property def region_name(self) -> str: return self._client('apigateway').meta.region_name def iter_log_events( self, log_group_name: str, start_time: Optional[datetime] = None, interleaved: bool = True, ) -> Iterator[CWLogEvent]: logs = self._client('logs') paginator = logs.get_paginator('filter_log_events') pages = paginator.paginate( logGroupName=log_group_name, interleaved=True ) try: yield from self._iter_log_messages(pages) except logs.exceptions.ResourceNotFoundException: # If the lambda function exists but has not been invoked yet, # it's possible that the log group does not exist and we'll get # a ResourceNotFoundException. If this happens we return instead # of propagating an exception back to the user. pass def _iter_log_messages( self, pages: Iterable[Dict[str, Any]] ) -> Iterator[CWLogEvent]: for page in pages: events = page['events'] for event in events: # timestamp is modeled as a 'long', so we'll # convert to a datetime to make it easier to use # in python. event['ingestionTime'] = self._convert_to_datetime( event['ingestionTime'] ) event['timestamp'] = self._convert_to_datetime( event['timestamp'] ) yield event def _convert_to_datetime(self, integer_timestamp: int) -> datetime: return datetime.utcfromtimestamp(integer_timestamp / 1000.0) def filter_log_events( self, log_group_name: str, start_time: Optional[datetime] = None, next_token: Optional[str] = None, ) -> LogEventsResponse: logs = self._client('logs') kwargs = { 'logGroupName': log_group_name, 'interleaved': True, } if start_time is not None: kwargs['startTime'] = int(datetime2timestamp(start_time) * 1000) if next_token is not None: kwargs['nextToken'] = next_token try: response = logs.filter_log_events(**kwargs) except logs.exceptions.ResourceNotFoundException: # If there's no log messages yet then we'll just return # an empty response. return {'events': []} # We want to convert the individual events that have integer # types over to datetime objects so it's easier for us to # work with. self._convert_types_on_response(response) return response def _convert_types_on_response(self, response: Dict[str, Any]) -> None: response['events'] = list(self._iter_log_messages([response])) def _client(self, service_name: str) -> Any: if service_name not in self._client_cache: self._client_cache[service_name] = self._session.create_client( service_name ) return self._client_cache[service_name] def add_permission_for_authorizer( self, rest_api_id: str, function_arn: str, random_id: Optional[str] = None, ) -> None: client = self._client('apigateway') # This is actually a paginated operation, but botocore does not # support this style of pagination right now. The max authorizers # for an API is 10, so we're ok for now. We will need to circle # back on this eventually. authorizers = client.get_authorizers(restApiId=rest_api_id) for authorizer in authorizers['items']: if function_arn in authorizer['authorizerUri']: authorizer_id = authorizer['id'] break else: raise ResourceDoesNotExistError( "Unable to find authorizer associated " "with function ARN: %s" % function_arn ) parts = function_arn.split(':') partition = parts[1] region_name = parts[3] account_id = parts[4] function_name = parts[-1] source_arn = "arn:%s:execute-api:%s:%s:%s/authorizers/%s" % ( partition, region_name, account_id, rest_api_id, authorizer_id, ) dns_suffix = self.endpoint_dns_suffix('apigateway', region_name) if random_id is None: random_id = self._random_id() self._client('lambda').add_permission( Action='lambda:InvokeFunction', FunctionName=function_name, StatementId=random_id, Principal=self.service_principal( 'apigateway', self.region_name, dns_suffix ), SourceArn=source_arn, ) def get_or_create_rule_arn( self, rule_name: str, schedule_expression: Optional[str] = None, event_pattern: Optional[str] = None, rule_description: Optional[str] = None, ) -> str: events = self._client('events') # put_rule is idempotent so we can safely call it even if it already # exists. params = {'Name': rule_name} if schedule_expression: params['ScheduleExpression'] = schedule_expression elif event_pattern: params['EventPattern'] = event_pattern else: raise ValueError("schedule_expression or event_pattern required") if rule_description is not None: params['Description'] = rule_description rule_arn = events.put_rule(**params) return rule_arn['RuleArn'] def delete_rule(self, rule_name: str) -> None: events = self._client('events') # In put_targets call, we have used Id='1' events.remove_targets(Rule=rule_name, Ids=['1']) events.delete_rule(Name=rule_name) def connect_rule_to_lambda( self, rule_name: str, function_arn: str ) -> None: events = self._client('events') events.put_targets( Rule=rule_name, Targets=[{'Id': '1', 'Arn': function_arn}] ) def add_permission_for_cloudwatch_event( self, rule_arn: str, function_arn: str ) -> None: self._add_lambda_permission_if_needed( source_arn=rule_arn, function_arn=function_arn, service_name='events', ) def connect_s3_bucket_to_lambda( self, bucket: str, function_arn: str, events: List[str], prefix: OptStr = None, suffix: OptStr = None, ) -> None: """Configure S3 bucket to invoke a lambda function. The S3 bucket must already have permission to invoke the lambda function before you call this function, otherwise the service will return an error. You can add permissions by using the ``add_permission_for_s3_event`` below. The ``events`` param matches the event strings supported by the service. This method also only supports a single prefix/suffix for now, which is what's offered in the Lambda console. """ s3 = self._client('s3') existing_config = s3.get_bucket_notification_configuration( Bucket=bucket ) # Because we're going to PUT this config back to S3, we need # to remove `ResponseMetadata` because that's added in botocore # and isn't a param of the put_bucket_notification_configuration. existing_config.pop('ResponseMetadata', None) existing_lambda_config = existing_config.get( 'LambdaFunctionConfigurations', [] ) single_config: Dict[str, Any] = { 'LambdaFunctionArn': function_arn, 'Events': events, } filter_rules = [] if prefix is not None: filter_rules.append({'Name': 'Prefix', 'Value': prefix}) if suffix is not None: filter_rules.append({'Name': 'Suffix', 'Value': suffix}) if filter_rules: single_config['Filter'] = {'Key': {'FilterRules': filter_rules}} new_config = self._merge_s3_notification_config( existing_lambda_config, single_config ) existing_config['LambdaFunctionConfigurations'] = new_config s3.put_bucket_notification_configuration( Bucket=bucket, NotificationConfiguration=existing_config, ) def _merge_s3_notification_config( self, existing_config: List[Dict[str, Any]], new_config: Dict[str, Any] ) -> List[Dict[str, Any]]: # Add the new_config to the existing_config. # We have to handle two cases: # 1. There's an existing config associated with the lambda arn. # In this case we replace the specific lambda config with the # new_config. # 2. The new_config isn't part of the existing_config. In # this case we just add it to the end of the existing config. final_config = [] added_config = False for config in existing_config: if config['LambdaFunctionArn'] != new_config['LambdaFunctionArn']: final_config.append(config) else: # Case 1, replace the existing config. final_config.append(new_config) added_config = True if not added_config: # Case 2, add it to the end of the existing list of configs. final_config.append(new_config) return final_config def add_permission_for_s3_event( self, bucket: str, function_arn: str, account_id: str ) -> None: bucket_arn = 'arn:{partition}:s3:::{bucket}'.format( partition=self.partition_name, bucket=bucket ) self._add_lambda_permission_if_needed( source_arn=bucket_arn, function_arn=function_arn, service_name='s3', source_account=account_id, ) def remove_permission_for_s3_event( self, bucket: str, function_arn: str, account_id: str ) -> None: bucket_arn = 'arn:{partition}:s3:::{bucket}'.format( partition=self.partition_name, bucket=bucket ) self._remove_lambda_permission_if_needed( source_arn=bucket_arn, function_arn=function_arn, service_name='s3', source_account=account_id, ) def disconnect_s3_bucket_from_lambda( self, bucket: str, function_arn: str ) -> None: s3 = self._client('s3') existing_config = s3.get_bucket_notification_configuration( Bucket=bucket ) existing_config.pop('ResponseMetadata', None) existing_lambda_config = existing_config.get( 'LambdaFunctionConfigurations', [] ) new_lambda_config = [] for config in existing_lambda_config: if config['LambdaFunctionArn'] == function_arn: continue new_lambda_config.append(config) existing_config['LambdaFunctionConfigurations'] = new_lambda_config s3.put_bucket_notification_configuration( Bucket=bucket, NotificationConfiguration=existing_config, ) def _add_lambda_permission_if_needed( self, source_arn: str, function_arn: str, service_name: str, source_account: Optional[str] = None, ) -> None: policy = self.get_function_policy(function_arn) if self._policy_gives_access(policy, source_arn, service_name): return random_id = self._random_id() dns_suffix = self.endpoint_dns_suffix_from_arn(source_arn) kwargs = { 'Action': 'lambda:InvokeFunction', 'FunctionName': function_arn, 'StatementId': random_id, 'Principal': self.service_principal( service_name, self.region_name, dns_suffix ), 'SourceArn': source_arn, } if source_account is not None: kwargs['SourceAccount'] = source_account self._client('lambda').add_permission(**kwargs) def _policy_gives_access( self, policy: Dict[str, Any], source_arn: str, service_name: str ) -> bool: # Here's what a sample policy looks like after add_permission() # has been previously called: # { # "Id": "default", # "Statement": [ # { # "Action": "lambda:InvokeFunction", # "Condition": { # "ArnLike": { # "AWS:SourceArn": # } # }, # "Effect": "Allow", # "Principal": { # "Service": "apigateway.amazonaws.com" # }, # "Resource": "arn:aws:lambda:us-west-2:aid:function:name", # "Sid": "e4755709-067e-4254-b6ec-e7f9639e6f7b" # } # ], # "Version": "2012-10-17" # } # So we need to check if there's a policy that looks like this. for statement in policy.get('Statement', []): if self._statement_gives_arn_access( statement, source_arn, service_name ): return True return False def _statement_gives_arn_access( self, statement: Dict[str, Any], source_arn: str, service_name: str, source_account: Optional[str] = None, ) -> bool: dns_suffix = self.endpoint_dns_suffix_from_arn(source_arn) principal = self.service_principal( service_name, self.region_name, dns_suffix ) if not statement['Action'] == 'lambda:InvokeFunction': return False if ( statement.get('Condition', {}) .get('ArnLike', {}) .get('AWS:SourceArn', '') != source_arn ): return False if statement.get('Principal', {}).get('Service', '') != principal: return False if source_account is not None: if ( statement.get('Condition', {}) .get('StringEquals', {}) .get('AWS:SourceAccount', '') != source_account ): return False # We're not checking the "Resource" key because we're assuming # that lambda.get_policy() is returning the policy for the particular # resource in question. return True def _remove_lambda_permission_if_needed( self, source_arn: str, function_arn: str, service_name: str, source_account: Optional[str] = None, ) -> None: client = self._client('lambda') policy = self.get_function_policy(function_arn) for statement in policy.get('Statement', []): kwargs = { 'statement': statement, 'source_arn': source_arn, 'service_name': service_name, } if source_account is not None: kwargs['source_account'] = source_account if self._statement_gives_arn_access(**kwargs): client.remove_permission( FunctionName=function_arn, StatementId=statement['Sid'], ) def create_lambda_event_source( self, event_source_arn: str, function_name: str, batch_size: int, starting_position: Optional[str] = None, maximum_batching_window_in_seconds: Optional[int] = 0, maximum_concurrency: Optional[int] = None, ) -> None: lambda_client = self._client('lambda') batch_window = maximum_batching_window_in_seconds kwargs = { 'EventSourceArn': event_source_arn, 'FunctionName': function_name, 'BatchSize': batch_size, 'MaximumBatchingWindowInSeconds': batch_window, } if maximum_concurrency: kwargs['ScalingConfig'] = { 'MaximumConcurrency': maximum_concurrency } if starting_position is not None: kwargs['StartingPosition'] = starting_position return self._call_client_method_with_retries( lambda_client.create_event_source_mapping, kwargs, max_attempts=self.LAMBDA_CREATE_ATTEMPTS, )['UUID'] def update_lambda_event_source( self, event_uuid: str, batch_size: int, maximum_batching_window_in_seconds: Optional[int] = 0, maximum_concurrency: Optional[int] = None, ) -> None: lambda_client = self._client('lambda') batch_window = maximum_batching_window_in_seconds kwargs = { 'UUID': event_uuid, 'BatchSize': batch_size, 'MaximumBatchingWindowInSeconds': batch_window, } if maximum_concurrency: kwargs['ScalingConfig'] = { 'MaximumConcurrency': maximum_concurrency } self._call_client_method_with_retries( lambda_client.update_event_source_mapping, kwargs, max_attempts=10, should_retry=self._is_settling_error, ) def remove_lambda_event_source(self, event_uuid: str) -> None: lambda_client = self._client('lambda') self._call_client_method_with_retries( lambda_client.delete_event_source_mapping, {'UUID': event_uuid}, max_attempts=10, should_retry=self._is_settling_error, ) def verify_event_source_current( self, event_uuid: str, resource_name: str, service_name: str, function_arn: str, ) -> bool: """Check if the uuid matches the resource and function arn provided. Given a uuid representing an event source mapping for a lambda function, verify that the associated source arn and function arn match up to the parameters passed in. Instead of providing the event source arn, the resource name is provided along with the service name. For example, if we're checking an SQS queue event source, the resource name would be the queue name (e.g. ``myqueue``) and the service would be ``sqs``. """ client = self._client('lambda') try: attributes = client.get_event_source_mapping(UUID=event_uuid) actual_arn = attributes['EventSourceArn'] arn_start, actual_name = actual_arn.rsplit(':', 1) return bool( actual_name == resource_name and re.match("^arn:aws[a-z\\-]*:%s" % service_name, arn_start) and attributes['FunctionArn'] == function_arn ) except client.exceptions.ResourceNotFoundException: return False def verify_event_source_arn_current( self, event_uuid: str, event_source_arn: str, function_arn: str ) -> bool: """Check if the uuid matches the event and function ARN. This is similar to verify_event_source_current, except that you provide an explicit event_source_arn here. This is useful for cases where you know the event source ARN or where you can't construct the event source arn solely based on the resource_name and the service_name. """ client = self._client('lambda') try: attributes = client.get_event_source_mapping(UUID=event_uuid) except client.exceptions.ResourceNotFoundException: return False return bool( event_source_arn == attributes['EventSourceArn'] and function_arn == attributes['FunctionArn'] ) def create_websocket_api(self, name: str) -> str: client = self._client('apigatewayv2') return self._call_client_method_with_retries( client.create_api, kwargs={ 'Name': name, 'ProtocolType': 'WEBSOCKET', 'RouteSelectionExpression': '$request.body.action', }, max_attempts=10, should_retry=self._is_settling_error, )['ApiId'] def get_websocket_api_id(self, name: str) -> Optional[str]: apis = self._client('apigatewayv2').get_apis()['Items'] for api in apis: if api['Name'] == name: return api['ApiId'] return None def websocket_api_exists(self, api_id: str) -> bool: """Check if an API Gateway WEBSOCKET API exists.""" client = self._client('apigatewayv2') try: client.get_api(ApiId=api_id) return True except client.exceptions.NotFoundException: return False def delete_websocket_api(self, api_id: str) -> None: client = self._client('apigatewayv2') try: client.delete_api(ApiId=api_id) except client.exceptions.NotFoundException: raise ResourceDoesNotExistError(api_id) def create_websocket_integration( self, api_id: str, lambda_function: str, handler_type: str, ) -> str: client = self._client('apigatewayv2') return client.create_integration( ApiId=api_id, ConnectionType='INTERNET', ContentHandlingStrategy='CONVERT_TO_TEXT', Description=handler_type, IntegrationType='AWS_PROXY', IntegrationUri=lambda_function, )['IntegrationId'] def create_websocket_route( self, api_id: str, route_key: str, integration_id: str ) -> None: client = self._client('apigatewayv2') client.create_route( ApiId=api_id, RouteKey=route_key, RouteResponseSelectionExpression='$default', Target='integrations/%s' % integration_id, ) def delete_websocket_routes(self, api_id: str, routes: List[str]) -> None: client = self._client('apigatewayv2') for route_id in routes: client.delete_route( ApiId=api_id, RouteId=route_id, ) def delete_websocket_integrations( self, api_id: str, integrations: Dict[str, str] ) -> None: client = self._client('apigatewayv2') for integration_id in integrations: client.delete_integration( ApiId=api_id, IntegrationId=integration_id, ) def deploy_websocket_api(self, api_id: str) -> str: client = self._client('apigatewayv2') return client.create_deployment( ApiId=api_id, )['DeploymentId'] def get_websocket_routes(self, api_id: str) -> List[str]: client = self._client('apigatewayv2') return [ i['RouteId'] for i in client.get_routes( ApiId=api_id, )['Items'] ] def get_websocket_integrations(self, api_id: str) -> List[str]: client = self._client('apigatewayv2') return [ item['IntegrationId'] for item in client.get_integrations(ApiId=api_id)['Items'] ] def create_stage( self, api_id: str, stage_name: str, deployment_id: str ) -> None: client = self._client('apigatewayv2') client.create_stage( ApiId=api_id, StageName=stage_name, DeploymentId=deployment_id, ) def _call_client_method_with_retries( self, method: ClientMethod, kwargs: Dict[str, Any], max_attempts: int, should_retry: Optional[Callable[[Exception], bool]] = None, delay_time: int = DELAY_TIME, retryable_exceptions: Optional[Sequence[Exception]] = None, ) -> Dict[str, Any]: attempts = 0 if should_retry is None: should_retry = self._is_iam_role_related_error if not retryable_exceptions: client = self._client('lambda') retryable_exceptions = ( # We're assuming that if we receive an # InvalidParameterValueException, it's because the role we just # created can't be used by Lambda so retry until it can be. client.exceptions.InvalidParameterValueException, client.exceptions.ResourceInUseException, ) while True: try: response = method(**kwargs) except retryable_exceptions as e: # type: ignore self._sleep(delay_time) attempts += 1 if attempts >= max_attempts or not should_retry(e): raise continue return response def _random_id(self) -> str: return str(uuid.uuid4()) ================================================ FILE: chalice/cdk/__init__.py ================================================ import warnings try: from chalice.cdk.construct import Chalice except ImportError: warnings.warn('Unable to import the Chalice CDK construct due to missing ' 'dependencies.\nYou can install these by running ' "'pip install \"chalice[cdk]\"'") __all__ = ['Chalice'] ================================================ FILE: chalice/cdk/construct.py ================================================ import json import os import uuid from typing import List, Dict, Optional, Any # noqa from aws_cdk import ( aws_s3_assets as assets, cloudformation_include, aws_iam as iam, aws_lambda as lambda_, ) try: from aws_cdk.core import Construct from aws_cdk import core as cdk # noqa except ImportError: import aws_cdk as cdk # noqa from constructs import Construct from chalice import api class Chalice(Construct): """Chalice construct for CDK. Packages the application into AWS SAM format and imports the resulting template into the construct tree under the provided ``scope``. """ # pylint: disable=redefined-builtin # The 'id' parameter name is CDK convention. def __init__(self, scope, # type: Construct id, # type: str source_dir, # type: str stage_config=None, # type: Optional[Dict[str, Any]] preserve_logical_ids=True, # type: bool **kwargs # type: Dict[str, Any] ): # type: (...) -> None """Initialize Chalice construct. :param str source_dir: Path to Chalice application source code. :param dict stage_config: Chalice stage configuration. The configuration object should have the same structure as Chalice JSON stage configuration. :param bool preserve_logical_ids: Whether the resources should have the same logical IDs in the resulting CDK template as they did in the original CloudFormation template file. If you're vending a Construct using cdk-chalice, make sure to pass this as ``False``. Note: regardless of whether this option is true or false, the :attr:`sam_template`'s ``get_resource`` and related methods always uses the original logical ID of the resource/element, as specified in the template file. :raises `ChaliceError`: Error packaging the Chalice application. """ super(Chalice, self).__init__(scope, id, **kwargs) #: (:class:`str`) Path to Chalice application source code. self.source_dir = os.path.abspath(source_dir) #: (:class:`str`) Chalice stage name. #: It is automatically assigned the encompassing CDK ``scope``'s name. self.stage_name = scope.to_string() #: (:class:`dict`) Chalice stage configuration. #: The object has the same structure as Chalice JSON stage #: configuration. self.stage_config = stage_config chalice_out_dir = os.path.join(os.getcwd(), 'chalice.out') package_id = uuid.uuid4().hex self._sam_package_dir = os.path.join(chalice_out_dir, package_id) self._package_app() sam_template_filename = self._generate_sam_template_with_assets( chalice_out_dir, package_id) #: (:class:`aws_cdk.cloudformation_include.CfnInclude`) AWS SAM #: template updated with AWS CDK values where applicable. Can be #: used to reference, access, and customize resources generated #: by `chalice package` commandas CDK native objects. self.sam_template = cloudformation_include.CfnInclude( self, 'ChaliceApp', template_file=sam_template_filename, preserve_logical_ids=preserve_logical_ids) self._function_cache = {} # type: Dict[str, lambda_.IFunction] self._role_cache = {} # type: Dict[str, iam.IRole] def _package_app(self): # type: () -> None api.package_app( project_dir=self.source_dir, output_dir=self._sam_package_dir, stage=self.stage_name, chalice_config=self.stage_config, ) def _generate_sam_template_with_assets(self, chalice_out_dir, package_id): # type: (str, str) -> str deployment_zip_path = os.path.join( self._sam_package_dir, 'deployment.zip') sam_deployment_asset = assets.Asset( self, 'ChaliceAppCode', path=deployment_zip_path) sam_template_path = os.path.join(self._sam_package_dir, 'sam.json') sam_template_with_assets_path = os.path.join( chalice_out_dir, '%s.sam_with_assets.json' % package_id) with open(sam_template_path) as sam_template_file: sam_template = json.load(sam_template_file) for function in self._filter_resources( sam_template, 'AWS::Serverless::Function'): function['Properties']['CodeUri'] = { 'Bucket': sam_deployment_asset.s3_bucket_name, 'Key': sam_deployment_asset.s3_object_key } managed_layers = self._filter_resources( sam_template, 'AWS::Serverless::LayerVersion') if len(managed_layers) == 1: layer_filename = os.path.join( self._sam_package_dir, 'layer-deployment.zip') layer_asset = assets.Asset( self, 'ChaliceManagedLayer', path=layer_filename) managed_layers[0]['Properties']['ContentUri'] = { 'Bucket': layer_asset.s3_bucket_name, 'Key': layer_asset.s3_object_key } with open(sam_template_with_assets_path, 'w') as f: f.write(json.dumps(sam_template, indent=2)) return sam_template_with_assets_path def _filter_resources(self, template, resource_type): # type: (Dict[str, Any], str) -> List[Dict[str, Any]] return [resource for resource in template['Resources'].values() if resource['Type'] == resource_type] def get_resource(self, resource_name): # type: (str) -> cdk.core.CfnResource return self.sam_template.get_resource(resource_name) def get_role(self, role_name): # type: (str) -> iam.IRole if role_name not in self._role_cache: cfn_role = self.sam_template.get_resource(role_name) # Pylint is incorrectly identifying this as a static method call # but it's actually decorated as a @builtins.classmethod method. # pylint: disable=no-value-for-parameter role = iam.Role.from_role_arn(self, role_name, cfn_role.attr_arn) self._role_cache[role_name] = role return self._role_cache[role_name] def get_function(self, function_name): # type: (str) -> lambda_.IFunction if function_name not in self._function_cache: cfn_lambda = self.sam_template.get_resource(function_name) arn_ref = cfn_lambda.get_att('Arn') # Pylint is incorrectly identifying this as a static method call # but it's actually decorated as a @builtins.classmethod method. # pylint: disable=no-value-for-parameter function = lambda_.Function.from_function_arn( self, function_name, arn_ref.to_string()) self._function_cache[function_name] = function return self._function_cache[function_name] def add_environment_variable(self, key, value, function_name): # type: (str, str, str) -> None cfn_function = self.sam_template.get_resource(function_name) cfn_function.add_override( 'Properties.Environment.Variables.%s' % key, value) ================================================ FILE: chalice/cli/__init__.py ================================================ """Command line interface for chalice. Contains commands for deploying chalice. """ from __future__ import annotations import logging import os import platform import sys import tempfile import shutil import traceback import functools import json import botocore.exceptions import click from typing import Dict, Any, Optional, cast # noqa from chalice import __version__ as chalice_version from chalice.app import Chalice # noqa from chalice.awsclient import TypedAWSClient from chalice.awsclient import ReadTimeout from chalice.cli.factory import CLIFactory from chalice.cli.factory import NoSuchFunctionError from chalice.config import Config # noqa from chalice.logs import display_logs, LogRetrieveOptions from chalice.utils import create_zip_file from chalice.deploy.validate import validate_routes, validate_python_version from chalice.deploy.validate import ExperimentalFeatureError from chalice.utils import UI, serialize_to_json from chalice.constants import DEFAULT_STAGE_NAME from chalice.local import LocalDevServer # noqa from chalice.constants import DEFAULT_HANDLER_NAME from chalice.invoke import UnhandledLambdaError from chalice.deploy.swagger import TemplatedSwaggerGenerator from chalice.deploy.planner import PlanEncoder from chalice.deploy.appgraph import ApplicationGraphBuilder, GraphPrettyPrint from chalice.cli import newproj def _configure_logging(level, format_string=None): # type: (int, Optional[str]) -> None if format_string is None: format_string = "%(asctime)s %(name)s [%(levelname)s] %(message)s" logger = logging.getLogger('') logger.setLevel(level) handler = logging.StreamHandler() handler.setLevel(level) formatter = logging.Formatter(format_string) handler.setFormatter(formatter) logger.addHandler(handler) def get_system_info(): # type: () -> str python_info = "python {}.{}.{}".format(sys.version_info[0], sys.version_info[1], sys.version_info[2]) platform_system = platform.system().lower() platform_release = platform.release() platform_info = "{} {}".format(platform_system, platform_release) return "{}, {}".format(python_info, platform_info) @click.group() @click.version_option(version=chalice_version, message='%(prog)s %(version)s, {}' .format(get_system_info())) @click.option('--project-dir', help='The project directory path (absolute or relative).' 'Defaults to CWD') @click.option('--debug/--no-debug', default=False, help='Print debug logs to stderr.') @click.pass_context def cli(ctx, project_dir, debug=False): # type: (click.Context, str, bool) -> None if project_dir is None: project_dir = os.getcwd() elif not os.path.isabs(project_dir): project_dir = os.path.abspath(project_dir) if debug is True: _configure_logging(logging.DEBUG) _configure_cli_env_vars() ctx.obj['project_dir'] = project_dir ctx.obj['debug'] = debug ctx.obj['factory'] = CLIFactory(project_dir, debug, environ=os.environ) os.chdir(project_dir) def _configure_cli_env_vars(): # type: () -> None # This will set chalice specific env vars so users can detect if # we're running a Chalice CLI command. This is useful if you want # conditional behavior only when we're actually running in Lambda # in your app.py file. os.environ['AWS_CHALICE_CLI_MODE'] = 'true' @cli.command() @click.option('--host', default='127.0.0.1') @click.option('--port', default=8000, type=click.INT) @click.option('--stage', default=DEFAULT_STAGE_NAME, help='Name of the Chalice stage for the local server to use.') @click.option('--autoreload/--no-autoreload', default=True, help='Automatically restart server when code changes.') @click.pass_context def local(ctx, host='127.0.0.1', port=8000, stage=DEFAULT_STAGE_NAME, autoreload=True): # type: (click.Context, str, int, str, bool) -> None factory = ctx.obj['factory'] # type: CLIFactory from chalice.cli import reloader # We don't create the server here because that will bind the # socket and we only want to do this in the worker process. server_factory = functools.partial( create_local_server, factory, host, port, stage) # When running `chalice local`, a stdout logger is configured # so you'll see the same stdout logging as you would when # running in lambda. This is configuring the root logger. # The app-specific logger (app.log) will still continue # to work. logging.basicConfig( stream=sys.stdout, level=logging.INFO, format='%(message)s') if autoreload: project_dir = factory.create_config_obj( chalice_stage_name=stage).project_dir rc = reloader.run_with_reloader( server_factory, os.environ, project_dir) # Click doesn't sys.exit() with the RC this function. The # recommended way to do this is to use sys.exit() directly, # see: https://github.com/pallets/click/issues/747 sys.exit(rc) run_local_server(factory, host, port, stage) def create_local_server(factory, host, port, stage): # type: (CLIFactory, str, int, str) -> LocalDevServer config = factory.create_config_obj( chalice_stage_name=stage ) app_obj = config.chalice_app # Check that `chalice deploy` would let us deploy these routes, otherwise # there is no point in testing locally. routes = config.chalice_app.routes validate_routes(routes) server = factory.create_local_server(app_obj, config, host, port) return server def run_local_server(factory, host, port, stage): # type: (CLIFactory, str, int, str) -> None server = create_local_server(factory, host, port, stage) server.serve_forever() @cli.command() @click.option('--autogen-policy/--no-autogen-policy', default=None, help='Automatically generate IAM policy for app code.') @click.option('--profile', help='Override profile at deploy time.') @click.option('--api-gateway-stage', help='Name of the API gateway stage to deploy to.') @click.option('--stage', default=DEFAULT_STAGE_NAME, help=('Name of the Chalice stage to deploy to. ' 'Specifying a new chalice stage will create ' 'an entirely new set of AWS resources.')) @click.option('--connection-timeout', type=int, help=('Overrides the default botocore connection ' 'timeout.')) @click.pass_context def deploy(ctx, autogen_policy, profile, api_gateway_stage, stage, connection_timeout): # type: (click.Context, Optional[bool], str, str, str, int) -> None factory = ctx.obj['factory'] # type: CLIFactory factory.profile = profile config = factory.create_config_obj( chalice_stage_name=stage, autogen_policy=autogen_policy, api_gateway_stage=api_gateway_stage, ) session = factory.create_botocore_session( connection_timeout=connection_timeout) ui = UI() d = factory.create_default_deployer(session=session, config=config, ui=ui) deployed_values = d.deploy(config, chalice_stage_name=stage) reporter = factory.create_deployment_reporter(ui=ui) reporter.display_report(deployed_values) @cli.group() def dev(): # type: () -> None """Development and debugging commands for chalice. All the commands under the "chalice dev" namespace are provided to help chalice developers introspect the internals of chalice. They are also useful for users to better understand the chalice deployment process. These commands are provided for informational purposes only. There is NO guarantee of backwards compatibility for any "chalice dev" commands. Do not rely on the output of these commands. These commands allow introspection of chalice internals, and the internals of chalice are subject to change as needed. """ @dev.command() @click.option('--autogen-policy/--no-autogen-policy', default=None, help='Automatically generate IAM policy for app code.') @click.option('--profile', help='Override profile at deploy time.') @click.option('--api-gateway-stage', help='Name of the API gateway stage to deploy to.') @click.option('--stage', default=DEFAULT_STAGE_NAME, help=('Name of the Chalice stage to deploy to. ' 'Specifying a new chalice stage will create ' 'an entirely new set of AWS resources.')) @click.pass_context def plan(ctx, autogen_policy, profile, api_gateway_stage, stage): # type: (click.Context, Optional[bool], str, str, str) -> None """Generate and display deployment plan. This command will calculate and pretty print the deployment plan without actually executing the plan. It's primarily used to better understand the chalice deployment process. """ factory = ctx.obj['factory'] # type: CLIFactory factory.profile = profile config = factory.create_config_obj( chalice_stage_name=stage, autogen_policy=autogen_policy, api_gateway_stage=api_gateway_stage, ) session = factory.create_botocore_session() ui = UI() d = factory.create_plan_only_deployer( session=session, config=config, ui=ui) d.deploy(config, chalice_stage_name=stage) @dev.command() @click.option('--autogen-policy/--no-autogen-policy', default=None, help='Automatically generate IAM policy for app code.') @click.option('--profile', help='Override profile at deploy time.') @click.option('--api-gateway-stage', help='Name of the API gateway stage to deploy to.') @click.option('--stage', default=DEFAULT_STAGE_NAME, help=('Name of the Chalice stage to deploy to. ' 'Specifying a new chalice stage will create ' 'an entirely new set of AWS resources.')) @click.pass_context def appgraph(ctx, autogen_policy, profile, api_gateway_stage, stage): # type: (click.Context, Optional[bool], str, str, str) -> None """Generate and display the application graph.""" factory = ctx.obj['factory'] # type: CLIFactory factory.profile = profile config = factory.create_config_obj( chalice_stage_name=stage, autogen_policy=autogen_policy, api_gateway_stage=api_gateway_stage, ) graph_build = ApplicationGraphBuilder() graph = graph_build.build(config, stage) ui = UI() GraphPrettyPrint(ui).display_graph(graph) @cli.command('invoke') @click.option('-n', '--name', metavar='NAME', required=True, help=('The name of the function to invoke. ' 'This is the logical name of the function. If the ' 'function is decorated by app.route use the name ' 'api_handler instead.')) @click.option('--profile', metavar='PROFILE', help='Override profile at deploy time.') @click.option('--stage', metavar='STAGE', default=DEFAULT_STAGE_NAME, help=('Name of the Chalice stage to deploy to. ' 'Specifying a new chalice stage will create ' 'an entirely new set of AWS resources.')) @click.pass_context def invoke(ctx, name, profile, stage): # type: (click.Context, str, str, str) -> None """Invoke the deployed lambda function NAME. Reads payload from STDIN. """ factory = ctx.obj['factory'] # type: CLIFactory factory.profile = profile try: invoke_handler = factory.create_lambda_invoke_handler(name, stage) payload = factory.create_stdin_reader().read() invoke_handler.invoke(payload) except NoSuchFunctionError as e: err = click.ClickException( "could not find a lambda function named %s." % e.name) err.exit_code = 2 raise err except botocore.exceptions.ClientError as e: error = e.response['Error'] err = click.ClickException( "got '%s' exception back from Lambda\n%s" % (error['Code'], error['Message'])) err.exit_code = 1 raise err except UnhandledLambdaError: err = click.ClickException( "Unhandled exception in Lambda function, details above.") err.exit_code = 1 raise err except ReadTimeout as e: err = click.ClickException(e.message) err.exit_code = 1 raise err @cli.command('delete') @click.option('--profile', help='Override profile at deploy time.') @click.option('--stage', default=DEFAULT_STAGE_NAME, help='Name of the Chalice stage to delete.') @click.pass_context def delete(ctx, profile, stage): # type: (click.Context, str, str) -> None factory = ctx.obj['factory'] # type: CLIFactory factory.profile = profile config = factory.create_config_obj(chalice_stage_name=stage) session = factory.create_botocore_session() d = factory.create_deletion_deployer(session=session, ui=UI()) d.deploy(config, chalice_stage_name=stage) @cli.command() @click.option('--num-entries', default=None, type=int, help='Max number of log entries to show.') @click.option('--include-lambda-messages/--no-include-lambda-messages', default=False, help='Controls whether or not lambda log messages are included.') @click.option('--stage', default=DEFAULT_STAGE_NAME, help='Name of the Chalice stage to get logs for.') @click.option('-n', '--name', help='The name of the lambda function to retrieve logs from.', default=DEFAULT_HANDLER_NAME) @click.option('-s', '--since', help=('Only display logs since the provided time. If the ' '-f/--follow option is specified, then this value will ' 'default to 10 minutes from the current time. Otherwise ' 'by default all log messages are displayed. This value ' 'can either be an ISO8601 formatted timestamp or a ' 'relative time. For relative times provide a number ' 'and a single unit. Units can be "s" for seconds, ' '"m" for minutes, "h" for hours, "d" for days, and "w" ' 'for weeks. For example "5m" would indicate to display ' 'logs starting five minutes in the past.'), default=None) @click.option('-f', '--follow/--no-follow', default=False, help=('Continuously poll for new log messages. Note that this ' 'is a best effort attempt, and in certain cases can ' 'miss log messages. This option is intended for ' 'interactive usage only.')) @click.option('--profile', help='The profile to use for fetching logs.') @click.pass_context def logs(ctx, num_entries, include_lambda_messages, stage, name, since, follow, profile): # type: (click.Context, int, bool, str, str, str, bool, str) -> None factory = ctx.obj['factory'] # type: CLIFactory factory.profile = profile config = factory.create_config_obj(stage, False) deployed = config.deployed_resources(stage) if name in deployed.resource_names(): lambda_arn = deployed.resource_values(name)['lambda_arn'] session = factory.create_botocore_session() retriever = factory.create_log_retriever( session, lambda_arn, follow) options = LogRetrieveOptions.create( max_entries=num_entries, since=since, include_lambda_messages=include_lambda_messages, ) display_logs(retriever, sys.stdout, options) @cli.command('gen-policy') @click.option('--filename', help='The filename to analyze. Otherwise app.py is assumed.') @click.pass_context def gen_policy(ctx, filename): # type: (click.Context, str) -> None from chalice import policy if filename is None: filename = os.path.join(ctx.obj['project_dir'], 'app.py') if not os.path.isfile(filename): click.echo("App file does not exist: %s" % filename, err=True) raise click.Abort() with open(filename) as f: contents = f.read() generated = policy.policy_from_source_code(contents) click.echo(serialize_to_json(generated)) @cli.command('new-project') @click.argument('project_name', required=False) @click.option('--profile', required=False) @click.option('-t', '--project-type', required=False, default='legacy') @click.pass_context def new_project(ctx, project_name, profile, project_type): # type: (click.Context, str, str, str) -> None if project_name is None: prompter = ctx.obj.get('prompter', newproj.getting_started_prompt) answers = prompter() project_name = answers['project_name'] project_type = answers['project_type'] if os.path.isdir(project_name): click.echo("Directory already exists: %s" % project_name, err=True) raise click.Abort() newproj.create_new_project_skeleton( project_name, project_type=project_type) validate_python_version(Config.create()) click.echo("Your project has been generated in ./%s" % project_name) @cli.command('url') @click.option('--stage', default=DEFAULT_STAGE_NAME, help='Name of the Chalice stage to get the deployed URL for.') @click.pass_context def url(ctx, stage): # type: (click.Context, str) -> None factory = ctx.obj['factory'] # type: CLIFactory config = factory.create_config_obj(stage) deployed = config.deployed_resources(stage) if deployed is not None and 'rest_api' in deployed.resource_names(): click.echo(deployed.resource_values('rest_api')['rest_api_url']) else: e = click.ClickException( "Could not find a record of a Rest API in chalice stage: '%s'" % stage) e.exit_code = 2 raise e @cli.command('generate-sdk') @click.option('--sdk-type', default='javascript', type=click.Choice(['javascript'])) @click.option('--stage', default=DEFAULT_STAGE_NAME, help='Name of the Chalice stage to generate an SDK for.') @click.argument('outdir') @click.pass_context def generate_sdk(ctx, sdk_type, stage, outdir): # type: (click.Context, str, str, str) -> None factory = ctx.obj['factory'] # type: CLIFactory config = factory.create_config_obj(stage) session = factory.create_botocore_session() client = TypedAWSClient(session) deployed = config.deployed_resources(stage) if deployed is not None and 'rest_api' in deployed.resource_names(): rest_api_id = deployed.resource_values('rest_api')['rest_api_id'] api_gateway_stage = config.api_gateway_stage client.download_sdk(rest_api_id, outdir, api_gateway_stage=api_gateway_stage, sdk_type=sdk_type) else: click.echo("Could not find API ID, has this application " "been deployed?", err=True) raise click.Abort() @cli.command('generate-models') @click.option('--stage', default=DEFAULT_STAGE_NAME, help="Chalice Stage for which to generate models.") @click.pass_context def generate_models(ctx, stage): # type: (click.Context, str) -> None """Generate a model from Chalice routes. Currently only supports generating Swagger 2.0 models. """ factory = ctx.obj['factory'] # type: CLIFactory config = factory.create_config_obj(stage) if not config.chalice_app.routes: click.echo('No REST API found to generate model from.') raise click.Abort() swagger_generator = TemplatedSwaggerGenerator() model = swagger_generator.generate_swagger( config.chalice_app, ) ui = UI() ui.write(json.dumps(model, indent=4, cls=PlanEncoder)) ui.write('\n') @cli.command('package') @click.option('--pkg-format', default='cloudformation', help=('Specify the provisioning engine to use for ' 'template output. Chalice supports both ' 'CloudFormation and Terraform. Default ' 'is CloudFormation.'), type=click.Choice(['cloudformation', 'terraform'])) @click.option('--stage', default=DEFAULT_STAGE_NAME, help="Chalice Stage to package.") @click.option('--single-file', is_flag=True, default=False, help=("Create a single packaged file. " "By default, the 'out' argument " "specifies a directory in which the " "package assets will be placed. If " "this argument is specified, a single " "zip file will be created instead. CloudFormation Only.")) @click.option('--merge-template', help=('Specify a JSON or YAML template to be merged ' 'into the generated template. This is useful ' 'for adding resources to a Chalice template or ' 'modify values in the template. CloudFormation Only.')) @click.option('--template-format', default='json', type=click.Choice(['json', 'yaml'], case_sensitive=False), help=('Specify if the generated template should be serialized ' 'as either JSON or YAML. CloudFormation only.')) @click.option('--profile', help='Override profile at packaging time.') @click.argument('out') @click.pass_context def package(ctx, single_file, stage, merge_template, out, pkg_format, template_format, profile): # type: (click.Context, bool, str, str, str, str, str, str) -> None factory = ctx.obj['factory'] # type: CLIFactory factory.profile = profile config = factory.create_config_obj(stage) options = factory.create_package_options() packager = factory.create_app_packager(config, options, pkg_format, template_format, merge_template) if pkg_format == 'terraform' and (merge_template or single_file or template_format != 'json'): # I don't see any reason we couldn't support --single-file for # terraform if we wanted to. click.echo(( "Terraform format does not support " "--merge-template, --single-file, or --template-format")) raise click.Abort() if single_file: dirname = tempfile.mkdtemp() try: packager.package_app(config, dirname, stage) create_zip_file(source_dir=dirname, outfile=out) finally: shutil.rmtree(dirname) else: packager.package_app(config, out, stage) @cli.command('generate-pipeline') @click.option('--pipeline-version', default='v1', type=click.Choice(['v1', 'v2']), help='Which version of the pipeline template to generate.') @click.option('-i', '--codebuild-image', help=("Specify default codebuild image to use. " "This option must be provided when using a python " "version besides 2.7.")) @click.option('-s', '--source', default='codecommit', type=click.Choice(['codecommit', 'github']), help=("Specify the input source. The default value of " "'codecommit' will create a CodeCommit repository " "for you. The 'github' value allows you to " "reference an existing GitHub repository.")) @click.option('-b', '--buildspec-file', help=("Specify path for buildspec.yml file. " "By default, the build steps are included in the " "generated cloudformation template. If this option " "is provided, a buildspec.yml will be generated " "as a separate file and not included in the cfn " "template. This allows you to make changes to how " "the project is built without having to redeploy " "a CloudFormation template. This file should be " "named 'buildspec.yml' and placed in the root " "directory of your app.")) @click.argument('filename') @click.pass_context def generate_pipeline(ctx, pipeline_version, codebuild_image, source, buildspec_file, filename): # type: (click.Context, str, str, str, str, str) -> None """Generate a cloudformation template for a starter CD pipeline. This command will write a starter cloudformation template to the filename you provide. It contains a CodeCommit repo, a CodeBuild stage for packaging your chalice app, and a CodePipeline stage to deploy your application using cloudformation. You can use any AWS SDK or the AWS CLI to deploy this stack. Here's an example using the AWS CLI: \b $ chalice generate-pipeline pipeline.json $ aws cloudformation deploy --stack-name mystack \b --template-file pipeline.json --capabilities CAPABILITY_IAM """ from chalice import pipeline factory = ctx.obj['factory'] # type: CLIFactory config = factory.create_config_obj() p = cast(pipeline.BasePipelineTemplate, None) if pipeline_version == 'v1': p = pipeline.CreatePipelineTemplateLegacy() else: p = pipeline.CreatePipelineTemplateV2() params = pipeline.PipelineParameters( app_name=config.app_name, lambda_python_version=config.lambda_python_version, codebuild_image=codebuild_image, code_source=source, pipeline_version=pipeline_version, ) output = p.create_template(params) if buildspec_file: extractor = pipeline.BuildSpecExtractor() buildspec_contents = extractor.extract_buildspec(output) with open(buildspec_file, 'w') as f: f.write(buildspec_contents) with open(filename, 'w') as f: f.write(serialize_to_json(output)) def main(): # type: () -> int # click's dynamic attrs will allow us to pass through # 'obj' via the context object, so we're ignoring # these error messages from pylint because we know it's ok. # pylint: disable=unexpected-keyword-arg,no-value-for-parameter try: return cli(obj={}) except botocore.exceptions.NoRegionError: click.echo("No region configured. " "Either export the AWS_DEFAULT_REGION " "environment variable or set the " "region value in our ~/.aws/config file.", err=True) return 2 except ExperimentalFeatureError as e: click.echo(str(e)) return 2 except Exception: click.echo(traceback.format_exc(), err=True) return 2 ================================================ FILE: chalice/cli/factory.py ================================================ from __future__ import annotations import sys import os import json import importlib import logging import functools import click from botocore.config import Config as BotocoreConfig from botocore.session import Session from typing import Any, Optional, Dict, MutableMapping, cast # noqa from chalice import __version__ as chalice_version from chalice.awsclient import TypedAWSClient from chalice.app import Chalice # noqa from chalice.config import Config from chalice.config import DeployedResources # noqa from chalice.package import create_app_packager from chalice.package import AppPackager # noqa from chalice.package import PackageOptions from chalice.constants import DEFAULT_STAGE_NAME from chalice.constants import DEFAULT_APIGATEWAY_STAGE_NAME from chalice.constants import DEFAULT_ENDPOINT_TYPE from chalice.logs import LogRetriever, LogEventGenerator from chalice.logs import FollowLogEventGenerator from chalice.logs import BaseLogEventGenerator from chalice import local from chalice.utils import UI # noqa from chalice.utils import PipeReader # noqa from chalice.deploy import deployer # noqa from chalice.deploy import validate from chalice.invoke import LambdaInvokeHandler from chalice.invoke import LambdaInvoker from chalice.invoke import LambdaResponseFormatter OptStr = Optional[str] OptInt = Optional[int] def create_botocore_session( profile: OptStr = None, debug: bool = False, connection_timeout: OptInt = None, read_timeout: OptInt = None, max_retries: OptInt = None, ) -> Session: s = Session(profile=profile) _add_chalice_user_agent(s) if debug: _inject_large_request_body_filter() config_args: Dict[str, Any] = {} if connection_timeout is not None: config_args['connect_timeout'] = connection_timeout if read_timeout is not None: config_args['read_timeout'] = read_timeout if max_retries is not None: config_args['retries'] = {'max_attempts': max_retries} if config_args: config = BotocoreConfig(**config_args) s.set_default_client_config(config) return s def _add_chalice_user_agent(session: Session) -> None: suffix = '%s/%s' % (session.user_agent_name, session.user_agent_version) session.user_agent_name = 'aws-chalice' session.user_agent_version = chalice_version session.user_agent_extra = suffix def _inject_large_request_body_filter() -> None: log = logging.getLogger('botocore.endpoint') log.addFilter(LargeRequestBodyFilter()) class NoSuchFunctionError(Exception): """The specified function could not be found.""" def __init__(self, name: str) -> None: self.name = name super(NoSuchFunctionError, self).__init__() class UnknownConfigFileVersion(Exception): def __init__(self, version: str) -> None: super(UnknownConfigFileVersion, self).__init__( "Unknown version '%s' in config.json" % version ) class LargeRequestBodyFilter(logging.Filter): def filter(self, record: Any) -> bool: # Note: the proper type should be "logging.LogRecord", but # the typechecker complains about 'Invalid index type "int" for "dict"' # so we're using Any for now. if record.msg.startswith('Making request'): if record.args[0].name in ['UpdateFunctionCode', 'CreateFunction']: # When using the ZipFile argument (which is used in chalice), # the entire deployment package zip is sent as a base64 encoded # string. We don't want this to clutter the debug logs # so we don't log the request body for lambda operations # that have the ZipFile arg. record.args = record.args[:-1] + ( '(... omitted from logs due to size ...)', ) return True class CLIFactory(object): def __init__( self, project_dir: str, debug: bool = False, profile: Optional[str] = None, environ: Optional[MutableMapping] = None, ) -> None: self.project_dir = project_dir self.debug = debug self.profile = profile if environ is None: environ = dict(os.environ) self._environ = environ def create_botocore_session( self, connection_timeout: OptInt = None, read_timeout: OptInt = None, max_retries: OptInt = None, ) -> Session: return create_botocore_session( profile=self.profile, debug=self.debug, connection_timeout=connection_timeout, read_timeout=read_timeout, max_retries=max_retries, ) def create_default_deployer( self, session: Session, config: Config, ui: UI ) -> deployer.Deployer: return deployer.create_default_deployer(session, config, ui) def create_plan_only_deployer( self, session: Session, config: Config, ui: UI ) -> deployer.Deployer: return deployer.create_plan_only_deployer(session, config, ui) def create_deletion_deployer( self, session: Session, ui: UI ) -> deployer.Deployer: return deployer.create_deletion_deployer(TypedAWSClient(session), ui) def create_deployment_reporter( self, ui: UI ) -> deployer.DeploymentReporter: return deployer.DeploymentReporter(ui=ui) def create_config_obj( self, chalice_stage_name: str = DEFAULT_STAGE_NAME, autogen_policy: Optional[bool] = None, api_gateway_stage: Optional[str] = None, user_provided_params: Optional[Dict[str, Any]] = None, ) -> Config: if user_provided_params is None: user_provided_params = {} default_params = { 'project_dir': self.project_dir, 'api_gateway_stage': DEFAULT_APIGATEWAY_STAGE_NAME, 'api_gateway_endpoint_type': DEFAULT_ENDPOINT_TYPE, 'autogen_policy': True, } try: config_from_disk = self.load_project_config() except (OSError, IOError): raise RuntimeError( "Unable to load the project config file. " "Are you sure this is a chalice project?" ) except ValueError as err: raise RuntimeError( "Unable to load the project config file: %s" % err ) self._validate_config_from_disk(config_from_disk) if autogen_policy is not None: user_provided_params['autogen_policy'] = autogen_policy if self.profile is not None: user_provided_params['profile'] = self.profile if api_gateway_stage is not None: user_provided_params['api_gateway_stage'] = api_gateway_stage config = Config( chalice_stage=chalice_stage_name, user_provided_params=user_provided_params, config_from_disk=config_from_disk, default_params=default_params, ) user_provided_params['chalice_app'] = functools.partial( self.load_chalice_app, config.environment_variables ) return config def _validate_config_from_disk(self, config: Dict[str, Any]) -> None: string_version = config.get('version', '1.0') try: version = float(string_version) if version > 2.0: raise UnknownConfigFileVersion(string_version) except ValueError: raise UnknownConfigFileVersion(string_version) def create_app_packager( self, config: Config, options: PackageOptions, package_format: str, template_format: str, merge_template: OptStr = None, ) -> AppPackager: return create_app_packager( config, options, package_format, template_format, merge_template=merge_template, ) def create_log_retriever( self, session: Session, lambda_arn: str, follow_logs: bool ) -> LogRetriever: client = TypedAWSClient(session) if follow_logs: event_generator = cast( BaseLogEventGenerator, FollowLogEventGenerator(client) ) else: event_generator = cast( BaseLogEventGenerator, LogEventGenerator(client) ) retriever = LogRetriever.create_from_lambda_arn( event_generator, lambda_arn ) return retriever def create_stdin_reader(self) -> PipeReader: stream = click.get_binary_stream('stdin') reader = PipeReader(stream) return reader def create_lambda_invoke_handler( self, name: str, stage: str ) -> LambdaInvokeHandler: config = self.create_config_obj(stage) deployed = config.deployed_resources(stage) try: resource = deployed.resource_values(name) arn = resource['lambda_arn'] except (KeyError, ValueError): raise NoSuchFunctionError(name) function_scoped_config = config.scope(stage, name) # The session for max retries needs to be set to 0 for invoking a # lambda function because in the case of a timeout or other retriable # error the underlying client will call the function again. session = self.create_botocore_session( read_timeout=function_scoped_config.lambda_timeout, max_retries=0, ) client = TypedAWSClient(session) invoker = LambdaInvoker(arn, client) handler = LambdaInvokeHandler( invoker, LambdaResponseFormatter(), UI(), ) return handler def load_chalice_app( self, environment_variables: Optional[MutableMapping] = None, validate_feature_flags: Optional[bool] = True, ) -> Chalice: # validate_features indicates that we should validate that # any expiremental features used have the appropriate feature flags. if self.project_dir not in sys.path: sys.path.insert(0, self.project_dir) # The vendor directory has its contents copied up to the top level of # the deployment package. This means that imports will work in the # lambda function as if the vendor directory is on the python path. # For loading the config locally we must add the vendor directory to # the path so it will be treated the same as if it were running on # lambda. vendor_dir = os.path.join(self.project_dir, 'vendor') if os.path.isdir(vendor_dir) and vendor_dir not in sys.path: # This is a tradeoff we have to make for local use. # The common use case of vendor/ is to include # extension modules built for AWS Lambda. If you're # running on a non-linux dev machine, then attempting # to import these files will raise exceptions. As # a workaround, the vendor is added to the end of # sys.path so it's after `./lib/site-packages`. # This gives you a change to install the correct # version locally and still keep the lambda # specific one in vendor/ sys.path.append(vendor_dir) if environment_variables is not None: self._environ.update(environment_variables) try: app = importlib.import_module('app') chalice_app = getattr(app, 'app') except SyntaxError as e: message = ( 'Unable to import your app.py file:\n\n' 'File "%s", line %s\n' ' %s\n' 'SyntaxError: %s' ) % (getattr(e, 'filename'), e.lineno, e.text, e.msg) raise RuntimeError(message) if validate_feature_flags: validate.validate_feature_flags(chalice_app) return chalice_app def load_project_config(self) -> Dict[str, Any]: """Load the chalice config file from the project directory. :raise: OSError/IOError if unable to load the config file. """ config_file = os.path.join(self.project_dir, '.chalice', 'config.json') with open(config_file) as f: return json.loads(f.read()) def create_local_server( self, app_obj: Chalice, config: Config, host: str, port: int ) -> local.LocalDevServer: return local.create_local_server(app_obj, config, host, port) def create_package_options(self) -> PackageOptions: """Create the package options that are required to target regions.""" s = Session(profile=self.profile) client = TypedAWSClient(session=s) return PackageOptions(client) ================================================ FILE: chalice/cli/filewatch/__init__.py ================================================ import threading from typing import Callable, Optional, Type # noqa from chalice.local import HTTPServerThread # noqa RESTART_REQUEST_RC = 3 class FileWatcher(object): """Base class for watching files for changes.""" def watch_for_file_changes(self, root_dir, callback): # type: (str, Callable[[], None]) -> None """Recursively watch directory for changes. When a changed file is detected, the provided callback is immediately invoked and the current scan stops. """ raise NotImplementedError("watch_for_file_changes") class WorkerProcess(object): """Worker that runs the chalice dev server.""" def __init__(self, http_thread): # type: (HTTPServerThread) -> None self._http_thread = http_thread self._restart_event = threading.Event() def main(self, project_dir, timeout=None): # type: (str, Optional[int]) -> int self._http_thread.start() self._start_file_watcher(project_dir) if self._restart_event.wait(timeout): self._http_thread.shutdown() return RESTART_REQUEST_RC return 0 def _start_file_watcher(self, project_dir): # type: (str) -> None raise NotImplementedError("_start_file_watcher") ================================================ FILE: chalice/cli/filewatch/eventbased.py ================================================ import threading # noqa from typing import Callable, Optional # noqa import watchdog.observers # pylint: disable=import-error from watchdog import events # pylint: disable=import-error from chalice.cli.filewatch import FileWatcher, WorkerProcess class WatchdogWorkerProcess(WorkerProcess): """Worker that runs the chalice dev server.""" def _start_file_watcher(self, project_dir): # type: (str) -> None restart_callback = WatchdogRestarter(self._restart_event) watcher = WatchdogFileWatcher() watcher.watch_for_file_changes( project_dir, restart_callback) class WatchdogFileWatcher(FileWatcher): def watch_for_file_changes(self, root_dir, callback): # type: (str, Callable[[], None]) -> None observer = watchdog.observers.Observer() observer.schedule(callback, root_dir, recursive=True) observer.start() class WatchdogRestarter(events.FileSystemEventHandler): def __init__(self, restart_event): # type: (threading.Event) -> None # The reason we're using threading self.restart_event = restart_event def on_any_event(self, event): # type: (events.FileSystemEvent) -> None # If we modify a file we'll get a FileModifiedEvent # as well as a DirectoryModifiedEvent. # We only care about reloading is a file is modified. if event.is_directory: return self() def __call__(self): # type: () -> None self.restart_event.set() ================================================ FILE: chalice/cli/filewatch/stat.py ================================================ import logging import threading import time from typing import Callable, Dict, Optional, Iterator # noqa from chalice.cli.filewatch import FileWatcher, WorkerProcess from chalice.utils import OSUtils LOGGER = logging.getLogger(__name__) class StatWorkerProcess(WorkerProcess): def _start_file_watcher(self, project_dir): # type: (str) -> None watcher = StatFileWatcher() watcher.watch_for_file_changes(project_dir, self._on_file_change) def _on_file_change(self): # type: () -> None self._restart_event.set() class StatFileWatcher(FileWatcher): POLL_INTERVAL = 1 def __init__(self, osutils=None): # type: (Optional[OSUtils]) -> None self._mtime_cache = {} # type: Dict[str, float] self._shutdown_event = threading.Event() self._thread = None # type: Optional[threading.Thread] if osutils is None: osutils = OSUtils() self._osutils = osutils def watch_for_file_changes(self, root_dir, callback): # type: (str, Callable[[], None]) -> None t = threading.Thread(target=self.poll_for_changes_until_shutdown, args=(root_dir, callback)) t.daemon = True t.start() self._thread = t LOGGER.debug("Stat file watching: %s, with callback: %s", root_dir, callback) def poll_for_changes_until_shutdown(self, root_dir, callback): # type: (str, Callable[[], None]) -> None self._seed_mtime_cache(root_dir) while not self._shutdown_event.is_set(): self._single_pass_poll(root_dir, callback) time.sleep(self.POLL_INTERVAL) def _seed_mtime_cache(self, root_dir): # type: (str) -> None for rootdir, _, filenames in self._osutils.walk(root_dir): for filename in filenames: path = self._osutils.joinpath(rootdir, filename) self._mtime_cache[path] = self._osutils.mtime(path) def _single_pass_poll(self, root_dir, callback): # type: (str, Callable[[], None]) -> None new_mtimes = {} # type: Dict[str, float] for path in self._recursive_walk_files(root_dir): if self._is_changed_file(path, new_mtimes): callback() return if new_mtimes != self._mtime_cache: # Files were removed. LOGGER.debug("Files removed, triggering restart.") self._mtime_cache = new_mtimes callback() return def _is_changed_file(self, path, new_mtimes): # type: (str, Dict[str, float]) -> bool last_mtime = self._mtime_cache.get(path) if last_mtime is None: LOGGER.debug("File added: %s, triggering restart.", path) return True try: new_mtime = self._osutils.mtime(path) if new_mtime > last_mtime: LOGGER.debug("File updated: %s, triggering restart.", path) return True new_mtimes[path] = new_mtime return False except (OSError, IOError): return False def _recursive_walk_files(self, root_dir): # type: (str) -> Iterator[str] for rootdir, _, filenames in self._osutils.walk(root_dir): for filename in filenames: path = self._osutils.joinpath(rootdir, filename) yield path ================================================ FILE: chalice/cli/newproj.py ================================================ """New project generation. How it Works ============ Project template are placed with the ./chalice/templates directory. Each directory corresponds to a single template. The name of the directory has the structure ``0123-template-name``, where the first part consists of a four digit number followed by a ``-``, then the name of the template. The leading number is not exposed externally to the user and is used solely for sorting purposes so we can display the project templates in the order we prefer. The template name is the name that's used in the ``--project-type`` value for the ``new-project`` command. Each template can have a ``DESCRIPTION`` file that contains a short description of the project template. This will be used as the display value instead of the project type key if this file is available. The ``DESCRIPTION`` file is not copied over when generating the new project files. There's basic support for templating values. This allows you to write templates with placeholder values that are filled in during project generation time. These values are denoted via ``{{template_var}}``. The following keys are supported: * ``app_name`` - The name of the project. * ``chalice_version`` - The current version of chalice generating the project. """ from __future__ import print_function import os import re import json import fnmatch from dataclasses import dataclass from typing import Optional, Dict, Any, Iterator, Tuple, Match, List # noqa import inquirer from chalice.constants import WELCOME_PROMPT from chalice.utils import OSUtils from chalice.app import __version__ as chalice_version TEMPLATES_DIR = os.path.join( os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'templates' ) VAR_REF_REGEX = r'{{(.*?)}}' IGNORE_FILES = ['metadata.json', '*.pyc'] class BadTemplateError(Exception): pass def create_new_project_skeleton( project_name: str, project_type: Optional[str] = 'legacy' ) -> None: osutils = OSUtils() all_projects = list_available_projects(TEMPLATES_DIR, osutils) project = [p for p in all_projects if p.key == project_type][0] template_kwargs = { 'app_name': project_name, 'chalice_version': chalice_version, } project_creator = ProjectCreator(osutils) project_creator.create_new_project( os.path.join(TEMPLATES_DIR, project.dirname), project_name, template_kwargs=template_kwargs, ) @dataclass class ProjectTemplate: dirname: str metadata: Dict[str, Any] key: str @property def description(self) -> str: # Pylint doesn't understand the attrs types. # pylint: disable=no-member return self.metadata.get('description', self.key) class ProjectCreator(object): def __init__(self, osutils: Optional[OSUtils] = None) -> None: if osutils is None: osutils = OSUtils() self._osutils = osutils def create_new_project( self, source_dir: str, destination_dir: str, template_kwargs: Dict[str, Any], ) -> None: for full_src_path, full_dst_path in self._iter_files( source_dir, destination_dir ): dest_dir = self._osutils.dirname(full_dst_path) if not self._osutils.directory_exists(dest_dir): self._osutils.makedirs(dest_dir) contents = self._osutils.get_file_contents( full_src_path, binary=False ) templated_contents = get_templated_content( contents, template_kwargs ) self._osutils.set_file_contents( full_dst_path, templated_contents, binary=False ) def _iter_files( self, source_dir: str, destination_dir: str ) -> Iterator[Tuple[str, str]]: for rootdir, _, filenames in self._osutils.walk(source_dir): for filename in filenames: if self._should_ignore(filename): continue full_src_path = os.path.join(rootdir, filename) # The starting index needs `+ 1` to account for the # trailing `/` char (e.g. foo/bar -> foo/bar/). full_dst_path = os.path.join( destination_dir, full_src_path[len(source_dir) + 1:] ) yield full_src_path, full_dst_path def _should_ignore(self, filename: str) -> bool: for ignore in IGNORE_FILES: if fnmatch.fnmatch(filename, ignore): return True return False def get_templated_content( contents: str, template_kwargs: Dict[str, Any] ) -> str: def lookup_var(match: Match) -> str: var_name = match.group(1) try: return template_kwargs[var_name] except KeyError: raise BadTemplateError( "Bad template, referenced template var that does not " "exist: '%s', for template contents:\n%s" % (var_name, contents) ) new_contents = re.sub(VAR_REF_REGEX, lookup_var, contents) return new_contents def list_available_projects( templates_dir: str, osutils: OSUtils ) -> List[ProjectTemplate]: projects = [] for dirname in sorted(osutils.get_directory_contents(templates_dir)): filename = osutils.joinpath(templates_dir, dirname, 'metadata.json') metadata = json.loads(osutils.get_file_contents(filename, False)) key = dirname.split('-', 1)[1] projects.append(ProjectTemplate(dirname, metadata, key=key)) return projects def getting_started_prompt() -> Dict[str, Any]: print(WELCOME_PROMPT) projects = list_available_projects(TEMPLATES_DIR, OSUtils()) questions = [ inquirer.Text('project_name', message='Enter the project name'), inquirer.List( 'project_type', message='Select your project type', choices=[(p.description, p.key) for p in projects], ), ] answers = inquirer.prompt(questions) return answers ================================================ FILE: chalice/cli/reloader.py ================================================ """Automatically reload chalice app when files change. How It Works ============ This approach borrow from what django, flask, and other frameworks do. Essentially, with reloading enabled ``chalice local`` will start up a worker process that runs the dev http server. This means there will be a total of two processes running (both will show as ``chalice local`` in ps). One process is the parent process. It's job is to start up a child process and restart it if it exits (due to a restart request). The child process is the process that actually starts up the web server for local mode. The child process also sets up a watcher thread. It's job is to monitor directories for changes. If a change is encountered it sys.exit()s the process with a known RC (the RESTART_REQUEST_RC constant in the module). The parent process runs in an infinite loop. If the child process exits with an RC of RESTART_REQUEST_RC the parent process starts up another child process. The child worker is denoted by setting the ``CHALICE_WORKER`` env var. If this env var is set, the process is intended to be a worker process (as opposed the parent process which just watches for restart requests from the worker process). """ import subprocess import logging import copy import sys from typing import MutableMapping, Type, Callable, Optional # noqa from chalice.cli.filewatch import RESTART_REQUEST_RC, WorkerProcess from chalice.local import LocalDevServer, HTTPServerThread # noqa LOGGER = logging.getLogger(__name__) WorkerProcType = Optional[Type[WorkerProcess]] def get_best_worker_process(): # type: () -> Type[WorkerProcess] try: from chalice.cli.filewatch.eventbased import WatchdogWorkerProcess LOGGER.debug("Using watchdog worker process.") return WatchdogWorkerProcess except ImportError: from chalice.cli.filewatch.stat import StatWorkerProcess LOGGER.debug("Using stat() based worker process.") return StatWorkerProcess def start_parent_process(env): # type: (MutableMapping) -> None process = ParentProcess(env, subprocess.Popen) process.main() def start_worker_process(server_factory, root_dir, worker_process_cls=None): # type: (Callable[[], LocalDevServer], str, WorkerProcType) -> int if worker_process_cls is None: worker_process_cls = get_best_worker_process() t = HTTPServerThread(server_factory) worker = worker_process_cls(t) LOGGER.debug("Starting worker...") rc = worker.main(root_dir) LOGGER.info("Restarting local dev server.") return rc class ParentProcess(object): """Spawns a child process and restarts it as needed.""" def __init__(self, env, popen): # type: (MutableMapping, Type[subprocess.Popen]) -> None self._env = copy.copy(env) self._popen = popen def main(self): # type: () -> None # This method launches a child worker and restarts it if it # exits with RESTART_REQUEST_RC. This method doesn't return. # A user can Ctrl-C to stop the parent process. while True: self._env['CHALICE_WORKER'] = 'true' LOGGER.debug("Parent process starting child worker process...") process = self._popen(sys.argv, env=self._env) try: process.communicate() if process.returncode != RESTART_REQUEST_RC: return except KeyboardInterrupt: process.terminate() raise def run_with_reloader(server_factory, env, root_dir, worker_process_cls=None): # type: (Callable, MutableMapping, str, WorkerProcType) -> int # This function is invoked in two possible modes, as the parent process # or as a chalice worker. try: if env.get('CHALICE_WORKER') is not None: # This is a chalice worker. We need to start the main dev server # in a daemon thread and install a file watcher. return start_worker_process(server_factory, root_dir, worker_process_cls) else: # This is the parent process. It's just is to spawn an identical # process but with the ``CHALICE_WORKER`` env var set. It then # will monitor this process and restart it if it exits with a # RESTART_REQUEST exit code. start_parent_process(env) except KeyboardInterrupt: pass return 0 ================================================ FILE: chalice/compat.py ================================================ import socket import six import os from typing import Dict, Any # noqa from urllib.parse import urlparse, parse_qs from six import StringIO STRING_TYPES = six.string_types def pip_import_string(): # type: () -> str import pip pip_major_version = int(pip.__version__.split('.')[0]) pip_minor_version = int(pip.__version__.split('.')[1]) pip_major_minor = (pip_major_version, pip_minor_version) # Pip moved its internals to an _internal module in version 10. # In order to be compatible with version 9 which has it at at the # top level we need to figure out the correct import path here. if (9, 0) <= pip_major_minor < (10, 0): return 'from pip import main' elif (10, 0) <= pip_major_minor < (19, 3): # Pip changed their import structure again in 19.3 # https://github.com/pypa/pip/commit/09fd200 return 'from pip._internal import main' elif (19, 3) <= pip_major_minor < (20, 0): return 'from pip._internal.main import main' elif pip_major_minor >= (20, 0): # More changes! https://github.com/pypa/pip/issues/7498 # We'll assume that anything >= v20.0 will use this import # string. We're already specifying our supported versions of # pip as a dependency so assuming this stays the same, pip # upgrades will just require bumping our dependency range in # setup.py. return 'from pip._internal.cli.main import main' raise RuntimeError("Unknown import string for pip version: %s" % str(pip_major_minor)) if os.name == 'nt': # windows # This is the actual patch used on windows to prevent distutils from # compiling C extensions. The msvc compiler base class has its compile # method overridden to raise a CompileError. This can be caught by # setup.py code which can then fallback to making a pure python # package if possible. # We need mypy to ignore these since they are never actually called from # within our process they do not need to be a part of our typechecking # pass. def prevent_msvc_compiling_patch(): # type: ignore import distutils import distutils._msvccompiler import distutils.msvc9compiler import distutils.msvccompiler from distutils.errors import CompileError def raise_compile_error(*args, **kwargs): # type: ignore raise CompileError('Chalice blocked C extension compiling.') distutils._msvccompiler.MSVCCompiler.compile = raise_compile_error distutils.msvc9compiler.MSVCCompiler.compile = raise_compile_error distutils.msvccompiler.MSVCCompiler.compile = raise_compile_error # This is the setuptools shim used to execute setup.py by pip. # Lines 2 and 3 have been added to call the above function # `prevent_msvc_compiling_patch` and extra escapes have been added on line # 5 because it is passed through another layer of string parsing before it # is executed. _SETUPTOOLS_SHIM = ( r"import setuptools, tokenize;__file__=%r;" r"from chalice.compat import prevent_msvc_compiling_patch;" r"prevent_msvc_compiling_patch();" r"f=getattr(tokenize, 'open', open)(__file__);" r"code=f.read().replace('\\r\\n', '\\n');" r"f.close();" r"exec(compile(code, __file__, 'exec'))" ) # On windows the C compiling story is much more complex than on posix as # there are many different C compilers that setuptools and disutils will # try and find using a combination of known filepaths, registry entries, # and environment variables. Since there is no simple environment variable # we can replace when starting the subprocess that builds the package; # we need to apply a patch at runtime to prevent pip/setuptools/distutils # from being able to build C extensions. # Patching out every possible technique for finding each compiler would # be a losing game of whack-a-mole. In addition we need to apply a patch # two layers down through subprocess calls, specifically: # * Chalice creates a subprocess of `pip wheel ...` to build sdists # into wheel files. # * Pip creates another python subprocess to call the setup.py file in # the sdist. Before doing so it applies the above shim to make the # setup file compatible with setuptools. This shim layer also reads # and executes the code in the setup.py. # * Setuptools (which will have been executed by the shim) will # eventually call distutils to do the heavy lifting for C compiling. # # Our patch needs to affect the bottom level here (distutils) and patch # it out to prevent it from compiling C in a graceful way that results in # falling back to building a purepython library if possible. # The below line will be injected just before the `pip wheel ...` portion # of the subprocess that Chalice starts. This replaces the # SETUPTOOLS_SHIM that pip normally uses with the one defined above. # When pip goes to run its subprocess for executing setup.py it will # inject _SETUPTOOLS_SHIM rather than the usual SETUPTOOLS_SHIM in pip. # This lets us apply our patches in the same process that will compile # the c extensions before the setup.py file has been executed. # The actual patches used are decribed in the comment above # _SETUPTOOLS_SHIM. pip_no_compile_c_shim = ( 'import pip;' 'pip.wheel.SETUPTOOLS_SHIM = """%s""";' ) % _SETUPTOOLS_SHIM pip_no_compile_c_env_vars = {} # type: Dict[str, Any] else: # posix # On posix systems setuptools/distutils uses the CC env variable to # locate a C compiler for building C extensions. All we need to do is set # it to /var/false, and the module building process will fail to build. # C extensions, and any fallback processes in place to build a pure python # package will be kicked off. # No need to monkey patch the process. pip_no_compile_c_shim = '' pip_no_compile_c_env_vars = { 'CC': '/var/false' } def is_broken_pipe_error(error): # type: (Exception) -> bool return isinstance(error, BrokenPipeError) # noqa ================================================ FILE: chalice/config.py ================================================ from __future__ import annotations import os import sys import json from typing import Dict, Any, Optional, List, Union # noqa from chalice import __version__ as current_chalice_version from chalice.app import Chalice # noqa from chalice.constants import DEFAULT_STAGE_NAME from chalice.constants import DEFAULT_HANDLER_NAME StrMap = Dict[str, Any] class Config(object): """Configuration information for a chalice app. Configuration values for a chalice app can come from a number of locations, files on disk, CLI params, default values, etc. This object is an abstraction that normalizes these values. In general, there's a precedence for looking up config values: * User specified params * Config file values * Default values A user specified parameter would mean values explicitly specified by a user. Generally these come from command line parameters (e.g ``--profile prod``), but for the purposes of this object would also mean values passed explicitly to this config object when instantiated. Additionally, there are some configurations that can vary per chalice stage (note that a chalice stage is different from an api gateway stage). For config values loaded from disk, we allow values to be specified for all stages or for a specific stage. For example, take ``environment_variables``. You can set this as a top level key to specify env vars to set for all stages, or you can set this value per chalice stage to set stage-specific environment variables. Consider this config file:: { "environment_variables": { "TABLE": "foo" }, "stages": { "dev": { "environment_variables": { "S3BUCKET": "devbucket" } }, "prod": { "environment_variables": { "S3BUCKET": "prodbucket", "TABLE": "prodtable" } } } } If the currently configured chalice stage is "dev", then the config.environment_variables would be:: {"TABLE": "foo", "S3BUCKET": "devbucket"} The "prod" stage would be:: {"TABLE": "prodtable", "S3BUCKET": "prodbucket"} """ def __init__(self, chalice_stage: str = DEFAULT_STAGE_NAME, function_name: str = DEFAULT_HANDLER_NAME, user_provided_params: Optional[StrMap] = None, config_from_disk: Optional[StrMap] = None, default_params: Optional[StrMap] = None, layers: Optional[List[str]] = None, ) -> None: #: Params that a user provided explicitly, #: typically via the command line. self.chalice_stage = chalice_stage self.function_name = function_name if user_provided_params is None: user_provided_params = {} self._user_provided_params = user_provided_params #: The json.loads() from .chalice/config.json if config_from_disk is None: config_from_disk = {} self._config_from_disk = config_from_disk if default_params is None: default_params = {} self._default_params = default_params self._chalice_app = None self._layers = layers @classmethod def create(cls, chalice_stage: str = DEFAULT_STAGE_NAME, function_name: str = DEFAULT_HANDLER_NAME, **kwargs: Any) -> Config: return cls(chalice_stage=chalice_stage, user_provided_params=kwargs.copy()) @property def profile(self) -> str: return self._chain_lookup('profile') @property def app_name(self) -> str: return self._chain_lookup('app_name') @property def project_dir(self) -> str: return self._chain_lookup('project_dir') @property def chalice_app(self) -> Chalice: v = self._chain_lookup('chalice_app') # There's two value we support. If the value # is a chalice app, we return it as is. # Otherwise, we assume it's a callable that creates # a chalice app. This is used to lazy load the chalice # app. if isinstance(v, Chalice): return v elif self._chalice_app is not None: return self._chalice_app elif not callable(v): raise TypeError("Unable to load chalice app, lazy loader is " "not callable: %s" % v) app = v() self._chalice_app = app # Keep mypy happy. return app @property def config_from_disk(self) -> StrMap: return self._config_from_disk @property def lambda_python_version(self) -> str: # We may open this up to configuration later, but for now, # we attempt to match your python version to the closest version # supported by lambda. major, minor = sys.version_info[0], sys.version_info[1] if (major, minor) < (3, 9): return 'python3.9' elif (major, minor) <= (3, 12): # Otherwise we use your current version of python if Lambda # supports it. return f'python{major}.{minor}' return 'python3.13' @property def log_retention_in_days(self) -> int: return self._chain_lookup('log_retention_in_days', varies_per_chalice_stage=True, varies_per_function=True) @property def layers(self) -> List: return self._chain_lookup('layers', varies_per_chalice_stage=True, varies_per_function=True) @property def api_gateway_custom_domain(self) -> StrMap: return self._chain_lookup('api_gateway_custom_domain', varies_per_chalice_stage=True) @property def websocket_api_custom_domain(self) -> StrMap: return self._chain_lookup('websocket_api_custom_domain', varies_per_chalice_stage=True) def _chain_lookup(self, name: str, varies_per_chalice_stage: bool = False, varies_per_function: bool = False) -> Any: search_dicts = [self._user_provided_params] if varies_per_chalice_stage: search_dicts.append( self._config_from_disk.get('stages', {}).get( self.chalice_stage, {})) if varies_per_function: # search order: # config['stages']['lambda_functions'] # config['stages'] # config['lambda_functions'] search_dicts.insert( 0, self._config_from_disk.get('stages', {}).get( self.chalice_stage, {}).get('lambda_functions', {}).get( self.function_name, {})) search_dicts.append( self._config_from_disk.get('lambda_functions', {}).get( self.function_name, {})) search_dicts.extend([self._config_from_disk, self._default_params]) for cfg_dict in search_dicts: if isinstance(cfg_dict, dict) and cfg_dict.get(name) is not None: return cfg_dict[name] def _chain_merge(self, name: str) -> Dict[str, Any]: # Merge values for all search dicts instead of returning on first # found. search_dicts = [ # This is reverse order to _chain_lookup(). self._default_params, self._config_from_disk, self._config_from_disk.get('stages', {}).get( self.chalice_stage, {}), self._config_from_disk.get('stages', {}).get( self.chalice_stage, {}).get('lambda_functions', {}).get( self.function_name, {}), self._user_provided_params, ] final = {} for cfg_dict in search_dicts: value = cfg_dict.get(name, {}) if isinstance(value, dict): final.update(value) return final @property def config_file_version(self) -> str: return self._config_from_disk.get('version', '1.0') # These are all config values that can vary per # chalice stage. @property def api_gateway_stage(self) -> str: return self._chain_lookup('api_gateway_stage', varies_per_chalice_stage=True) @property def api_gateway_endpoint_type(self) -> str: return self._chain_lookup('api_gateway_endpoint_type', varies_per_chalice_stage=True) @property def api_gateway_endpoint_vpce(self) -> Union[str, List[str]]: return self._chain_lookup('api_gateway_endpoint_vpce', varies_per_chalice_stage=True) @property def api_gateway_policy_file(self) -> str: return self._chain_lookup('api_gateway_policy_file', varies_per_chalice_stage=True) @property def minimum_compression_size(self) -> int: return self._chain_lookup('minimum_compression_size', varies_per_chalice_stage=True) @property def iam_policy_file(self) -> str: return self._chain_lookup('iam_policy_file', varies_per_chalice_stage=True, varies_per_function=True) @property def lambda_memory_size(self) -> int: return self._chain_lookup('lambda_memory_size', varies_per_chalice_stage=True, varies_per_function=True) @property def lambda_timeout(self) -> int: return self._chain_lookup('lambda_timeout', varies_per_chalice_stage=True, varies_per_function=True) @property def automatic_layer(self) -> bool: v = self._chain_lookup('automatic_layer', varies_per_chalice_stage=True, varies_per_function=False) if v is None: return False return v @property def iam_role_arn(self) -> str: return self._chain_lookup('iam_role_arn', varies_per_chalice_stage=True, varies_per_function=True) @property def manage_iam_role(self) -> bool: result = self._chain_lookup('manage_iam_role', varies_per_chalice_stage=True, varies_per_function=True) if result is None: # To simplify downstream code, if manage_iam_role # is None (indicating the user hasn't configured/specified this # value anywhere), then we'll return a default value of True. # Otherwise client code has to do an awkward # "if manage_iam_role is None and not manage_iam_role". return True return result @property def autogen_policy(self) -> bool: return self._chain_lookup('autogen_policy', varies_per_chalice_stage=True, varies_per_function=True) @property def xray_enabled(self) -> bool: return self._chain_lookup('xray', varies_per_chalice_stage=True, varies_per_function=True) @property def environment_variables(self) -> Dict[str, str]: return self._chain_merge('environment_variables') @property def tags(self) -> Dict[str, str]: tags = self._chain_merge('tags') tags['aws-chalice'] = 'version=%s:stage=%s:app=%s' % ( current_chalice_version, self.chalice_stage, self.app_name) return tags @property def security_group_ids(self) -> List[str]: return self._chain_lookup('security_group_ids', varies_per_chalice_stage=True, varies_per_function=True) @property def subnet_ids(self) -> List[str]: return self._chain_lookup('subnet_ids', varies_per_chalice_stage=True, varies_per_function=True) @property def reserved_concurrency(self) -> int: return self._chain_lookup('reserved_concurrency', varies_per_chalice_stage=True, varies_per_function=True) def scope(self, chalice_stage: str, function_name: str) -> Config: # Used to create a new config object that's scoped to a different # stage and/or function. This creates a completely separate copy. # This is preferred over mutating the existing config obj. # We technically don't need to do a copy here, but this avoids # any possible issues if we ever mutate the config values. clone = self.__class__( chalice_stage=chalice_stage, function_name=function_name, user_provided_params=self._user_provided_params, config_from_disk=self._config_from_disk, default_params=self._default_params, ) return clone def deployed_resources(self, chalice_stage_name: str) -> DeployedResources: """Return resources associated with a given stage. If a deployment to a given stage has never happened, this method will return a value of None. """ # This is arguably the wrong level of abstraction. # We might be able to move this elsewhere. deployed_file = os.path.join( self.project_dir, '.chalice', 'deployed', '%s.json' % chalice_stage_name) data = self._load_json_file(deployed_file) if data is not None: schema_version = data.get('schema_version', '1.0') if schema_version != '2.0': raise ValueError("Unsupported schema version (%s) in file: %s" % (schema_version, deployed_file)) return DeployedResources(data) return self._try_old_deployer_values(chalice_stage_name) def _try_old_deployer_values(self, chalice_stage_name: str) -> DeployedResources: # They are upgrading from v1.0 to v2.0 of the deployed.json # schema. Attempt to auto convert for them. old_deployed_file = os.path.join(self.project_dir, '.chalice', 'deployed.json') data = self._load_json_file(old_deployed_file) if data is None or chalice_stage_name not in data: return DeployedResources.empty() return self._upgrade_deployed_values(chalice_stage_name, data) def _load_json_file(self, deployed_file: str) -> Any: if not os.path.isfile(deployed_file): return None with open(deployed_file, 'r') as f: return json.load(f) def _upgrade_deployed_values(self, chalice_stage_name: str, data: Any) -> DeployedResources: deployed = data[chalice_stage_name] prefix = '%s-%s-' % (self.app_name, chalice_stage_name) resources: List[Dict[str, Any]] = [] self._upgrade_lambda_functions(resources, deployed, prefix) self._upgrade_rest_api(resources, deployed) return DeployedResources( {'resources': resources, 'schema_version': '2.0'}) def _upgrade_lambda_functions(self, resources: List[Dict[str, Any]], deployed: Dict[str, Any], prefix: str) -> None: lambda_functions = deployed.get('lambda_functions', {}) # In chalice 0.10.0, the lambda_functions had the format # {"function-name": "lambda_arn"} as opposed to # {"function-name": {"arn": "lambda_arn", "type": "...'}} used # in later versions of chalice. We'll check for both cases # so people can upgrade from 0.10.0 to the new deployer. is_pre_10_format = not all( isinstance(v, dict) for v in lambda_functions.values() ) if is_pre_10_format: lambda_functions = { # The only supported lambda functions in 0.10.0 # was built in authorizers. k: {'type': 'authorizer', 'arn': v} for k, v in lambda_functions.items() } for name, values in lambda_functions.items(): short_name = name[len(prefix):] current = { 'resource_type': 'lambda_function', 'lambda_arn': values['arn'], 'name': short_name, } resources.append(current) def _upgrade_rest_api(self, resources: List[Dict[str, Any]], deployed: Dict[str, Any]) -> None: resources.extend([ {'name': 'api_handler', 'resource_type': 'lambda_function', 'lambda_arn': deployed['api_handler_arn']}, {'name': 'rest_api', 'resource_type': 'rest_api', 'rest_api_id': deployed['rest_api_id']}, ]) class DeployedResources(object): def __init__(self, deployed_values: Dict[str, Any]) -> None: self._deployed_values = deployed_values['resources'] self._deployed_values_by_name = { resource['name']: resource for resource in deployed_values['resources'] } @classmethod def empty(cls) -> DeployedResources: return cls({'resources': [], 'schema_version': '2.0'}) def resource_values(self, name: str) -> Dict[str, Any]: if 'api_mapping' in name: name = name.split('.')[0] try: return self._deployed_values_by_name[name] except KeyError: raise ValueError("Resource does not exist: %s" % name) def resource_names(self) -> List[str]: return [r['name'] for r in self._deployed_values] ================================================ FILE: chalice/constants.py ================================================ # This is the version that's written to the config file # on a `chalice new-project`. It's also how chalice is able # to know when to warn you when changing behavior is introduced. CONFIG_VERSION = '2.0' TEMPLATE_APP = """\ from chalice import Chalice app = Chalice(app_name='%s') @app.route('/') def index(): return {'hello': 'world'} # The view function above will return {"hello": "world"} # whenever you make an HTTP GET request to '/'. # # Here are a few more examples: # # @app.route('/hello/{name}') # def hello_name(name): # # '/hello/james' -> {"hello": "james"} # return {'hello': name} # # @app.route('/users', methods=['POST']) # def create_user(): # # This is the JSON body the user sent in their POST request. # user_as_json = app.current_request.json_body # # We'll echo the json body back to the user in a 'user' key. # return {'user': user_as_json} # # See the README documentation for more examples. # """ GITIGNORE = """\ .chalice/deployments/ .chalice/venv/ """ DEFAULT_STAGE_NAME = 'dev' DEFAULT_APIGATEWAY_STAGE_NAME = 'api' DEFAULT_ENDPOINT_TYPE = 'EDGE' DEFAULT_TLS_VERSION = 'TLS_1_2' DEFAULT_LAMBDA_TIMEOUT = 60 DEFAULT_LAMBDA_MEMORY_SIZE = 128 MAX_LAMBDA_DEPLOYMENT_SIZE = 50 * (1024 ** 2) # This is the name of the main handler used to # handle API gateway requests. This is used as a key # in the config module. DEFAULT_HANDLER_NAME = 'api_handler' MIN_COMPRESSION_SIZE = 0 MAX_COMPRESSION_SIZE = 10485760 LAMBDA_TRUST_POLICY = { "Version": "2012-10-17", "Statement": [{ "Sid": "", "Effect": "Allow", "Principal": { "Service": "lambda.amazonaws.com" }, "Action": "sts:AssumeRole" }] } CLOUDWATCH_LOGS = { "Effect": "Allow", "Action": [ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents" ], "Resource": "arn:*:logs:*:*:*" } VPC_ATTACH_POLICY = { "Effect": "Allow", "Action": [ "ec2:CreateNetworkInterface", "ec2:DescribeNetworkInterfaces", "ec2:DetachNetworkInterface", "ec2:DeleteNetworkInterface" ], "Resource": "*" } XRAY_POLICY = { 'Effect': 'Allow', 'Action': [ 'xray:PutTraceSegments', 'xray:PutTelemetryRecords', ], 'Resource': '*' } CODEBUILD_POLICY = { "Version": "2012-10-17", # This is the policy straight from the console. "Statement": [ { "Action": [ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents" ], "Resource": "*", "Effect": "Allow" }, { "Action": [ "s3:GetObject", "s3:GetObjectVersion", "s3:PutObject" ], "Resource": "arn:*:s3:::*", "Effect": "Allow" } ] } CODEPIPELINE_POLICY = { "Version": "2012-10-17", # Also straight from the console setup. "Statement": [ { "Action": [ "s3:GetObject", "s3:GetObjectVersion", "s3:GetBucketVersioning", "s3:CreateBucket", "s3:PutObject", "s3:PutBucketVersioning" ], "Resource": "*", "Effect": "Allow" }, { "Action": [ "codecommit:CancelUploadArchive", "codecommit:GetBranch", "codecommit:GetCommit", "codecommit:GetUploadArchiveStatus", "codecommit:UploadArchive" ], "Resource": "*", "Effect": "Allow" }, { "Action": [ "cloudwatch:*", "iam:PassRole" ], "Resource": "*", "Effect": "Allow" }, { "Action": [ "lambda:InvokeFunction", "lambda:ListFunctions" ], "Resource": "*", "Effect": "Allow" }, { "Action": [ "cloudformation:CreateStack", "cloudformation:DeleteStack", "cloudformation:DescribeStacks", "cloudformation:UpdateStack", "cloudformation:CreateChangeSet", "cloudformation:DeleteChangeSet", "cloudformation:DescribeChangeSet", "cloudformation:ExecuteChangeSet", "cloudformation:SetStackPolicy", "cloudformation:ValidateTemplate", "iam:PassRole" ], "Resource": "*", "Effect": "Allow" }, { "Action": [ "codebuild:BatchGetBuilds", "codebuild:StartBuild" ], "Resource": "*", "Effect": "Allow" } ] } WELCOME_PROMPT = r""" ___ _ _ _ _ ___ ___ ___ / __|| || | /_\ | | |_ _|/ __|| __| | (__ | __ | / _ \ | |__ | || (__ | _| \___||_||_|/_/ \_\|____||___|\___||___| The python serverless microframework for AWS allows you to quickly create and deploy applications using Amazon API Gateway and AWS Lambda. Please enter the project name""" MISSING_DEPENDENCIES_TEMPLATE = r""" Could not install dependencies: %s You will have to build these yourself and vendor them in the chalice vendor folder. Your deployment will continue but may not work correctly if missing dependencies are not present. For more information: http://aws.github.io/chalice/topics/packaging.html """ EXPERIMENTAL_ERROR_MSG = """ You are using experimental features without explicitly opting in. Experimental features do not guarantee backwards compatibility and may be removed in the future. If you'd still like to use these experimental features, you can opt in by adding this to your app.py file:\n\n%s See https://aws.github.io/chalice/topics/experimental.html for more details. """ SQS_EVENT_SOURCE_POLICY = { "Effect": "Allow", "Action": [ "sqs:ReceiveMessage", "sqs:DeleteMessage", "sqs:GetQueueAttributes", ], "Resource": "*", } KINESIS_EVENT_SOURCE_POLICY = { "Effect": "Allow", "Action": [ "kinesis:GetRecords", "kinesis:GetShardIterator", "kinesis:DescribeStream", "kinesis:ListStreams", ], "Resource": "*", } DDB_EVENT_SOURCE_POLICY = { "Effect": "Allow", "Action": [ "dynamodb:DescribeStream", "dynamodb:GetRecords", "dynamodb:GetShardIterator", "dynamodb:ListStreams" ], "Resource": "*" } POST_TO_WEBSOCKET_CONNECTION_POLICY = { "Effect": "Allow", "Action": [ "execute-api:ManageConnections" ], "Resource": "arn:*:execute-api:*:*:*/@connections/*" } ================================================ FILE: chalice/deploy/__init__.py ================================================ ================================================ FILE: chalice/deploy/appgraph.py ================================================ import json import os from dataclasses import asdict from typing import cast from typing import Dict, List, Tuple, Any, Set, Optional, Text, Union # noqa from chalice.config import Config # noqa from chalice import app from chalice.constants import LAMBDA_TRUST_POLICY from chalice.deploy import models from chalice.utils import UI # noqa StrMapAny = Dict[str, Any] class ChaliceBuildError(Exception): pass class ApplicationGraphBuilder(object): def __init__(self) -> None: self._known_roles: Dict[str, models.IAMRole] = {} self._managed_layer: Optional[models.LambdaLayer] = None def build(self, config: Config, stage_name: str) -> models.Application: resources: List[models.Model] = [] deployment = models.DeploymentPackage(models.Placeholder.BUILD_STAGE) for function in config.chalice_app.pure_lambda_functions: resource = self._create_lambda_model( config=config, deployment=deployment, name=function.name, handler_name=function.handler_string, stage_name=stage_name, ) resources.append(resource) event_resources = self._create_lambda_event_resources( config, deployment, stage_name ) resources.extend(event_resources) if config.chalice_app.routes: rest_api = self._create_rest_api_model( config, deployment, stage_name ) resources.append(rest_api) if config.chalice_app.websocket_handlers: websocket_api = self._create_websocket_api_model( config, deployment, stage_name ) resources.append(websocket_api) return models.Application(stage_name, resources) def _create_log_group( self, config: Config, resource_name: str, log_group_name: str ) -> models.LogGroup: return models.LogGroup( resource_name=resource_name, log_group_name=log_group_name, retention_in_days=config.log_retention_in_days, ) def _create_custom_domain_name( self, api_type: models.APIType, domain_name_data: StrMapAny, endpoint_configuration: str, api_gateway_stage: str, ) -> models.DomainName: url_prefix = domain_name_data.get("url_prefix", '(none)') api_mapping_model = self._create_api_mapping_model( url_prefix, api_gateway_stage ) domain_name = self._create_domain_name_model( api_type, domain_name_data, endpoint_configuration, api_mapping_model, ) return domain_name def _create_api_mapping_model( self, key: str, stage: str ) -> models.APIMapping: if key == '/': key = '(none)' return models.APIMapping( resource_name='api_mapping', mount_path=key, api_gateway_stage=stage, ) def _create_lambda_event_resources( self, config: Config, deployment: models.DeploymentPackage, stage_name: str, ) -> List[models.Model]: resources: List[models.Model] = [] for event_source in config.chalice_app.event_sources: if isinstance(event_source, app.S3EventConfig): resources.append( self._create_bucket_notification( config, deployment, event_source, stage_name ) ) elif isinstance(event_source, app.SNSEventConfig): resources.append( self._create_sns_subscription( config, deployment, event_source, stage_name, ) ) elif isinstance(event_source, app.CloudWatchEventConfig): resources.append( self._create_cwe_subscription( config, deployment, event_source, stage_name ) ) elif isinstance(event_source, app.ScheduledEventConfig): resources.append( self._create_scheduled_model( config, deployment, event_source, stage_name ) ) elif isinstance(event_source, app.SQSEventConfig): resources.append( self._create_sqs_subscription( config, deployment, event_source, stage_name, ) ) elif isinstance(event_source, app.KinesisEventConfig): resources.append( self._create_kinesis_subscription( config, deployment, event_source, stage_name, ) ) elif isinstance(event_source, app.DynamoDBEventConfig): resources.append( self._create_ddb_subscription( config, deployment, event_source, stage_name, ) ) return resources def _create_rest_api_model( self, config: Config, deployment: models.DeploymentPackage, stage_name: str, ) -> models.RestAPI: # Need to mess with the function name for back-compat. lambda_function = self._create_lambda_model( config=config, deployment=deployment, name='api_handler', handler_name='app.app', stage_name=stage_name, ) # For backwards compatibility with the old deployer, the # lambda function for the API handler doesn't have the # resource_name appended to its complete function_name, # it's just -. function_name = '%s-%s' % (config.app_name, config.chalice_stage) lambda_function.function_name = function_name if config.minimum_compression_size is None: minimum_compression = '' else: minimum_compression = str(config.minimum_compression_size) authorizers = [] for auth in config.chalice_app.builtin_auth_handlers: auth_lambda = self._create_lambda_model( config=config, deployment=deployment, name=auth.name, handler_name=auth.handler_string, stage_name=stage_name, ) authorizers.append(auth_lambda) policy = None policy_path = config.api_gateway_policy_file if config.api_gateway_endpoint_type == 'PRIVATE' and not policy_path: policy = models.IAMPolicy( document=self._get_default_private_api_policy(config) ) elif policy_path: policy = models.FileBasedIAMPolicy( document=models.Placeholder.BUILD_STAGE, filename=os.path.join( config.project_dir, '.chalice', policy_path ), ) vpce_ids = None if config.api_gateway_endpoint_vpce: vpce = config.api_gateway_endpoint_vpce vpce_ids = [vpce] if isinstance(vpce, str) else vpce custom_domain_name = None if config.api_gateway_custom_domain: custom_domain_name = self._create_custom_domain_name( models.APIType.HTTP, config.api_gateway_custom_domain, config.api_gateway_endpoint_type, config.api_gateway_stage, ) return models.RestAPI( resource_name='rest_api', swagger_doc=models.Placeholder.BUILD_STAGE, endpoint_type=config.api_gateway_endpoint_type, minimum_compression=minimum_compression, api_gateway_stage=config.api_gateway_stage, lambda_function=lambda_function, authorizers=authorizers, policy=policy, domain_name=custom_domain_name, xray=config.xray_enabled, vpce_ids=vpce_ids, ) def _get_default_private_api_policy(self, config: Config) -> StrMapAny: statements = [ { "Effect": "Allow", "Principal": "*", "Action": "execute-api:Invoke", "Resource": "arn:*:execute-api:*:*:*", "Condition": { "StringEquals": { "aws:SourceVpce": config.api_gateway_endpoint_vpce } }, } ] return {"Version": "2012-10-17", "Statement": statements} def _create_websocket_api_model( self, config: Config, deployment: models.DeploymentPackage, stage_name: str, ) -> models.WebsocketAPI: connect_handler: Optional[models.LambdaFunction] = None message_handler: Optional[models.LambdaFunction] = None disconnect_handler: Optional[models.LambdaFunction] = None routes = { h.route_key_handled: h.handler_string for h in config.chalice_app.websocket_handlers.values() } if '$connect' in routes: connect_handler = self._create_lambda_model( config=config, deployment=deployment, name='websocket_connect', handler_name=routes['$connect'], stage_name=stage_name, ) routes.pop('$connect') if '$disconnect' in routes: disconnect_handler = self._create_lambda_model( config=config, deployment=deployment, name='websocket_disconnect', handler_name=routes['$disconnect'], stage_name=stage_name, ) routes.pop('$disconnect') if routes: # If there are left over routes they are message handlers. handler_string = list(routes.values())[0] message_handler = self._create_lambda_model( config=config, deployment=deployment, name='websocket_message', handler_name=handler_string, stage_name=stage_name, ) custom_domain_name = None if config.websocket_api_custom_domain: custom_domain_name = self._create_custom_domain_name( models.APIType.WEBSOCKET, config.websocket_api_custom_domain, config.api_gateway_endpoint_type, config.api_gateway_stage, ) return models.WebsocketAPI( name='%s-%s-websocket-api' % (config.app_name, stage_name), resource_name='websocket_api', connect_function=connect_handler, message_function=message_handler, disconnect_function=disconnect_handler, routes=[ h.route_key_handled for h in config.chalice_app.websocket_handlers.values() ], api_gateway_stage=config.api_gateway_stage, domain_name=custom_domain_name, ) def _create_cwe_subscription( self, config: Config, deployment: models.DeploymentPackage, event_source: app.CloudWatchEventConfig, stage_name: str, ) -> models.CloudWatchEvent: lambda_function = self._create_lambda_model( config=config, deployment=deployment, name=event_source.name, handler_name=event_source.handler_string, stage_name=stage_name, ) resource_name = event_source.name + '-event' rule_name = '%s-%s-%s' % ( config.app_name, config.chalice_stage, resource_name, ) cwe = models.CloudWatchEvent( resource_name=resource_name, rule_name=rule_name, event_pattern=json.dumps(event_source.event_pattern), lambda_function=lambda_function, ) return cwe def _create_scheduled_model( self, config: Config, deployment: models.DeploymentPackage, event_source: app.ScheduledEventConfig, stage_name: str, ) -> models.ScheduledEvent: lambda_function = self._create_lambda_model( config=config, deployment=deployment, name=event_source.name, handler_name=event_source.handler_string, stage_name=stage_name, ) # Resource names must be unique across a chalice app. # However, in the original deployer code, the cloudwatch # event + lambda function was considered a single resource. # Now that they're treated as two separate resources we need # a unique name for the event_source that's not the lambda # function resource name. We handle this by just appending # '-event' to the name. Ideally this is handled in app.py # but we won't be able to do that until the old deployer # is gone. resource_name = event_source.name + '-event' if isinstance( event_source.schedule_expression, app.ScheduleExpression ): expression = event_source.schedule_expression.to_string() else: expression = event_source.schedule_expression rule_name = '%s-%s-%s' % ( config.app_name, config.chalice_stage, resource_name, ) scheduled_event = models.ScheduledEvent( resource_name=resource_name, rule_name=rule_name, rule_description=event_source.description, schedule_expression=expression, lambda_function=lambda_function, ) return scheduled_event def _create_domain_name_model( self, protocol: models.APIType, data: StrMapAny, endpoint_type: str, api_mapping: models.APIMapping, ) -> models.DomainName: default_name = 'api_gateway_custom_domain' resource_name_map: Dict[str, str] = { 'HTTP': default_name, 'WEBSOCKET': 'websocket_api_custom_domain', } domain_name = models.DomainName( protocol=protocol, resource_name=resource_name_map.get(protocol.value, default_name), domain_name=data['domain_name'], tls_version=models.TLSVersion.create(data.get('tls_version', '')), certificate_arn=data['certificate_arn'], tags=data.get('tags'), api_mapping=api_mapping, ) return domain_name def _create_lambda_model( self, config: Config, deployment: models.DeploymentPackage, name: str, handler_name: str, stage_name: str, ) -> models.LambdaFunction: new_config = config.scope( chalice_stage=config.chalice_stage, function_name=name ) role = self._get_role_reference(new_config, stage_name, name) resource = self._build_lambda_function( new_config, name, handler_name, deployment, role ) if new_config.log_retention_in_days: log_resource_name = '%s-log-group' % name log_group_name = '/aws/lambda/%s-%s-%s' % ( new_config.app_name, stage_name, name, ) resource.log_group = self._create_log_group( new_config, log_resource_name, log_group_name ) return resource def _get_managed_lambda_layer( self, config: Config ) -> Optional[models.LambdaLayer]: if not config.automatic_layer: return None if self._managed_layer is None: self._managed_layer = models.LambdaLayer( resource_name='managed-layer', layer_name='%s-%s-%s' % (config.app_name, config.chalice_stage, 'managed-layer'), runtime=config.lambda_python_version, deployment_package=models.DeploymentPackage( models.Placeholder.BUILD_STAGE ), ) return self._managed_layer def _get_role_reference( self, config: Config, stage_name: str, function_name: str ) -> models.IAMRole: role = self._create_role_reference(config, stage_name, function_name) role_identifier = self._get_role_identifier(role) if role_identifier in self._known_roles: # If we've already create a models.IAMRole with the same # identifier, we'll use the existing object instead of # creating a new one. return self._known_roles[role_identifier] self._known_roles[role_identifier] = role return role def _get_role_identifier(self, role: models.IAMRole) -> str: if isinstance(role, models.PreCreatedIAMRole): return role.role_arn # We know that if it's not a PreCreatedIAMRole, it's # a managed role, so we're using cast() to make mypy happy. role = cast(models.ManagedIAMRole, role) return role.resource_name def _create_role_reference( self, config: Config, stage_name: str, function_name: str ) -> models.IAMRole: # First option, the user doesn't want us to manage # the role at all. if not config.manage_iam_role: # We've already validated the iam_role_arn is provided # if manage_iam_role is set to False. return models.PreCreatedIAMRole( role_arn=config.iam_role_arn, ) policy = models.IAMPolicy(document=models.Placeholder.BUILD_STAGE) if not config.autogen_policy: resource_name = '%s_role' % function_name role_name = '%s-%s-%s' % ( config.app_name, stage_name, function_name, ) if config.iam_policy_file is not None: filename = os.path.join( config.project_dir, '.chalice', config.iam_policy_file ) else: filename = os.path.join( config.project_dir, '.chalice', 'policy-%s.json' % stage_name, ) policy = models.FileBasedIAMPolicy( filename=filename, document=models.Placeholder.BUILD_STAGE ) else: resource_name = 'default-role' role_name = '%s-%s' % (config.app_name, stage_name) policy = models.AutoGenIAMPolicy( document=models.Placeholder.BUILD_STAGE, traits=set([]), ) return models.ManagedIAMRole( resource_name=resource_name, role_name=role_name, trust_policy=LAMBDA_TRUST_POLICY, policy=policy, ) def _get_vpc_params( self, function_name: str, config: Config ) -> Tuple[List[str], List[str]]: security_group_ids = config.security_group_ids subnet_ids = config.subnet_ids if security_group_ids and subnet_ids: return security_group_ids, subnet_ids elif not security_group_ids and not subnet_ids: return [], [] else: raise ChaliceBuildError( "Invalid VPC params for function '%s', in order to configure " "VPC for a Lambda function, you must provide the subnet_ids " "as well as the security_group_ids, got subnet_ids: %s, " "security_group_ids: %s" % (function_name, subnet_ids, security_group_ids) ) def _get_lambda_layers(self, config: Config) -> List[str]: layers = config.layers return layers if layers else [] def _build_lambda_function( self, config: Config, name: str, handler_name: str, deployment: models.DeploymentPackage, role: models.IAMRole, ) -> models.LambdaFunction: function_name = '%s-%s-%s' % ( config.app_name, config.chalice_stage, name, ) security_group_ids, subnet_ids = self._get_vpc_params(name, config) lambda_layers = self._get_lambda_layers(config) function = models.LambdaFunction( resource_name=name, function_name=function_name, environment_variables=config.environment_variables, runtime=config.lambda_python_version, handler=handler_name, tags=config.tags, timeout=config.lambda_timeout, memory_size=config.lambda_memory_size, deployment_package=deployment, role=role, security_group_ids=security_group_ids, subnet_ids=subnet_ids, reserved_concurrency=config.reserved_concurrency, layers=lambda_layers, managed_layer=self._get_managed_lambda_layer(config), xray=config.xray_enabled, ) self._inject_role_traits(function, role) return function def _inject_role_traits( self, function: models.LambdaFunction, role: models.IAMRole ) -> None: if not isinstance(role, models.ManagedIAMRole): return policy = role.policy if not isinstance(policy, models.AutoGenIAMPolicy): return if function.security_group_ids and function.subnet_ids: policy.traits.add(models.RoleTraits.VPC_NEEDED) def _create_bucket_notification( self, config: Config, deployment: models.DeploymentPackage, s3_event: app.S3EventConfig, stage_name: str, ) -> models.S3BucketNotification: lambda_function = self._create_lambda_model( config=config, deployment=deployment, name=s3_event.name, handler_name=s3_event.handler_string, stage_name=stage_name, ) resource_name = s3_event.name + '-s3event' s3_bucket = models.S3BucketNotification( resource_name=resource_name, bucket=s3_event.bucket, prefix=s3_event.prefix, suffix=s3_event.suffix, events=s3_event.events, lambda_function=lambda_function, ) return s3_bucket def _create_sns_subscription( self, config: Config, deployment: models.DeploymentPackage, sns_config: app.SNSEventConfig, stage_name: str, ) -> models.SNSLambdaSubscription: lambda_function = self._create_lambda_model( config=config, deployment=deployment, name=sns_config.name, handler_name=sns_config.handler_string, stage_name=stage_name, ) resource_name = sns_config.name + '-sns-subscription' sns_subscription = models.SNSLambdaSubscription( resource_name=resource_name, topic=sns_config.topic, lambda_function=lambda_function, ) return sns_subscription def _create_sqs_subscription( self, config: Config, deployment: models.DeploymentPackage, sqs_config: app.SQSEventConfig, stage_name: str, ) -> models.SQSEventSource: lambda_function = self._create_lambda_model( config=config, deployment=deployment, name=sqs_config.name, handler_name=sqs_config.handler_string, stage_name=stage_name, ) resource_name = sqs_config.name + '-sqs-event-source' queue: Union[str, models.QueueARN] = '' if sqs_config.queue_arn is not None: queue = models.QueueARN(arn=sqs_config.queue_arn) elif sqs_config.queue is not None: queue = sqs_config.queue batch_window = sqs_config.maximum_batching_window_in_seconds sqs_event_source = models.SQSEventSource( resource_name=resource_name, queue=queue, batch_size=sqs_config.batch_size, lambda_function=lambda_function, maximum_batching_window_in_seconds=batch_window, maximum_concurrency=sqs_config.maximum_concurrency ) return sqs_event_source def _create_kinesis_subscription( self, config: Config, deployment: models.DeploymentPackage, kinesis_config: app.KinesisEventConfig, stage_name: str, ) -> models.KinesisEventSource: lambda_function = self._create_lambda_model( config=config, deployment=deployment, name=kinesis_config.name, handler_name=kinesis_config.handler_string, stage_name=stage_name, ) resource_name = kinesis_config.name + '-kinesis-event-source' batch_window = kinesis_config.maximum_batching_window_in_seconds kinesis_event_source = models.KinesisEventSource( resource_name=resource_name, stream=kinesis_config.stream, batch_size=kinesis_config.batch_size, maximum_batching_window_in_seconds=batch_window, starting_position=kinesis_config.starting_position, lambda_function=lambda_function, ) return kinesis_event_source def _create_ddb_subscription( self, config: Config, deployment: models.DeploymentPackage, ddb_config: app.DynamoDBEventConfig, stage_name: str, ) -> models.DynamoDBEventSource: lambda_function = self._create_lambda_model( config=config, deployment=deployment, name=ddb_config.name, handler_name=ddb_config.handler_string, stage_name=stage_name, ) resource_name = ddb_config.name + '-dynamodb-event-source' batch_window = ddb_config.maximum_batching_window_in_seconds ddb_event_source = models.DynamoDBEventSource( resource_name=resource_name, stream_arn=ddb_config.stream_arn, batch_size=ddb_config.batch_size, maximum_batching_window_in_seconds=batch_window, starting_position=ddb_config.starting_position, lambda_function=lambda_function, ) return ddb_event_source class DependencyBuilder(object): def __init__(self) -> None: pass def build_dependencies(self, graph: models.Model) -> List[models.Model]: seen: Set[int] = set() ordered: List[models.Model] = [] for resource in graph.dependencies(): self._traverse(resource, ordered, seen) return ordered def _traverse( self, resource: models.Model, ordered: List[models.Model], seen: Set[int], ) -> None: for dep in resource.dependencies(): if id(dep) not in seen: seen.add(id(dep)) self._traverse(dep, ordered, seen) # If recreating this list is a perf issue later on, # we can create yet-another set of ids that gets updated # when we add a resource to the ordered list. if id(resource) not in [id(r) for r in ordered]: ordered.append(resource) class GraphPrettyPrint(object): _NEW_SECTION = '\u251c\u2500\u2500' _LINE_VERTICAL = '\u2502' def __init__(self, ui: UI) -> None: self._ui = ui def display_graph(self, graph: models.Model) -> None: self._ui.write("Application\n") for model in graph.dependencies(): self._traverse(model, level=0) def _traverse(self, graph: models.Model, level: int) -> None: prefix = ('%s ' % self._LINE_VERTICAL) * level spaces = prefix + self._NEW_SECTION + ' ' model_text = self._get_model_text(graph, spaces, level) current_line = cast(str, '%s%s\n' % (spaces, model_text)) self._ui.write(current_line) for model in graph.dependencies(): self._traverse(model, level + 1) def _get_model_text( self, model: models.Model, spaces: Text, level: int ) -> Text: name = model.__class__.__name__ filtered = self._get_filtered_params(model) if not filtered: return '%s()' % name total_len_prefix = len(spaces) + len(name) + 1 prefix = ('%s ' % self._LINE_VERTICAL) * (level + 2) full = '%s%s' % (prefix, ' ' * (total_len_prefix - len(prefix))) param_items = list(filtered.items()) first = param_items[0] remaining = param_items[1:] lines = ['%s(%s=%s,' % (name, first[0], first[1])] self._add_remaining_lines(lines, remaining, full) return '\n'.join(lines) + ')' def _add_remaining_lines( self, lines: List[str], remaining: List[Tuple[str, Any]], full: Text ) -> None: for key, value in remaining: if isinstance(value, (list, dict)): value = key.upper() current = cast(str, '%s%s=%s,' % (full, key, value)) lines.append(current) def _get_filtered_params(self, model: models.Model) -> StrMapAny: dependencies = model.dependencies() filtered = { k: v for k, v in asdict(model).items() if v not in dependencies } return filtered ================================================ FILE: chalice/deploy/deployer.py ================================================ """Chalice deployer module. The deployment system in chalice is broken down into a pipeline of multiple stages. Each stage takes the input and transforms it to some other form. The reason for this is so that each stage can stay simple and focused on only a single part of the deployment process. This makes the code easier to follow and easier to test. The biggest downside is that adding support for a new resource type is split across several objects now, but I imagine as we add support for more resource types, we'll see common patterns emerge that we can extract out into higher levels of abstraction. These are the stages of the deployment process. Application Graph Builder ========================= The first stage is the resource graph builder. This takes the objects in the ``Chalice`` app and structures them into an ``Application`` object which consists of various ``models.Model`` objects. These models are just python objects that describe the attributes of various AWS resources. These models don't have any behavior on their own. Dependency Builder ================== This process takes the graph of resources created from the previous step and orders them such that all objects are listed before objects that depend on them. The AWS resources in the ``chalice.deploy.models`` module also model their required dependencies (see the ``dependencies()`` methods of the models). This is the mechanism that's used to build the correct dependency ordering. Local Build Stage ================= This takes the ordered list of resources and allows any local build processes to occur. The rule of thumb here is no remote AWS calls. This stage includes auto policy generation, pip packaging, injecting default values, etc. To clarify which attributes are affected by the build stage, they'll usually have a value of ``models.Placeholder.BUILD_STAGE``. Processors in the build stage will replaced those ``models.Placeholder.BUILD_STAGE`` values with whatever the "built" value is (e.g the filename of the zipped deployment package). For example, we know when we create a lambda function that we need to create a deployment package, but we don't know the name nor contents of the deployment package until the ``LambdaDeploymentPackager`` runs. Therefore, the Resource Builder stage can record the fact that it knows that a ``models.DeploymentPackage`` is needed, but use ``models.Placeholder.BUILD_STAGE`` for the value of the filename. The enum values aren't strictly necessary, they just add clarity about when this value is expected to be filled in. These could also just be set to ``None`` and be of type ``Optional[T]``. Execution Plan Stage ==================== This stage takes the ordered list of resources and figures out what AWS API calls we have to make. For example, if a resource doesn't exist at all, we'll need to make a ``create_*`` call. If the resource exists, we may need to make a series of ``update_*`` calls. If the resource exists and is already up to date, we might not need to make any calls at all. The output of this stage is a list of ``APICall`` objects. This stage doesn't actually make the mutating API calls, it only figures out what calls we should make. This stage will typically only make ``describe/list`` AWS calls. The Executor ============ This takes the list of ``APICall`` objects from the previous stage and finally executes them. It also manages taking the output of API calls and storing them in variables so they can be referenced in subsequent ``APICall`` objects (see the ``Variable`` class to see how this is used). For example, if a lambda function needs the ``role_arn`` that's the result of a previous ``create_role`` API call, a ``Variable`` object is used to forward this information. The executor also records these variables with their associated resources so a ``deployed.json`` file can be written to disk afterwards. An ``APICall`` takes an optional resource object when it's created whose ``resource_name`` is used as the key in the ``deployed.json`` dictionary. """ # pylint: disable=too-many-lines import json import textwrap import socket import logging import botocore.exceptions from botocore.vendored.requests import ConnectionError as \ RequestsConnectionError from botocore.session import Session # noqa from typing import Optional, Dict, List, Any, Type, cast # noqa from chalice.config import Config # noqa from chalice.compat import is_broken_pipe_error from chalice.awsclient import DeploymentPackageTooLargeError from chalice.awsclient import LambdaClientError from chalice.awsclient import AWSClientError from chalice.awsclient import TypedAWSClient from chalice.constants import MAX_LAMBDA_DEPLOYMENT_SIZE from chalice.constants import VPC_ATTACH_POLICY from chalice.constants import DEFAULT_LAMBDA_TIMEOUT from chalice.constants import DEFAULT_LAMBDA_MEMORY_SIZE from chalice.constants import DEFAULT_TLS_VERSION from chalice.constants import SQS_EVENT_SOURCE_POLICY from chalice.constants import KINESIS_EVENT_SOURCE_POLICY from chalice.constants import DDB_EVENT_SOURCE_POLICY from chalice.constants import POST_TO_WEBSOCKET_CONNECTION_POLICY from chalice.deploy import models from chalice.deploy.appgraph import ApplicationGraphBuilder, DependencyBuilder from chalice.deploy.executor import BaseExecutor # noqa from chalice.deploy.executor import Executor from chalice.deploy.executor import DisplayOnlyExecutor from chalice.deploy.packager import PipRunner from chalice.deploy.packager import SubprocessPip from chalice.deploy.packager import DependencyBuilder as PipDependencyBuilder from chalice.deploy.packager import LambdaDeploymentPackager from chalice.deploy.packager import AppOnlyDeploymentPackager from chalice.deploy.packager import LayerDeploymentPackager from chalice.deploy.packager import BaseLambdaDeploymentPackager # noqa from chalice.deploy.packager import EmptyPackageError from chalice.deploy.planner import PlanStage from chalice.deploy.planner import RemoteState from chalice.deploy.planner import NoopPlanner from chalice.deploy.swagger import TemplatedSwaggerGenerator from chalice.deploy.swagger import SwaggerGenerator # noqa from chalice.deploy.sweeper import ResourceSweeper from chalice.deploy.validate import validate_configuration from chalice.policy import AppPolicyGenerator from chalice.utils import OSUtils from chalice.utils import UI from chalice.utils import serialize_to_json OptStr = Optional[str] LOGGER = logging.getLogger(__name__) _AWSCLIENT_EXCEPTIONS = ( botocore.exceptions.ClientError, AWSClientError ) class ChaliceDeploymentError(Exception): def __init__(self, error): # type: (Exception) -> None self.original_error = error where = self._get_error_location(error) msg = self._wrap_text( 'ERROR - %s, received the following error:' % where ) msg += '\n\n' msg += self._wrap_text(self._get_error_message(error), indent=' ') msg += '\n\n' suggestion = self._get_error_suggestion(error) if suggestion is not None: msg += self._wrap_text(suggestion) super(ChaliceDeploymentError, self).__init__(msg) def _get_error_location(self, error): # type: (Exception) -> str where = 'While deploying your chalice application' if isinstance(error, LambdaClientError): where = ( 'While sending your chalice handler code to Lambda to %s ' 'function "%s"' % ( self._get_verb_from_client_method( error.context.client_method_name), error.context.function_name ) ) return where def _get_error_message(self, error): # type: (Exception) -> str msg = str(error) if isinstance(error, LambdaClientError): if isinstance(error.original_error, RequestsConnectionError): msg = self._get_error_message_for_connection_error( error.original_error) return msg def _get_error_message_for_connection_error(self, connection_error): # type: (RequestsConnectionError) -> str # To get the underlying error that raised the # requests.ConnectionError it is required to go down two levels of # arguments to get the underlying exception. The instantiation of # one of these exceptions looks like this: # # requests.ConnectionError( # urllib3.exceptions.ProtocolError( # 'Connection aborted.', ) # ) message = connection_error.args[0].args[0] underlying_error = connection_error.args[0].args[1] if is_broken_pipe_error(underlying_error): message += ( ' Lambda closed the connection before chalice finished ' 'sending all of the data.' ) elif isinstance(underlying_error, socket.timeout): message += ' Timed out sending your app to Lambda.' return message def _get_error_suggestion(self, error): # type: (Exception) -> OptStr suggestion = None if isinstance(error, DeploymentPackageTooLargeError): suggestion = ( 'To avoid this error, decrease the size of your chalice ' 'application by removing code or removing ' 'dependencies from your chalice application.' ) deployment_size = error.context.deployment_size if deployment_size > MAX_LAMBDA_DEPLOYMENT_SIZE: size_warning = ( 'This is likely because the deployment package is %s. ' 'Lambda only allows deployment packages that are %s or ' 'less in size.' % ( self._get_mb(deployment_size), self._get_mb(MAX_LAMBDA_DEPLOYMENT_SIZE) ) ) suggestion = size_warning + ' ' + suggestion return suggestion def _wrap_text(self, text, indent=''): # type: (str, str) -> str return '\n'.join( textwrap.wrap( text, 79, replace_whitespace=False, drop_whitespace=False, initial_indent=indent, subsequent_indent=indent ) ) def _get_verb_from_client_method(self, client_method_name): # type: (str) -> str client_method_name_to_verb = { 'update_function_code': 'update', 'create_function': 'create' } return client_method_name_to_verb.get( client_method_name, client_method_name) def _get_mb(self, value): # type: (int) -> str return '%.1f MB' % (float(value) / (1024 ** 2)) def create_plan_only_deployer(session, config, ui): # type: (Session, Config, UI) -> Deployer return _create_deployer(session, config, ui, DisplayOnlyExecutor, NoopResultsRecorder) def create_default_deployer(session, config, ui): # type: (Session, Config, UI) -> Deployer return _create_deployer(session, config, ui, Executor, ResultsRecorder) def _create_deployer(session, # type: Session config, # type: Config ui, # type: UI executor_cls, # type: Type[BaseExecutor] recorder_cls, # type: Type[ResultsRecorder] ): # type: (...) -> Deployer client = TypedAWSClient(session) osutils = OSUtils() return Deployer( application_builder=ApplicationGraphBuilder(), deps_builder=DependencyBuilder(), build_stage=create_build_stage( osutils, UI(), TemplatedSwaggerGenerator(), config ), plan_stage=PlanStage( osutils=osutils, remote_state=RemoteState( client, config.deployed_resources(config.chalice_stage)), ), sweeper=ResourceSweeper(), executor=executor_cls(client, ui), recorder=recorder_cls(osutils=osutils), ) def create_build_stage(osutils, ui, swagger_gen, config): # type: (OSUtils, UI, SwaggerGenerator, Config) -> BuildStage pip_runner = PipRunner(pip=SubprocessPip(osutils=osutils), osutils=osutils) dependency_builder = PipDependencyBuilder( osutils=osutils, pip_runner=pip_runner ) deployment_packager = cast(BaseDeployStep, None) if config.automatic_layer: deployment_packager = ManagedLayerDeploymentPackager( lambda_packager=AppOnlyDeploymentPackager( osutils=osutils, dependency_builder=dependency_builder, ui=ui, ), layer_packager=LayerDeploymentPackager( osutils=osutils, dependency_builder=dependency_builder, ui=ui, ) ) else: deployment_packager = DeploymentPackager( packager=LambdaDeploymentPackager( osutils=osutils, dependency_builder=dependency_builder, ui=ui, ) ) build_stage = BuildStage( steps=[ InjectDefaults(), deployment_packager, PolicyGenerator( policy_gen=AppPolicyGenerator( osutils=osutils ), osutils=osutils, ), SwaggerBuilder( swagger_generator=swagger_gen, ), LambdaEventSourcePolicyInjector(), WebsocketPolicyInjector() ], ) return build_stage def create_deletion_deployer(client, ui): # type: (TypedAWSClient, UI) -> Deployer return Deployer( application_builder=ApplicationGraphBuilder(), deps_builder=DependencyBuilder(), build_stage=BuildStage(steps=[]), plan_stage=NoopPlanner(), sweeper=ResourceSweeper(), executor=Executor(client, ui), recorder=ResultsRecorder(osutils=OSUtils()), ) class Deployer(object): BACKEND_NAME = 'api' def __init__(self, application_builder, # type: ApplicationGraphBuilder deps_builder, # type: DependencyBuilder build_stage, # type: BuildStage plan_stage, # type: PlanStage sweeper, # type: ResourceSweeper executor, # type: BaseExecutor recorder, # type: ResultsRecorder ): # type: (...) -> None self._application_builder = application_builder self._deps_builder = deps_builder self._build_stage = build_stage self._plan_stage = plan_stage self._sweeper = sweeper self._executor = executor self._recorder = recorder def deploy(self, config, chalice_stage_name): # type: (Config, str) -> Dict[str, Any] try: return self._deploy(config, chalice_stage_name) except _AWSCLIENT_EXCEPTIONS as e: raise ChaliceDeploymentError(e) def _deploy(self, config, chalice_stage_name): # type: (Config, str) -> Dict[str, Any] self._validate_config(config) application = self._application_builder.build( config, chalice_stage_name) resources = self._deps_builder.build_dependencies(application) self._build_stage.execute(config, resources) # Rebuild dependencies in case the build stage modified # the app graph. resources = self._deps_builder.build_dependencies(application) plan = self._plan_stage.execute(resources) self._sweeper.execute(plan, config) self._executor.execute(plan) deployed_values = { 'resources': self._executor.resource_values, 'schema_version': '2.0', 'backend': self.BACKEND_NAME, } self._recorder.record_results( deployed_values, chalice_stage_name, config.project_dir, ) return deployed_values def _validate_config(self, config): # type: (Config) -> None try: validate_configuration(config) except ValueError as e: raise ChaliceDeploymentError(e) class BaseDeployStep(object): def handle(self, config, resource): # type: (Config, models.Model) -> None name = 'handle_%s' % resource.__class__.__name__.lower() handler = getattr(self, name, None) if handler is not None: handler(config, resource) class InjectDefaults(BaseDeployStep): def __init__(self, lambda_timeout=DEFAULT_LAMBDA_TIMEOUT, lambda_memory_size=DEFAULT_LAMBDA_MEMORY_SIZE, tls_version=DEFAULT_TLS_VERSION): # type: (int, int, str) -> None self._lambda_timeout = lambda_timeout self._lambda_memory_size = lambda_memory_size self._tls_version = DEFAULT_TLS_VERSION def handle_lambdafunction(self, config, resource): # type: (Config, models.LambdaFunction) -> None if resource.timeout is None: resource.timeout = self._lambda_timeout if resource.memory_size is None: resource.memory_size = self._lambda_memory_size def handle_domainname(self, config, resource): # type: (Config, models.DomainName) -> None if resource.tls_version is None: resource.tls_version = models.TLSVersion.create( DEFAULT_TLS_VERSION) class DeploymentPackager(BaseDeployStep): def __init__(self, packager): # type: (LambdaDeploymentPackager) -> None self._packager = packager def handle_deploymentpackage(self, config, resource): # type: (Config, models.DeploymentPackage) -> None if isinstance(resource.filename, models.Placeholder): zip_filename = self._packager.create_deployment_package( config.project_dir, config.lambda_python_version) resource.filename = zip_filename class ManagedLayerDeploymentPackager(BaseDeployStep): # If we're creating a layer for non-app code there's two different # packagers we need. One for the Lambda functions (app code) and # one for the Lambda layer (requirements.txt + vendor). def __init__(self, lambda_packager, # type: BaseLambdaDeploymentPackager layer_packager, # type: BaseLambdaDeploymentPackager ): # type: (...) -> None self._lambda_packager = lambda_packager self._layer_packager = layer_packager def handle_lambdafunction(self, config, resource): # type: (Config, models.LambdaFunction) -> None if isinstance(resource.deployment_package.filename, models.Placeholder): zip_filename = self._lambda_packager.create_deployment_package( config.project_dir, config.lambda_python_version ) resource.deployment_package.filename = zip_filename if resource.managed_layer is not None and \ resource.managed_layer.is_empty: # Lambda doesn't allow us to create an empty layer so if we've # tried to create the deployment package and determined it's empty # we should remove the managed layer from the model entirely so # downstream consumers don't have to worry about it. resource.managed_layer = None def handle_lambdalayer(self, config, resource): # type: (Config, models.LambdaLayer) -> None if isinstance(resource.deployment_package.filename, models.Placeholder): try: zip_filename = self._layer_packager.create_deployment_package( config.project_dir, config.lambda_python_version ) resource.deployment_package.filename = zip_filename except EmptyPackageError: # An empty package is not valid in Lambda. There's # no point in trying to represent it in the model # because nothing downstream (planner, SAM/tf) can # consume as a useful entity. resource.is_empty = True class SwaggerBuilder(BaseDeployStep): def __init__(self, swagger_generator): # type: (SwaggerGenerator) -> None self._swagger_generator = swagger_generator def handle_restapi(self, config, resource): # type: (Config, models.RestAPI) -> None swagger_doc = self._swagger_generator.generate_swagger( config.chalice_app, resource) resource.swagger_doc = swagger_doc class LambdaEventSourcePolicyInjector(BaseDeployStep): def __init__(self): # type: () -> None self._sqs_policy_injected = False self._kinesis_policy_injected = False self._ddb_policy_injected = False def handle_sqseventsource(self, config, resource): # type: (Config, models.SQSEventSource) -> None # The sqs integration works by polling for # available records so the lambda function needs # permission to call sqs. role = resource.lambda_function.role if not self._sqs_policy_injected and \ self._needs_policy_injected(role): # mypy can't follow the type narrowing from # _needs_policy_injected so we're working around # that by explicitly casting the role. role = cast(models.ManagedIAMRole, role) document = cast(Dict[str, Any], role.policy.document) self._inject_trigger_policy(document, SQS_EVENT_SOURCE_POLICY.copy()) self._sqs_policy_injected = True def handle_kinesiseventsource(self, config, resource): # type: (Config, models.KinesisEventSource) -> None role = resource.lambda_function.role if not self._kinesis_policy_injected and \ self._needs_policy_injected(role): # See commen in handle_kinesiseventsource about this cast. role = cast(models.ManagedIAMRole, role) document = cast(Dict[str, Any], role.policy.document) self._inject_trigger_policy(document, KINESIS_EVENT_SOURCE_POLICY.copy()) self._kinesis_policy_injected = True def handle_dynamodbeventsource(self, config, resource): # type: (Config, models.KinesisEventSource) -> None role = resource.lambda_function.role if not self._ddb_policy_injected and \ self._needs_policy_injected(role): # See commen in handle_kinesiseventsource about this cast. role = cast(models.ManagedIAMRole, role) document = cast(Dict[str, Any], role.policy.document) self._inject_trigger_policy(document, DDB_EVENT_SOURCE_POLICY.copy()) self._ddb_policy_injected = True def _needs_policy_injected(self, role): # type: (models.IAMRole) -> bool return ( isinstance(role, models.ManagedIAMRole) and isinstance(role.policy, models.AutoGenIAMPolicy) and not isinstance(role.policy.document, models.Placeholder) ) def _inject_trigger_policy(self, document, policy): # type: (Dict[str, Any], Dict[str, Any]) -> None document['Statement'].append(policy) class WebsocketPolicyInjector(BaseDeployStep): def __init__(self): # type: () -> None self._policy_injected = False def handle_websocketapi(self, config, resource): # type: (Config, models.WebsocketAPI) -> None self._inject_into_function(config, resource.connect_function) self._inject_into_function(config, resource.message_function) self._inject_into_function(config, resource.disconnect_function) def _inject_into_function(self, config, lambda_function): # type: (Config, Optional[models.LambdaFunction]) -> None if lambda_function is None: return role = lambda_function.role if role is None: return if (not self._policy_injected and isinstance(role, models.ManagedIAMRole) and isinstance(role.policy, models.AutoGenIAMPolicy) and not isinstance(role.policy.document, models.Placeholder)): self._inject_policy( role.policy.document, POST_TO_WEBSOCKET_CONNECTION_POLICY.copy()) self._policy_injected = True def _inject_policy(self, document, policy): # type: (Dict[str, Any], Dict[str, Any]) -> None document['Statement'].append(policy) class PolicyGenerator(BaseDeployStep): def __init__(self, policy_gen, osutils): # type: (AppPolicyGenerator, OSUtils) -> None self._policy_gen = policy_gen self._osutils = osutils def _read_document_from_file(self, filename): # type: (PolicyGenerator, str) -> Dict[str, Any] try: return json.loads(self._osutils.get_file_contents(filename)) except IOError as e: raise RuntimeError("Unable to load IAM policy file %s: %s" % (filename, e)) def handle_filebasediampolicy(self, config, resource): # type: (Config, models.FileBasedIAMPolicy) -> None resource.document = self._read_document_from_file(resource.filename) def handle_restapi(self, config, resource): # type: (Config, models.RestAPI) -> None if resource.policy and isinstance( resource.policy, models.FileBasedIAMPolicy): resource.policy.document = self._read_document_from_file( resource.policy.filename) def handle_autogeniampolicy(self, config, resource): # type: (Config, models.AutoGenIAMPolicy) -> None if isinstance(resource.document, models.Placeholder): policy = self._policy_gen.generate_policy(config) if models.RoleTraits.VPC_NEEDED in resource.traits: policy['Statement'].append(VPC_ATTACH_POLICY) resource.document = policy class BuildStage(object): def __init__(self, steps): # type: (List[BaseDeployStep]) -> None self._steps = steps def execute(self, config, resources): # type: (Config, List[models.Model]) -> None for resource in resources: for step in self._steps: step.handle(config, resource) class ResultsRecorder(object): def __init__(self, osutils): # type: (OSUtils) -> None self._osutils = osutils def record_results(self, results, chalice_stage_name, project_dir): # type: (Any, str, str) -> None deployed_dir = self._osutils.joinpath( project_dir, '.chalice', 'deployed') deployed_filename = self._osutils.joinpath( deployed_dir, '%s.json' % chalice_stage_name) if not self._osutils.directory_exists(deployed_dir): self._osutils.makedirs(deployed_dir) serialized = serialize_to_json(results) self._osutils.set_file_contents( filename=deployed_filename, contents=serialized, binary=False ) class NoopResultsRecorder(ResultsRecorder): def record_results(self, results, chalice_stage_name, project_dir): # type: (Any, str, str) -> None return None class DeploymentReporter(object): # We want the API URLs to be displayed last. _SORT_ORDER = { 'rest_api': 100, 'websocket_api': 100, 'domain_name': 100 } # The default is chosen to sort before the rest_api _DEFAULT_ORDERING = 50 def __init__(self, ui): # type: (UI) -> None self._ui = ui def generate_report(self, deployed_values): # type: (Dict[str, Any]) -> str report = [ 'Resources deployed:', ] ordered = sorted( deployed_values['resources'], key=lambda x: self._SORT_ORDER.get(x['resource_type'], self._DEFAULT_ORDERING)) for resource in ordered: getattr(self, '_report_%s' % resource['resource_type'], self._default_report)(resource, report) report.append('') return '\n'.join(report) def _report_rest_api(self, resource, report): # type: (Dict[str, Any], List[str]) -> None report.append(' - Rest API URL: %s' % resource['rest_api_url']) def _report_websocket_api(self, resource, report): # type: (Dict[str, Any], List[str]) -> None report.append( ' - Websocket API URL: %s' % resource['websocket_api_url']) def _report_domain_name(self, resource, report): # type: (Dict[str, Any], List[str]) -> None report.append( ' - Custom domain name:\n' ' HostedZoneId: %s\n' ' AliasDomainName: %s' % ( resource['hosted_zone_id'], resource['alias_domain_name'] ) ) def _report_lambda_function(self, resource, report): # type: (Dict[str, Any], List[str]) -> None report.append(' - Lambda ARN: %s' % resource['lambda_arn']) def _report_lambda_layer(self, resource, report): # type: (Dict[str, Any], List[str]) -> None report.append(' - Lambda Layer ARN: %s' % ( resource['layer_version_arn'])) def _default_report(self, resource, report): # type: (Dict[str, Any], List[str]) -> None # The default behavior is to not report a resource. This # cuts down on the output verbosity. pass def display_report(self, deployed_values): # type: (Dict[str, Any]) -> None report = self.generate_report(deployed_values) self._ui.write(report) ================================================ FILE: chalice/deploy/executor.py ================================================ import re import pprint from dataclasses import asdict, is_dataclass import jmespath from typing import Dict, List, Any # noqa from chalice.deploy import models # noqa from chalice.awsclient import TypedAWSClient # noqa from chalice.utils import UI # noqa class BaseExecutor(object): def __init__(self, client, ui): # type: (TypedAWSClient, UI) -> None self._client = client self._ui = ui self.resource_values = [] # type: List[Dict[str, Any]] def execute(self, plan): # type: (models.Plan) -> None pass class Executor(BaseExecutor): def __init__(self, client, ui): # type: (TypedAWSClient, UI) -> None super(Executor, self).__init__(client, ui) # A mapping of variables that's populated as API calls # are made. These can be used in subsequent API calls. self.variables = {} # type: Dict[str, Any] self._resource_value_index = {} # type: Dict[str, Any] self._variable_resolver = VariableResolver() def execute(self, plan): # type: (models.Plan) -> None messages = plan.messages for instruction in plan.instructions: message = messages.get(id(instruction)) if message is not None: self._ui.write(message) getattr(self, '_do_%s' % instruction.__class__.__name__.lower(), self._default_handler)(instruction) def _default_handler(self, instruction): # type: (models.Instruction) -> None raise RuntimeError("Deployment executor encountered an " "unknown instruction: %s" % instruction.__class__.__name__) def _do_apicall(self, instruction): # type: (models.APICall) -> None final_kwargs = self._resolve_variables(instruction) method = getattr(self._client, instruction.method_name) result = method(**final_kwargs) if instruction.output_var is not None: self.variables[instruction.output_var] = result def _do_copyvariable(self, instruction): # type: (models.CopyVariable) -> None to_var = instruction.to_var from_var = instruction.from_var self.variables[to_var] = self.variables[from_var] def _do_storevalue(self, instruction): # type: (models.StoreValue) -> None result = self._variable_resolver.resolve_variables( instruction.value, self.variables) self.variables[instruction.name] = result def _do_storemultiplevalue(self, instruction): # type: (models.StoreValue) -> None result = self._variable_resolver.resolve_variables( instruction.value, self.variables) data = self.variables.get(instruction.name) if data and isinstance(data, list): self.variables[instruction.name].extend(result) else: self.variables[instruction.name] = result def _do_recordresourcevariable(self, instruction): # type: (models.RecordResourceVariable) -> None payload = { 'name': instruction.resource_name, 'resource_type': instruction.resource_type, instruction.name: self.variables[instruction.variable_name], } self._add_to_deployed_values(payload) def _do_recordresourcevalue(self, instruction): # type: (models.RecordResourceValue) -> None payload = { 'name': instruction.resource_name, 'resource_type': instruction.resource_type, instruction.name: instruction.value, } self._add_to_deployed_values(payload) def _add_to_deployed_values(self, payload): # type: (Dict[str, str]) -> None key = payload['name'] if key not in self._resource_value_index: self._resource_value_index[key] = payload self.resource_values.append(payload) else: # If the key already exists, we merge the new payload # with the existing payload. self._resource_value_index[key].update(payload) def _do_jpsearch(self, instruction): # type: (models.JPSearch) -> None v = self.variables[instruction.input_var] result = jmespath.search(instruction.expression, v) self.variables[instruction.output_var] = result def _do_builtinfunction(self, instruction): # type: (models.BuiltinFunction) -> None # Split this out to a separate class of built in functions # once we add more functions. if instruction.function_name == 'parse_arn': resolved_args = self._variable_resolver.resolve_variables( instruction.args, self.variables) value = resolved_args[0] parts = value.split(':') result = { 'partition': parts[1], 'service': parts[2], 'region': parts[3], 'account_id': parts[4], 'dns_suffix': self._client.endpoint_dns_suffix(parts[2], parts[3]) } self.variables[instruction.output_var] = result elif instruction.function_name == 'interrogate_profile': region = self._client.region_name result = { 'partition': self._client.partition_name, 'region': region, 'dns_suffix': self._client.endpoint_dns_suffix('apigateway', region) } self.variables[instruction.output_var] = result elif instruction.function_name == 'service_principal': resolved_args = self._variable_resolver.resolve_variables( instruction.args, self.variables) service_name = resolved_args[0] region_name = self._client.region_name dns_suffix = self._client.endpoint_dns_suffix(service_name, region_name) result = { 'principal': self._client.service_principal(service_name, region_name, dns_suffix) } self.variables[instruction.output_var] = result else: raise ValueError("Unknown builtin function: %s" % instruction.function_name) def _resolve_variables(self, api_call): # type: (models.APICall) -> Dict[str, Any] try: return self._variable_resolver.resolve_variables( api_call.params, self.variables) except UnresolvedValueError as e: e.method_name = api_call.method_name raise class VariableResolver(object): def resolve_variables(self, value, variables): # type: (Any, Dict[str, str]) -> Any value_type = type(value).__name__.lower() handler_name = '_resolve_%s' % value_type handler = getattr(self, handler_name, None) if handler: return handler(value, variables) else: return value def _resolve_variable(self, value, variables): # type: (Any, Dict[str, str]) -> Any return variables[value.name] def _resolve_stringformat(self, value, variables): # type: (Any, Dict[str, str]) -> Any v = {k: variables[k] for k in value.variables} return value.template.format(**v) def _resolve_keydatavariable(self, value, variables): # type: (Any, Dict[str, str]) -> Any return variables[value.name][value.key] def _resolve_placeholder(self, value, variables): # type: (Any, Dict[str, str]) -> Any # The key and method_name values are added # as the exception propagates up the stack. raise UnresolvedValueError('', value, '') def _resolve_dict(self, value, variables): # type: (Any, Dict[str, str]) -> Any final = {} for k, v in value.items(): try: final[k] = self.resolve_variables(v, variables) except UnresolvedValueError as e: e.key = k raise return final def _resolve_list(self, value, variables): # type: (Any, Dict[str, str]) -> Any final_list = [] for v in value: final_list.append(self.resolve_variables(v, variables)) return final_list # This class is used for the ``chalice dev plan`` command. # The dev commands don't have any backwards compatibility guarantees # so we can alter this output as needed. class DisplayOnlyExecutor(BaseExecutor): # Max length of bytes object before we truncate with '' _MAX_BYTE_LENGTH = 30 _LINE_VERTICAL = '\u2502' def execute(self, plan): # type: (models.Plan) -> None spillover_values = {} # type: Dict[str, Any] self._ui.write("Plan\n") self._ui.write("====\n\n") for instruction in plan.instructions: getattr(self, '_do_%s' % instruction.__class__.__name__.lower(), self._default_handler)(instruction, spillover_values) self._write_spillover(spillover_values) def _write_spillover(self, spillover_values): # type: (Dict[str, Any]) -> None if not spillover_values: return self._ui.write("Variable Pool\n") self._ui.write("=============\n\n") for key, value in spillover_values.items(): self._ui.write('%s:\n' % key) self._ui.write(pprint.pformat(value) + '\n\n') def _default_handler(self, instruction, spillover_values): # type: (models.Instruction, Dict[str, Any]) -> None instruction_name = self._upper_snake_case( instruction.__class__.__name__) # Need this to make typing happy . We're certain that we're always # dealing with a dataclass, but the base type `Instruction` has # no dataclass pieces. There's probably a better way to represent # this type hierarchy. assert is_dataclass(instruction) and not isinstance(instruction, type) for key, value in asdict(instruction).items(): if isinstance(value, dict): value = self._format_dict(value, spillover_values) line = ('%-30s %s%20s %-10s' % ( instruction_name, self._LINE_VERTICAL, '%s:' % key, value) ) self._ui.write(line + '\n') instruction_name = '' self._ui.write('\n') def _format_dict(self, dict_value, spillover_values): # type: (Dict[str, Any], Dict[str, Any]) -> str lines = [''] for key, value in dict_value.items(): if not value: continue if isinstance(value, bytes) and len(value) > self._MAX_BYTE_LENGTH: value = '' if isinstance(value, (dict, list)): # We need a unique name to use so we just use a simple # incrementing counter with the name prefixed. spillover_name = '${%s_%s}' % ( key.upper(), len(spillover_values)) spillover_values[spillover_name] = value value = spillover_name line = '%-31s%s%-15s%s%20s %-10s' % ( ' ', self._LINE_VERTICAL, ' ', self._LINE_VERTICAL, '%s:' % key, value ) lines.append(line) return '\n'.join(lines) def _upper_snake_case(self, v): # type: (str) -> str first_cap_regex = re.compile('(.)([A-Z][a-z]+)') end_cap_regex = re.compile('([a-z0-9])([A-Z])') first = first_cap_regex.sub(r'\1_\2', v) transformed = end_cap_regex.sub(r'\1_\2', first).upper() return transformed class UnresolvedValueError(Exception): MSG = ( "The API parameter '%s' has an unresolved value " "of %s in the method call: %s" ) def __init__(self, key, value, method_name): # type: (str, models.Placeholder, str) -> None super(UnresolvedValueError, self).__init__() self.key = key self.value = value self.method_name = method_name def __str__(self): # type: () -> str return self.MSG % (self.key, self.value, self.method_name) ================================================ FILE: chalice/deploy/models.py ================================================ # pylint: disable=line-too-long from __future__ import annotations from dataclasses import dataclass, field import enum from typing import List, Dict, Optional as Opt, Any, TypeVar, Union, Set # noqa from typing import cast class Placeholder(enum.Enum): BUILD_STAGE = 'build_stage' class Instruction(object): pass class RoleTraits(enum.Enum): VPC_NEEDED = 'vpc_needed' class APIType(enum.Enum): WEBSOCKET = 'WEBSOCKET' HTTP = 'HTTP' class TLSVersion(enum.Enum): TLS_1_0 = 'TLS_1_0' TLS_1_1 = 'TLS_1_1' TLS_1_2 = 'TLS_1_2' @classmethod def create(cls, str_version: str) -> Opt[TLSVersion]: for version in cls: if version.value == str_version: return version return None T = TypeVar('T') DV = Union[Placeholder, T] StrMap = Dict[str, str] @dataclass class Plan: instructions: List[Instruction] = field(default_factory=list) messages: Dict[int, str] = field(default_factory=dict) @dataclass(frozen=True) class APICall(Instruction): method_name: str params: Dict[str, Any] output_var: Opt[str] = None @dataclass(frozen=True) class StoreValue(Instruction): name: str value: Any @dataclass(frozen=True) class StoreMultipleValue(Instruction): name: str value: List[Any] = field(default_factory=list) @dataclass(frozen=True) class CopyVariable(Instruction): from_var: str to_var: str @dataclass(frozen=True) class CopyVariableFromDict(Instruction): from_var: str key: str to_var: str @dataclass(frozen=True) class RecordResource(Instruction): resource_type: str resource_name: str name: str @dataclass(frozen=True) class RecordResourceVariable(RecordResource): variable_name: str @dataclass(frozen=True) class RecordResourceValue(RecordResource): value: Any @dataclass(frozen=True) class JPSearch(Instruction): expression: Any input_var: Any output_var: Any @dataclass(frozen=True) class BuiltinFunction(Instruction): function_name: str args: List[Any] output_var: str @dataclass class Model(object): def dependencies(self) -> List[Model]: return [] @dataclass class ManagedModel(Model): resource_name: str # Subclasses must fill in this attribute. resource_type = '' @dataclass class Application(Model): stage: str resources: List[Model] def dependencies(self) -> List[Model]: return self.resources @dataclass class DeploymentPackage(Model): filename: DV[str] @dataclass class IAMPolicy(Model): document: DV[Dict[str, Any]] @dataclass class FileBasedIAMPolicy(IAMPolicy): filename: str @dataclass class AutoGenIAMPolicy(IAMPolicy): traits: Set[RoleTraits] = field(default_factory=set) @dataclass class IAMRole(Model): pass @dataclass class PreCreatedIAMRole(IAMRole): role_arn: str @dataclass class ManagedIAMRole(IAMRole, ManagedModel): resource_type = 'iam_role' role_name: str trust_policy: Dict[str, Any] policy: IAMPolicy def dependencies(self) -> List[Model]: return [self.policy] @dataclass class LambdaLayer(ManagedModel): resource_type = 'lambda_layer' layer_name: str runtime: str deployment_package: DeploymentPackage is_empty: bool = False def dependencies(self) -> List[Model]: return [self.deployment_package] @dataclass class LambdaFunction(ManagedModel): resource_type = 'lambda_function' function_name: str deployment_package: DeploymentPackage environment_variables: StrMap xray: bool runtime: str handler: str tags: StrMap timeout: int memory_size: int role: IAMRole security_group_ids: List[str] subnet_ids: List[str] reserved_concurrency: int # These are customer created layers. layers: List[str] managed_layer: Opt[LambdaLayer] = None log_group: Opt[LogGroup] = None def dependencies(self) -> List[Model]: resources: List[Model] = [] if self.managed_layer is not None: resources.append(self.managed_layer) if self.log_group is not None: resources.append(self.log_group) resources.extend([self.role, self.deployment_package]) return resources @dataclass class FunctionEventSubscriber(ManagedModel): lambda_function: LambdaFunction def dependencies(self) -> List[Model]: return [self.lambda_function] @dataclass class CloudWatchEventBase(FunctionEventSubscriber): rule_name: str @dataclass class CloudWatchEvent(CloudWatchEventBase): resource_type = 'cloudwatch_event' event_pattern: str @dataclass class ScheduledEvent(CloudWatchEventBase): resource_type = 'scheduled_event' schedule_expression: str rule_description: Opt[str] = None @dataclass class LogGroup(ManagedModel): resource_type = 'log_group' log_group_name: str retention_in_days: int @dataclass class APIMapping(ManagedModel): resource_type = 'api_mapping' mount_path: str api_gateway_stage: str @dataclass class DomainName(ManagedModel): resource_type = 'domain_name' domain_name: str protocol: APIType api_mapping: APIMapping certificate_arn: str tags: Opt[Dict[str, Any]] = None tls_version: Opt[TLSVersion] = None def dependencies(self) -> List[Model]: return [self.api_mapping] @dataclass class RestAPI(ManagedModel): resource_type = 'rest_api' swagger_doc: DV[Dict[str, Any]] minimum_compression: str api_gateway_stage: str endpoint_type: str lambda_function: LambdaFunction xray: bool = False policy: Opt[IAMPolicy] = None authorizers: List[LambdaFunction] = field(default_factory=list) domain_name: Opt[DomainName] = None vpce_ids: Opt[List[str]] = None def dependencies(self) -> List[Model]: resources: List[Model] = [] resources.extend([self.lambda_function] + self.authorizers) if self.domain_name is not None: resources.append(self.domain_name) return cast(List[Model], resources) @dataclass class WebsocketAPI(ManagedModel): resource_type = 'websocket_api' name: str api_gateway_stage: str routes: List[str] connect_function: Opt[LambdaFunction] message_function: Opt[LambdaFunction] disconnect_function: Opt[LambdaFunction] domain_name: Opt[DomainName] = None def dependencies(self) -> List[Model]: resources: List[Model] = [] if self.domain_name is not None: resources.append(self.domain_name) if self.connect_function is not None: resources.append(self.connect_function) if self.message_function is not None: resources.append(self.message_function) if self.disconnect_function is not None: resources.append(self.disconnect_function) return resources @dataclass class S3BucketNotification(FunctionEventSubscriber): resource_type = 's3_event' bucket: str events: List[str] prefix: Opt[str] suffix: Opt[str] @dataclass class SNSLambdaSubscription(FunctionEventSubscriber): resource_type = 'sns_event' topic: str @dataclass class QueueARN(object): arn: str @property def queue_name(self) -> str: # Pylint 2.x validates this correctly, but for py27, we have to # use Pylint 1.x which doesn't support dataclass. return self.arn.rpartition(':')[2] # pylint: disable=no-member @dataclass class SQSEventSource(FunctionEventSubscriber): resource_type = 'sqs_event' queue: Union[str, QueueARN] batch_size: int maximum_batching_window_in_seconds: int maximum_concurrency: Opt[int] = None @dataclass class KinesisEventSource(FunctionEventSubscriber): resource_type = 'kinesis_event' stream: str batch_size: int starting_position: str maximum_batching_window_in_seconds: int @dataclass class DynamoDBEventSource(FunctionEventSubscriber): resource_type = 'dynamodb_event' stream_arn: str batch_size: int starting_position: str maximum_batching_window_in_seconds: int ================================================ FILE: chalice/deploy/packager.py ================================================ # pylint: disable=too-many-lines from __future__ import annotations import sys import hashlib import inspect import re import subprocess import logging import functools from email.parser import FeedParser from email.message import Message # noqa from zipfile import ZipFile # noqa from typing import Any, Set, List, Optional, Tuple, Iterable, Callable # noqa from typing import Iterator # noqa from typing import Dict, MutableMapping, cast # noqa from chalice.compat import pip_import_string from chalice.compat import pip_no_compile_c_env_vars from chalice.compat import pip_no_compile_c_shim from chalice.utils import OSUtils from chalice.utils import UI # noqa from chalice.constants import MISSING_DEPENDENCIES_TEMPLATE import chalice from chalice import app StrMap = Dict[str, Any] OptStrMap = Optional[StrMap] EnvVars = MutableMapping OptStr = Optional[str] OptBytes = Optional[bytes] logger = logging.getLogger(__name__) class InvalidSourceDistributionNameError(Exception): pass class MissingDependencyError(Exception): """Raised when some dependencies could not be packaged for any reason.""" def __init__(self, missing: Set[Package]) -> None: self.missing = missing class NoSuchPackageError(Exception): """Raised when a package name or version could not be found.""" def __init__(self, package_name: str) -> None: super(NoSuchPackageError, self).__init__( 'Could not satisfy the requirement: %s' % package_name ) class PackageDownloadError(Exception): """Generic networking error during a package download.""" class EmptyPackageError(Exception): """A deployment package cannot be an empty zip file.""" class UnsupportedPackageError(Exception): """Unable to parse package metadata.""" def __init__(self, package_name: str) -> None: super(UnsupportedPackageError, self).__init__( 'Unable to retrieve name/version for package: %s' % package_name ) class BaseLambdaDeploymentPackager(object): _CHALICE_LIB_DIR = 'chalicelib' _VENDOR_DIR = 'vendor' _RUNTIME_TO_ABI = { 'python3.9': 'cp39', 'python3.10': 'cp310', 'python3.11': 'cp311', 'python3.12': 'cp312', 'python3.13': 'cp313', } def __init__( self, osutils: OSUtils, dependency_builder: DependencyBuilder, ui: UI ) -> None: self._osutils = osutils self._dependency_builder = dependency_builder self._ui = ui def create_deployment_package( self, project_dir: str, python_version: str ) -> str: raise NotImplementedError("create_deployment_package") def _get_requirements_filename(self, project_dir: str) -> str: # Gets the path to a requirements.txt file out of a project dir path return self._osutils.joinpath(project_dir, 'requirements.txt') def _add_vendor_files( self, zipped: ZipFile, dirname: str, prefix: str = '' ) -> None: if not self._osutils.directory_exists(dirname): return prefix_len = len(dirname) + 1 for root, _, filenames in self._osutils.walk( dirname, followlinks=True ): for filename in filenames: full_path = self._osutils.joinpath(root, filename) zip_path = full_path[prefix_len:] if prefix: zip_path = self._osutils.joinpath(prefix, zip_path) zipped.write(full_path, zip_path) def deployment_package_filename( self, project_dir: str, python_version: str ) -> str: # Computes the name of the deployment package zipfile # based on a hash of the requirements file. # This is done so that we only "pip install -r requirements.txt" # when we know there's new dependencies we need to install. # The python version these depedencies were downloaded for is appended # to the end of the filename since the the dependencies may not change # but if the python version changes then the dependencies need to be # re-downloaded since they will not be compatible. return self._deployment_package_filename(project_dir, python_version) def _deployment_package_filename( self, project_dir: str, python_version: str, prefix: str = '' ) -> str: requirements_filename = self._get_requirements_filename(project_dir) hash_contents = self._hash_project_dir( requirements_filename, self._osutils.joinpath(project_dir, self._VENDOR_DIR), project_dir, ) filename = '%s%s-%s.zip' % (prefix, hash_contents, python_version) deployment_package_filename = self._osutils.joinpath( project_dir, '.chalice', 'deployments', filename ) return deployment_package_filename def _add_py_deps( self, zip_fileobj: ZipFile, deps_dir: str, prefix: str = '' ) -> None: prefix_len = len(deps_dir) + 1 for root, dirnames, filenames in self._osutils.walk(deps_dir): if root == deps_dir and 'chalice' in dirnames: # Don't include any chalice deps. We cherry pick # what we want to include in _add_app_files. dirnames.remove('chalice') for filename in filenames: full_path = self._osutils.joinpath(root, filename) zip_path = full_path[prefix_len:] if prefix: zip_path = self._osutils.joinpath(prefix, zip_path) zip_fileobj.write(full_path, zip_path) def _add_app_files(self, zip_fileobj: ZipFile, project_dir: str) -> None: for full_path, zip_path in self._iter_app_filenames(project_dir): zip_fileobj.write(full_path, zip_path) def _iter_app_filenames( self, project_dir: str ) -> Iterator[Tuple[str, str]]: chalice_router = inspect.getfile(app) if chalice_router.endswith('.pyc'): chalice_router = chalice_router[:-1] yield (chalice_router, 'chalice/app.py') chalice_init = inspect.getfile(chalice) if chalice_init.endswith('.pyc'): chalice_init = chalice_init[:-1] yield (chalice_init, 'chalice/__init__.py') yield (self._osutils.joinpath(project_dir, 'app.py'), 'app.py') yield from self._iter_chalice_lib_if_needed(project_dir) def _hash_project_dir( self, requirements_filename: str, vendor_dir: str, project_dir: str ) -> str: if not self._osutils.file_exists(requirements_filename): contents = b'' else: contents = cast( bytes, self._osutils.get_file_contents( requirements_filename, binary=True ), ) h = hashlib.md5(contents) for filename, _ in self._iter_app_filenames(project_dir): with self._osutils.open(filename, 'rb') as f: reader = functools.partial(f.read, 1024 * 1024) for chunk in iter(reader, b''): h.update(chunk) if self._osutils.directory_exists(vendor_dir): self._hash_vendor_dir(vendor_dir, h) return h.hexdigest() def _hash_vendor_dir(self, vendor_dir: str, md5: Any) -> None: for rootdir, _, filenames in self._osutils.walk( vendor_dir, followlinks=True ): for filename in filenames: fullpath = self._osutils.joinpath(rootdir, filename) with self._osutils.open(fullpath, 'rb') as f: # Not actually an issue, but pylint will complain # about the f var being used in the lambda function # is being used in a loop. This is ok because # we're immediately using the lambda function. # Also binding it as a default argument fixes # pylint, but mypy will complain that it can't # infer the types. So the compromise here is to # just write it the idiomatic way and have pylint # ignore this warning. reader = functools.partial(f.read, 1024 * 1024) for chunk in iter(reader, b''): md5.update(chunk) def inject_latest_app( self, deployment_package_filename: str, project_dir: str ) -> None: """Inject latest version of chalice app into a zip package. This method takes a pre-created deployment package and injects in the latest chalice app code. This is useful in the case where you have no new package deps but have updated your chalice app code. :type deployment_package_filename: str :param deployment_package_filename: The zipfile of the preexisting deployment package. :type project_dir: str :param project_dir: Path to chalice project dir. """ # Use the premade zip file and replace the app.py file # with the latest version. Python's zipfile does not have # a way to do this efficiently so we need to create a new # zip file that has all the same stuff except for the new # app file. self._ui.write("Regen deployment package.\n") tmpzip = deployment_package_filename + '.tmp.zip' with self._osutils.open_zip(deployment_package_filename, 'r') as inzip: with self._osutils.open_zip( tmpzip, 'w', self._osutils.ZIP_DEFLATED ) as outzip: for el in inzip.infolist(): if self._needs_latest_version(el.filename): continue contents = inzip.read(el.filename) outzip.writestr(el, contents) # Then at the end, add back the app.py, chalicelib, # and runtime files. self._add_app_files(outzip, project_dir) self._osutils.move(tmpzip, deployment_package_filename) def _needs_latest_version(self, filename: str) -> bool: return filename == 'app.py' or filename.startswith( ('chalicelib/', 'chalice/') ) def _iter_chalice_lib_if_needed( self, project_dir: str ) -> Iterator[Tuple[str, str]]: libdir = self._osutils.joinpath(project_dir, self._CHALICE_LIB_DIR) if self._osutils.directory_exists(libdir): for rootdir, _, filenames in self._osutils.walk(libdir): for filename in filenames: fullpath = self._osutils.joinpath(rootdir, filename) zip_path = self._osutils.joinpath( self._CHALICE_LIB_DIR, fullpath[len(libdir) + 1:] ) yield (fullpath, zip_path) def _create_output_dir_if_needed(self, package_filename: str) -> None: dirname = self._osutils.dirname( self._osutils.abspath(package_filename) ) if not self._osutils.directory_exists(dirname): self._osutils.makedirs(dirname) def _build_python_dependencies( self, python_version: str, requirements_filepath: str, site_packages_dir: str, ) -> None: try: abi = self._RUNTIME_TO_ABI[python_version] self._dependency_builder.build_site_packages( abi, requirements_filepath, site_packages_dir ) except MissingDependencyError as e: missing_packages = '\n'.join([p.identifier for p in e.missing]) self._ui.write(MISSING_DEPENDENCIES_TEMPLATE % missing_packages) class LambdaDeploymentPackager(BaseLambdaDeploymentPackager): def create_deployment_package( self, project_dir: str, python_version: str ) -> str: msg = "Creating deployment package." self._ui.write("%s\n" % msg) logger.debug(msg) package_filename = self.deployment_package_filename( project_dir, python_version ) if self._osutils.file_exists(package_filename): self._ui.write("Reusing existing deployment package.\n") return package_filename self._create_output_dir_if_needed(package_filename) with self._osutils.tempdir() as tmpdir: requirements_filepath = self._get_requirements_filename( project_dir ) self._build_python_dependencies( python_version, requirements_filepath, site_packages_dir=tmpdir ) with self._osutils.open_zip( package_filename, 'w', self._osutils.ZIP_DEFLATED ) as z: self._add_py_deps(z, deps_dir=tmpdir) self._add_app_files(z, project_dir) self._add_vendor_files( z, self._osutils.joinpath(project_dir, self._VENDOR_DIR) ) return package_filename class AppOnlyDeploymentPackager(BaseLambdaDeploymentPackager): def create_deployment_package( self, project_dir: str, python_version: str ) -> str: msg = "Creating app deployment package." self._ui.write("%s\n" % msg) logger.debug(msg) package_filename = self.deployment_package_filename( project_dir, python_version ) if self._osutils.file_exists(package_filename): self._ui.write(" Reusing existing app deployment package.\n") return package_filename self._create_output_dir_if_needed(package_filename) with self._osutils.open_zip( package_filename, 'w', self._osutils.ZIP_DEFLATED ) as z: self._add_app_files(z, project_dir) return package_filename def deployment_package_filename( self, project_dir: str, python_version: str ) -> str: return self._deployment_package_filename( project_dir, python_version, prefix='appcode-' ) def _deployment_package_filename( self, project_dir: str, python_version: str, prefix: str = '' ) -> str: h = hashlib.md5(b'') for filename, _ in self._iter_app_filenames(project_dir): with self._osutils.open(filename, 'rb') as f: reader = functools.partial(f.read, 1024 * 1024) for chunk in iter(reader, b''): h.update(chunk) digest = h.hexdigest() filename = '%s%s-%s.zip' % (prefix, digest, python_version) deployment_package_filename = self._osutils.joinpath( project_dir, '.chalice', 'deployments', filename ) return deployment_package_filename class LayerDeploymentPackager(BaseLambdaDeploymentPackager): # A Lambda layer will unzip into the /opt directory instead of # the current working directory of the function. This means # in order for our python dependencies to work we need. _PREFIX = 'python/lib/%s/site-packages' def create_deployment_package( self, project_dir: str, python_version: str ) -> str: msg = "Creating shared layer deployment package." self._ui.write("%s\n" % msg) logger.debug(msg) package_filename = self.deployment_package_filename( project_dir, python_version ) self._create_output_dir_if_needed(package_filename) if self._osutils.file_exists(package_filename): self._ui.write( " Reusing existing shared layer deployment package.\n" ) return package_filename with self._osutils.tempdir() as tmpdir: requirements_filepath = self._get_requirements_filename( project_dir ) self._build_python_dependencies( python_version, requirements_filepath, site_packages_dir=tmpdir ) with self._osutils.open_zip( package_filename, 'w', self._osutils.ZIP_DEFLATED ) as z: prefix = self._PREFIX % python_version self._add_py_deps(z, deps_dir=tmpdir, prefix=prefix) self._add_vendor_files( z, self._osutils.joinpath(project_dir, self._VENDOR_DIR), prefix=prefix, ) self._check_valid_package(package_filename) return package_filename def _check_valid_package(self, package_filename: str) -> None: # Lambda does not allow empty deployment packages, so if there are no # requirements.txt deps or anything in vendor/, we need to let the # call know that we couldn't generate a useful deployment package # for them. The caller will then need make the appropriate adjustments # i.e remove that LambdaLayer model from the app. with self._osutils.open_zip( package_filename, 'r', self._osutils.ZIP_DEFLATED ) as z: total_size = sum(f.file_size for f in z.infolist()) # We have to check the total archive size, Lambda will still error # out if you have a zip file with all empty files. It's not enough # to check if the zipfile is empty. if not total_size > 0: # We want to make sure we remove any deployment packages we # know are invalid, we don't want them being used as cache # hits in subsequent create_deployment_package() requests. self._osutils.remove_file(package_filename) raise EmptyPackageError(package_filename) def deployment_package_filename( self, project_dir: str, python_version: str ) -> str: return self._deployment_package_filename( project_dir, python_version, prefix='managed-layer-' ) def _deployment_package_filename( self, project_dir: str, python_version: str, prefix: str = '' ) -> str: requirements_filename = self._get_requirements_filename(project_dir) if not self._osutils.file_exists(requirements_filename): contents = b'' else: contents = cast( bytes, self._osutils.get_file_contents( requirements_filename, binary=True ), ) h = hashlib.md5(contents) vendor_dir = self._osutils.joinpath(project_dir, self._VENDOR_DIR) if self._osutils.directory_exists(vendor_dir): self._hash_vendor_dir(vendor_dir, h) hash_contents = h.hexdigest() filename = '%s%s-%s.zip' % (prefix, hash_contents, python_version) deployment_package_filename = self._osutils.joinpath( project_dir, '.chalice', 'deployments', filename ) return deployment_package_filename class DependencyBuilder(object): """Build site-packages by manually downloading and unpacking wheels. Pip is used to download all the dependency sdists. Then wheels that are compatible with lambda are downloaded. Any source packages that do not have a matching wheel file are built into a wheel and that file is checked for compatibility with the lambda python runtime environment. All compatible wheels that are downloaded/built this way are unpacked into a site-packages directory, to be included in the bundle by the packager. """ _ADDITIONAL_COMPATIBLE_PLATFORM = {'any', 'linux_x86_64'} _MANYLINUX_LEGACY_MAP = { 'manylinux1_x86_64': 'manylinux_2_5_x86_64', 'manylinux2010_x86_64': 'manylinux_2_12_x86_64', 'manylinux2014_x86_64': 'manylinux_2_17_x86_64', } # Mapping of abi to glibc version in Lambda runtime. _RUNTIME_GLIBC = { 'cp27mu': (2, 17), 'cp36m': (2, 17), 'cp37m': (2, 17), 'cp38': (2, 26), 'cp310': (2, 26), 'cp311': (2, 26), 'cp312': (2, 34), 'cp313': (2, 34), } # Fallback version if we're on an unknown python version # not in _RUNTIME_GLIBC. # Unlikely to hit this case. _DEFAULT_GLIBC = (2, 17) _COMPATIBLE_PACKAGE_WHITELIST = { 'sqlalchemy', 'pyyaml', 'pyrsistent', } def __init__( self, osutils: OSUtils, pip_runner: Optional[PipRunner] = None ) -> None: self._osutils = osutils if pip_runner is None: pip_runner = PipRunner(SubprocessPip(osutils)) self._pip = pip_runner def _is_compatible_wheel_filename( self, expected_abi: str, filename: str ) -> bool: wheel = filename[:-4] all_compatibility_tags = self._iter_all_compatibility_tags(wheel) for implementation, abi, platform in all_compatibility_tags: # Verify platform is compatible if not self._is_compatible_platform_tag(expected_abi, platform): continue # Verify that the ABI is compatible with lambda. Either none or the # correct type for the python version cp27mu for py27 and cp36m for # py36. if abi == 'none': return True prefix_version = implementation[:3] expected_abis = [expected_abi] if prefix_version == 'cp3': # Deploying python 3 function which means we can accept the # version specific abi, or we can accept the CPython 3 stable # ABI of 'abi3'. expected_abis.append('abi3') if abi in expected_abis: return True return False def _is_compatible_platform_tag( self, expected_abi: str, platform: str ) -> bool: # From PEP 600, the new manylinux tag is # manylinux_${GLIBCMAJOR}_${GLIBCMINOR}_${ARCH} # e.g. manylinux_2_17_x86_64. # To check if the wheel is compatible, we first need to map any of the # legacy manylinux formats to the new perennial format. # Then we verify that the glibc version is compatible with the version # on the Lambda runtime (from _RUNTIME_GLIBC). if platform in self._ADDITIONAL_COMPATIBLE_PLATFORM: logger.debug("Found compatible platform tag: %s", platform) return True elif platform.startswith('manylinux'): # This is roughly based on the "Package Installers" section from # PEP 600. perennial_tag = self._MANYLINUX_LEGACY_MAP.get(platform, platform) m = re.match("manylinux_([0-9]+)_([0-9]+)_(.*)", perennial_tag) if m is None: return False tag_major, tag_minor = [int(x) for x in m.groups()[:2]] runtime_major, runtime_minor = self._RUNTIME_GLIBC.get( expected_abi, self._DEFAULT_GLIBC ) if (tag_major, tag_minor) <= (runtime_major, runtime_minor): logger.debug( "Tag glibc (%s, %s) is compatible with " "runtime glibc (%s, %s)", tag_major, tag_minor, runtime_major, runtime_minor, ) return True return False def _iter_all_compatibility_tags( self, wheel: str ) -> Iterator[Tuple[str, str, str]]: # From PEP 425, section "Compressed Tag Sets" # # "To allow for compact filenames of bdists that work with more than # one compatibility tag triple, each tag in a filename can instead be a # '.'-separated, sorted, set of tags." # # So this means that we have to iterate over all the possible # compatibility tag tuples and check if the wheel is compatible. # We just need any of the combinations to match for us to consider the # wheel compatible. implementation_tag, abi_tag, platform_tag = wheel.split('-')[-3:] for implementation in implementation_tag.split('.'): for abi in abi_tag.split('.'): for platform in platform_tag.split('.'): yield (implementation, abi, platform) def _has_at_least_one_package(self, filename: str) -> bool: if not self._osutils.file_exists(filename): return False with open(filename, 'r') as f: # This is meant to be a best effort attempt. # This can return True and still have no packages # actually being specified, but those aren't common # cases. for line in f: line = line.strip() if line and not line.startswith('#'): return True return False def _download_all_dependencies( self, requirements_filename: str, directory: str ) -> Set[Package]: # Download dependencies prefering wheel files but falling back to # raw source dependences to get the transitive closure over # the dependency graph. Return the set of all package objects # which will serve as the master list of dependencies needed to deploy # successfully. self._pip.download_all_dependencies(requirements_filename, directory) deps = { Package(directory, filename) for filename in self._osutils.get_directory_contents(directory) } logger.debug("Full dependency closure: %s", deps) return deps def _download_binary_wheels( self, abi: str, packages: Set[Package], directory: str ) -> None: # Try to get binary wheels for each package that isn't compatible. logger.debug("Downloading manylinux wheels: %s", packages) self._pip.download_manylinux_wheels( abi, [pkg.identifier for pkg in packages], directory ) def _download_sdists(self, packages: Set[Package], directory: str) -> None: logger.debug("Downloading missing sdists: %s", packages) self._pip.download_sdists( [pkg.identifier for pkg in packages], directory ) def _find_sdists(self, directory: str) -> Set[Package]: packages = [ Package(directory, filename) for filename in self._osutils.get_directory_contents(directory) ] sdists = { package for package in packages if package.dist_type == 'sdist' } return sdists def _build_sdists( self, sdists: Set[Package], directory: str, compile_c: bool = True ) -> None: logger.debug( "Build missing wheels from sdists (C compiling %s): %s", compile_c, sdists, ) for sdist in sdists: path_to_sdist = self._osutils.joinpath(directory, sdist.filename) self._pip.build_wheel(path_to_sdist, directory, compile_c) def _categorize_wheel_files( self, abi: str, directory: str ) -> Tuple[Set[Package], Set[Package]]: final_wheels = [ Package(directory, filename) for filename in self._osutils.get_directory_contents(directory) if filename.endswith('.whl') ] compatible_wheels, incompatible_wheels = set(), set() for wheel in final_wheels: if self._is_compatible_wheel_filename(abi, wheel.filename): compatible_wheels.add(wheel) else: incompatible_wheels.add(wheel) return compatible_wheels, incompatible_wheels def _categorize_deps(self, abi: str, deps: Set[Package]) -> Any: compatible_wheels = set() incompatible_wheels = set() sdists = set() for package in deps: if package.dist_type == 'sdist': sdists.add(package) else: if self._is_compatible_wheel_filename(abi, package.filename): compatible_wheels.add(package) else: incompatible_wheels.add(package) return sdists, compatible_wheels, incompatible_wheels def _download_dependencies( self, abi: str, directory: str, requirements_filename: str ) -> Tuple[Set[Package], Set[Package]]: # Download all dependencies we can, letting pip choose what to # download. # deps should represent the best effort we can make to gather all the # dependencies. deps = self._download_all_dependencies( requirements_filename, directory ) # Sort the downloaded packages into three categories: # - sdists (Pip could not get a wheel so it gave us an sdist) # - lambda compatible wheel files # - lambda incompatible wheel files # Pip will give us a wheel when it can, but some distributions do not # ship with wheels at all in which case we will have an sdist for it. # In some cases a platform specific wheel file may be availble so pip # will have downloaded that, if our platform does not match the # platform lambda runs on (linux_x86_64/manylinux) then the downloaded # wheel file may not be compatible with lambda. Pure python wheels # still will be compatible because they have no platform dependencies. sdists, compatible_wheels, incompatible_wheels = self._categorize_deps( abi, deps ) logger.debug("Compatible wheels for Lambda: %s", compatible_wheels) logger.debug( "Initial incompatible wheels for Lambda: %s", incompatible_wheels | sdists, ) # Next we need to go through the downloaded packages and pick out any # dependencies that do not have a compatible wheel file downloaded. # For these packages we need to explicitly try to download a # compatible wheel file. missing_wheels = sdists.union(incompatible_wheels) self._download_binary_wheels(abi, missing_wheels, directory) # Re-count the wheel files after the second download pass. Anything # that has an sdist but not a valid wheel file is still not going to # work on lambda and we must now try and build the sdist into a wheel # file ourselves. # There also may be the case where no sdist was ever downloaded. For # example if we are on MacOS, and the package in question has a mac # compatible wheel file but no linux ones, we will only have an # incompatible wheel file and no sdist. So we need to get any missing # sdists before we can build them. compatible_wheels, incompatible_wheels = self._categorize_wheel_files( abi, directory ) # The self._download_binary_wheels() can now introduce duplicate # entries. For example, if we download a macOS whl at first but # then we're able to download a manylinux1 wheel, we'll now have # two wheels for the package, so we have to remove any compatible # wheels from our set of incompatible wheels. incompatible_wheels -= compatible_wheels missing_sdists = incompatible_wheels - sdists self._download_sdists(missing_sdists, directory) sdists = self._find_sdists(directory) logger.debug( "compatible wheels after second download pass: %s", compatible_wheels, ) missing_wheels = sdists - compatible_wheels self._build_sdists(missing_wheels, directory, compile_c=True) # There is still the case where the package had optional C dependencies # for speedups. In this case the wheel file will have built above with # the C dependencies if it managed to find a C compiler. If we are on # an incompatible architecture this means the wheel file generated will # not be compatible. If we categorize our files once more and find that # there are missing dependencies we can try our last ditch effort of # building the package and trying to sever its ability to find a C # compiler. compatible_wheels, incompatible_wheels = self._categorize_wheel_files( abi, directory ) logger.debug( "compatible after building wheels (C compiling): %s", compatible_wheels, ) missing_wheels = sdists - compatible_wheels self._build_sdists(missing_wheels, directory, compile_c=False) # Final pass to find the compatible wheel files and see if there are # any unmet dependencies left over. At this point there is nothing we # can do about any missing wheel files. We tried downloading a # compatible version directly and building from source. compatible_wheels, incompatible_wheels = self._categorize_wheel_files( abi, directory ) logger.debug( "compatible after building wheels (no C compiling): %s", compatible_wheels, ) # Now there is still the case left over where the setup.py has been # made in such a way to be incompatible with python's setup tools, # causing it to lie about its compatibility. To fix this we have a # manually curated whitelist of packages that will work, despite # claiming otherwise. compatible_wheels, incompatible_wheels = self._apply_wheel_whitelist( compatible_wheels, incompatible_wheels ) missing_wheels = deps - compatible_wheels logger.debug("Final compatible: %s", compatible_wheels) logger.debug("Final incompatible: %s", incompatible_wheels) logger.debug("Final missing wheels: %s", missing_wheels) return compatible_wheels, missing_wheels def _apply_wheel_whitelist( self, compatible_wheels: Set[Package], incompatible_wheels: Set[Package], ) -> Tuple[Set[Package], Set[Package]]: compatible_wheels = set(compatible_wheels) actual_incompatible_wheels = set() for missing_package in incompatible_wheels: if missing_package.name in self._COMPATIBLE_PACKAGE_WHITELIST: compatible_wheels.add(missing_package) else: actual_incompatible_wheels.add(missing_package) return compatible_wheels, actual_incompatible_wheels def _install_purelib_and_platlib(self, wheel: Package, root: str) -> None: # Take a wheel package and the directory it was just unpacked into and # unpackage the purelib/platlib directories if they are present into # the parent directory. On some systems purelib and platlib need to # be installed into separate locations, for lambda this is not the case # and both should be installed in site-packages. dirnames = self._osutils.get_directory_contents(root) for dirname in dirnames: if wheel.matches_data_dir(dirname): data_dir = self._osutils.joinpath(root, dirname) break else: return unpack_dirs = {'purelib', 'platlib'} data_contents = self._osutils.get_directory_contents(data_dir) for content_name in data_contents: if content_name in unpack_dirs: source = self._osutils.joinpath(data_dir, content_name) self._osutils.copytree(source, root) # No reason to keep the purelib/platlib source directory around # so we delete it to conserve space in the package. self._osutils.rmtree(source) def _install_wheels( self, src_dir: str, dst_dir: str, wheels: Set[Package] ) -> None: if self._osutils.directory_exists(dst_dir): self._osutils.rmtree(dst_dir) self._osutils.makedirs(dst_dir) for wheel in wheels: zipfile_path = self._osutils.joinpath(src_dir, wheel.filename) self._osutils.extract_zipfile(zipfile_path, dst_dir) self._install_purelib_and_platlib(wheel, dst_dir) def build_site_packages( self, abi: str, requirements_filepath: str, target_directory: str ) -> None: if self._has_at_least_one_package(requirements_filepath): with self._osutils.tempdir() as tempdir: wheels, packages_without_wheels = self._download_dependencies( abi, tempdir, requirements_filepath ) self._install_wheels(tempdir, target_directory, wheels) if packages_without_wheels: raise MissingDependencyError(packages_without_wheels) class Package(object): """A class to represent a package downloaded but not yet installed.""" def __init__( self, directory: str, filename: str, osutils: Optional[OSUtils] = None ) -> None: self.dist_type = 'wheel' if filename.endswith('.whl') else 'sdist' self._directory = directory self.filename = filename if osutils is None: osutils = OSUtils() self._osutils = osutils self._name, self._version = self._calculate_name_and_version() @property def name(self) -> str: return self._name @property def data_dir(self) -> str: # The directory format is {distribution}-{version}.data return '%s-%s.data' % (self._name, self._version) def matches_data_dir(self, dirname: str) -> bool: """Check if a directory name matches the data_dir of a package. This will normalize the directory name and perform a case-insensitive match against the package name's data dir. """ if not self.dist_type == 'wheel' or '-' not in dirname: return False name, version = dirname.split('-')[:2] comparison_data_dir = '%s-%s' % (self._normalize_name(name), version) return self.data_dir == comparison_data_dir @property def identifier(self) -> str: return '%s==%s' % (self._name, self._version) def __str__(self) -> str: return '%s(%s)' % (self.identifier, self.dist_type) def __repr__(self) -> str: return str(self) def __eq__(self, other: Any) -> bool: if not isinstance(other, Package): return False return self.identifier == other.identifier def __hash__(self) -> int: return hash(self.identifier) def _calculate_name_and_version(self) -> Tuple[str, str]: if self.dist_type == 'wheel': # From the wheel spec (PEP 427) # {distribution}-{version}(-{build tag})?-{python tag}-{abi tag}- # {platform tag}.whl name, version = self.filename.split('-')[:2] else: info_fetcher = SDistMetadataFetcher(osutils=self._osutils) sdist_path = self._osutils.joinpath(self._directory, self.filename) name, version = info_fetcher.get_package_name_and_version( sdist_path ) normalized_name = self._normalize_name(name) return normalized_name, version def _normalize_name(self, name: str) -> str: # Taken directly from PEP 503 return re.sub(r"[-_.]+", "-", name).lower() class SDistMetadataFetcher(object): """This is the "correct" way to get name and version from an sdist.""" # https://git.io/vQkwV _SETUPTOOLS_SHIM = ( "import setuptools, tokenize;__file__=%r;" "f=getattr(tokenize, 'open', open)(__file__);" "code=f.read().replace('\\r\\n', '\\n');" "f.close();" "exec(compile(code, __file__, 'exec'))" ) def __init__(self, osutils: Optional[OSUtils] = None) -> None: if osutils is None: osutils = OSUtils() self._osutils = osutils def _parse_pkg_info_file(self, filepath: str) -> Message: # The PKG-INFO generated by the egg-info command is in an email feed # format, so we use an email feedparser here to extract the metadata # from the PKG-INFO file. data = self._osutils.get_file_contents(filepath, binary=False) parser = FeedParser() parser.feed(data) return parser.close() def _get_pkg_info_filepath(self, package_dir: str) -> str: setup_py = self._osutils.joinpath(package_dir, 'setup.py') script = self._SETUPTOOLS_SHIM % setup_py cmd = [ sys.executable, '-c', script, '--no-user-cfg', 'egg_info', '--egg-base', 'egg-info', ] egg_info_dir = self._osutils.joinpath(package_dir, 'egg-info') self._osutils.makedirs(egg_info_dir) p = subprocess.Popen( cmd, cwd=package_dir, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) _, stderr = p.communicate() if p.returncode != 0: logger.debug( "Non zero rc (%s) from the setup.py egg_info command: %s", p.returncode, stderr, ) info_contents = self._osutils.get_directory_contents(egg_info_dir) if info_contents: pkg_info_path = self._osutils.joinpath( egg_info_dir, info_contents[0], 'PKG-INFO' ) else: # This might be a pep 517 package in which case this PKG-INFO file # should be available right in the top level directory of the sdist # in the case where the egg_info command fails. logger.debug( "Using fallback location for PKG-INFO file in " "package directory: %s", package_dir, ) pkg_info_path = self._osutils.joinpath(package_dir, 'PKG-INFO') if not self._osutils.file_exists(pkg_info_path): raise UnsupportedPackageError(self._osutils.basename(package_dir)) return pkg_info_path def _unpack_sdist_into_dir(self, sdist_path: str, unpack_dir: str) -> str: if sdist_path.endswith('.zip'): self._osutils.extract_zipfile(sdist_path, unpack_dir) elif sdist_path.endswith(('.tar.gz', '.tar.bz2')): self._osutils.extract_tarfile(sdist_path, unpack_dir) else: raise InvalidSourceDistributionNameError(sdist_path) # There should only be one directory unpacked. contents = self._osutils.get_directory_contents(unpack_dir) return self._osutils.joinpath(unpack_dir, contents[0]) def get_package_name_and_version(self, sdist_path: str) -> Tuple[str, str]: with self._osutils.tempdir() as tempdir: package_dir = self._unpack_sdist_into_dir(sdist_path, tempdir) pkg_info_filepath = self._get_pkg_info_filepath(package_dir) metadata = self._parse_pkg_info_file(pkg_info_filepath) name = metadata['Name'] version = metadata['Version'] return name, version class SubprocessPip(object): """Wrapper around calling pip through a subprocess.""" def __init__( self, osutils: Optional[OSUtils] = None, import_string: OptStr = None ) -> None: if osutils is None: osutils = OSUtils() self._osutils = osutils if import_string is None: import_string = pip_import_string() self._import_string = import_string def main( self, args: List[str], env_vars: Optional[EnvVars] = None, shim: OptStr = None, ) -> Tuple[int, bytes, bytes]: if env_vars is None: env_vars = self._osutils.environ() if shim is None: shim = '' python_exe = sys.executable run_pip = ('import sys; %s; sys.exit(main(%s))') % ( self._import_string, args, ) exec_string = '%s%s' % (shim, run_pip) invoke_pip = [python_exe, '-c', exec_string] p = self._osutils.popen( invoke_pip, stdout=self._osutils.pipe, stderr=self._osutils.pipe, env=env_vars, ) out, err = p.communicate() rc = p.returncode return rc, out, err class PipRunner(object): """Wrapper around pip calls used by chalice.""" _LINK_IS_DIR_PATTERN = ( "Processing (.+?)\n Link is a directory, ignoring download_dir" ) def __init__( self, pip: SubprocessPip, osutils: Optional[OSUtils] = None ) -> None: if osutils is None: osutils = OSUtils() self._wrapped_pip = pip self._osutils = osutils def _execute( self, command: str, args: List[str], env_vars: Optional[EnvVars] = None, shim: OptStr = None, ) -> Tuple[int, bytes, bytes]: """Execute a pip command with the given arguments.""" main_args = [command] + args logger.debug("calling pip %s", ' '.join(main_args)) rc, out, err = self._wrapped_pip.main( main_args, env_vars=env_vars, shim=shim ) return rc, out, err def build_wheel( self, wheel: str, directory: str, compile_c: bool = True ) -> None: """Build an sdist into a wheel file.""" arguments = ['--no-deps', '--wheel-dir', directory, wheel] env_vars = self._osutils.environ() shim = '' if not compile_c: env_vars.update(pip_no_compile_c_env_vars) shim = pip_no_compile_c_shim # Ignore rc and stderr from this command since building the wheels # may fail and we will find out when we categorize the files that were # generated. self._execute('wheel', arguments, env_vars=env_vars, shim=shim) def download_all_dependencies( self, requirements_filename: str, directory: str ) -> None: """Download all dependencies as sdist or wheel.""" arguments = ['-r', requirements_filename, '--dest', directory] rc, out, err = self._execute('download', arguments) # When downloading all dependencies we expect to get an rc of 0 back # since we are casting a wide net here letting pip have options about # what to download. If a package is not found it is likely because it # does not exist and was mispelled. In this case we raise an error with # the package name. Otherwise a nonzero rc results in a generic # download error where we pass along the stderr. if rc != 0: if err is None: err = b'Unknown error' error = err.decode() match = re.search( r"Could not find a version that satisfies the " r"requirement ([^\s]+)", error, ) if match: package_name = match.group(1) raise NoSuchPackageError(str(package_name)) raise PackageDownloadError(error) stdout = out.decode() matches = re.finditer(self._LINK_IS_DIR_PATTERN, stdout) for match in matches: wheel_package_path = str(match.group(1)) # Looks odd we do not check on the error status of building the # wheel here. We can assume this is a valid package path since # we already passed the pip download stage. This stage would have # thrown a PackageDownloadError if any of the listed packages were # not valid. # If it fails the actual build step, it will have the same behavior # as any other package we fail to build a valid wheel for, and # complain at deployment time. self.build_wheel(wheel_package_path, directory) def download_manylinux_wheels( self, abi: str, packages: List[str], directory: str ) -> None: """Download wheel files for manylinux for all the given packages.""" # If any one of these dependencies fails pip will bail out. Since we # are only interested in all the ones we can download, we need to feed # each package to pip individually. The return code of pip doesn't # matter here since we will inspect the working directory to see which # wheels were downloaded. We are only interested in wheel files # compatible with lambda, which means manylinux1_x86_64 platform and # cpython implementation. The compatible abi depends on the python # version and is checked later. for package in packages: arguments = [ '--only-binary=:all:', '--no-deps', '--platform', 'manylinux2014_x86_64', '--implementation', 'cp', '--abi', abi, '--dest', directory, package, ] self._execute('download', arguments) def download_sdists(self, packages: List[str], directory: str) -> None: for package in packages: arguments = [ "--no-binary=:all:", "--no-deps", "--dest", directory, package, ] self._execute('download', arguments) ================================================ FILE: chalice/deploy/planner.py ================================================ # pylint: disable=too-many-lines import re import json from collections import OrderedDict from typing import List, Dict, Any, Optional, Union, Tuple, Set, cast # noqa from typing import Sequence # noqa from chalice.config import Config, DeployedResources # noqa from chalice.utils import OSUtils # noqa from chalice.deploy import models from chalice.awsclient import TypedAWSClient, ResourceDoesNotExistError # noqa InstructionMsg = Union[models.Instruction, Tuple[models.Instruction, str]] MarkedResource = Dict[str, List[models.RecordResource]] CacheTuples = Union[Tuple[str, str, str], Tuple[str, str]] ApiMap = Union[models.RestAPI, models.WebsocketAPI] class RemoteState(object): def __init__(self, client, deployed_resources): # type: (TypedAWSClient, DeployedResources) -> None self._client = client self._cache = {} # type: Dict[CacheTuples, bool] self._deployed_resources = deployed_resources def _cache_key(self, resource): # type: (models.ManagedModel) -> CacheTuples if isinstance(resource, models.APIMapping): return ( resource.resource_type, resource.resource_name, resource.mount_path ) return resource.resource_type, resource.resource_name def resource_deployed_values(self, resource): # type: (models.ManagedModel) -> Dict[str, Any] try: return self._deployed_resources.resource_values( resource.resource_name) except ValueError: return self._dynamically_lookup_values(resource) def _dynamically_lookup_values(self, resource): # type: (models.ManagedModel) -> Dict[str, str] if isinstance(resource, models.ManagedIAMRole): arn = self._client.get_role_arn_for_name(resource.role_name) return { "role_name": resource.role_name, "role_arn": arn, "name": resource.resource_name, "resource_type": "iam_role", } raise ValueError("Deployed values for resource does not exist: %s" % resource.resource_name) def resource_exists(self, resource, *args): # type: (models.ManagedModel, Optional[Any]) -> bool key = self._cache_key(resource) if key in self._cache: return self._cache[key] try: handler = getattr(self, '_resource_exists_%s' % resource.__class__.__name__.lower()) except AttributeError: raise ValueError("RemoteState received an unsupported resource: %s" % resource.resource_type) result = handler(resource, *args) self._cache[key] = result return result def _resource_exists_snslambdasubscription(self, resource): # type: (models.SNSLambdaSubscription) -> bool try: deployed_values = self._deployed_resources.resource_values( resource.resource_name) except ValueError: return False return self._client.verify_sns_subscription_current( deployed_values['subscription_arn'], topic_name=resource.topic, function_arn=deployed_values['lambda_arn'], ) def _resource_exists_sqseventsource(self, resource): # type: (models.SQSEventSource) -> bool try: deployed_values = self._deployed_resources.resource_values( resource.resource_name) except ValueError: return False if isinstance(resource.queue, models.QueueARN): resource_name = resource.queue.queue_name else: resource_name = resource.queue return self._client.verify_event_source_current( event_uuid=deployed_values['event_uuid'], resource_name=resource_name, service_name='sqs', function_arn=deployed_values['lambda_arn'], ) def _resource_exists_kinesiseventsource(self, resource): # type: (models.KinesisEventSource) -> bool try: deployed_values = self._deployed_resources.resource_values( resource.resource_name) except ValueError: return False return self._client.verify_event_source_current( event_uuid=deployed_values['event_uuid'], resource_name='stream/%s' % resource.stream, service_name='kinesis', function_arn=deployed_values['lambda_arn'], ) def _resource_exists_dynamodbeventsource(self, resource): # type: (models.DynamoDBEventSource) -> bool try: deployed_values = self._deployed_resources.resource_values( resource.resource_name) except ValueError: return False return self._client.verify_event_source_arn_current( event_uuid=deployed_values['event_uuid'], event_source_arn=deployed_values['stream_arn'], function_arn=deployed_values['lambda_arn'], ) def _resource_exists_lambdalayer(self, resource): # type: (models.LambdaLayer) -> bool try: deployed_values = self._deployed_resources.resource_values( resource.resource_name) except ValueError: return False return bool(self._client.get_layer_version( deployed_values['layer_version_arn'])) def _resource_exists_loggroup(self, resource): # type: (models.LogGroup) -> bool return self._client.log_group_exists(resource.log_group_name) def _resource_exists_lambdafunction(self, resource): # type: (models.LambdaFunction) -> bool return self._client.lambda_function_exists(resource.function_name) def _resource_exists_managediamrole(self, resource): # type: (models.ManagedIAMRole) -> bool try: self._client.get_role_arn_for_name(resource.role_name) return True except ResourceDoesNotExistError: return False def _resource_exists_apimapping(self, resource, domain_name): # type: (models.APIMapping, str) -> bool map_key = resource.mount_path if map_key == '(none)': map_key = '' elif map_key.startswith('/'): map_key = map_key.lstrip('/') return self._client.api_mapping_exists(domain_name, map_key) def _resource_exists_domainname(self, resource): # type: (models.DomainName) -> bool if resource.protocol == models.APIType.WEBSOCKET: return self._client.domain_name_exists_v2( resource.domain_name) return self._client.domain_name_exists(resource.domain_name) def _resource_exists_restapi(self, resource): # type: (models.RestAPI) -> bool try: deployed_values = self._deployed_resources.resource_values( resource.resource_name) except ValueError: return False rest_api_id = deployed_values['rest_api_id'] return bool(self._client.get_rest_api(rest_api_id)) def _resource_exists_websocketapi(self, resource): # type: (models.WebsocketAPI) -> bool try: deployed_values = self._deployed_resources.resource_values( resource.resource_name) except ValueError: return False api_id = deployed_values['websocket_api_id'] return self._client.websocket_api_exists(api_id) class PlanStage(object): def __init__(self, remote_state, osutils): # type: (RemoteState, OSUtils) -> None self._remote_state = remote_state self._osutils = osutils def execute(self, resources): # type: (List[models.Model]) -> models.Plan plan = [] # type: List[models.Instruction] messages = {} # type: Dict[int, str] for resource in resources: name = '_plan_%s' % resource.__class__.__name__.lower() handler = getattr(self, name, None) if handler is not None: result = handler(resource) if result: self._add_result_to_plan(result, plan, messages) return models.Plan(plan, messages) def _add_result_to_plan(self, result, # type: Sequence[InstructionMsg] plan, # type: List[models.Instruction] messages, # type: Dict[int, str] ): # type: (...) -> None for single in result: if isinstance(single, tuple): instruction, message = single plan.append(instruction) messages[id(instruction)] = message else: plan.append(single) # TODO: This code will likely be refactored and pulled into # per-resource classes so the PlanStage object doesn't need # to know about every type of resource. def _add_apimapping_plan(self, resource, # type: models.APIMapping domain_name # type: models.DomainName ): # type: (...) -> Sequence[InstructionMsg] api_calls = [] # type: List[InstructionMsg] params = { 'domain_name': domain_name.domain_name, 'path_key': resource.mount_path, 'stage': resource.api_gateway_stage } # type: Dict[str, Any] if domain_name.protocol == models.APIType.WEBSOCKET: params['api_id'] = Variable('websocket_api_id') variable_name = 'websocket_api_mapping' api_call = models.APICall( method_name='create_api_mapping', params=params, output_var='api_mapping' ) else: params['api_id'] = Variable('rest_api_id') variable_name = 'rest_api_mapping' api_call = models.APICall( method_name='create_base_path_mapping', params=params, output_var='api_mapping' ) if not self._remote_state.resource_exists( resource, domain_name.domain_name ): path_to_print = '/' if resource.mount_path != '(none)' and \ not resource.mount_path.startswith("/"): path_to_print = '/%s' % resource.mount_path api_calls.extend([ (api_call, "Creating api mapping: %s\n" % path_to_print), models.StoreMultipleValue( name=variable_name, value=[Variable('api_mapping')] ), models.RecordResourceVariable( resource_type='domain_name', resource_name=domain_name.resource_name, name='api_mapping', variable_name=variable_name ), ]) else: deployed = self._remote_state.resource_deployed_values( domain_name ) for api_mapping in deployed['api_mapping']: mount_path = api_mapping['key'].lstrip('/') if not mount_path: mount_path = '(none)' if mount_path != resource.mount_path: continue api_calls.extend([ models.StoreMultipleValue( name=variable_name, value=[api_mapping] ), models.RecordResourceVariable( resource_type='domain_name', resource_name=domain_name.resource_name, name='api_mapping', variable_name=variable_name ), ]) return api_calls def _add_domainname_plan(self, resource, endpoint_type): # type: (models.DomainName, str) -> Sequence[InstructionMsg] api_calls = [] # type: List[InstructionMsg] params = { 'protocol': resource.protocol.value, 'tags': resource.tags, 'endpoint_type': endpoint_type, 'domain_name': resource.domain_name, } params['certificate_arn'] = resource.certificate_arn if resource.tls_version is not None: params['security_policy'] = resource.tls_version.value if not self._remote_state.resource_exists(resource): domain_name_api_call = ( models.APICall( method_name='create_domain_name', params=params, output_var=resource.resource_name ), "Creating custom domain name: %s\n" % resource.domain_name ) else: domain_name_api_call = ( models.APICall( method_name='update_domain_name', params=params, output_var=resource.resource_name ), "Updating custom domain name: %s\n" % resource.domain_name ) api_calls.extend([ domain_name_api_call, models.StoreValue( name='hosted_zone_id', value=KeyDataVariable(resource.resource_name, 'hosted_zone_id') ), models.RecordResourceVariable( resource_type='domain_name', resource_name=resource.resource_name, name='hosted_zone_id', variable_name='hosted_zone_id' ), models.StoreValue( name='alias_domain_name', value=KeyDataVariable(resource.resource_name, 'alias_domain_name') ), models.RecordResourceVariable( resource_type='domain_name', resource_name=resource.resource_name, name='alias_domain_name', variable_name='alias_domain_name' ), models.StoreValue( name='certificate_arn', value=KeyDataVariable(resource.resource_name, 'certificate_arn') ), models.RecordResourceVariable( resource_type='domain_name', resource_name=resource.resource_name, name='certificate_arn', variable_name='certificate_arn' ), models.StoreValue( name='security_policy', value=KeyDataVariable(resource.resource_name, 'security_policy') ), models.RecordResourceVariable( resource_type='domain_name', resource_name=resource.resource_name, name='security_policy', variable_name='security_policy' ), models.RecordResourceValue( resource_type='domain_name', resource_name=resource.resource_name, name='domain_name', value=resource.domain_name ) ]) return api_calls def _plan_lambdalayer(self, resource): # type: (models.LambdaLayer) -> Sequence[InstructionMsg] api_calls = [] # type: List[InstructionMsg] filename = cast(str, resource.deployment_package.filename) # Automatically clean up old layer versions. # See: # https://docs.aws.amazon.com/lambda/latest/dg/API_DeleteLayerVersion.html msg = 'Creating' if self._remote_state.resource_exists(resource): state = self._remote_state.resource_deployed_values(resource) # Deleting a layer version won't break functions still using it. # From the doc link above: # # "To avoid breaking functions, a copy of the version remains in # Lambda until no functions refer to it." api_calls.append( models.APICall( method_name='delete_layer_version', params={'layer_version_arn': state['layer_version_arn']} ) ) msg = 'Updating' api_calls.extend([( models.APICall( method_name='publish_layer', params={'layer_name': resource.layer_name, 'zip_contents': self._osutils.get_file_contents( filename, binary=True), 'runtime': resource.runtime}, output_var='layer_version_arn' ), "%s lambda layer: %s\n" % (msg, resource.layer_name)), models.RecordResourceVariable( resource_type='lambda_layer', resource_name=resource.resource_name, name='layer_version_arn', variable_name='layer_version_arn', )]) return api_calls def _plan_lambdafunction(self, resource): # type: (models.LambdaFunction) -> Sequence[InstructionMsg] role_arn = self._get_role_arn(resource.role) # Make mypy happy, it complains if we don't "declare" this upfront. params = {} # type: Dict[str, Any] varname = '%s_lambda_arn' % resource.resource_name # Not sure the best way to express this via mypy, but we know # that in the build stage we replace the deployment package # name with the actual filename generated from the pip # packager. For now we resort to a cast. filename = cast(str, resource.deployment_package.filename) if resource.reserved_concurrency is None: concurrency_api_call = models.APICall( method_name='delete_function_concurrency', params={ 'function_name': resource.function_name, }, output_var='reserved_concurrency_result' ) else: concurrency = resource.reserved_concurrency concurrency_api_call = ( models.APICall( method_name='put_function_concurrency', params={ 'function_name': resource.function_name, 'reserved_concurrent_executions': concurrency, }, output_var='reserved_concurrency_result'), "Updating lambda function concurrency limit: %s\n" % resource.function_name ) api_calls = [] # type: List[InstructionMsg] layers = [] # type: List[Any] if resource.managed_layer is not None: layers.append(Variable('layer_version_arn')) if resource.layers: layers.extend(resource.layers) if not self._remote_state.resource_exists(resource): params = { 'function_name': resource.function_name, 'role_arn': role_arn, 'zip_contents': self._osutils.get_file_contents( filename, binary=True), 'runtime': resource.runtime, 'handler': resource.handler, 'environment_variables': resource.environment_variables, 'xray': resource.xray, 'tags': resource.tags, 'timeout': resource.timeout, 'memory_size': resource.memory_size, 'security_group_ids': resource.security_group_ids, 'subnet_ids': resource.subnet_ids, 'layers': layers } api_calls.extend([ (models.APICall( method_name='create_function', params=params, output_var=varname, ), "Creating lambda function: %s\n" % resource.function_name), models.RecordResourceVariable( resource_type='lambda_function', resource_name=resource.resource_name, name='lambda_arn', variable_name=varname, ) ]) else: # TODO: Consider a smarter diff where we check if we even need # to do an update() API call. params = { 'function_name': resource.function_name, 'role_arn': role_arn, 'zip_contents': self._osutils.get_file_contents( filename, binary=True), 'runtime': resource.runtime, 'environment_variables': resource.environment_variables, 'xray': resource.xray, 'tags': resource.tags, 'timeout': resource.timeout, 'memory_size': resource.memory_size, 'security_group_ids': resource.security_group_ids, 'subnet_ids': resource.subnet_ids, 'layers': layers } api_calls.extend([ (models.APICall( method_name='update_function', params=params, output_var='update_function_result', ), "Updating lambda function: %s\n" % resource.function_name), models.JPSearch( 'FunctionArn', input_var='update_function_result', output_var=varname, ), models.RecordResourceVariable( resource_type='lambda_function', resource_name=resource.resource_name, name='lambda_arn', variable_name=varname, ) ]) api_calls.append(concurrency_api_call) return api_calls def _plan_managediamrole(self, resource): # type: (models.ManagedIAMRole) -> Sequence[InstructionMsg] document = resource.policy.document role_exists = self._remote_state.resource_exists(resource) varname = '%s_role_arn' % resource.role_name if not role_exists: return [ models.BuiltinFunction( 'service_principal', ['lambda'], output_var='lambda_service_principal', ), models.JPSearch('principal', input_var='lambda_service_principal', output_var='lambda_principal'), models.StoreValue( name='lambda_principal', value=StringFormat('{lambda_principal}', ['lambda_principal']), ), models.StoreValue( name='lambda_trust_policy', value={ "Version": "2012-10-17", "Statement": [{ "Sid": "", "Effect": "Allow", "Principal": { "Service": Variable('lambda_principal') }, "Action": "sts:AssumeRole" }] }, ), (models.APICall( method_name='create_role', params={'name': resource.role_name, 'trust_policy': Variable('lambda_trust_policy'), 'policy': document}, output_var=varname, ), "Creating IAM role: %s\n" % resource.role_name), models.RecordResourceVariable( resource_type='iam_role', resource_name=resource.resource_name, name='role_arn', variable_name=varname, ), models.RecordResourceValue( resource_type='iam_role', resource_name=resource.resource_name, name='role_name', value=resource.role_name, ) ] role_arn = self._remote_state.resource_deployed_values( resource)['role_arn'] return [ models.StoreValue(name=varname, value=role_arn), (models.APICall( method_name='put_role_policy', params={'role_name': resource.role_name, 'policy_name': resource.role_name, 'policy_document': document}, ), "Updating policy for IAM role: %s\n" % resource.role_name), models.RecordResourceVariable( resource_type='iam_role', resource_name=resource.resource_name, name='role_arn', variable_name=varname, ), models.RecordResourceValue( resource_type='iam_role', resource_name=resource.resource_name, name='role_name', value=resource.role_name, ) ] def _plan_snslambdasubscription(self, resource): # type: (models.SNSLambdaSubscription) -> Sequence[InstructionMsg] function_arn = Variable( '%s_lambda_arn' % resource.lambda_function.resource_name ) topic_arn_varname = '%s_topic_arn' % resource.resource_name subscribe_varname = '%s_subscription_arn' % resource.resource_name instruction_for_topic_arn = [] # type: List[InstructionMsg] if re.match(r"^arn:aws[a-z\-]*:sns:", resource.topic): instruction_for_topic_arn += [ models.StoreValue( name=topic_arn_varname, value=resource.topic, ) ] else: # To keep the user API simple, we only require the topic # name and not the ARN. However, the APIs require the topic # ARN so we need to reconstruct it here in the planner. instruction_for_topic_arn += self._arn_parse_instructions( function_arn) + [ models.StoreValue( name=topic_arn_varname, value=StringFormat( 'arn:{partition}:sns:{region_name}:{account_id}:%s' % ( resource.topic ), ['partition', 'region_name', 'account_id'], ), ) ] if self._remote_state.resource_exists(resource): # Given there's nothing about an SNS subscription you can # configure for now, if the resource exists, we don't do # anything. The resource sweeper will verify that if the # subscription doesn't actually apply that we should unsubscribe # from the topic. deployed = self._remote_state.resource_deployed_values(resource) subscription_arn = deployed['subscription_arn'] return instruction_for_topic_arn + self._batch_record_resource( 'sns_event', resource.resource_name, { 'topic': resource.topic, 'lambda_arn': Variable(function_arn.name), 'subscription_arn': subscription_arn, 'topic_arn': Variable(topic_arn_varname), } ) return instruction_for_topic_arn + [ models.APICall( method_name='add_permission_for_sns_topic', params={'topic_arn': Variable(topic_arn_varname), 'function_arn': function_arn}, ), (models.APICall( method_name='subscribe_function_to_topic', params={'topic_arn': Variable(topic_arn_varname), 'function_arn': function_arn}, output_var=subscribe_varname, ), 'Subscribing %s to SNS topic %s\n' % (resource.lambda_function.function_name, resource.topic) ) ] + self._batch_record_resource( 'sns_event', resource.resource_name, { 'topic': resource.topic, 'lambda_arn': Variable(function_arn.name), 'subscription_arn': Variable(subscribe_varname), 'topic_arn': Variable(topic_arn_varname), } ) def _plan_sqseventsource(self, resource): # type: (models.SQSEventSource) -> Sequence[InstructionMsg] queue_arn_varname = '%s_queue_arn' % resource.resource_name uuid_varname = '%s_uuid' % resource.resource_name function_arn = Variable( '%s_lambda_arn' % resource.lambda_function.resource_name ) if not isinstance(resource.queue, models.QueueARN): instruction_for_queue_arn = self._arn_parse_instructions( function_arn) instruction_for_queue_arn.append( models.StoreValue( name=queue_arn_varname, value=StringFormat( 'arn:{partition}:sqs:{region_name}:{account_id}:%s' % ( resource.queue ), ['partition', 'region_name', 'account_id'], ), ) ) queue_name = resource.queue else: instruction_for_queue_arn = [ models.StoreValue( name=queue_arn_varname, value=resource.queue.arn, ) ] queue_name = resource.queue.queue_name if self._remote_state.resource_exists(resource): deployed = self._remote_state.resource_deployed_values(resource) uuid = deployed['event_uuid'] return instruction_for_queue_arn + [ models.APICall( method_name='update_lambda_event_source', params={ 'event_uuid': uuid, 'batch_size': resource.batch_size, 'maximum_batching_window_in_seconds': resource.maximum_batching_window_in_seconds, 'maximum_concurrency': resource.maximum_concurrency } ) ] + self._batch_record_resource( 'sqs_event', resource.resource_name, { 'queue_arn': deployed['queue_arn'], 'event_uuid': uuid, 'queue': queue_name, 'lambda_arn': deployed['lambda_arn'], } ) return instruction_for_queue_arn + [ (models.APICall( method_name='create_lambda_event_source', params={'event_source_arn': Variable(queue_arn_varname), 'batch_size': resource.batch_size, 'maximum_batching_window_in_seconds': resource.maximum_batching_window_in_seconds, 'maximum_concurrency': resource.maximum_concurrency, 'function_name': function_arn}, output_var=uuid_varname, ), 'Subscribing %s to SQS queue %s\n' % (resource.lambda_function.function_name, resource.queue) ), ] + self._batch_record_resource( 'sqs_event', resource.resource_name, { 'queue_arn': Variable(queue_arn_varname), 'event_uuid': Variable(uuid_varname), 'queue': queue_name, 'lambda_arn': Variable(function_arn.name) } ) def _plan_kinesiseventsource(self, resource): # type: (models.KinesisEventSource) -> Sequence[InstructionMsg] stream_arn_varname = '%s_stream_arn' % resource.resource_name uuid_varname = '%s_uuid' % resource.resource_name function_arn = Variable( '%s_lambda_arn' % resource.lambda_function.resource_name ) instruction_for_stream_arn = self._arn_parse_instructions(function_arn) instruction_for_stream_arn.append( models.StoreValue( name=stream_arn_varname, value=StringFormat( 'arn:{partition}:kinesis:{region_name}:{account_id}:' 'stream/%s' % resource.stream, ['partition', 'region_name', 'account_id'], ), ) ) if self._remote_state.resource_exists(resource): deployed = self._remote_state.resource_deployed_values(resource) uuid = deployed['event_uuid'] return instruction_for_stream_arn + [ models.APICall( method_name='update_lambda_event_source', params={'event_uuid': uuid, 'batch_size': resource.batch_size, 'maximum_batching_window_in_seconds': resource.maximum_batching_window_in_seconds} ) ] + self._batch_record_resource( 'kinesis_event', resource.resource_name, { 'kinesis_arn': deployed['kinesis_arn'], 'event_uuid': uuid, 'stream': resource.stream, 'lambda_arn': deployed['lambda_arn'], } ) return instruction_for_stream_arn + [ (models.APICall( method_name='create_lambda_event_source', params={'event_source_arn': Variable(stream_arn_varname), 'batch_size': resource.batch_size, 'function_name': function_arn, 'starting_position': resource.starting_position, 'maximum_batching_window_in_seconds': resource.maximum_batching_window_in_seconds}, output_var=uuid_varname, ), 'Subscribing %s to Kinesis stream %s\n' % (resource.lambda_function.function_name, resource.stream) ) ] + self._batch_record_resource( 'kinesis_event', resource.resource_name, { 'kinesis_arn': Variable(stream_arn_varname), 'event_uuid': Variable(uuid_varname), 'stream': resource.stream, 'lambda_arn': Variable(function_arn.name), } ) def _plan_loggroup(self, resource): # type: (models.LogGroup) -> Sequence[InstructionMsg] instructions = [] # type: List[InstructionMsg] if self._remote_state.resource_exists(resource): return instructions + [ models.APICall( method_name='put_retention_policy', params={'name': resource.log_group_name, 'retention_in_days': resource.retention_in_days} ), models.RecordResourceValue( resource_type='log_group', resource_name=resource.resource_name, name='log_group_name', value=resource.log_group_name, ) ] return instructions + [ models.APICall( method_name='create_log_group', params={'log_group_name': resource.log_group_name} ), models.APICall( method_name='put_retention_policy', params={'name': resource.log_group_name, 'retention_in_days': resource.retention_in_days} ), models.RecordResourceValue( resource_type='log_group', resource_name=resource.resource_name, name='log_group_name', value=resource.log_group_name, ) ] def _plan_dynamodbeventsource(self, resource): # type: (models.DynamoDBEventSource) -> Sequence[InstructionMsg] uuid_varname = '%s_uuid' % resource.resource_name function_arn = Variable( '%s_lambda_arn' % resource.lambda_function.resource_name ) instructions = [] # type: List[InstructionMsg] if self._remote_state.resource_exists(resource): deployed = self._remote_state.resource_deployed_values(resource) uuid = deployed['event_uuid'] return instructions + [ models.APICall( method_name='update_lambda_event_source', params={'event_uuid': uuid, 'batch_size': resource.batch_size, 'maximum_batching_window_in_seconds': resource.maximum_batching_window_in_seconds} ) ] + self._batch_record_resource( 'dynamodb_event', resource.resource_name, { 'stream_arn': deployed['stream_arn'], 'event_uuid': deployed['event_uuid'], 'lambda_arn': deployed['lambda_arn'], } ) return instructions + [ (models.APICall( method_name='create_lambda_event_source', params={'event_source_arn': resource.stream_arn, 'batch_size': resource.batch_size, 'function_name': function_arn, 'starting_position': resource.starting_position, 'maximum_batching_window_in_seconds': resource.maximum_batching_window_in_seconds}, output_var=uuid_varname, ), 'Subscribing %s to DynamoDB stream %s\n' % (resource.lambda_function.function_name, resource.stream_arn)) ] + self._batch_record_resource( 'dynamodb_event', resource.resource_name, { 'stream_arn': resource.stream_arn, 'event_uuid': Variable(uuid_varname), 'lambda_arn': function_arn, } ) def _arn_parse_instructions(self, function_arn): # type: (Variable) -> List[InstructionMsg] instruction_for_stream_arn = [ models.BuiltinFunction('parse_arn', [function_arn], output_var='parsed_lambda_arn'), models.JPSearch('account_id', input_var='parsed_lambda_arn', output_var='account_id'), models.JPSearch('region', input_var='parsed_lambda_arn', output_var='region_name'), models.JPSearch('partition', input_var='parsed_lambda_arn', output_var='partition'), ] # type: List[InstructionMsg] return instruction_for_stream_arn def _plan_s3bucketnotification(self, resource): # type: (models.S3BucketNotification) -> Sequence[InstructionMsg] function_arn = Variable( '%s_lambda_arn' % resource.lambda_function.resource_name ) return self._arn_parse_instructions(function_arn) + [ models.APICall( method_name='add_permission_for_s3_event', params={'bucket': resource.bucket, 'function_arn': function_arn, 'account_id': Variable('account_id')}, ), (models.APICall( method_name='connect_s3_bucket_to_lambda', params={'bucket': resource.bucket, 'function_arn': function_arn, 'prefix': resource.prefix, 'suffix': resource.suffix, 'events': resource.events} ), 'Configuring S3 events in bucket %s to function %s\n' % (resource.bucket, resource.lambda_function.function_name) ), models.RecordResourceValue( resource_type='s3_event', resource_name=resource.resource_name, name='bucket', value=resource.bucket, ), models.RecordResourceVariable( resource_type='s3_event', resource_name=resource.resource_name, name='lambda_arn', variable_name=function_arn.name, ), ] def _create_cloudwatchevent(self, resource): # type: (models.CloudWatchEventBase) -> Sequence[InstructionMsg] function_arn = Variable( '%s_lambda_arn' % resource.lambda_function.resource_name ) params = {'rule_name': resource.rule_name} if isinstance(resource, models.ScheduledEvent): resource = cast(models.ScheduledEvent, resource) params['schedule_expression'] = resource.schedule_expression if resource.rule_description is not None: params['rule_description'] = resource.rule_description else: resource = cast(models.CloudWatchEvent, resource) params['event_pattern'] = resource.event_pattern plan = [ models.APICall( method_name='get_or_create_rule_arn', params=params, output_var='rule-arn', ), models.APICall( method_name='connect_rule_to_lambda', params={'rule_name': resource.rule_name, 'function_arn': function_arn} ), models.APICall( method_name='add_permission_for_cloudwatch_event', params={'rule_arn': Variable('rule-arn'), 'function_arn': function_arn}, ), # You need to remove targets (which have IDs) # before you can delete a rule. models.RecordResourceValue( resource_type='cloudwatch_event', resource_name=resource.resource_name, name='rule_name', value=resource.rule_name, ) ] return plan def _plan_cloudwatchevent(self, resource): # type: (models.CloudWatchEvent) -> Sequence[InstructionMsg] return self._create_cloudwatchevent(resource) def _plan_scheduledevent(self, resource): # type: (models.ScheduledEvent) -> Sequence[InstructionMsg] return self._create_cloudwatchevent(resource) def _create_websocket_function_configs(self, resource): # type: (models.WebsocketAPI) -> Dict[str, Dict[str, Any]] configs = OrderedDict() # type: Dict[str, Dict[str, Any]] if resource.connect_function is not None: configs['connect'] = self._create_websocket_function_config( resource.connect_function) if resource.message_function is not None: configs['message'] = self._create_websocket_function_config( resource.message_function) if resource.disconnect_function is not None: configs['disconnect'] = self._create_websocket_function_config( resource.disconnect_function) return configs def _create_websocket_function_config(self, function): # type: (models.LambdaFunction) -> Dict[str, Any] varname = '%s_lambda_arn' % function.resource_name return { 'function': function, 'name': function.function_name, 'varname': varname, 'lambda_arn_var': Variable(varname), } def _inject_websocket_integrations(self, configs): # type: (Dict[str, Any]) -> Sequence[InstructionMsg] instructions = [] # type: List[InstructionMsg] for key, config in configs.items(): instructions.append( models.StoreValue( name='websocket-%s-integration-lambda-path' % key, value=StringFormat( 'arn:{partition}:apigateway:{region_name}:lambda:path/' '2015-03-31/functions/arn:{partition}' ':lambda:{region_name}:{account_id}:function' ':%s/invocations' % config['name'], ['partition', 'region_name', 'account_id'], ), ), ) instructions.append( models.APICall( method_name='create_websocket_integration', params={ 'api_id': Variable('websocket_api_id'), 'lambda_function': Variable( 'websocket-%s-integration-lambda-path' % key), 'handler_type': key, }, output_var='%s-integration-id' % key, ), ) return instructions def _create_route_for_key(self, route_key): # type: (str) -> models.APICall integration_id = { '$connect': 'connect-integration-id', '$disconnect': 'disconnect-integration-id', }.get(route_key, 'message-integration-id') return models.APICall( method_name='create_websocket_route', params={ 'api_id': Variable('websocket_api_id'), 'route_key': route_key, 'integration_id': Variable(integration_id), }, ) def _plan_websocketapi(self, resource): # type: (models.WebsocketAPI) -> Sequence[InstructionMsg] configs = self._create_websocket_function_configs(resource) routes = resource.routes # Which lambda function we use here does not matter. We are only using # it to find the account id and the region. lambda_arn_var = list(configs.values())[0]['lambda_arn_var'] shared_plan_preamble = self._arn_parse_instructions(lambda_arn_var) + [ models.JPSearch('dns_suffix', input_var='parsed_lambda_arn', output_var='dns_suffix'), ] # type: List[InstructionMsg] # There's also a set of instructions that are needed # at the end of deploying a websocket API that apply to both # the update and create case. shared_plan_epilogue = [ models.StoreValue( name='websocket_api_url', value=StringFormat( 'wss://{websocket_api_id}.execute-api.{region_name}' '.{dns_suffix}/%s/' % resource.api_gateway_stage, ['websocket_api_id', 'region_name', 'dns_suffix'], ), ), models.RecordResourceVariable( resource_type='websocket_api', resource_name=resource.resource_name, name='websocket_api_url', variable_name='websocket_api_url', ), models.RecordResourceVariable( resource_type='websocket_api', resource_name=resource.resource_name, name='websocket_api_id', variable_name='websocket_api_id', ), ] # type: List[InstructionMsg] shared_plan_epilogue += [ models.APICall( method_name='add_permission_for_apigateway_v2', params={'function_name': function_config['name'], 'region_name': Variable('region_name'), 'account_id': Variable('account_id'), 'api_id': Variable('websocket_api_id')}, ) for function_config in configs.values() ] main_plan = [] # type: List[InstructionMsg] if not self._remote_state.resource_exists(resource): # The resource does not exist, we create it in full here. main_plan += [ (models.APICall( method_name='create_websocket_api', params={'name': resource.name}, output_var='websocket_api_id', ), "Creating websocket api: %s\n" % resource.name), models.StoreValue( name='routes', value=[], ), ] main_plan += self._inject_websocket_integrations(configs) for route_key in routes: main_plan += [self._create_route_for_key(route_key)] main_plan += [ models.APICall( method_name='deploy_websocket_api', params={ 'api_id': Variable('websocket_api_id'), }, output_var='deployment-id', ), models.APICall( method_name='create_stage', params={ 'api_id': Variable('websocket_api_id'), 'stage_name': resource.api_gateway_stage, 'deployment_id': Variable('deployment-id'), } ), ] else: # Already exists. Need to sync up the routes, the easiest way to do # this is to delete them and their integrations and re-create them. # They will not work if the lambda function changes from under # them, and the logic for detecting that and making just the needed # changes is complex. There is an integration test to ensure there # no dropped messages during a redeployment. deployed = self._remote_state.resource_deployed_values(resource) main_plan += [ models.StoreValue( name='websocket_api_id', value=deployed['websocket_api_id'] ), models.APICall( method_name='get_websocket_routes', params={'api_id': Variable('websocket_api_id')}, output_var='routes', ), models.APICall( method_name='delete_websocket_routes', params={ 'api_id': Variable('websocket_api_id'), 'routes': Variable('routes'), }, ), models.APICall( method_name='get_websocket_integrations', params={ 'api_id': Variable('websocket_api_id'), }, output_var='integrations' ), models.APICall( method_name='delete_websocket_integrations', params={ 'api_id': Variable('websocket_api_id'), 'integrations': Variable('integrations'), } ) ] main_plan += self._inject_websocket_integrations(configs) for route_key in routes: main_plan += [self._create_route_for_key(route_key)] ws_plan = shared_plan_preamble + main_plan + shared_plan_epilogue if resource.domain_name: custom_domain_plan = self._add_custom_domain_plan( resource.domain_name, 'REGIONAL', ) ws_plan += custom_domain_plan return ws_plan def _plan_restapi(self, resource): # type: (models.RestAPI) -> Sequence[InstructionMsg] function = resource.lambda_function function_name = function.function_name varname = '%s_lambda_arn' % function.resource_name lambda_arn_var = Variable(varname) # There's a set of shared instructions that are needed # in both the update as well as the initial create case. # That's what this shared_plan_premable is for. shared_plan_preamble = self._arn_parse_instructions(lambda_arn_var) + [ models.JPSearch('dns_suffix', input_var='parsed_lambda_arn', output_var='dns_suffix'), # The swagger doc uses the 'api_handler_lambda_arn' # var name so we need to make sure we populate this variable # before importing the rest API. models.CopyVariable(from_var=varname, to_var='api_handler_lambda_arn'), ] # type: List[InstructionMsg] # There's also a set of instructions that are needed # at the end of deploying a rest API that apply to both # the update and create case. shared_plan_patch_ops = [{ 'op': 'replace', 'path': '/minimumCompressionSize', 'value': resource.minimum_compression} ] # type: List[Dict] shared_plan_epilogue = [ models.APICall( method_name='update_rest_api', params={ 'rest_api_id': Variable('rest_api_id'), 'patch_operations': shared_plan_patch_ops } ), models.APICall( method_name='add_permission_for_apigateway', params={'function_name': function_name, 'region_name': Variable('region_name'), 'account_id': Variable('account_id'), 'rest_api_id': Variable('rest_api_id')}, ), models.APICall( method_name='deploy_rest_api', params={'rest_api_id': Variable('rest_api_id'), 'xray': resource.xray, 'api_gateway_stage': resource.api_gateway_stage}, ), models.StoreValue( name='rest_api_url', value=StringFormat( 'https://{rest_api_id}.execute-api.{region_name}' '.{dns_suffix}/%s/' % resource.api_gateway_stage, ['rest_api_id', 'region_name', 'dns_suffix'], ), ), models.RecordResourceVariable( resource_type='rest_api', resource_name=resource.resource_name, name='rest_api_url', variable_name='rest_api_url', ), ] # type: List[InstructionMsg] for auth in resource.authorizers: shared_plan_epilogue.append( models.APICall( method_name='add_permission_for_apigateway', params={'function_name': auth.function_name, 'region_name': Variable('region_name'), 'account_id': Variable('account_id'), 'rest_api_id': Variable('rest_api_id')}, ) ) if not self._remote_state.resource_exists(resource): plan = shared_plan_preamble + [ (models.APICall( method_name='import_rest_api', params={'swagger_document': resource.swagger_doc, 'endpoint_type': resource.endpoint_type}, output_var='rest_api_id', ), "Creating Rest API\n"), models.RecordResourceVariable( resource_type='rest_api', resource_name=resource.resource_name, name='rest_api_id', variable_name='rest_api_id', ), ] else: deployed = self._remote_state.resource_deployed_values(resource) shared_plan_epilogue.insert( 0, models.APICall( method_name='get_rest_api', params={'rest_api_id': Variable('rest_api_id')}, output_var='rest_api') ) shared_plan_patch_ops.append({ 'op': 'replace', 'path': StringFormat( '/endpointConfiguration/types/%s' % ( '{rest_api[endpointConfiguration][types][0]}'), ['rest_api']), 'value': resource.endpoint_type} ) plan = shared_plan_preamble + [ models.StoreValue( name='rest_api_id', value=deployed['rest_api_id']), models.RecordResourceVariable( resource_type='rest_api', resource_name=resource.resource_name, name='rest_api_id', variable_name='rest_api_id', ), (models.APICall( method_name='update_api_from_swagger', params={ 'rest_api_id': Variable('rest_api_id'), 'swagger_document': resource.swagger_doc, }, ), "Updating rest API\n"), ] plan.extend(shared_plan_epilogue) if resource.domain_name: custom_domain_plan = self._add_custom_domain_plan( resource.domain_name, resource.endpoint_type ) plan += custom_domain_plan return plan def _add_custom_domain_plan(self, resource, endpoint_type): # type: (models.DomainName, str) -> Sequence[InstructionMsg] result = [] # type: List[InstructionMsg] custom_domain_plan = self._add_domainname_plan( resource, endpoint_type ) result += custom_domain_plan api_mapping_plan = self._add_apimapping_plan( resource.api_mapping, resource ) result += api_mapping_plan return result def _get_role_arn(self, resource): # type: (models.IAMRole) -> Union[str, Variable] if isinstance(resource, models.PreCreatedIAMRole): return resource.role_arn elif isinstance(resource, models.ManagedIAMRole): return Variable('%s_role_arn' % resource.role_name) # Make mypy happy. raise RuntimeError("Unknown resource type: %s" % resource) def _batch_record_resource(self, resource_type, resource_name, mapping): # type: (str, str, Dict[str, Any]) -> List[InstructionMsg] # This is a helper function for recording multiple values into # the same resource dict. The mapping is the set of variables # you want to record. If the value in a pair is a Variable type, # then RecordResourceVariable is used, otherwise, RecordResourceValue # is used. instructions = [] # type: List[InstructionMsg] for key, value in mapping.items(): instruction = cast(InstructionMsg, None) if isinstance(value, Variable): instruction = models.RecordResourceVariable( resource_type=resource_type, resource_name=resource_name, name=key, variable_name=value.name ) else: instruction = models.RecordResourceValue( resource_type=resource_type, resource_name=resource_name, name=key, value=value ) instructions.append(instruction) return instructions class NoopPlanner(PlanStage): def __init__(self): # type: () -> None pass def execute(self, resources): # type: (List[models.Model]) -> models.Plan return models.Plan(instructions=[], messages={}) class Variable(object): def __init__(self, name): # type: (str) -> None self.name = name def __repr__(self): # type: () -> str return 'Variable("%s")' % self.name def __eq__(self, other): # type: (Any) -> bool return isinstance(other, Variable) and self.name == other.name class StringFormat(object): def __init__(self, template, variables): # type: (str, List[str]) -> None self.template = template self.variables = variables def __repr__(self): # type: () -> str return 'StringFormat("%s")' % self.template def __eq__(self, other): # type: (Any) -> bool return ( isinstance(other, StringFormat) and self.template == other.template and self.variables == other.variables ) class PlanEncoder(json.JSONEncoder): # pylint false positive overriden below # https://github.com/PyCQA/pylint/issues/414 def default(self, o): # pylint: disable=E0202 # type: (Any) -> Any if isinstance(o, StringFormat): return o.template return o class KeyDataVariable(object): def __init__(self, name, key): # type: (str, str) -> None self.name = name self.key = key def __repr__(self): # type: () -> str return 'KeyDataVariable("%s", "%s")' % (self.name, self.key) def __eq__(self, other): # type: (Any) -> bool return ( isinstance(other, KeyDataVariable) and self.name == other.name and self.key == other.key ) ================================================ FILE: chalice/deploy/swagger.py ================================================ import copy import inspect from typing import Any, List, Dict, Optional, Union # noqa from chalice.app import Chalice, RouteEntry, Authorizer, CORSConfig # noqa from chalice.app import ChaliceAuthorizer from chalice.deploy.planner import StringFormat from chalice.deploy.models import RestAPI # noqa from chalice.utils import to_cfn_resource_name class SwaggerGenerator(object): _BASE_TEMPLATE = { 'swagger': '2.0', 'info': { 'version': '1.0', 'title': '' }, 'schemes': ['https'], 'paths': {}, 'definitions': { 'Empty': { 'type': 'object', 'title': 'Empty Schema', } } } # type: Dict[str, Any] def __init__(self, region, deployed_resources): # type: (str, Dict[str, Any]) -> None self._region = region self._deployed_resources = deployed_resources def generate_swagger(self, app, rest_api=None): # type: (Chalice, Optional[RestAPI]) -> Dict[str, Any] api = copy.deepcopy(self._BASE_TEMPLATE) api['info']['title'] = app.app_name self._add_binary_types(api, app) self._add_route_paths(api, app) self._add_resource_policy(api, rest_api) self._add_vpc_endpoint(api, rest_api) return api def _add_resource_policy(self, api, rest_api): # type: (Dict[str, Any], Optional[RestAPI]) -> None if rest_api and rest_api.policy: api['x-amazon-apigateway-policy'] = rest_api.policy.document def _add_vpc_endpoint(self, api, rest_api): # type: (Dict[str, Any], Optional[RestAPI]) -> None if rest_api and rest_api.vpce_ids: api['x-amazon-apigateway-endpoint-configuration'] = { "vpcEndpointIds": rest_api.vpce_ids } def _add_binary_types(self, api, app): # type: (Dict[str, Any], Chalice) -> None api['x-amazon-apigateway-binary-media-types'] = app.api.binary_types def _add_route_paths(self, api, app): # type: (Dict[str, Any], Chalice) -> None for path, methods in app.routes.items(): swagger_for_path = {} # type: Dict[str, Any] api['paths'][path] = swagger_for_path cors_config = None methods_with_cors = [] for http_method, view in methods.items(): current = self._generate_route_method(view) if 'security' in current: self._add_to_security_definition( current['security'], api, view) swagger_for_path[http_method.lower()] = current if view.cors is not None: cors_config = view.cors methods_with_cors.append(http_method) # Chalice ensures that routes with multiple views have the same # CORS configuration. So if any entry has CORS enabled, use that # entry's CORS configuration for the preflight setup. if cors_config is not None: self._add_preflight_request( cors_config, methods_with_cors, swagger_for_path) def _generate_security_from_auth_obj(self, api_config, authorizer): # type: (Dict[str, Any], Authorizer) -> None if isinstance(authorizer, ChaliceAuthorizer): auth_config = authorizer.config config = { 'in': 'header', 'type': 'apiKey', 'name': auth_config.header, 'x-amazon-apigateway-authtype': 'custom' } api_gateway_authorizer = { 'type': 'token', 'authorizerUri': self._auth_uri(authorizer) } if auth_config.execution_role is not None: api_gateway_authorizer['authorizerCredentials'] = \ auth_config.execution_role if auth_config.ttl_seconds is not None: api_gateway_authorizer['authorizerResultTtlInSeconds'] = \ auth_config.ttl_seconds config['x-amazon-apigateway-authorizer'] = api_gateway_authorizer else: config = authorizer.to_swagger() api_config.setdefault( 'securityDefinitions', {})[authorizer.name] = config def _auth_uri(self, authorizer): # type: (ChaliceAuthorizer) -> str function_name = '%s-%s' % ( self._deployed_resources['api_handler_name'], authorizer.config.name ) return self._uri( self._deployed_resources['lambda_functions'][function_name]['arn']) def _add_to_security_definition(self, security, api_config, view): # type: (Any, Dict[str, Any], RouteEntry) -> None if view.authorizer is not None: self._generate_security_from_auth_obj(api_config, view.authorizer) for auth in security: name = list(auth.keys())[0] if name == 'api_key': # This is just the api_key_required=True config swagger_snippet = { 'type': 'apiKey', 'name': 'x-api-key', 'in': 'header', } # type: Dict[str, Any] api_config.setdefault( 'securityDefinitions', {})[name] = swagger_snippet def _generate_route_method(self, view): # type: (RouteEntry) -> Dict[str, Any] current = { 'consumes': view.content_types, 'produces': ['application/json'], 'responses': self._generate_precanned_responses(), 'x-amazon-apigateway-integration': self._generate_apig_integ( view), } # type: Dict[str, Any] docstring = inspect.getdoc(view.view_function) if docstring: doc_lines = docstring.splitlines() current['summary'] = doc_lines[0] if len(doc_lines) > 1: current['description'] = '\n'.join(doc_lines[1:]).strip('\n') if view.api_key_required: # When this happens we also have to add the relevant portions # to the security definitions. We have to someone indicate # this because this neeeds to be added to the global config # file. current.setdefault('security', []).append({'api_key': []}) if view.authorizer: current.setdefault('security', []).append( {view.authorizer.name: view.authorizer.scopes}) if view.view_args: self._add_view_args(current, view.view_args) return current def _generate_precanned_responses(self): # type: () -> Dict[str, Any] responses = { '200': { 'description': '200 response', 'schema': { '$ref': '#/definitions/Empty', } } } return responses def _uri(self, lambda_arn=None): # type: (Optional[str]) -> Any if lambda_arn is None: lambda_arn = self._deployed_resources['api_handler_arn'] partition = lambda_arn.split(':')[1] return ('arn:{partition}:apigateway:{region}:lambda:path/2015-03-31' '/functions/{lambda_arn}/invocations').format( partition=partition, region=self._region, lambda_arn=lambda_arn) def _generate_apig_integ(self, view): # type: (RouteEntry) -> Dict[str, Any] apig_integ = { 'responses': { 'default': { 'statusCode': "200", } }, 'uri': self._uri(), 'passthroughBehavior': 'when_no_match', 'httpMethod': 'POST', 'contentHandling': 'CONVERT_TO_TEXT', 'type': 'aws_proxy', } return apig_integ def _add_view_args(self, single_method, view_args): # type: (Dict[str, Any], List[str]) -> None single_method['parameters'] = [ {'name': name, 'in': 'path', 'required': True, 'type': 'string'} for name in view_args ] def _add_preflight_request(self, cors, methods, swagger_for_path): # type: (CORSConfig, List[str], Dict[str, Any]) -> None methods = methods + ['OPTIONS'] allowed_methods = ','.join(methods) response_params = { 'Access-Control-Allow-Methods': '%s' % allowed_methods } response_params.update(cors.get_access_control_headers()) headers = {k: {'type': 'string'} for k, _ in response_params.items()} response_params = {'method.response.header.%s' % k: "'%s'" % v for k, v in response_params.items()} options_request = { "consumes": ["application/json"], "produces": ["application/json"], "responses": { "200": { "description": "200 response", "schema": {"$ref": "#/definitions/Empty"}, "headers": headers } }, "x-amazon-apigateway-integration": { "responses": { "default": { "statusCode": "200", "responseParameters": response_params, } }, "requestTemplates": { "application/json": "{\"statusCode\": 200}" }, "passthroughBehavior": "when_no_match", "type": "mock", "contentHandling": "CONVERT_TO_TEXT" } } swagger_for_path['options'] = options_request class CFNSwaggerGenerator(SwaggerGenerator): def __init__(self): # type: () -> None pass def _uri(self, lambda_arn=None): # type: (Optional[str]) -> Any return { 'Fn::Sub': ( 'arn:${AWS::Partition}:apigateway:${AWS::Region}' ':lambda:path/2015-03-31' '/functions/${APIHandler.Arn}/invocations' ) } def _auth_uri(self, authorizer): # type: (ChaliceAuthorizer) -> Any return { 'Fn::Sub': ( 'arn:${AWS::Partition}:apigateway:${AWS::Region}' ':lambda:path/2015-03-31' '/functions/${%s.Arn}/invocations' % to_cfn_resource_name( authorizer.name) ) } class TemplatedSwaggerGenerator(SwaggerGenerator): def __init__(self): # type: () -> None pass def _uri(self, lambda_arn=None): # type: (Optional[str]) -> Any return StringFormat( 'arn:{partition}:apigateway:{region_name}:lambda:path/2015-03-31' '/functions/{api_handler_lambda_arn}/invocations', ['partition', 'region_name', 'api_handler_lambda_arn'], ) def _auth_uri(self, authorizer): # type: (ChaliceAuthorizer) -> Any varname = '%s_lambda_arn' % authorizer.name return StringFormat( 'arn:{partition}:apigateway:{region_name}:lambda:path/2015-03-31' '/functions/{%s}/invocations' % varname, ['partition', 'region_name', varname], ) class TerraformSwaggerGenerator(SwaggerGenerator): def __init__(self): # type: () -> None pass def _uri(self, lambda_arn=None): # type: (Optional[str]) -> Any return '${aws_lambda_function.api_handler.invoke_arn}' def _auth_uri(self, authorizer): # type: (ChaliceAuthorizer) -> Any return '${aws_lambda_function.%s.invoke_arn}' % (authorizer.name) ================================================ FILE: chalice/deploy/sweeper.py ================================================ from typing import ( # noqa List, Dict, Optional, Tuple, Any, Union, Sequence, cast, NoReturn, ) from chalice.config import Config, DeployedResources # noqa from chalice.deploy import models from chalice.deploy.planner import Variable from chalice.deploy.models import Instruction, StoreMultipleValue # noqa MarkedResource = Dict[str, List[models.RecordResource]] ResourceValueType = Dict[str, Union[Sequence[Instruction], str]] HandlerArgsType = List[Union[Dict[str, Any], str]] class ResourceSweeper(object): specific_resources = ( 's3_event', 'sns_event', 'sqs_event', 'kinesis_event', 'dynamodb_event', 'domain_name' ) def __init__(self): # type: () -> None self.plan = models.Plan() self.marked = {} # type: Dict def execute(self, plan, config): # type: (models.Plan, Config) -> None self.plan = plan self.marked = self._mark_resources() deployed = config.deployed_resources(config.chalice_stage) if deployed is not None: remaining = self._determine_remaining(deployed) self._plan_deletion(remaining, deployed) def _determine_sns_event(self, name, resource_values): # type: (str, Dict[str, str]) -> Optional[str] existing_topic = resource_values['topic'] referenced_topic = [instruction for instruction in self.marked[name] if instruction.name == 'topic' and isinstance(instruction, models.RecordResourceValue)][0] if referenced_topic.value != existing_topic: return name return None def _determine_s3_event(self, name, resource_values): # type: (str, Dict[str, str]) -> Optional[str] # Special case, we have to check the resource values # to see if they've changed. For s3 events, the resource # name is not tied to the bucket, which means if you change # the bucket, the resource name will stay the same. # So we match up the bucket referenced in the instruction # and the bucket recorded in the deployed values match up. # If they don't then we need to clean up the bucket config # referenced in the deployed values. bucket = [instruction for instruction in self.marked[name] if instruction.name == 'bucket' and isinstance(instruction, models.RecordResourceValue)][0] if bucket.value != resource_values['bucket']: return name return None def _determine_sqs_event(self, name, resource_values): # type: (str, Dict[str, str]) -> Optional[str] existing_queue = resource_values['queue'] referenced_queue = [instruction for instruction in self.marked[name] if instruction.name == 'queue' and isinstance(instruction, models.RecordResourceValue)][0] if referenced_queue.value != existing_queue: return name return None def _determine_kinesis_event(self, name, resource_values): # type: (str, Dict[str, str]) -> Optional[str] existing_stream = resource_values['stream'] referenced_stream = [instruction for instruction in self.marked[name] if instruction.name == 'stream' and isinstance(instruction, models.RecordResourceValue)][0] if referenced_stream.value != existing_stream: return name return None def _determine_dynamodb_event(self, name, resource_values): # type: (str, Dict[str, str]) -> Optional[str] existing_stream_arn = resource_values['stream_arn'] referenced_stream = [instruction for instruction in self.marked[name] if instruction.name == 'stream_arn' and isinstance(instruction, models.RecordResourceValue)][0] if referenced_stream.value != existing_stream_arn: return name return None def _determine_domain_name(self, name, resource_values): # type: (str, Dict[str, Any]) -> Optional[List[str]] api_mapping = resource_values.get('api_mapping') if not api_mapping: return None deployed_api_mappings_ids = { api_map['key'] for api_map in api_mapping } api_mapping_data = ( 'rest_api_mapping', 'websocket_api_mapping' ) instructions = self.plan.instructions planned_api_mappings_ids = { instr.value[0]['key'] for instr in instructions if isinstance(instr, StoreMultipleValue) and (instr.name in api_mapping_data and isinstance(instr.value[0], dict)) } api_mappings_to_remove = list( deployed_api_mappings_ids - planned_api_mappings_ids ) result_api_mappings = [ "%s.api_mapping.%s" % (name, api_map) for api_map in api_mappings_to_remove ] return result_api_mappings def _determine_remaining(self, deployed): # type: (DeployedResources) -> List[str] remaining = [] deployed_resource_names = reversed(deployed.resource_names()) for name in deployed_resource_names: resource_values = deployed.resource_values(name) if name not in self.marked: remaining.append(name) elif resource_values['resource_type'] in self.specific_resources: method = '_determine_%s' % resource_values['resource_type'] handler = getattr(self, method) resource_name = handler(name, resource_values) if resource_name: if isinstance(resource_name, list): remaining.extend(resource_name) else: remaining.append(resource_name) return remaining def _mark_resources(self): # type: () -> MarkedResource marked = {} # type: MarkedResource for instruction in self.plan.instructions: if isinstance(instruction, models.RecordResource): marked.setdefault(instruction.resource_name, []).append( instruction) return marked def _delete_domain_name(self, resource_values # type: Dict[str, Any] ): # type: (...) -> ResourceValueType params = { 'domain_name': resource_values['domain_name'] } msg = 'Deleting custom domain name: %s\n' % resource_values['name'] return { 'instructions': ( models.APICall( method_name='delete_domain_name', params=params, ), ), 'message': msg } def _delete_api_mapping(self, domain_name, # type: str api_mapping # type: Dict[str, Any] ): # type: (...) -> ResourceValueType if api_mapping['key'] == '/': path_key = '(none)' else: path_key = api_mapping['key'].lstrip("/") params = { 'domain_name': domain_name, 'path_key': path_key } msg = 'Deleting base path mapping from %s custom domain name: %s\n' % ( domain_name, api_mapping['key'] ) return { 'instructions': ( models.APICall( method_name='delete_api_mapping', params=params, ), ), 'message': msg } def _delete_lambda_function(self, resource_values # type: Dict[str, Any] ): # type: (...) -> ResourceValueType msg = 'Deleting function: %s\n' % resource_values['lambda_arn'] return { 'instructions': ( models.APICall( method_name='delete_function', params={'function_name': resource_values['lambda_arn']}, ), ), 'message': msg } def _delete_log_group(self, resource_values # type: Dict[str, Any] ): # type: (...) -> ResourceValueType log_group_name = resource_values['log_group_name'] msg = 'Deleting retention policy for log group: %s\n' % log_group_name return { 'instructions': ( models.APICall( method_name='delete_retention_policy', params={'log_group_name': log_group_name} ), ), 'message': msg } def _delete_lambda_layer(self, resource_values): # type: (Dict[str, str]) -> ResourceValueType apicall = models.APICall( method_name='delete_layer_version', params={'layer_version_arn': resource_values[ 'layer_version_arn']}) return { 'instructions': (apicall,), 'message': ( "Deleting layer version: %s\n" % resource_values['layer_version_arn'] ) } def _delete_iam_role(self, resource_values): # type: (Dict[str, Any]) -> ResourceValueType return { 'instructions': ( models.APICall( method_name='delete_role', params={'name': resource_values['role_name']}, ), ), 'message': 'Deleting IAM role: %s\n' % resource_values['role_name'] } def _delete_cloudwatch_event(self, resource_values): # type: (Dict[str, Any]) -> ResourceValueType return { 'instructions': ( models.APICall( method_name='delete_rule', params={'rule_name': resource_values['rule_name']}, ), ) } def _delete_rest_api(self, resource_values): # type: (Dict[str, Any]) -> ResourceValueType msg = 'Deleting Rest API: %s\n' % resource_values['rest_api_id'] return { 'instructions': ( models.APICall( method_name='delete_rest_api', params={'rest_api_id': resource_values['rest_api_id']} ), ), 'message': msg } def _delete_s3_event(self, resource_values): # type: (Dict[str, Any]) -> ResourceValueType bucket = resource_values['bucket'] function_arn = resource_values['lambda_arn'] return { 'instructions': ( models.BuiltinFunction('parse_arn', [function_arn], output_var='parsed_lambda_arn'), models.JPSearch('account_id', input_var='parsed_lambda_arn', output_var='account_id'), models.APICall( method_name='disconnect_s3_bucket_from_lambda', params={'bucket': bucket, 'function_arn': function_arn} ), models.APICall( method_name='remove_permission_for_s3_event', params={'bucket': bucket, 'function_arn': function_arn, 'account_id': Variable('account_id')} ), ) } def _delete_sns_event(self, resource_values): # type: (Dict[str, Any]) -> ResourceValueType subscription_arn = resource_values['subscription_arn'] return { 'instructions': ( models.APICall( method_name='unsubscribe_from_topic', params={'subscription_arn': subscription_arn}, ), models.APICall( method_name='remove_permission_for_sns_topic', params={ 'topic_arn': resource_values['topic_arn'], 'function_arn': resource_values['lambda_arn'], }, ), ) } def _delete_sqs_event(self, resource_values): # type: (Dict[str, Any]) -> ResourceValueType return { 'instructions': ( models.APICall( method_name='remove_lambda_event_source', params={'event_uuid': resource_values['event_uuid']}, ), ) } def _delete_kinesis_event(self, resource_values): # type: (Dict[str, Any]) -> ResourceValueType return { 'instructions': ( models.APICall( method_name='remove_lambda_event_source', params={'event_uuid': resource_values['event_uuid']}, ), ) } def _delete_dynamodb_event(self, resource_values): # type: (Dict[str, Any]) -> ResourceValueType return { 'instructions': ( models.APICall( method_name='remove_lambda_event_source', params={'event_uuid': resource_values['event_uuid']}, ), ) } def _delete_websocket_api(self, resource_values): # type: (Dict[str, Any]) -> ResourceValueType msg = 'Deleting Websocket API: %s\n' % ( resource_values['websocket_api_id'] ) return { 'instructions': ( models.APICall( method_name='delete_websocket_api', params={'api_id': resource_values['websocket_api_id']}, ), ), 'message': msg } def _default_delete(self, *resource_values): # type: (Any) -> NoReturn err_msg = "Sweeper encountered an unknown resource: %s" % \ str(resource_values) raise RuntimeError(err_msg) def _update_plan(self, instructions, message=None, insert=False): # type: (Tuple[Instruction], Optional[str], bool) -> None if insert: for instruction in instructions: self.plan.instructions.insert( 0, cast(Instruction, instruction) ) if message: instr_id = id(self.plan.instructions[0]) self.plan.messages[instr_id] = cast( str, message ) else: self.plan.instructions.extend(instructions) if message: self.plan.messages[id(self.plan.instructions[-1])] = message def _delete_domain_api_mappings(self, resource_values, name): # type: (Dict[str, Any], str) -> ResourceValueType path_key = name.split('.')[-1] api_mapping = { k: v for api_map in resource_values['api_mapping'] for k, v in api_map.items() if api_map['key'] == path_key } # type: Dict[str, str] resource_data = self._delete_api_mapping( resource_values['domain_name'], api_mapping ) return resource_data def _plan_deletion(self, remaining, # type: List[str] deployed, # type: DeployedResources ): # type: (...) -> None for name in remaining: resource_values = deployed.resource_values(name) resource_type = resource_values['resource_type'] handler_args = [resource_values] # type: HandlerArgsType insert = False if 'api_mapping' in name: resource_type = 'domain_api_mappings' handler_args.append(name) insert = True method_name = '_delete_%s' % resource_type handler = getattr(self, method_name, self._default_delete) resource_data = handler(*handler_args) instructions = cast( Tuple[Instruction], resource_data['instructions'] ) message = cast(Optional[str], resource_data.get('message')) self._update_plan( instructions, message, insert=insert ) ================================================ FILE: chalice/deploy/validate.py ================================================ import sys import warnings from typing import Dict, List, Set, Iterator, Optional, Any # noqa from chalice import app # noqa from chalice.config import Config # noqa from chalice.constants import EXPERIMENTAL_ERROR_MSG from chalice.constants import MIN_COMPRESSION_SIZE from chalice.constants import MAX_COMPRESSION_SIZE from chalice.compat import STRING_TYPES class ExperimentalFeatureError(Exception): def __init__(self, features_missing_opt_in): # type: (Set[str]) -> None self.features_missing_opt_in = features_missing_opt_in msg = self._generate_msg(features_missing_opt_in) super(ExperimentalFeatureError, self).__init__(msg) def _generate_msg(self, missing_features): # type: (Set[str]) -> str opt_in_line = ( 'app.experimental_feature_flags.update([\n' '%s\n' '])\n' % ',\n'.join([" '%s'" % feature for feature in missing_features])) return EXPERIMENTAL_ERROR_MSG % opt_in_line def validate_configuration(config): # type: (Config) -> None """Validate app configuration. The purpose of this method is to provide a fail fast mechanism for anything we know is going to fail deployment. We can detect common error cases and provide the user with helpful error messages. """ routes = config.chalice_app.routes validate_routes(routes) validate_route_content_types(routes, config.chalice_app.api.binary_types) validate_minimum_compression_size(config) _validate_manage_iam_role(config) validate_python_version(config) validate_unique_function_names(config) validate_feature_flags(config.chalice_app) validate_endpoint_type(config) validate_resource_policy(config) validate_sqs_configuration(config.chalice_app) validate_environment_variables_type(config) def validate_resource_policy(config): # type: (Config) -> None if (config.api_gateway_endpoint_type != 'PRIVATE' and config.api_gateway_endpoint_vpce): raise ValueError( "config.api_gateway_endpoint_vpce should only be " "specified for PRIVATE api_gateway_endpoint_type") if config.api_gateway_endpoint_type != 'PRIVATE': return if config.api_gateway_policy_file and config.api_gateway_endpoint_vpce: raise ValueError( "Can only specify one of api_gateway_policy_file and " "api_gateway_endpoint_vpce") if config.api_gateway_policy_file: return if not config.api_gateway_endpoint_vpce: raise ValueError( ("Private Endpoints require api_gateway_policy_file or " "api_gateway_endpoint_vpce specified")) def validate_endpoint_type(config): # type: (Config) -> None if not config.api_gateway_endpoint_type: return valid_types = ('EDGE', 'REGIONAL', 'PRIVATE') if config.api_gateway_endpoint_type not in valid_types: raise ValueError( "api gateway endpoint type must be one of %s" % ( ", ".join(valid_types))) def validate_feature_flags(chalice_app): # type: (app.Chalice) -> None missing_opt_in = set() # pylint: disable=protected-access for feature in chalice_app._features_used: if feature not in chalice_app.experimental_feature_flags: missing_opt_in.add(feature) if missing_opt_in: raise ExperimentalFeatureError(missing_opt_in) def validate_routes(routes): # type: (Dict[str, Dict[str, app.RouteEntry]]) -> None # We're trying to validate any kind of route that will fail # when we send the request to API gateway. # We check for: # # * any routes that end with a trailing slash. for route_name, methods in routes.items(): if not route_name: raise ValueError("Route cannot be the empty string") if route_name != '/' and route_name.endswith('/'): raise ValueError("Route cannot end with a trailing slash: %s" % route_name) _validate_cors_for_route(route_name, methods) def validate_python_version(config, actual_py_version=None): # type: (Config, Optional[str]) -> None """Validate configuration matches a specific python version. If the ``actual_py_version`` is not provided, it will default to the major/minor version of the currently running python interpreter. :param actual_py_version: The major/minor python version in the form "pythonX.Y", e.g "python2.7", "python3.6". """ lambda_version = config.lambda_python_version if actual_py_version is None: actual_py_version = 'python%s.%s' % sys.version_info[:2] if actual_py_version != lambda_version: # We're not making this a hard error for now, but we may # turn this into a hard fail. warnings.warn("You are currently running %s, but the closest " "supported version on AWS Lambda is %s\n" "Please use %s, otherwise you may run into " "deployment issues. " % (actual_py_version, lambda_version, lambda_version), stacklevel=2) def validate_route_content_types(routes, binary_types): # type: (Dict[str, Dict[str, app.RouteEntry]], List[str]) -> None for methods in routes.values(): for route_entry in methods.values(): _validate_entry_content_type(route_entry, binary_types) def _validate_entry_content_type(route_entry, binary_types): # type: (app.RouteEntry, List[str]) -> None binary, non_binary = [], [] for content_type in route_entry.content_types: if content_type in binary_types: binary.append(content_type) else: non_binary.append(content_type) if binary and non_binary: # A routes content_types be homogeneous in their binary support. raise ValueError( 'In view function "%s", the content_types %s support binary ' 'and %s do not. All content_types must be consistent in their ' 'binary support.' % (route_entry.view_name, binary, non_binary)) def _validate_cors_for_route(route_url, route_methods): # type: (str, Dict[str, app.RouteEntry]) -> None entries_with_cors = [ entry for entry in route_methods.values() if entry.cors ] if entries_with_cors: # If the user has enabled CORS, they can't also have an OPTIONS # method because we'll create one for them. API gateway will # raise an error about duplicate methods. if 'OPTIONS' in route_methods: raise ValueError( "Route entry cannot have both cors=True and " "methods=['OPTIONS', ...] configured. When " "CORS is enabled, an OPTIONS method is automatically " "added for you. Please remove 'OPTIONS' from the list of " "configured HTTP methods for: %s" % route_url) if not all(entries_with_cors[0].cors == entry.cors for entry in entries_with_cors): raise ValueError( "Route may not have multiple differing CORS configurations. " "Please ensure all views for \"%s\" that have CORS configured " "have the same CORS configuration." % route_url ) def validate_minimum_compression_size(config): # type: (Config) -> None if config.minimum_compression_size is None: return if not isinstance(config.minimum_compression_size, int): raise ValueError("'minimum_compression_size' must be an int.") if config.minimum_compression_size < MIN_COMPRESSION_SIZE \ or config.minimum_compression_size > MAX_COMPRESSION_SIZE: raise ValueError("'minimum_compression_size' must be equal to or " "greater than %s and less than or equal to %s." % (MIN_COMPRESSION_SIZE, MAX_COMPRESSION_SIZE)) def _validate_manage_iam_role(config): # type: (Config) -> None # We need to check if manage_iam_role is None because that's the value # it the user hasn't specified this value. # However, if the manage_iam_role value is not None, the user set it # to something, in which case we care if they set it to False. if not config.manage_iam_role: # If they don't want us to manage the role, they # have to specify an iam_role_arn. if not config.iam_role_arn: raise ValueError( "When 'manage_iam_role' is set to false, you " "must provide an 'iam_role_arn' in config.json." ) def validate_unique_function_names(config): # type: (Config) -> None names = set() # type: Set[str] for name in _get_all_function_names(config.chalice_app): if name in names: raise ValueError("Duplicate function name detected: %s\n" "Names must be unique across all lambda " "functions in your Chalice app." % name) names.add(name) def _get_all_function_names(chalice_app): # type: (app.Chalice) -> Iterator[str] for auth_handler in chalice_app.builtin_auth_handlers: yield auth_handler.name for event in chalice_app.event_sources: yield event.name for function in chalice_app.pure_lambda_functions: yield function.name def validate_sqs_configuration(chalice_app): # type: (app.Chalice) -> None for event in chalice_app.event_sources: if not isinstance(event, app.SQSEventConfig): continue if not _is_valid_queue_name(event.queue, event.queue_arn): raise ValueError("The 'queue' parameter for the " "'@app.on_sqs_message()' handler must be the " "name of the queue, not the queue URL or the " "queue ARN. Invalid value: %s" % event.queue) def _is_valid_queue_name(queue_name, queue_arn): # type: (Optional[str], Optional[str]) -> bool # The mutually exclusiveness is verified in the on_sqs_message decorator. if queue_name is not None and queue_name.startswith(('https:', 'arn:')): return False if queue_arn is not None and not queue_arn.startswith('arn:'): return False # We're not validating that the queue has only valid chars because SQS # won't let you create a queue with that name in the first place. We just # want to detect the case where a user puts the queue URL/ARN instead of # the name for the queue_name. return True def validate_environment_variables_type(config): # type: (Config) -> None _validate_environment_variables(config.environment_variables) for name in _get_all_function_names(config.chalice_app): _validate_environment_variables( config.scope(config.chalice_stage, name).environment_variables) def _validate_environment_variables(environment_variables): # type: (Dict[str, Any]) -> None for key, value in environment_variables.items(): if not isinstance(value, STRING_TYPES): raise ValueError("Environment variable values must be strings, " "got 'type' %s for key '%s'" % ( type(value).__name__, key)) ================================================ FILE: chalice/invoke.py ================================================ """Abstraction for invoking a lambda function.""" import json from typing import Any, Optional, Dict, List, Union, Tuple # noqa from chalice.config import DeployedResources # noqa from chalice.awsclient import TypedAWSClient # noqa from chalice.utils import UI # noqa from chalice.compat import StringIO OptBytes = Optional[bytes] _ERROR_KEY = 'FunctionError' _ERROR_VALUE = 'Unhandled' def _response_is_error(response): # type: (Dict[str, Any]) -> bool return response.get(_ERROR_KEY) == _ERROR_VALUE class UnhandledLambdaError(Exception): pass class LambdaInvokeHandler(object): """Handler class to coordinate making an invoke call to lambda. This class takes a LambdaInvoker, a LambdaResponseFormatter, and a UI object in order to make an invoke call against lambda, format the response and render it to the UI. """ def __init__(self, invoker, formatter, ui): # type: (LambdaInvoker, LambdaResponseFormatter, UI) -> None self._invoker = invoker self._formatter = formatter self._ui = ui def invoke(self, payload=None): # type: (OptBytes) -> None response = self._invoker.invoke(payload) formatted_response = self._formatter.format_response(response) if _response_is_error(response): self._ui.error(formatted_response) raise UnhandledLambdaError() self._ui.write(formatted_response) class LambdaInvoker(object): def __init__(self, lambda_arn, client): # type: (str, TypedAWSClient) -> None self._lambda_arn = lambda_arn self._client = client def invoke(self, payload=None): # type: (OptBytes) -> Dict[str, Any] return self._client.invoke_function( self._lambda_arn, payload=payload ) class LambdaResponseFormatter(object): _PAYLOAD_KEY = 'Payload' _TRACEBACK_HEADING = 'Traceback (most recent call last):\n' def format_response(self, response): # type: (Dict[str, Any]) -> str formatted = StringIO() payload = response[self._PAYLOAD_KEY].read() if _response_is_error(response): self._format_error(formatted, payload) else: self._format_success(formatted, payload) return str(formatted.getvalue()) def _format_error(self, formatted, payload): # type: (StringIO, bytes) -> None loaded_error = json.loads(payload) error_message = loaded_error['errorMessage'] error_type = loaded_error.get('errorType') stack_trace = loaded_error.get('stackTrace') if stack_trace is not None: self._format_stacktrace(formatted, stack_trace) if error_type is not None: formatted.write('{}: {}\n'.format(error_type, error_message)) else: formatted.write('{}\n'.format(error_message)) def _format_stacktrace(self, formatted, stack_trace): # type: (StringIO, List[List[Union[str, int]]]) -> None formatted.write(self._TRACEBACK_HEADING) for frame in stack_trace: self._format_frame(formatted, frame) def _format_frame(self, formatted, frame): # type: (StringIO, Union[str, List[Union[str, int]]]) -> None if isinstance(frame, list): # If the output is a list, it came from a 4-tuple as a result of # an extract_tb call. This is the behavior up to and including # python 3.6. path, lineno, function, code = frame formatted.write( ' File "{}", line {}, in {}\n'.format(path, lineno, function)) formatted.write( ' {}\n'.format(code)) else: # If it is not a list, its a string. This is because the 4-tuple # was replaced with a FrameSummary object which is serialized as # a string by Lambda. In this case we can just print it directly. formatted.write(frame) def _format_success(self, formatted, payload): # type: (StringIO, bytes) -> None formatted.write('{}\n'.format(str(payload.decode('utf-8')))) ================================================ FILE: chalice/local.py ================================================ """Dev server used for running a chalice app locally. This is intended only for local development purposes. """ from __future__ import print_function from __future__ import annotations import re import threading import time import uuid import base64 import functools import warnings from collections import namedtuple import json from six.moves.BaseHTTPServer import HTTPServer from six.moves.BaseHTTPServer import BaseHTTPRequestHandler from six.moves.socketserver import ThreadingMixIn from typing import ( List, Any, Dict, Tuple, Callable, Optional, Union, ) # noqa from chalice.app import Chalice # noqa from chalice.app import CORSConfig # noqa from chalice.app import ChaliceAuthorizer # noqa from chalice.app import CognitoUserPoolAuthorizer # noqa from chalice.app import RouteEntry # noqa from chalice.app import Request # noqa from chalice.app import AuthResponse # noqa from chalice.app import BuiltinAuthConfig # noqa from chalice.config import Config # noqa from chalice.compat import urlparse, parse_qs MatchResult = namedtuple('MatchResult', ['route', 'captured', 'query_params']) EventType = Dict[str, Any] ContextType = Dict[str, Any] HeaderType = Dict[str, Any] ResponseType = Dict[str, Any] HandlerCls = Callable[..., 'ChaliceRequestHandler'] ServerCls = Callable[..., 'HTTPServer'] class Clock(object): def time(self) -> float: return time.time() def create_local_server(app_obj: Chalice, config: Config, host: str, port: int) -> LocalDevServer: CustomLocalChalice.__bases__ = (LocalChalice, app_obj.__class__) app_obj.__class__ = CustomLocalChalice return LocalDevServer(app_obj, config, host, port) class LocalARNBuilder(object): ARN_FORMAT = ('arn:aws:execute-api:{region}:{account_id}' ':{api_id}/{stage}/{method}/{resource_path}') LOCAL_REGION = 'mars-west-1' LOCAL_ACCOUNT_ID = '123456789012' LOCAL_API_ID = 'ymy8tbxw7b' LOCAL_STAGE = 'api' def build_arn(self, method: str, path: str) -> str: # In API Gateway the method and URI are separated by a / so typically # the uri portion omits the leading /. In the case where the entire # url is just '/' API Gateway adds a / to the end so that the arn end # with a '//'. if path != '/': path = path[1:] path = path.split('?')[0] return self.ARN_FORMAT.format( region=self.LOCAL_REGION, account_id=self.LOCAL_ACCOUNT_ID, api_id=self.LOCAL_API_ID, stage=self.LOCAL_STAGE, method=method, resource_path=path ) class ARNMatcher(object): def __init__(self, target_arn: str) -> None: self._arn = target_arn def _resource_match(self, resource: str) -> bool: # Arn matching supports two special case characetrs that are not # escapable. * represents a glob which translates to a non-greedy # match of any number of characters. ? which is any single character. # These are easy to translate to a regex using .*? and . respectivly. escaped_resource = re.escape(resource) resource_regex = escaped_resource.replace(r'\?', '.').replace( r'\*', '.*?') resource_regex = '^%s$' % resource_regex return re.match(resource_regex, self._arn) is not None def does_any_resource_match(self, resources: List[str]) -> bool: for resource in resources: if self._resource_match(resource): return True return False class RouteMatcher(object): def __init__(self, route_urls: List[str]) -> None: # Sorting the route_urls ensures we always check # the concrete routes for a prefix before the # variable/capture parts of the route, e.g # '/foo/bar' before '/foo/{capture}' self.route_urls = sorted(route_urls) def match_route(self, url: str) -> MatchResult: """Match the url against known routes. This method takes a concrete route "/foo/bar", and matches it against a set of routes. These routes can use param substitution corresponding to API gateway patterns. For example:: match_route('/foo/bar') -> '/foo/{name}' """ # Otherwise we need to check for param substitution parsed_url = urlparse(url) query_params = parse_qs(parsed_url.query, keep_blank_values=True) path = parsed_url.path # API Gateway removes the trailing slash if the route is not the root # path. We do the same here so our route matching works the same way. if path != '/' and path.endswith('/'): path = path[:-1] parts = path.split('/') captured = {} for route_url in self.route_urls: url_parts = route_url.split('/') if len(parts) == len(url_parts): for i, j in zip(parts, url_parts): if j.startswith('{') and j.endswith('}'): captured[j[1:-1]] = i continue if i != j: break else: return MatchResult(route_url, captured, query_params) raise ValueError("No matching route found for: %s" % url) class LambdaEventConverter(object): LOCAL_SOURCE_IP = '127.0.0.1' """Convert an HTTP request to an event dict used by lambda.""" def __init__(self, route_matcher: RouteMatcher, binary_types: Optional[List[str]] = None) -> None: self._route_matcher = route_matcher if binary_types is None: binary_types = [] self._binary_types = binary_types def _is_binary(self, headers: Dict[str, Any]) -> bool: return headers.get('content-type', '') in self._binary_types def create_lambda_event(self, method: str, path: str, headers: Dict[str, str], body: Optional[bytes] = None) -> EventType: view_route = self._route_matcher.match_route(path) event = { 'requestContext': { 'httpMethod': method, 'resourcePath': view_route.route, 'identity': { 'sourceIp': self.LOCAL_SOURCE_IP }, 'path': path.split('?')[0], }, 'headers': {k.lower(): v for k, v in headers.items()}, 'pathParameters': view_route.captured, 'stageVariables': {}, } if view_route.query_params: event['multiValueQueryStringParameters'] = view_route.query_params else: # If no query parameters are provided, API gateway maps # this to None so we're doing this for parity. event['multiValueQueryStringParameters'] = None if self._is_binary(headers) and body is not None: event['body'] = base64.b64encode(body).decode('ascii') event['isBase64Encoded'] = True else: event['body'] = body return event class LocalGatewayException(Exception): CODE = 0 def __init__(self, headers: HeaderType, body: Optional[bytes] = None) -> None: self.headers = headers self.body = body class InvalidAuthorizerError(LocalGatewayException): CODE = 500 class ForbiddenError(LocalGatewayException): CODE = 403 class NotAuthorizedError(LocalGatewayException): CODE = 401 class LambdaContext(object): def __init__(self, function_name: str, memory_size: int, max_runtime_ms: int = 3000, time_source: Optional[Clock] = None) -> None: if time_source is None: time_source = Clock() self._time_source = time_source self._start_time = self._current_time_millis() self._max_runtime = max_runtime_ms # Below are properties that are found on the real LambdaContext passed # by lambda and their associated documentation. # Name of the Lambda function that is executing. self.function_name = function_name # The Lambda function version that is executing. If an alias is used # to invoke the function, then function_version will be the version # the alias points to. # Chalice local obviously does not support versioning so it will always # be set to $LATEST. self.function_version = '$LATEST' # The ARN used to invoke this function. It can be function ARN or # alias ARN. An unqualified ARN executes the $LATEST version and # aliases execute the function version it is pointing to. self.invoked_function_arn = '' # Memory limit, in MB, you configured for the Lambda function. You set # the memory limit at the time you create a Lambda function and you # can change it later. self.memory_limit_in_mb = memory_size # AWS request ID associated with the request. This is the ID returned # to the client that called the invoke method. self.aws_request_id = str(uuid.uuid4()) # The name of the CloudWatch log group where you can find logs written # by your Lambda function. self.log_group_name = '' # The name of the CloudWatch log stream where you can find logs # written by your Lambda function. The log stream may or may not # change for each invocation of the Lambda function. # # The value is null if your Lambda function is unable to create a log # stream, which can happen if the execution role that grants necessary # permissions to the Lambda function does not include permissions for # the CloudWatch Logs actions. self.log_stream_name = '' # The last two attributes have the following comment in the # documentation: # Information about the client application and device when invoked # through the AWS Mobile SDK, it can be null. # Chalice local doens't need to set these since they are specifically # for the mobile SDK. self.identity = None self.client_context = None def _current_time_millis(self) -> float: return self._time_source.time() * 1000 def get_remaining_time_in_millis(self) -> float: runtime = self._current_time_millis() - self._start_time return self._max_runtime - runtime LocalAuthPair = Tuple[EventType, LambdaContext] class LocalGatewayAuthorizer(object): """A class for running user defined authorizers in local mode.""" def __init__(self, app_object: Chalice) -> None: self._app_object = app_object self._arn_builder = LocalARNBuilder() def authorize(self, raw_path: str, lambda_event: EventType, lambda_context: LambdaContext) -> LocalAuthPair: method = lambda_event['requestContext']['httpMethod'] route_entry = self._route_for_event(lambda_event) if not route_entry: return lambda_event, lambda_context authorizer = route_entry.authorizer if not authorizer: return lambda_event, lambda_context # If authorizer is Cognito then try to parse the JWT and simulate an # APIGateway validated request if isinstance(authorizer, CognitoUserPoolAuthorizer): if "headers" in lambda_event\ and "authorization" in lambda_event["headers"]: token = lambda_event["headers"]["authorization"] claims = self._decode_jwt_payload(token) try: cognito_username = claims["cognito:username"] except KeyError: # If a key error is raised when trying to get the cognito # username then it is a machine-to-machine communication. # This kind of cognito authorization flow is not # supported in local mode. We can ignore it here to allow # users to test their code local with a different cognito # authorization flow. warnings.warn( '%s for machine-to-machine communicaiton is not ' 'supported in local mode. All requests made against ' 'a route will be authorized to allow local testing.' % authorizer.__class__.__name__ ) return lambda_event, lambda_context auth_result = {"context": {"claims": claims}, "principalId": cognito_username} lambda_event = self._update_lambda_event(lambda_event, auth_result) if not isinstance(authorizer, ChaliceAuthorizer): # Currently the only supported local authorizer is the # BuiltinAuthConfig type. Anything else we will err on the side of # allowing local testing by simply admiting the request. Otherwise # there is no way for users to test their code in local mode. warnings.warn( '%s is not a supported in local mode. All requests made ' 'against a route will be authorized to allow local testing.' % authorizer.__class__.__name__ ) return lambda_event, lambda_context arn = self._arn_builder.build_arn(method, raw_path) auth_event = self._prepare_authorizer_event(arn, lambda_event, lambda_context) auth_result = authorizer(auth_event, lambda_context) if auth_result is None: raise InvalidAuthorizerError( {'x-amzn-RequestId': lambda_context.aws_request_id, 'x-amzn-ErrorType': 'AuthorizerConfigurationException'}, b'{"message":null}' ) authed = self._check_can_invoke_view_function(arn, auth_result) if authed: lambda_event = self._update_lambda_event(lambda_event, auth_result) else: raise ForbiddenError( {'x-amzn-RequestId': lambda_context.aws_request_id, 'x-amzn-ErrorType': 'AccessDeniedException'}, (b'{"Message": ' b'"User is not authorized to access this resource"}')) return lambda_event, lambda_context def _check_can_invoke_view_function(self, arn: str, auth_result: ResponseType) -> bool: policy = auth_result.get('policyDocument', {}) statements = policy.get('Statement', []) allow_resource_statements = [] for statement in statements: if statement.get('Effect') == 'Allow' and \ (statement.get('Action') == 'execute-api:Invoke' or 'execute-api:Invoke' in statement.get('Action')): for resource in statement.get('Resource'): allow_resource_statements.append(resource) arn_matcher = ARNMatcher(arn) return arn_matcher.does_any_resource_match(allow_resource_statements) def _route_for_event(self, lambda_event: EventType) -> Optional[RouteEntry]: # Authorizer had to be made into an Any type since mypy couldn't # detect that app.ChaliceAuthorizer was callable. resource_path = lambda_event.get( 'requestContext', {}).get('resourcePath') http_method = lambda_event['requestContext']['httpMethod'] try: route_entry = self._app_object.routes[resource_path][http_method] except KeyError: # If a key error is raised when trying to get the route entry # then this route does not support this method. A method error # will be raised by the chalice handler method. We can ignore it # here by returning no authorizer to avoid duplicating the logic. return None return route_entry def _update_lambda_event(self, lambda_event: EventType, auth_result: ResponseType) -> EventType: auth_context = auth_result['context'] auth_context.update({ 'principalId': auth_result['principalId'] }) lambda_event['requestContext']['authorizer'] = auth_context return lambda_event def _prepare_authorizer_event(self, arn: str, lambda_event: EventType, lambda_context: LambdaContext) -> EventType: """Translate event for an authorizer input.""" authorizer_event = lambda_event.copy() authorizer_event['type'] = 'TOKEN' try: authorizer_event['authorizationToken'] = authorizer_event.get( 'headers', {})['authorization'] except KeyError: raise NotAuthorizedError( {'x-amzn-RequestId': lambda_context.aws_request_id, 'x-amzn-ErrorType': 'UnauthorizedException'}, b'{"message":"Unauthorized"}') authorizer_event['methodArn'] = arn return authorizer_event def _decode_jwt_payload(self, jwt: str) -> Dict: payload_segment = jwt.split(".", 2)[1] payload = base64.urlsafe_b64decode(self._base64_pad(payload_segment)) return json.loads(payload) def _base64_pad(self, value: str) -> str: rem = len(value) % 4 if rem > 0: value += "=" * (4 - rem) return value class LocalGateway(object): """A class for faking the behavior of API Gateway.""" MAX_LAMBDA_EXECUTION_TIME = 900 def __init__(self, app_object: Chalice, config: Config) -> None: self._app_object = app_object self._config = config self.event_converter = LambdaEventConverter( RouteMatcher(list(app_object.routes)), self._app_object.api.binary_types ) self._authorizer = LocalGatewayAuthorizer(app_object) def _generate_lambda_context(self) -> LambdaContext: if self._config.lambda_timeout is None: timeout = self.MAX_LAMBDA_EXECUTION_TIME * 1000 else: timeout = self._config.lambda_timeout * 1000 return LambdaContext( function_name=self._config.function_name, memory_size=self._config.lambda_memory_size, max_runtime_ms=timeout ) def _generate_lambda_event(self, method: str, path: str, headers: HeaderType, body: Optional[bytes]) -> EventType: lambda_event = self.event_converter.create_lambda_event( method=method, path=path, headers=headers, body=body, ) return lambda_event def _has_user_defined_options_method(self, lambda_event: EventType) -> bool: route_key = lambda_event['requestContext']['resourcePath'] return 'OPTIONS' in self._app_object.routes[route_key] def handle_request(self, method: str, path: str, headers: HeaderType, body: Optional[bytes]) -> ResponseType: lambda_context = self._generate_lambda_context() try: lambda_event = self._generate_lambda_event( method, path, headers, body) except ValueError: # API Gateway will return a different error on route not found # depending on whether or not we have an authorization token in our # request. Since we do not do that check until we actually find # the authorizer that we will call we do not have that information # available at this point. Instead we just check to see if that # header is present and change our response if it is. This will # need to be refactored later if we decide to more closely mirror # how API Gateway does their auth and routing. error_headers = {'x-amzn-RequestId': lambda_context.aws_request_id, 'x-amzn-ErrorType': 'UnauthorizedException'} auth_header = headers.get('authorization') if auth_header is None: auth_header = headers.get('Authorization') if auth_header is not None: raise ForbiddenError( error_headers, (b'{"message": "Authorization header requires ' b'\'Credential\'' b' parameter. Authorization header requires \'Signature\'' b' parameter. Authorization header requires ' b'\'SignedHeaders\' parameter. Authorization header ' b'requires existence of either a \'X-Amz-Date\' or a' b' \'Date\' header. Authorization=%s"}' % auth_header.encode('ascii'))) raise ForbiddenError( error_headers, b'{"message": "Missing Authentication Token"}') # This can either be because the user's provided an OPTIONS method # *or* this is a preflight request, which chalice automatically # responds to without invoking a user defined route. if method == 'OPTIONS' and \ not self._has_user_defined_options_method(lambda_event): # No options route was defined for this path. API Gateway should # automatically generate our CORS headers. options_headers = self._autogen_options_headers(lambda_event) return { 'statusCode': 200, 'headers': options_headers, 'multiValueHeaders': {}, 'body': None } # The authorizer call will be a noop if there is no authorizer method # defined for route. Otherwise it will raise a ForbiddenError # which will be caught by the handler that called this and a 403 or # 401 will be sent back over the wire. lambda_event, lambda_context = self._authorizer.authorize( path, lambda_event, lambda_context) response = self._app_object(lambda_event, lambda_context) return response def _autogen_options_headers(self, lambda_event: EventType) -> HeaderType: route_key = lambda_event['requestContext']['resourcePath'] route_dict = self._app_object.routes[route_key] route_methods = [method for method in route_dict.keys() if route_dict[method].cors is not None] # If there are no views with CORS enabled # then OPTIONS is the only allowed method. if not route_methods: return {'Access-Control-Allow-Methods': 'OPTIONS'} # Chalice ensures that routes with multiple views have the same # CORS configuration, so if any view has a CORS Config we can use # that config since they will all be the same. cors_config = route_dict[route_methods[0]].cors cors_headers = cors_config.get_access_control_headers() # We need to add OPTIONS since it is not a part of the CORSConfig # object. APIGateway handles this entirely based on the API definition. # So our local version needs to add this manually to our set of allowed # headers. route_methods.append('OPTIONS') # The Access-Control-Allow-Methods header is not added by the # CORSConfig object it is added to the API Gateway route during # deployment, so we need to manually add those headers here. cors_headers.update({ 'Access-Control-Allow-Methods': '%s' % ','.join(route_methods) }) return cors_headers class ChaliceRequestHandler(BaseHTTPRequestHandler): """A class for mapping raw HTTP events to and from LocalGateway.""" protocol_version = 'HTTP/1.1' def __init__(self, request: bytes, client_address: Tuple[str, int], server: HTTPServer, app_object: Chalice, config: Config) -> None: self.local_gateway = LocalGateway(app_object, config) BaseHTTPRequestHandler.__init__( self, request, client_address, server) # type: ignore def _parse_payload(self) -> Tuple[HeaderType, Optional[bytes]]: body = None content_length = int(self.headers.get('content-length', '0')) if content_length > 0: body = self.rfile.read(content_length) converted_headers = dict(self.headers) return converted_headers, body def _generic_handle(self) -> None: headers, body = self._parse_payload() try: response = self.local_gateway.handle_request( method=self.command, path=self.path, headers=headers, body=body ) status_code = response['statusCode'] headers = response['headers'].copy() headers.update(response['multiValueHeaders']) response = self._handle_binary(response) body = response['body'] self._send_http_response(status_code, headers, body) except LocalGatewayException as e: self._send_error_response(e) def _handle_binary(self, response: Dict[str, Any]) -> Dict[str, Any]: if response.get('isBase64Encoded'): body = base64.b64decode(response['body']) response['body'] = body return response def _send_error_response(self, error: LocalGatewayException) -> None: code = error.CODE headers = error.headers body = error.body self._send_http_response(code, headers, body) def _send_http_response(self, code: int, headers: HeaderType, body: Optional[Union[str, bytes]]) -> None: if body is None: self._send_http_response_no_body(code, headers) else: self._send_http_response_with_body(code, headers, body) def _send_http_response_with_body(self, code: int, headers: HeaderType, body: Union[str, bytes]) -> None: self.send_response(code) if not isinstance(body, bytes): body = body.encode('utf-8') self.send_header('Content-Length', str(len(body))) content_type = headers.pop( 'Content-Type', 'application/json') self.send_header('Content-Type', content_type) self._send_headers(headers) self.wfile.write(body) do_GET = do_PUT = do_POST = do_HEAD = do_DELETE = \ do_PATCH = do_OPTIONS = _generic_handle def _send_http_response_no_body(self, code: int, headers: HeaderType) -> None: headers['Content-Length'] = '0' self.send_response(code) self._send_headers(headers) def _send_headers(self, headers: HeaderType) -> None: for header_name, header_value in headers.items(): if isinstance(header_value, list): for value in header_value: self.send_header(header_name, value) else: self.send_header(header_name, header_value) self.end_headers() class ThreadedHTTPServer(ThreadingMixIn, HTTPServer): """Threading mixin to better support browsers. When a browser sends a GET request to Chalice it keeps the connection open for reuse. In the single threaded model this causes Chalice local to become unresponsive to all clients other than that browser socket. Even sending a header requesting that the client close the connection is not good enough, the browswer will simply open another one and sit on it. """ daemon_threads = True class LocalDevServer(object): def __init__(self, app_object: Chalice, config: Config, host: str, port: int, handler_cls: HandlerCls = ChaliceRequestHandler, server_cls: ServerCls = ThreadedHTTPServer) -> None: self.app_object = app_object self.host = host self.port = port self._wrapped_handler = functools.partial( handler_cls, app_object=app_object, config=config) self.server = server_cls((host, port), self._wrapped_handler) def handle_single_request(self) -> None: self.server.handle_request() def serve_forever(self) -> None: print("Serving on http://%s:%s" % (self.host, self.port)) self.server.serve_forever() def shutdown(self) -> None: # This must be called from another thread of else it # will deadlock. self.server.shutdown() class HTTPServerThread(threading.Thread): """Thread that manages starting/stopping local HTTP server. This is a small wrapper around a normal threading.Thread except that it adds shutdown capability of the HTTP server, which is not part of the normal threading.Thread interface. """ def __init__(self, server_factory: Callable[[], LocalDevServer]) -> None: threading.Thread.__init__(self) self._server_factory = server_factory self._server: Optional[LocalDevServer] = None self.daemon = True def run(self) -> None: self._server = self._server_factory() self._server.serve_forever() def shutdown(self) -> None: if self._server is not None: self._server.shutdown() class LocalChalice(Chalice): _THREAD_LOCAL = threading.local() # This is a known mypy bug where you can't override instance # variables with properties. So this should be type safe, which # is why we're adding the type: ignore comments here. # See: https://github.com/python/mypy/issues/4125 @property # type: ignore def current_request(self) -> Request: # type: ignore return self._THREAD_LOCAL.current_request @current_request.setter def current_request(self, value: Request) -> None: # type: ignore self._THREAD_LOCAL.current_request = value class CustomLocalChalice(LocalChalice): pass ================================================ FILE: chalice/logs.py ================================================ """Module for inspecting chalice logs. This module provides APIs for searching, interacting with the logs generated by AWS Lambda. """ from __future__ import annotations import time from datetime import datetime, timedelta from dataclasses import dataclass from collections import defaultdict from typing import Any, Optional, Iterator, Dict, IO, Callable, Set # noqa from botocore.session import Session # noqa from chalice.awsclient import TypedAWSClient, CWLogEvent # noqa from chalice.utils import TimestampConverter @dataclass class LogRetrieveOptions(object): max_entries: Optional[int] = None start_time: Optional[datetime] = None include_lambda_messages: bool = False @classmethod def create( cls, follow: bool = False, since: Optional[str] = None, **kwargs: Any ) -> LogRetrieveOptions: start_time = None if since is not None: start_time = TimestampConverter().timestamp_to_datetime(since) elif follow: # If they've specified --follow with no start time, we'll default # to 10 minutes prior to now. This is a backwards compat # quirk where we can't change the default for `chalice logs`, but # for the --follow mode, we can default to only showing 10 minutes # worth of logs before we start polling for new logs. In a # future major version we can just change the default for all log # retrieval to 10 minutes. start_time = datetime.utcnow() - timedelta(minutes=10) kwargs['start_time'] = start_time return cls(**kwargs) def display_logs( retriever: LogRetriever, stream: IO[str], retrieve_options: LogRetrieveOptions, ) -> None: events = retriever.retrieve_logs(retrieve_options) for event in events: stream.write( '%s %s %s\n' % ( event['timestamp'], event['logShortId'], event['message'].strip(), ) ) class LogRetriever(object): def __init__( self, log_event_generator: BaseLogEventGenerator, log_group_name: str ) -> None: self._log_event_generator = log_event_generator self._log_group_name = log_group_name @classmethod def create_from_lambda_arn( cls, log_event_generator: BaseLogEventGenerator, lambda_arn: str ) -> LogRetriever: """Create a LogRetriever from a client and lambda arn. :type client: botocore.client.Logs :param client: A ``logs`` client. :type lambda_arn: str :param lambda_arn: The ARN of the lambda function. :return: An instance of ``LogRetriever``. """ lambda_name = lambda_arn.split(':')[6] log_group_name = '/aws/lambda/%s' % lambda_name return cls(log_event_generator, log_group_name) def _is_lambda_message(self, event: CWLogEvent) -> bool: # Lambda will also inject log messages into your log streams. # They look like: # START RequestId: guid Version: $LATEST # END RequestId: guid # REPORT RequestId: guid Duration: 0.35 ms Billed Duration: ... # By default, these message are included in retrieve_logs(). # But you can also request that retrieve_logs() filter out # these message so that we only include log messages generated # by your chalice app. msg = event['message'].strip() return msg.startswith( ('START RequestId', 'END RequestId', 'REPORT RequestId') ) def retrieve_logs( self, retrieve_options: LogRetrieveOptions ) -> Iterator[CWLogEvent]: """Retrieve logs from a log group. :type include_lambda_messages: boolean :param include_lambda_messages: Include logs generated by the AWS Lambda service. If this value is False, only chalice logs will be included. :type max_entries: int :param max_entries: Maximum number of log messages to include. :rtype: iterator :return: An iterator that yields event dicts. Each event dict has these keys: * logStreamName -> (string) The name of the log stream. * timestamp -> (datetime.datetime) - The timestamp for the msg. * message -> (string) The data contained in the log event. * ingestionTime -> (datetime.datetime) Ingestion time of event. * eventId -> (string) A unique identifier for this event. * logShortId -> (string) Short identifier for logStreamName. """ max_entries = retrieve_options.max_entries shown = 0 events = self._log_event_generator.iter_log_events( self._log_group_name, retrieve_options ) for event in events: if ( not retrieve_options.include_lambda_messages and self._is_lambda_message(event) ): continue # logStreamName is: '2016/07/05/[id]hash' # We want to extract the hash portion and # provide a short identifier. identifier = event['logStreamName'] if ']' in identifier: index = identifier.find(']') identifier = identifier[index + 1:index + 7] event['logShortId'] = identifier yield event shown += 1 if max_entries is not None and shown >= max_entries: return class BaseLogEventGenerator(object): def __init__(self, client: TypedAWSClient) -> None: self._client = client def iter_log_events( self, log_group_name: str, options: LogRetrieveOptions ) -> Iterator[CWLogEvent]: raise NotImplementedError("iter_log_events") class LogEventGenerator(BaseLogEventGenerator): def iter_log_events( self, log_group_name: str, options: LogRetrieveOptions ) -> Iterator[CWLogEvent]: logs = self._client.iter_log_events( log_group_name=log_group_name, start_time=options.start_time ) yield from logs class FollowLogEventGenerator(BaseLogEventGenerator): _POLL_TIME = 5 def __init__( self, client: TypedAWSClient, sleep: Callable[[int], None] = time.sleep, poll_time: int = _POLL_TIME, ) -> None: self._client = client self._sleep = sleep self._event_id_cache: Dict[datetime, Set[str]] = defaultdict(set) self._poll_time = poll_time def iter_log_events( self, log_group_name: str, options: LogRetrieveOptions ) -> Iterator[CWLogEvent]: start_time = options.start_time try: yield from self._loop_on_filter_log_events( log_group_name, start_time ) except KeyboardInterrupt: pass def _loop_on_filter_log_events( self, log_group_name: str, start_time: Optional[datetime] ) -> Iterator[CWLogEvent]: self._event_id_cache.clear() kwargs: Dict[str, Any] = { 'log_group_name': log_group_name, 'start_time': start_time, } while True: response = self._client.filter_log_events(**kwargs) for event in response['events']: if not self._in_cache(event): self._add_to_cache(event) yield event if 'nextToken' in response: # If there's more pages we go through the normal pagination # logic of following nextTokens. kwargs['next_token'] = response['nextToken'] else: kwargs.pop('next_token', None) if self._event_id_cache: # However, if there's no nextToken it means we've iterated # through all the existing log events. We now need to # start polling for new events. To do this, we need # to use start time and not nextToken, because once # we've exhausted the iteration we won't see new events # from any log streams we've already iterated through. # Querying only based on start time ensures we see new # messages from all new streams. # This isn't going to be perfect. It's possible that # we miss a gap between when we finished searching through # a log stream and the new start time we're going to use # to start polling, especially at high rates of log # generation. most_recent_start_time = max(self._event_id_cache) kwargs['start_time'] = most_recent_start_time self._prune_old_cache_entries(most_recent_start_time) self._sleep(self._poll_time) def _in_cache(self, event: CWLogEvent) -> bool: return event['eventId'] in self._event_id_cache[event['timestamp']] def _add_to_cache(self, event: CWLogEvent) -> None: self._event_id_cache[event['timestamp']].add(event['eventId']) def _prune_old_cache_entries(self, timestamp: datetime) -> None: surviving_entries = self._event_id_cache[timestamp] self._event_id_cache.clear() self._event_id_cache[timestamp] = surviving_entries ================================================ FILE: chalice/package.py ================================================ # pylint: disable=too-many-lines import copy import json import os import re import six from typing import Any, Optional, Dict, List, Set, Union # noqa from typing import cast import yaml from yaml.scanner import ScannerError from yaml.nodes import Node # noqa from yaml.nodes import ScalarNode, SequenceNode, MappingNode from chalice.deploy.swagger import ( CFNSwaggerGenerator, TerraformSwaggerGenerator) from chalice.utils import ( OSUtils, UI, serialize_to_json, to_cfn_resource_name ) from chalice.awsclient import TypedAWSClient # noqa from chalice.config import Config # noqa from chalice.deploy import models from chalice.deploy.appgraph import ApplicationGraphBuilder, DependencyBuilder from chalice.deploy.deployer import BuildStage # noqa from chalice.deploy.deployer import create_build_stage def create_app_packager( config, options, package_format='cloudformation', template_format='json', merge_template=None): # type: (Config, PackageOptions, str, str, Optional[str]) -> AppPackager osutils = OSUtils() ui = UI() application_builder = ApplicationGraphBuilder() deps_builder = DependencyBuilder() post_processors = [] # type: List[TemplatePostProcessor] generator = None # type: Union[None, TemplateGenerator] template_serializer = cast(TemplateSerializer, JSONTemplateSerializer()) if package_format == 'cloudformation': build_stage = create_build_stage( osutils, ui, CFNSwaggerGenerator(), config) use_yaml_serializer = template_format == 'yaml' if merge_template is not None and \ YAMLTemplateSerializer.is_yaml_template(merge_template): # Automatically switch the serializer to yaml if they specify # a yaml template to merge, regardless of what template format # they specify. use_yaml_serializer = True if use_yaml_serializer: template_serializer = YAMLTemplateSerializer() post_processors.extend([ SAMCodeLocationPostProcessor(osutils=osutils), TemplateMergePostProcessor( osutils=osutils, merger=TemplateDeepMerger(), template_serializer=template_serializer, merge_template=merge_template)]) generator = SAMTemplateGenerator(config, options) else: build_stage = create_build_stage( osutils, ui, TerraformSwaggerGenerator(), config) generator = TerraformGenerator(config, options) post_processors.append( TerraformCodeLocationPostProcessor(osutils=osutils)) resource_builder = ResourceBuilder( application_builder, deps_builder, build_stage) return AppPackager( generator, resource_builder, CompositePostProcessor(post_processors), template_serializer, osutils) class UnsupportedFeatureError(Exception): pass class DuplicateResourceNameError(Exception): pass class PackageOptions(object): def __init__(self, client): # type: (TypedAWSClient) -> None self._client = client # type: TypedAWSClient def service_principal(self, service): # type: (str) -> str dns_suffix = self._client.endpoint_dns_suffix(service, self._client.region_name) return self._client.service_principal(service, self._client.region_name, dns_suffix) class ResourceBuilder(object): def __init__(self, application_builder, # type: ApplicationGraphBuilder deps_builder, # type: DependencyBuilder build_stage, # type: BuildStage ): # type: (...) -> None self._application_builder = application_builder self._deps_builder = deps_builder self._build_stage = build_stage def construct_resources(self, config, chalice_stage_name): # type: (Config, str) -> List[models.Model] application = self._application_builder.build( config, chalice_stage_name) resources = self._deps_builder.build_dependencies(application) self._build_stage.execute(config, resources) # Rebuild dependencies in case the build stage modified # the application graph. resources = self._deps_builder.build_dependencies(application) return resources class TemplateGenerator(object): template_file = None # type: str def __init__(self, config, options): # type: (Config, PackageOptions) -> None self._config = config self._options = options def dispatch(self, resource, template): # type: (models.Model, Dict[str, Any]) -> None name = '_generate_%s' % resource.__class__.__name__.lower() handler = getattr(self, name, self._default) handler(resource, template) def generate(self, resources): # type: (List[models.Model]) -> Dict[str, Any] raise NotImplementedError() def _generate_filebasediampolicy(self, resource, template): # type: (models.FileBasedIAMPolicy, Dict[str, Any]) -> None pass def _generate_autogeniampolicy(self, resource, template): # type: (models.AutoGenIAMPolicy, Dict[str, Any]) -> None pass def _generate_deploymentpackage(self, resource, template): # type: (models.DeploymentPackage, Dict[str, Any]) -> None pass def _generate_precreatediamrole(self, resource, template): # type: (models.PreCreatedIAMRole, Dict[str, Any]) -> None pass def _default(self, resource, template): # type: (models.Model, Dict[str, Any]) -> None raise UnsupportedFeatureError(resource) class SAMTemplateGenerator(TemplateGenerator): _BASE_TEMPLATE = { 'AWSTemplateFormatVersion': '2010-09-09', 'Transform': 'AWS::Serverless-2016-10-31', 'Outputs': {}, 'Resources': {}, } template_file = "sam" def __init__(self, config, options): # type: (Config, PackageOptions) -> None super(SAMTemplateGenerator, self).__init__(config, options) self._seen_names = set([]) # type: Set[str] self._chalice_layer = "" def generate(self, resources): # type: (List[models.Model]) -> Dict[str, Any] template = copy.deepcopy(self._BASE_TEMPLATE) self._seen_names.clear() for resource in resources: self.dispatch(resource, template) return template def _generate_lambdalayer(self, resource, template): # type: (models.LambdaLayer, Dict[str, Any]) -> None layer = to_cfn_resource_name( resource.resource_name) template['Resources'][layer] = { "Type": "AWS::Serverless::LayerVersion", "Properties": { "CompatibleRuntimes": [resource.runtime], "ContentUri": resource.deployment_package.filename, "LayerName": resource.layer_name } } self._chalice_layer = layer def _generate_scheduledevent(self, resource, template): # type: (models.ScheduledEvent, Dict[str, Any]) -> None function_cfn_name = to_cfn_resource_name( resource.lambda_function.resource_name) function_cfn = template['Resources'][function_cfn_name] event_cfn_name = self._register_cfn_resource_name( resource.resource_name) function_cfn['Properties']['Events'] = { event_cfn_name: { 'Type': 'Schedule', 'Properties': { 'Schedule': resource.schedule_expression, } } } def _generate_cloudwatchevent(self, resource, template): # type: (models.CloudWatchEvent, Dict[str, Any]) -> None function_cfn_name = to_cfn_resource_name( resource.lambda_function.resource_name) function_cfn = template['Resources'][function_cfn_name] event_cfn_name = self._register_cfn_resource_name( resource.resource_name) function_cfn['Properties']['Events'] = { event_cfn_name: { 'Type': 'CloudWatchEvent', 'Properties': { # For api calls we need serialized string form, for # SAM Templates we need datastructures. 'Pattern': json.loads(resource.event_pattern) } } } def _generate_lambdafunction(self, resource, template): # type: (models.LambdaFunction, Dict[str, Any]) -> None resources = template['Resources'] cfn_name = self._register_cfn_resource_name(resource.resource_name) lambdafunction_definition = { 'Type': 'AWS::Serverless::Function', 'Properties': { 'Runtime': resource.runtime, 'Handler': resource.handler, 'CodeUri': resource.deployment_package.filename, 'Tags': resource.tags, 'Tracing': resource.xray and 'Active' or 'PassThrough', 'Timeout': resource.timeout, 'MemorySize': resource.memory_size, }, } # type: Dict[str, Any] if resource.environment_variables: environment_config = { 'Environment': { 'Variables': resource.environment_variables } } # type: Dict[str, Dict[str, Dict[str, str]]] lambdafunction_definition['Properties'].update(environment_config) if resource.security_group_ids and resource.subnet_ids: vpc_config = { 'VpcConfig': { 'SecurityGroupIds': resource.security_group_ids, 'SubnetIds': resource.subnet_ids, } } # type: Dict[str, Dict[str, List[str]]] lambdafunction_definition['Properties'].update(vpc_config) if resource.reserved_concurrency is not None: reserved_concurrency_config = { 'ReservedConcurrentExecutions': resource.reserved_concurrency } lambdafunction_definition['Properties'].update( reserved_concurrency_config) layers = list(resource.layers) or [] # type: List[Any] if self._chalice_layer: layers.insert(0, {'Ref': self._chalice_layer}) if layers: layers_config = { 'Layers': layers } # type: Dict[str, Any] lambdafunction_definition['Properties'].update(layers_config) if resource.log_group is not None: num_days = resource.log_group.retention_in_days log_name = self._register_cfn_resource_name( resource.log_group.resource_name) log_def = { 'Type': 'AWS::Logs::LogGroup', 'Properties': { 'LogGroupName': { 'Fn::Sub': '/aws/lambda/${%s}' % cfn_name }, 'RetentionInDays': num_days } } resources[log_name] = log_def resources[cfn_name] = lambdafunction_definition self._add_iam_role(resource, resources[cfn_name]) def _add_iam_role(self, resource, cfn_resource): # type: (models.LambdaFunction, Dict[str, Any]) -> None role = resource.role if isinstance(role, models.ManagedIAMRole): cfn_resource['Properties']['Role'] = { 'Fn::GetAtt': [ to_cfn_resource_name(role.resource_name), 'Arn' ], } else: # resource is a PreCreatedIAMRole. This is the only other # subclass of IAMRole. role = cast(models.PreCreatedIAMRole, role) cfn_resource['Properties']['Role'] = role.role_arn def _generate_loggroup(self, resource, template): # type: (models.LogGroup, Dict[str, Any]) -> None # Handled in LambdaFunction generation pass def _generate_restapi(self, resource, template): # type: (models.RestAPI, Dict[str, Any]) -> None resources = template['Resources'] resources['RestAPI'] = { 'Type': 'AWS::Serverless::Api', 'Properties': { 'EndpointConfiguration': resource.endpoint_type, 'StageName': resource.api_gateway_stage, 'DefinitionBody': resource.swagger_doc, } } if resource.minimum_compression: properties = resources['RestAPI']['Properties'] properties['MinimumCompressionSize'] = \ int(resource.minimum_compression) handler_cfn_name = to_cfn_resource_name( resource.lambda_function.resource_name) api_handler = template['Resources'].pop(handler_cfn_name) template['Resources']['APIHandler'] = api_handler resources['APIHandlerInvokePermission'] = { 'Type': 'AWS::Lambda::Permission', 'Properties': { 'FunctionName': {'Ref': 'APIHandler'}, 'Action': 'lambda:InvokeFunction', 'Principal': self._options.service_principal('apigateway'), 'SourceArn': { 'Fn::Sub': [ ('arn:${AWS::Partition}:execute-api:${AWS::Region}' ':${AWS::AccountId}:${RestAPIId}/*'), {'RestAPIId': {'Ref': 'RestAPI'}}, ] }, } } for auth in resource.authorizers: auth_cfn_name = to_cfn_resource_name(auth.resource_name) resources[auth_cfn_name + 'InvokePermission'] = { 'Type': 'AWS::Lambda::Permission', 'Properties': { 'FunctionName': {'Fn::GetAtt': [auth_cfn_name, 'Arn']}, 'Action': 'lambda:InvokeFunction', 'Principal': self._options.service_principal('apigateway'), 'SourceArn': { 'Fn::Sub': [ ('arn:${AWS::Partition}:execute-api' ':${AWS::Region}:${AWS::AccountId}' ':${RestAPIId}/*'), {'RestAPIId': {'Ref': 'RestAPI'}}, ] }, } } self._add_domain_name(resource, template) self._inject_restapi_outputs(template) def _inject_restapi_outputs(self, template): # type: (Dict[str, Any]) -> None # The 'Outputs' of the SAM template are considered # part of the public API of chalice and therefore # need to maintain backwards compatibility. This # method uses the same output key names as the old # deployer. # For now, we aren't adding any of the new resources # to the Outputs section until we can figure out # a consist naming scheme. Ideally we don't use # the autogen'd names that contain the md5 suffixes. stage_name = template['Resources']['RestAPI'][ 'Properties']['StageName'] outputs = template['Outputs'] outputs['RestAPIId'] = { 'Value': {'Ref': 'RestAPI'} } outputs['APIHandlerName'] = { 'Value': {'Ref': 'APIHandler'} } outputs['APIHandlerArn'] = { 'Value': {'Fn::GetAtt': ['APIHandler', 'Arn']} } outputs['EndpointURL'] = { 'Value': { 'Fn::Sub': ( 'https://${RestAPI}.execute-api.${AWS::Region}' # The api_gateway_stage is filled in when # the template is built. '.${AWS::URLSuffix}/%s/' ) % stage_name } } def _add_websocket_lambda_integration( self, api_ref, websocket_handler, resources): # type: (Dict[str, Any], str, Dict[str, Any]) -> None resources['%sAPIIntegration' % websocket_handler] = { 'Type': 'AWS::ApiGatewayV2::Integration', 'Properties': { 'ApiId': api_ref, 'ConnectionType': 'INTERNET', 'ContentHandlingStrategy': 'CONVERT_TO_TEXT', 'IntegrationType': 'AWS_PROXY', 'IntegrationUri': { 'Fn::Sub': [ ( 'arn:${AWS::Partition}:apigateway:${AWS::Region}' ':lambda:path/2015-03-31/functions/arn' ':${AWS::Partition}:lambda:${AWS::Region}' ':${AWS::AccountId}:function' ':${WebsocketHandler}/invocations' ), {'WebsocketHandler': {'Ref': websocket_handler}} ], } } } def _add_websocket_lambda_invoke_permission( self, api_ref, websocket_handler, resources): # type: (Dict[str, str], str, Dict[str, Any]) -> None resources['%sInvokePermission' % websocket_handler] = { 'Type': 'AWS::Lambda::Permission', 'Properties': { 'FunctionName': {'Ref': websocket_handler}, 'Action': 'lambda:InvokeFunction', 'Principal': self._options.service_principal('apigateway'), 'SourceArn': { 'Fn::Sub': [ ('arn:${AWS::Partition}:execute-api' ':${AWS::Region}:${AWS::AccountId}' ':${WebsocketAPIId}/*'), {'WebsocketAPIId': api_ref}, ], }, } } def _add_websocket_lambda_integrations(self, api_ref, resources): # type: (Dict[str, str], Dict[str, Any]) -> None websocket_handlers = [ 'WebsocketConnect', 'WebsocketMessage', 'WebsocketDisconnect', ] for handler in websocket_handlers: if handler in resources: self._add_websocket_lambda_integration( api_ref, handler, resources) self._add_websocket_lambda_invoke_permission( api_ref, handler, resources) def _create_route_for_key(self, route_key, api_ref): # type: (str, Dict[str, str]) -> Dict[str, Any] integration_ref = { '$connect': 'WebsocketConnectAPIIntegration', '$disconnect': 'WebsocketDisconnectAPIIntegration', }.get(route_key, 'WebsocketMessageAPIIntegration') return { 'Type': 'AWS::ApiGatewayV2::Route', 'Properties': { 'ApiId': api_ref, 'RouteKey': route_key, 'Target': { 'Fn::Join': [ '/', [ 'integrations', {'Ref': integration_ref}, ] ] }, }, } def _generate_websocketapi(self, resource, template): # type: (models.WebsocketAPI, Dict[str, Any]) -> None resources = template['Resources'] api_ref = {'Ref': 'WebsocketAPI'} resources['WebsocketAPI'] = { 'Type': 'AWS::ApiGatewayV2::Api', 'Properties': { 'Name': resource.name, 'RouteSelectionExpression': '$request.body.action', 'ProtocolType': 'WEBSOCKET', } } self._add_websocket_lambda_integrations(api_ref, resources) route_key_names = [] for route in resource.routes: key_name = 'Websocket%sRoute' % route.replace( '$', '').replace('default', 'message').capitalize() route_key_names.append(key_name) resources[key_name] = self._create_route_for_key(route, api_ref) resources['WebsocketAPIDeployment'] = { 'Type': 'AWS::ApiGatewayV2::Deployment', 'DependsOn': route_key_names, 'Properties': { 'ApiId': api_ref, } } resources['WebsocketAPIStage'] = { 'Type': 'AWS::ApiGatewayV2::Stage', 'Properties': { 'ApiId': api_ref, 'DeploymentId': {'Ref': 'WebsocketAPIDeployment'}, 'StageName': resource.api_gateway_stage, } } self._add_websocket_domain_name(resource, template) self._inject_websocketapi_outputs(template) def _inject_websocketapi_outputs(self, template): # type: (Dict[str, Any]) -> None # The 'Outputs' of the SAM template are considered # part of the public API of chalice and therefore # need to maintain backwards compatibility. This # method uses the same output key names as the old # deployer. # For now, we aren't adding any of the new resources # to the Outputs section until we can figure out # a consist naming scheme. Ideally we don't use # the autogen'd names that contain the md5 suffixes. stage_name = template['Resources']['WebsocketAPIStage'][ 'Properties']['StageName'] outputs = template['Outputs'] resources = template['Resources'] outputs['WebsocketAPIId'] = { 'Value': {'Ref': 'WebsocketAPI'} } if 'WebsocketConnect' in resources: outputs['WebsocketConnectHandlerArn'] = { 'Value': {'Fn::GetAtt': ['WebsocketConnect', 'Arn']} } outputs['WebsocketConnectHandlerName'] = { 'Value': {'Ref': 'WebsocketConnect'} } if 'WebsocketMessage' in resources: outputs['WebsocketMessageHandlerArn'] = { 'Value': {'Fn::GetAtt': ['WebsocketMessage', 'Arn']} } outputs['WebsocketMessageHandlerName'] = { 'Value': {'Ref': 'WebsocketMessage'} } if 'WebsocketDisconnect' in resources: outputs['WebsocketDisconnectHandlerArn'] = { 'Value': {'Fn::GetAtt': ['WebsocketDisconnect', 'Arn']} } # There is not a lot of green in here. outputs['WebsocketDisconnectHandlerName'] = { 'Value': {'Ref': 'WebsocketDisconnect'} } outputs['WebsocketConnectEndpointURL'] = { 'Value': { 'Fn::Sub': ( 'wss://${WebsocketAPI}.execute-api.${AWS::Region}' # The api_gateway_stage is filled in when # the template is built. '.${AWS::URLSuffix}/%s/' ) % stage_name } } # The various IAM roles/policies are handled in the # Lambda function generation. We're creating these # noop methods to indicate we've accounted for these # resources. def _generate_managediamrole(self, resource, template): # type: (models.ManagedIAMRole, Dict[str, Any]) -> None role_cfn_name = self._register_cfn_resource_name( resource.resource_name) resource.trust_policy['Statement'][0]['Principal']['Service'] = \ self._options.service_principal('lambda') template['Resources'][role_cfn_name] = { 'Type': 'AWS::IAM::Role', 'Properties': { 'AssumeRolePolicyDocument': resource.trust_policy, 'Policies': [ {'PolicyDocument': resource.policy.document, 'PolicyName': role_cfn_name + 'Policy'}, ], } } def _generate_s3bucketnotification(self, resource, template): # type: (models.S3BucketNotification, Dict[str, Any]) -> None message = ( "Unable to package chalice apps that @app.on_s3_event decorator. " "CloudFormation does not support modifying the event " "notifications of existing buckets. " "You can deploy this app using `chalice deploy`." ) raise NotImplementedError(message) def _generate_snslambdasubscription(self, resource, template): # type: (models.SNSLambdaSubscription, Dict[str, Any]) -> None function_cfn_name = to_cfn_resource_name( resource.lambda_function.resource_name) function_cfn = template['Resources'][function_cfn_name] sns_cfn_name = self._register_cfn_resource_name( resource.resource_name) if re.match(r"^arn:aws[a-z\-]*:sns:", resource.topic): topic_arn = resource.topic # type: Union[str, Dict[str, str]] else: topic_arn = { 'Fn::Sub': ( 'arn:${AWS::Partition}:sns' ':${AWS::Region}:${AWS::AccountId}:%s' % resource.topic ) } function_cfn['Properties']['Events'] = { sns_cfn_name: { 'Type': 'SNS', 'Properties': { 'Topic': topic_arn, } } } def _generate_sqseventsource(self, resource, template): # type: (models.SQSEventSource, Dict[str, Any]) -> None function_cfn_name = to_cfn_resource_name( resource.lambda_function.resource_name) function_cfn = template['Resources'][function_cfn_name] sqs_cfn_name = self._register_cfn_resource_name( resource.resource_name) queue = '' # type: Union[str, Dict[str, Any]] if isinstance(resource.queue, models.QueueARN): queue = resource.queue.arn else: queue = { 'Fn::Sub': ('arn:${AWS::Partition}:sqs:${AWS::Region}' ':${AWS::AccountId}:%s' % resource.queue) } properties = { 'Queue': queue, 'BatchSize': resource.batch_size, 'MaximumBatchingWindowInSeconds': resource.maximum_batching_window_in_seconds } if resource.maximum_concurrency: properties["ScalingConfig"] = { "MaximumConcurrency": resource.maximum_concurrency } function_cfn['Properties']['Events'] = { sqs_cfn_name: { 'Type': 'SQS', 'Properties': properties } } def _generate_kinesiseventsource(self, resource, template): # type: (models.KinesisEventSource, Dict[str, Any]) -> None function_cfn_name = to_cfn_resource_name( resource.lambda_function.resource_name) function_cfn = template['Resources'][function_cfn_name] kinesis_cfn_name = self._register_cfn_resource_name( resource.resource_name) properties = { 'Stream': { 'Fn::Sub': ( 'arn:${AWS::Partition}:kinesis:${AWS::Region}' ':${AWS::AccountId}:stream/%s' % resource.stream ) }, 'BatchSize': resource.batch_size, 'StartingPosition': resource.starting_position, 'MaximumBatchingWindowInSeconds': resource.maximum_batching_window_in_seconds, } function_cfn['Properties']['Events'] = { kinesis_cfn_name: { 'Type': 'Kinesis', 'Properties': properties } } def _generate_dynamodbeventsource(self, resource, template): # type: (models.DynamoDBEventSource, Dict[str, Any]) -> None function_cfn_name = to_cfn_resource_name( resource.lambda_function.resource_name) function_cfn = template['Resources'][function_cfn_name] ddb_cfn_name = self._register_cfn_resource_name( resource.resource_name) properties = { 'Stream': resource.stream_arn, 'BatchSize': resource.batch_size, 'StartingPosition': resource.starting_position, 'MaximumBatchingWindowInSeconds': resource.maximum_batching_window_in_seconds, } function_cfn['Properties']['Events'] = { ddb_cfn_name: { 'Type': 'DynamoDB', 'Properties': properties } } def _generate_apimapping(self, resource, template): # type: (models.APIMapping, Dict[str, Any]) -> None pass def _generate_domainname(self, resource, template): # type: (models.DomainName, Dict[str, Any]) -> None pass def _add_domain_name(self, resource, template): # type: (models.RestAPI, Dict[str, Any]) -> None if resource.domain_name is None: return domain_name = resource.domain_name endpoint_type = resource.endpoint_type cfn_name = to_cfn_resource_name(domain_name.resource_name) properties = { 'DomainName': domain_name.domain_name, 'EndpointConfiguration': { 'Types': [endpoint_type], } } # type: Dict[str, Any] if endpoint_type == 'EDGE': properties['CertificateArn'] = domain_name.certificate_arn else: properties['RegionalCertificateArn'] = domain_name.certificate_arn if domain_name.tls_version is not None: properties['SecurityPolicy'] = domain_name.tls_version.value if domain_name.tags: properties['Tags'] = [ {'Key': key, 'Value': value} for key, value in sorted(domain_name.tags.items()) ] template['Resources'][cfn_name] = { 'Type': 'AWS::ApiGateway::DomainName', 'Properties': properties } template['Resources'][cfn_name + 'Mapping'] = { 'Type': 'AWS::ApiGateway::BasePathMapping', 'Properties': { 'DomainName': {'Ref': 'ApiGatewayCustomDomain'}, 'RestApiId': {'Ref': 'RestAPI'}, 'BasePath': domain_name.api_mapping.mount_path, 'Stage': resource.api_gateway_stage, } } def _add_websocket_domain_name(self, resource, template): # type: (models.WebsocketAPI, Dict[str, Any]) -> None if resource.domain_name is None: return domain_name = resource.domain_name cfn_name = to_cfn_resource_name(domain_name.resource_name) properties = { 'DomainName': domain_name.domain_name, 'DomainNameConfigurations': [ {'CertificateArn': domain_name.certificate_arn, 'EndpointType': 'REGIONAL'}, ] } # type: Dict[str, Any] if domain_name.tags: properties['Tags'] = domain_name.tags template['Resources'][cfn_name] = { 'Type': 'AWS::ApiGatewayV2::DomainName', 'Properties': properties, } template['Resources'][cfn_name + 'Mapping'] = { 'Type': 'AWS::ApiGatewayV2::ApiMapping', 'Properties': { 'DomainName': {'Ref': cfn_name}, 'ApiId': {'Ref': 'WebsocketAPI'}, 'ApiMappingKey': domain_name.api_mapping.mount_path, 'Stage': {'Ref': 'WebsocketAPIStage'}, } } def _register_cfn_resource_name(self, name): # type: (str) -> str cfn_name = to_cfn_resource_name(name) if cfn_name in self._seen_names: raise DuplicateResourceNameError( 'A duplicate resource name was generated for ' 'the SAM template: %s' % cfn_name, ) self._seen_names.add(cfn_name) return cfn_name class TerraformGenerator(TemplateGenerator): template_file = "chalice.tf" def __init__(self, config, options): # type: (Config, PackageOptions) -> None super(TerraformGenerator, self).__init__(config, options) self._chalice_layer = "" def generate(self, resources): # type: (List[models.Model]) -> Dict[str, Any] template = { 'resource': {}, 'locals': {}, 'terraform': { 'required_version': '>= 0.12.26, < 1.4.0', 'required_providers': { 'aws': {'version': '>= 2, < 5'}, 'null': {'version': '>= 2, < 4'} } }, 'data': { 'aws_caller_identity': {'chalice': {}}, 'aws_partition': {'chalice': {}}, 'aws_region': {'chalice': {}}, 'null_data_source': { 'chalice': { 'inputs': { 'app': self._config.app_name, 'stage': self._config.chalice_stage } } } } } for resource in resources: self.dispatch(resource, template) return template def _fref(self, lambda_function, attr='arn'): # type: (models.ManagedModel, str) -> str return '${aws_lambda_function.%s.%s}' % ( lambda_function.resource_name, attr) def _arnref(self, arn_template, **kw): # type: (str, str) -> str d = dict( partition='${data.aws_partition.chalice.partition}', region='${data.aws_region.chalice.name}', account_id='${data.aws_caller_identity.chalice.account_id}') d.update(kw) return arn_template % d def _generate_managediamrole(self, resource, template): # type: (models.ManagedIAMRole, Dict[str, Any]) -> None resource.trust_policy['Statement'][0]['Principal']['Service'] = \ self._options.service_principal('lambda') template['resource'].setdefault('aws_iam_role', {})[ resource.resource_name] = { 'name': resource.role_name, 'assume_role_policy': json.dumps(resource.trust_policy) } template['resource'].setdefault('aws_iam_role_policy', {})[ resource.resource_name] = { 'name': resource.resource_name + 'Policy', 'policy': json.dumps(resource.policy.document), 'role': '${aws_iam_role.%s.id}' % resource.resource_name, } def _add_websocket_lambda_integration( self, websocket_api_id, websocket_handler, template): # type: (str, str, Dict[str, Any]) -> None websocket_handler_function_name = \ "${aws_lambda_function.%s.function_name}" % websocket_handler resource_definition = { 'api_id': websocket_api_id, 'connection_type': 'INTERNET', 'content_handling_strategy': 'CONVERT_TO_TEXT', 'integration_type': 'AWS_PROXY', 'integration_uri': self._arnref( "arn:%(partition)s:apigateway:%(region)s" ":lambda:path/2015-03-31/functions/arn" ":%(partition)s:lambda:%(region)s" ":%(account_id)s:function" ":%(websocket_handler_function_name)s/invocations", websocket_handler_function_name=websocket_handler_function_name ) } template['resource'].setdefault( 'aws_apigatewayv2_integration', {} )['%s_api_integration' % websocket_handler] = resource_definition def _add_websocket_lambda_invoke_permission( self, websocket_api_id, websocket_handler, template): # type: (str, str, Dict[str, Any]) -> None websocket_handler_function_name = \ "${aws_lambda_function.%s.function_name}" % websocket_handler resource_definition = { "function_name": websocket_handler_function_name, "action": "lambda:InvokeFunction", "principal": self._options.service_principal('apigateway'), "source_arn": self._arnref( "arn:%(partition)s:execute-api" ":%(region)s:%(account_id)s" ":%(websocket_api_id)s/*", websocket_api_id=websocket_api_id ) } template['resource'].setdefault( 'aws_lambda_permission', {} )['%s_invoke_permission' % websocket_handler] = resource_definition def _add_websockets_route(self, websocket_api_id, route_key, template): # type: (str, str, Dict[str, Any]) -> str integration_target = { '$connect': 'integrations/${aws_apigatewayv2_integration' '.websocket_connect_api_integration.id}', '$disconnect': 'integrations/${aws_apigatewayv2_integration' '.websocket_disconnect_api_integration.id}', }.get(route_key, 'integrations/${aws_apigatewayv2_integration' '.websocket_message_api_integration.id}') route_resource_name = { '$connect': 'websocket_connect_route', '$disconnect': 'websocket_disconnect_route', '$default': 'websocket_message_route', }.get(route_key, 'message') template['resource'].setdefault( 'aws_apigatewayv2_route', {} )[route_resource_name] = { "api_id": websocket_api_id, "route_key": route_key, "target": integration_target } return route_resource_name def _add_websocket_domain_name(self, websocket_api_id, resource, template): # type: (str, models.WebsocketAPI, Dict[str, Any]) -> None if resource.domain_name is None: return domain_name = resource.domain_name ws_domain_name_definition = { "domain_name": domain_name.domain_name, "domain_name_configuration": { 'certificate_arn': domain_name.certificate_arn, 'endpoint_type': 'REGIONAL', }, } if domain_name.tags: ws_domain_name_definition['tags'] = domain_name.tags template['resource'].setdefault( 'aws_apigatewayv2_domain_name', {} )[domain_name.resource_name] = ws_domain_name_definition template['resource'].setdefault( 'aws_apigatewayv2_api_mapping', {} )[domain_name.resource_name + '_mapping'] = { "api_id": websocket_api_id, "domain_name": "${aws_apigatewayv2_domain_name.%s.id}" % domain_name.resource_name, "stage": "${aws_apigatewayv2_stage.websocket_api_stage.id}", } def _inject_websocketapi_outputs(self, websocket_api_id, template): # type: (str, Dict[str, Any]) -> None aws_lambda_functions = template['resource']['aws_lambda_function'] stage_name = \ template['resource']['aws_apigatewayv2_stage'][ 'websocket_api_stage'][ 'name'] output = template.setdefault('output', {}) output['WebsocketAPIId'] = {"value": websocket_api_id} if 'websocket_connect' in aws_lambda_functions: output['WebsocketConnectHandlerArn'] = { "value": "${aws_lambda_function.websocket_connect.arn}"} output['WebsocketConnectHandlerName'] = { "value": ( "${aws_lambda_function.websocket_connect.function_name}")} if 'websocket_message' in aws_lambda_functions: output['WebsocketMessageHandlerArn'] = { "value": "${aws_lambda_function.websocket_message.arn}"} output['WebsocketMessageHandlerName'] = { "value": ( "${aws_lambda_function.websocket_message.function_name}")} if 'websocket_disconnect' in aws_lambda_functions: output['WebsocketDisconnectHandlerArn'] = { "value": "${aws_lambda_function.websocket_disconnect.arn}"} output['WebsocketDisconnectHandlerName'] = { "value": ( "${aws_lambda_function.websocket_disconnect" ".function_name}")} output['WebsocketConnectEndpointURL'] = { "value": ( 'wss://%(websocket_api_id)s.execute-api' # The api_gateway_stage is filled in when # the template is built. '.${data.aws_region.chalice.name}' '.amazonaws.com/%(stage_name)s/' ) % { "stage_name": stage_name, "websocket_api_id": websocket_api_id } } def _generate_websocketapi(self, resource, template): # type: (models.WebsocketAPI, Dict[str, Any]) -> None ws_definition = { 'name': resource.name, 'route_selection_expression': '$request.body.action', 'protocol_type': 'WEBSOCKET', } template['resource'].setdefault('aws_apigatewayv2_api', {})[ resource.resource_name] = ws_definition websocket_api_id = "${aws_apigatewayv2_api.%s.id}" % \ resource.resource_name websocket_handlers = [ 'websocket_connect', 'websocket_message', 'websocket_disconnect', ] for handler in websocket_handlers: if handler in template['resource']['aws_lambda_function']: self._add_websocket_lambda_integration(websocket_api_id, handler, template) self._add_websocket_lambda_invoke_permission(websocket_api_id, handler, template) route_resource_names = [] for route_key in resource.routes: route_resource_name = self._add_websockets_route(websocket_api_id, route_key, template) route_resource_names.append(route_resource_name) template['resource'].setdefault( 'aws_apigatewayv2_deployment', {} )['websocket_api_deployment'] = { "api_id": websocket_api_id, "depends_on": ["aws_apigatewayv2_route.%s" % name for name in route_resource_names] } template['resource'].setdefault( 'aws_apigatewayv2_stage', {} )['websocket_api_stage'] = { "api_id": websocket_api_id, "deployment_id": ("${aws_apigatewayv2_deployment" ".websocket_api_deployment.id}"), "name": resource.api_gateway_stage } self._add_websocket_domain_name(websocket_api_id, resource, template) self._inject_websocketapi_outputs(websocket_api_id, template) def _generate_s3bucketnotification(self, resource, template): # type: (models.S3BucketNotification, Dict[str, Any]) -> None bnotify = { 'events': resource.events, 'lambda_function_arn': self._fref(resource.lambda_function) } if resource.prefix: bnotify['filter_prefix'] = resource.prefix if resource.suffix: bnotify['filter_suffix'] = resource.suffix # we use the bucket name here because we need to aggregate # all the notifications subscribers for a bucket. # Due to cyclic references to buckets created in terraform # we also try to detect and resolve. if '{aws_s3_bucket.' in resource.bucket: bucket_name = resource.bucket.split('.')[1] else: bucket_name = resource.bucket template['resource'].setdefault( 'aws_s3_bucket_notification', {}).setdefault( bucket_name + '_notify', {'bucket': resource.bucket}).setdefault( 'lambda_function', []).append(bnotify) template['resource'].setdefault('aws_lambda_permission', {})[ resource.resource_name] = { 'statement_id': resource.resource_name, 'action': 'lambda:InvokeFunction', 'function_name': self._fref(resource.lambda_function), 'principal': self._options.service_principal('s3'), 'source_account': '${data.aws_caller_identity.chalice.account_id}', 'source_arn': ('arn:${data.aws_partition.chalice.partition}:' 's3:::%s' % resource.bucket) } def _generate_sqseventsource(self, resource, template): # type: (models.SQSEventSource, Dict[str, Any]) -> None if isinstance(resource.queue, models.QueueARN): event_source_arn = resource.queue.arn else: event_source_arn = self._arnref( "arn:%(partition)s:sqs:%(region)s" ":%(account_id)s:%(queue)s", queue=resource.queue ) aws_lambda_event_source_mapping = { 'event_source_arn': event_source_arn, 'batch_size': resource.batch_size, 'maximum_batching_window_in_seconds': resource.maximum_batching_window_in_seconds, 'function_name': self._fref(resource.lambda_function), } if resource.maximum_concurrency: aws_lambda_event_source_mapping["scaling_config"] = { "maximum_concurrency": resource.maximum_concurrency } template['resource'].setdefault('aws_lambda_event_source_mapping', {})[ resource.resource_name] = aws_lambda_event_source_mapping def _generate_kinesiseventsource(self, resource, template): # type: (models.KinesisEventSource, Dict[str, Any]) -> None template['resource'].setdefault('aws_lambda_event_source_mapping', {})[ resource.resource_name] = { 'event_source_arn': self._arnref( "arn:%(partition)s:kinesis:%(region)s" ":%(account_id)s:stream/%(stream)s", stream=resource.stream), 'batch_size': resource.batch_size, 'starting_position': resource.starting_position, 'maximum_batching_window_in_seconds': resource.maximum_batching_window_in_seconds, 'function_name': self._fref(resource.lambda_function) } def _generate_dynamodbeventsource(self, resource, template): # type: (models.DynamoDBEventSource, Dict[str, Any]) -> None template['resource'].setdefault('aws_lambda_event_source_mapping', {})[ resource.resource_name] = { 'event_source_arn': resource.stream_arn, 'batch_size': resource.batch_size, 'starting_position': resource.starting_position, 'maximum_batching_window_in_seconds': resource.maximum_batching_window_in_seconds, 'function_name': self._fref(resource.lambda_function), } def _generate_snslambdasubscription(self, resource, template): # type: (models.SNSLambdaSubscription, Dict[str, Any]) -> None if resource.topic.startswith('arn:aws'): topic_arn = resource.topic else: topic_arn = self._arnref( 'arn:%(partition)s:sns:%(region)s:%(account_id)s:%(topic)s', topic=resource.topic) template['resource'].setdefault('aws_sns_topic_subscription', {})[ resource.resource_name] = { 'topic_arn': topic_arn, 'protocol': 'lambda', 'endpoint': self._fref(resource.lambda_function) } template['resource'].setdefault('aws_lambda_permission', {})[ resource.resource_name] = { 'function_name': self._fref(resource.lambda_function), 'action': 'lambda:InvokeFunction', 'principal': self._options.service_principal('sns'), 'source_arn': topic_arn } def _generate_cloudwatchevent(self, resource, template): # type: (models.CloudWatchEvent, Dict[str, Any]) -> None template['resource'].setdefault( 'aws_cloudwatch_event_rule', {})[ resource.resource_name] = { 'name': resource.resource_name, 'event_pattern': resource.event_pattern } self._cwe_helper(resource, template) def _generate_scheduledevent(self, resource, template): # type: (models.ScheduledEvent, Dict[str, Any]) -> None template['resource'].setdefault( 'aws_cloudwatch_event_rule', {})[ resource.resource_name] = { 'name': resource.resource_name, 'schedule_expression': resource.schedule_expression, 'description': resource.rule_description, } self._cwe_helper(resource, template) def _cwe_helper(self, resource, template): # type: (models.CloudWatchEventBase, Dict[str, Any]) -> None template['resource'].setdefault( 'aws_cloudwatch_event_target', {})[ resource.resource_name] = { 'rule': '${aws_cloudwatch_event_rule.%s.name}' % ( resource.resource_name), 'target_id': resource.resource_name, 'arn': self._fref(resource.lambda_function) } template['resource'].setdefault( 'aws_lambda_permission', {})[ resource.resource_name] = { 'function_name': self._fref(resource.lambda_function), 'action': 'lambda:InvokeFunction', 'principal': self._options.service_principal('events'), 'source_arn': "${aws_cloudwatch_event_rule.%s.arn}" % ( resource.resource_name) } def _generate_lambdalayer(self, resource, template): # type: (models.LambdaLayer, Dict[str, Any]) -> None template['resource'].setdefault( "aws_lambda_layer_version", {})[ resource.resource_name] = { 'layer_name': resource.layer_name, 'compatible_runtimes': [resource.runtime], 'filename': resource.deployment_package.filename, } self._chalice_layer = resource.resource_name def _generate_lambdafunction(self, resource, template): # type: (models.LambdaFunction, Dict[str, Any]) -> None func_definition = { 'function_name': resource.function_name, 'runtime': resource.runtime, 'handler': resource.handler, 'memory_size': resource.memory_size, 'tags': resource.tags, 'timeout': resource.timeout, 'source_code_hash': '${filebase64sha256("%s")}' % ( resource.deployment_package.filename), 'filename': resource.deployment_package.filename } # type: Dict[str, Any] if resource.security_group_ids and resource.subnet_ids: func_definition['vpc_config'] = { 'subnet_ids': resource.subnet_ids, 'security_group_ids': resource.security_group_ids } if resource.reserved_concurrency is not None: func_definition['reserved_concurrent_executions'] = ( resource.reserved_concurrency ) if resource.environment_variables: func_definition['environment'] = { 'variables': resource.environment_variables } if resource.xray: func_definition['tracing_config'] = { 'mode': 'Active' } if self._chalice_layer: func_definition['layers'] = [ '${aws_lambda_layer_version.%s.arn}' % self._chalice_layer ] if resource.layers: func_definition.setdefault('layers', []).extend( list(resource.layers)) if isinstance(resource.role, models.ManagedIAMRole): func_definition['role'] = '${aws_iam_role.%s.arn}' % ( resource.role.resource_name) else: # resource is a PreCreatedIAMRole. role = cast(models.PreCreatedIAMRole, resource.role) func_definition['role'] = role.role_arn if resource.log_group is not None: log_group = resource.log_group num_days = log_group.retention_in_days template['resource'].setdefault('aws_cloudwatch_log_group', {})[ log_group.resource_name] = { 'name': log_group.resource_name, 'retention_in_days': num_days, } template['resource'].setdefault('aws_lambda_function', {})[ resource.resource_name] = func_definition def _generate_log_group(self, resource, remplate): # type: (models.LogGroup, Dict[str, Any]) -> None # Handled in LambdaFunction generation pass def _generate_restapi(self, resource, template): # type: (models.RestAPI, Dict[str, Any]) -> None # typechecker happiness swagger_doc = cast(Dict, resource.swagger_doc) template['locals']['chalice_api_swagger'] = json.dumps( swagger_doc) template['resource'].setdefault('aws_api_gateway_rest_api', {})[ resource.resource_name] = { 'body': '${local.chalice_api_swagger}', # Terraform will diff explicitly configured attributes # to the current state of the resource. Attributes configured # via swagger on the REST api need to be duplicated here, else # terraform will set them back to empty. 'name': swagger_doc['info']['title'], 'binary_media_types': swagger_doc[ 'x-amazon-apigateway-binary-media-types'], 'endpoint_configuration': {'types': [resource.endpoint_type]} } if 'x-amazon-apigateway-policy' in swagger_doc: template['resource'][ 'aws_api_gateway_rest_api'][ resource.resource_name]['policy'] = json.dumps( swagger_doc['x-amazon-apigateway-policy']) if resource.minimum_compression.isdigit(): template['resource'][ 'aws_api_gateway_rest_api'][ resource.resource_name][ 'minimum_compression_size'] = int( resource.minimum_compression) template['resource'].setdefault('aws_api_gateway_deployment', {})[ resource.resource_name] = { 'stage_name': resource.api_gateway_stage, # Ensure that the deployment gets redeployed if we update # the swagger description for the api by using its checksum # in the stage description. 'stage_description': ( "${md5(local.chalice_api_swagger)}"), 'rest_api_id': '${aws_api_gateway_rest_api.%s.id}' % ( resource.resource_name), 'lifecycle': {'create_before_destroy': True} } template['resource'].setdefault('aws_lambda_permission', {})[ resource.resource_name + '_invoke'] = { 'function_name': self._fref(resource.lambda_function), 'action': 'lambda:InvokeFunction', 'principal': self._options.service_principal('apigateway'), 'source_arn': "${aws_api_gateway_rest_api.%s.execution_arn}/*" % ( resource.resource_name) } template.setdefault('output', {})[ 'EndpointURL'] = { 'value': '${aws_api_gateway_deployment.%s.invoke_url}' % ( resource.resource_name) } template.setdefault('output', {})[ 'RestAPIId'] = { 'value': '${aws_api_gateway_rest_api.%s.id}' % ( resource.resource_name) } for auth in resource.authorizers: template['resource']['aws_lambda_permission'][ auth.resource_name + '_invoke'] = { 'function_name': self._fref(auth), 'action': 'lambda:InvokeFunction', 'principal': self._options.service_principal('apigateway'), 'source_arn': ( "${aws_api_gateway_rest_api.%s.execution_arn}" % ( resource.resource_name) + "/*" ) } self._add_domain_name(resource, template) def _add_domain_name(self, resource, template): # type: (models.RestAPI, Dict[str, Any]) -> None if resource.domain_name is None: return domain_name = resource.domain_name endpoint_type = resource.endpoint_type properties = { 'domain_name': domain_name.domain_name, 'endpoint_configuration': {'types': [endpoint_type]}, } if endpoint_type == 'EDGE': properties['certificate_arn'] = domain_name.certificate_arn else: properties[ 'regional_certificate_arn'] = domain_name.certificate_arn if domain_name.tls_version is not None: properties['security_policy'] = domain_name.tls_version.value if domain_name.tags: properties['tags'] = domain_name.tags template['resource']['aws_api_gateway_domain_name'] = { domain_name.resource_name: properties } template['resource']['aws_api_gateway_base_path_mapping'] = { domain_name.resource_name + '_mapping': { 'stage_name': resource.api_gateway_stage, 'domain_name': domain_name.domain_name, 'api_id': '${aws_api_gateway_rest_api.%s.id}' % ( resource.resource_name) } } self._add_domain_name_outputs(domain_name.resource_name, endpoint_type, template) def _add_domain_name_outputs(self, domain_resource_name, endpoint_type, template): # type: (str, str, Dict[str, Any]) -> None base = ( 'aws_api_gateway_domain_name.%s' % domain_resource_name ) if endpoint_type == 'EDGE': alias_domain_name = '${%s.cloudfront_domain_name}' % base hosted_zone_id = '${%s.cloudfront_zone_id}' % base else: alias_domain_name = '${%s.regional_domain_name}' % base hosted_zone_id = '${%s.regional_zone_id}' % base template.setdefault('output', {})['AliasDomainName'] = { 'value': alias_domain_name } template.setdefault('output', {})['HostedZoneId'] = { 'value': hosted_zone_id } def _generate_apimapping(self, resource, template): # type: (models.APIMapping, Dict[str, Any]) -> None pass def _generate_domainname(self, resource, template): # type: (models.DomainName, Dict[str, Any]) -> None pass class AppPackager(object): def __init__(self, templater, # type: TemplateGenerator resource_builder, # type: ResourceBuilder post_processor, # type: TemplatePostProcessor template_serializer, # type: TemplateSerializer osutils, # type: OSUtils ): # type: (...) -> None self._templater = templater self._resource_builder = resource_builder self._template_post_processor = post_processor self._template_serializer = template_serializer self._osutils = osutils def _to_json(self, doc): # type: (Any) -> str return serialize_to_json(doc) def _to_yaml(self, doc): # type: (Any) -> str return yaml.dump(doc, allow_unicode=True) def package_app(self, config, outdir, chalice_stage_name): # type: (Config, str, str) -> None # Deployment package resources = self._resource_builder.construct_resources( config, chalice_stage_name) template = self._templater.generate(resources) if not self._osutils.directory_exists(outdir): self._osutils.makedirs(outdir) self._template_post_processor.process( template, config, outdir, chalice_stage_name) contents = self._template_serializer.serialize_template(template) extension = self._template_serializer.file_extension filename = os.path.join( outdir, self._templater.template_file) + '.' + extension self._osutils.set_file_contents( filename=filename, contents=contents, binary=False ) class TemplatePostProcessor(object): def __init__(self, osutils): # type: (OSUtils) -> None self._osutils = osutils def process(self, template, config, outdir, chalice_stage_name): # type: (Dict[str, Any], Config, str, str) -> None raise NotImplementedError() class SAMCodeLocationPostProcessor(TemplatePostProcessor): def process(self, template, config, outdir, chalice_stage_name): # type: (Dict[str, Any], Config, str, str) -> None self._fixup_deployment_package(template, outdir) def _fixup_deployment_package(self, template, outdir): # type: (Dict[str, Any], str) -> None # NOTE: This isn't my ideal way to do this. I'd like # to move this into the build step where something # copies the DeploymentPackage.filename over to the # outdir. That would require plumbing through user # provided params such as "outdir" into the build stage # somehow, which isn't currently possible. copied = False for resource in template['Resources'].values(): if resource['Type'] == 'AWS::Serverless::Function': original_location = resource['Properties']['CodeUri'] new_location = os.path.join(outdir, 'deployment.zip') if not copied: self._osutils.copy(original_location, new_location) copied = True resource['Properties']['CodeUri'] = './deployment.zip' elif resource['Type'] == 'AWS::Serverless::LayerVersion': original_location = resource['Properties']['ContentUri'] new_location = os.path.join(outdir, 'layer-deployment.zip') self._osutils.copy(original_location, new_location) resource['Properties']['ContentUri'] = './layer-deployment.zip' class TerraformCodeLocationPostProcessor(TemplatePostProcessor): def process(self, template, config, outdir, chalice_stage_name): # type: (Dict[str, Any], Config, str, str) -> None copied = False resources = template['resource'] for r in resources.get('aws_lambda_function', {}).values(): if not copied: asset_path = os.path.join(outdir, 'deployment.zip') self._osutils.copy(r['filename'], asset_path) copied = True r['filename'] = "${path.module}/deployment.zip" r['source_code_hash'] = \ '${filebase64sha256("${path.module}/deployment.zip")}' copied = False for r in resources.get('aws_lambda_layer_version', {}).values(): if not copied: asset_path = os.path.join(outdir, 'layer-deployment.zip') self._osutils.copy(r['filename'], asset_path) copied = True r['filename'] = "${path.module}/layer-deployment.zip" r['source_code_hash'] = \ '${filebase64sha256("${path.module}/layer-deployment.zip")}' class TemplateMergePostProcessor(TemplatePostProcessor): def __init__(self, osutils, # type: OSUtils merger, # type: TemplateMerger template_serializer, # type: TemplateSerializer merge_template=None, # type: Optional[str] ): # type: (...) -> None super(TemplateMergePostProcessor, self).__init__(osutils) self._merger = merger self._template_serializer = template_serializer self._merge_template = merge_template def process(self, template, config, outdir, chalice_stage_name): # type: (Dict[str, Any], Config, str, str) -> None if self._merge_template is None: return loaded_template = self._load_template_to_merge() merged = self._merger.merge(loaded_template, template) template.clear() template.update(merged) def _load_template_to_merge(self): # type: () -> Dict[str, Any] template_name = cast(str, self._merge_template) filepath = os.path.abspath(template_name) if not self._osutils.file_exists(filepath): raise RuntimeError('Cannot find template file: %s' % filepath) template_data = self._osutils.get_file_contents(filepath, binary=False) loaded_template = self._template_serializer.load_template( template_data, filepath) return loaded_template class CompositePostProcessor(TemplatePostProcessor): def __init__(self, processors): # type: (List[TemplatePostProcessor]) -> None self._processors = processors def process(self, template, config, outdir, chalice_stage_name): # type: (Dict[str, Any], Config, str, str) -> None for processor in self._processors: processor.process(template, config, outdir, chalice_stage_name) class TemplateMerger(object): def merge(self, file_template, chalice_template): # type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any] raise NotImplementedError('merge') class TemplateDeepMerger(TemplateMerger): def merge(self, file_template, chalice_template): # type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any] return self._merge(file_template, chalice_template) def _merge(self, file_template, chalice_template): # type: (Any, Any) -> Any if isinstance(file_template, dict) and \ isinstance(chalice_template, dict): return self._merge_dict(file_template, chalice_template) return file_template def _merge_dict(self, file_template, chalice_template): # type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any] merged = chalice_template.copy() for key, value in file_template.items(): merged[key] = self._merge(value, chalice_template.get(key)) return merged class TemplateSerializer(object): file_extension = '' def load_template(self, file_contents, filename=''): # type: (str, str) -> Dict[str, Any] raise NotImplementedError("load_template") def serialize_template(self, contents): # type: (Dict[str, Any]) -> str raise NotImplementedError("serialize_template") class JSONTemplateSerializer(TemplateSerializer): file_extension = 'json' def serialize_template(self, contents): # type: (Dict[str, Any]) -> str return serialize_to_json(contents) def load_template(self, file_contents, filename=''): # type: (str, str) -> Dict[str, Any] try: return json.loads(file_contents) except ValueError: raise RuntimeError( 'Expected %s to be valid JSON template.' % filename) class YAMLTemplateSerializer(TemplateSerializer): file_extension = 'yaml' @classmethod def is_yaml_template(cls, template_name): # type: (str) -> bool file_extension = os.path.splitext(template_name)[1].lower() return file_extension in [".yaml", ".yml"] def serialize_template(self, contents): # type: (Dict[str, Any]) -> str return yaml.safe_dump(contents, allow_unicode=True) def load_template(self, file_contents, filename=''): # type: (str, str) -> Dict[str, Any] yaml.SafeLoader.add_multi_constructor( tag_prefix='!', multi_constructor=self._custom_sam_instrinsics) try: return yaml.load( file_contents, Loader=yaml.SafeLoader, ) except ScannerError: raise RuntimeError( 'Expected %s to be valid YAML template.' % filename) def _custom_sam_instrinsics(self, loader, tag_prefix, node): # type: (yaml.SafeLoader, str, Node) -> Dict[str, Any] tag = node.tag[1:] if tag not in ['Ref', 'Condition']: tag = 'Fn::%s' % tag value = self._get_value(loader, node) return {tag: value} def _get_value(self, loader, node): # type: (yaml.SafeLoader, Node) -> Any if node.tag[1:] == 'GetAtt' and isinstance(node.value, six.string_types): return node.value.split('.', 1) elif isinstance(node, ScalarNode): return loader.construct_scalar(node) elif isinstance(node, SequenceNode): return loader.construct_sequence(node) elif isinstance(node, MappingNode): return loader.construct_mapping(node) raise ValueError("Unknown YAML node: %s" % node) ================================================ FILE: chalice/pipeline.py ================================================ import copy import re from typing import List, Dict, Any, Optional, Callable # noqa import yaml from chalice.config import Config # noqa from chalice import constants from chalice import __version__ as chalice_version def create_buildspec_v2(pipeline_params): # type: (PipelineParameters) -> Dict[str, Any] install_commands = [ "pip install 'chalice%s'" % pipeline_params.chalice_version_range, "pip install -r requirements.txt", ] build_commands = [ "chalice package /tmp/packaged", ("aws cloudformation package --template-file /tmp/packaged/sam.json " "--s3-bucket ${APP_S3_BUCKET} " "--output-template-file transformed.yaml") ] buildspec = { "version": "0.2", "phases": { "install": { "commands": install_commands, "runtime-versions": { "python": pipeline_params.py_major_minor, } }, "build": { "commands": build_commands, } }, "artifacts": { "type": "zip", "files": [ "transformed.yaml" ] } } return buildspec def create_buildspec_legacy(pipeline_params): # type: (PipelineParameters) -> Dict[str, Any] install_commands = [ 'sudo pip install --upgrade awscli', 'aws --version', "sudo pip install 'chalice%s'" % pipeline_params.chalice_version_range, 'sudo pip install -r requirements.txt', 'chalice package /tmp/packaged', ('aws cloudformation package ' '--template-file /tmp/packaged/sam.json' ' --s3-bucket ${APP_S3_BUCKET} ' '--output-template-file transformed.yaml'), ] buildspec = { 'version': '0.1', 'phases': { 'install': { 'commands': install_commands, } }, 'artifacts': { 'type': 'zip', 'files': ['transformed.yaml'] } } return buildspec class InvalidCodeBuildPythonVersion(Exception): def __init__(self, version, msg=None): # type: (str, Optional[str]) -> None if msg is None: msg = 'CodeBuild does not yet support python version %s.' % version super(InvalidCodeBuildPythonVersion, self).__init__(msg) class PipelineParameters(object): _PYTHON_VERSION = re.compile('python(.+)') def __init__(self, app_name, lambda_python_version, codebuild_image=None, code_source='codecommit', chalice_version_range=None, pipeline_version='v1'): # type: (str, str, Optional[str], str, Optional[str], str) -> None self.app_name = app_name # lambda_python_version is what matches lambda, e.g. 'python3.9'. self.lambda_python_version = lambda_python_version # py_major_minor is just the version string, e.g. '3.9' self.py_major_minor = self._extract_version(lambda_python_version) self.codebuild_image = codebuild_image self.code_source = code_source if chalice_version_range is None: chalice_version_range = self._lock_to_minor_version() self.chalice_version_range = chalice_version_range self.pipeline_version = pipeline_version def _extract_version(self, lambda_python_version): # type: (str) -> str matched = self._PYTHON_VERSION.match(lambda_python_version) if matched is None: raise InvalidCodeBuildPythonVersion(lambda_python_version) return matched.group(1) def _lock_to_minor_version(self): # type: () -> str parts = [int(p) for p in chalice_version.split('.')] min_version = '%s.%s.%s' % (parts[0], parts[1], 0) max_version = '%s.%s.%s' % (parts[0], parts[1] + 1, 0) return '>=%s,<%s' % (min_version, max_version) class BasePipelineTemplate(object): def create_template(self, pipeline_params): # type: (PipelineParameters) -> Dict[str, Any] raise NotImplementedError("create_template") class CreatePipelineTemplateV2(BasePipelineTemplate): _BASE_TEMPLATE = { "AWSTemplateFormatVersion": "2010-09-09", "Parameters": { "ApplicationName": { "Default": "ChaliceApp", "Type": "String", "Description": "Enter the name of your application" }, "CodeBuildImage": { "Default": "aws/codebuild/amazonlinux2-x86_64-standard:3.0", "Type": "String", "Description": "Name of codebuild image to use." } }, "Resources": {}, "Outputs": {}, } def create_template(self, pipeline_params): # type: (PipelineParameters) -> Dict[str, Any] self._validate_python_version(pipeline_params.py_major_minor) t = copy.deepcopy(self._BASE_TEMPLATE) # type: Dict[str, Any] params = t['Parameters'] params['ApplicationName']['Default'] = pipeline_params.app_name resources = [] # type: List[BaseResource] if pipeline_params.code_source == 'github': resources.append(GithubSource()) else: resources.append(CodeCommitSourceRepository()) resources.extend([CodeBuild(create_buildspec_v2), CodePipeline()]) for resource in resources: resource.add_to_template(t, pipeline_params) return t def _validate_python_version(self, python_version): # type: (str) -> None major, minor = [ int(v) for v in python_version.split('.') ] if (major, minor) < (3, 9): raise InvalidCodeBuildPythonVersion( python_version, 'This CodeBuild image does not support python version: %s' % ( python_version ) ) class CreatePipelineTemplateLegacy(BasePipelineTemplate): _CODEBUILD_IMAGE = { 'python2.7': 'python:2.7.12', 'python3.6': 'python:3.6.5', 'python3.7': 'python:3.7.1', } _BASE_TEMPLATE = { "AWSTemplateFormatVersion": "2010-09-09", "Parameters": { "ApplicationName": { "Default": "ChaliceApp", "Type": "String", "Description": "Enter the name of your application" }, "CodeBuildImage": { "Default": "aws/codebuild/python:2.7.12", "Type": "String", "Description": "Name of codebuild image to use." } }, "Resources": {}, "Outputs": {}, } def create_template(self, pipeline_params): # type: (PipelineParameters) -> Dict[str, Any] t = copy.deepcopy(self._BASE_TEMPLATE) # type: Dict[str, Any] params = t['Parameters'] params['ApplicationName']['Default'] = pipeline_params.app_name params['CodeBuildImage']['Default'] = self._get_codebuild_image( pipeline_params) resources = [] # type: List[BaseResource] if pipeline_params.code_source == 'github': resources.append(GithubSource()) else: resources.append(CodeCommitSourceRepository()) resources.extend([CodeBuild(create_buildspec_legacy), CodePipeline()]) for resource in resources: resource.add_to_template(t, pipeline_params) return t def _get_codebuild_image(self, params): # type: (PipelineParameters) -> str if params.codebuild_image is not None: return params.codebuild_image try: image_suffix = self._CODEBUILD_IMAGE[params.lambda_python_version] return 'aws/codebuild/%s' % image_suffix except KeyError as e: raise InvalidCodeBuildPythonVersion(str(e)) class BaseResource(object): def add_to_template(self, template, pipeline_params): # type: (Dict[str, Any], PipelineParameters) -> None raise NotImplementedError("add_to_template") class CodeCommitSourceRepository(BaseResource): def add_to_template(self, template, pipeline_params): # type: (Dict[str, Any], PipelineParameters) -> None resources = template.setdefault('Resources', {}) resources['SourceRepository'] = { "Type": "AWS::CodeCommit::Repository", "Properties": { "RepositoryName": { "Ref": "ApplicationName" }, "RepositoryDescription": { "Fn::Sub": "Source code for ${ApplicationName}" } } } template.setdefault('Outputs', {})['SourceRepoURL'] = { "Value": { "Fn::GetAtt": "SourceRepository.CloneUrlHttp" } } class GithubSource(BaseResource): def add_to_template(self, template, pipeline_params): # type: (Dict[str, Any], PipelineParameters) -> None # For the github source, we don't create a github repo, # we just wire it up in the code pipeline. The # only thing we add to the template are parameters # we reference in other resources later. p = template.setdefault('Parameters', {}) p['GithubOwner'] = { 'Type': 'String', 'Description': 'The github owner or org name of the repository.', } p['GithubRepoName'] = { 'Type': 'String', 'Description': 'The name of the github repository.', } if pipeline_params.pipeline_version == 'v1': p['GithubPersonalToken'] = { 'Type': 'String', 'Description': 'Personal access token for the github repo.', 'NoEcho': True, } else: p['GithubRepoSecretId'] = { 'Type': 'String', 'Default': 'GithubRepoAccess', 'Description': ( 'The name/ID of the SecretsManager secret that ' 'contains the personal access token for the github repo.' ) } p['GithubRepoSecretJSONKey'] = { 'Type': 'String', 'Default': 'OAuthToken', 'Description': ( 'The name of the JSON key in the SecretsManager secret ' 'that contains the personal access token for the ' 'github repo.' ) } class CodeBuild(BaseResource): def __init__(self, buildspec_generator=create_buildspec_legacy): # type: (Callable[[PipelineParameters], Dict[str, Any]]) -> None self._buildspec_generator = buildspec_generator def add_to_template(self, template, pipeline_params): # type: (Dict[str, Any], PipelineParameters) -> None resources = template.setdefault('Resources', {}) outputs = template.setdefault('Outputs', {}) # Used to store the application source when the SAM # template is packaged. self._add_s3_bucket(resources, outputs) self._add_codebuild_role(resources, outputs) self._add_codebuild_policy(resources) self._add_package_build(resources, pipeline_params) def _add_package_build(self, resources, pipeline_params): # type: (Dict[str, Any], PipelineParameters) -> None resources['AppPackageBuild'] = { "Type": "AWS::CodeBuild::Project", "Properties": { "Artifacts": { "Type": "CODEPIPELINE" }, "Environment": { "ComputeType": "BUILD_GENERAL1_SMALL", "Image": { "Ref": "CodeBuildImage" }, "Type": "LINUX_CONTAINER", "EnvironmentVariables": [ { "Name": "APP_S3_BUCKET", "Value": { "Ref": "ApplicationBucket" } } ] }, "Name": { "Fn::Sub": "${ApplicationName}Build" }, "ServiceRole": { "Fn::GetAtt": "CodeBuildRole.Arn" }, "Source": { "Type": "CODEPIPELINE", "BuildSpec": yaml.dump( self._buildspec_generator(pipeline_params), ), } } } def _add_s3_bucket(self, resources, outputs): # type: (Dict[str, Any], Dict[str, Any]) -> None resources['ApplicationBucket'] = {'Type': 'AWS::S3::Bucket'} outputs['S3ApplicationBucket'] = { 'Value': {'Ref': 'ApplicationBucket'} } def _add_codebuild_role(self, resources, outputs): # type: (Dict[str, Any], Dict[str, Any]) -> None resources['CodeBuildRole'] = { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { "Version": "2012-10-17", "Statement": [ { "Action": [ "sts:AssumeRole" ], "Effect": "Allow", "Principal": { "Service": [ {'Fn::Sub': 'codebuild.${AWS::URLSuffix}'} ] } } ] } } } outputs['CodeBuildRoleArn'] = { "Value": { "Fn::GetAtt": "CodeBuildRole.Arn" } } def _add_codebuild_policy(self, resources): # type: (Dict[str, Any]) -> None resources['CodeBuildPolicy'] = { "Type": "AWS::IAM::Policy", "Properties": { "PolicyName": "CodeBuildPolicy", "PolicyDocument": constants.CODEBUILD_POLICY, "Roles": [ { "Ref": "CodeBuildRole" } ] } } class CodePipeline(BaseResource): def add_to_template(self, template, pipeline_params): # type: (Dict[str, Any], PipelineParameters) -> None resources = template.setdefault('Resources', {}) outputs = template.setdefault('Outputs', {}) self._add_pipeline(resources, pipeline_params) self._add_bucket_store(resources, outputs) self._add_codepipeline_role(resources, outputs) self._add_cfn_deploy_role(resources, outputs) def _add_cfn_deploy_role(self, resources, outputs): # type: (Dict[str, Any], Dict[str, Any]) -> None outputs['CFNDeployRoleArn'] = { 'Value': {'Fn::GetAtt': 'CFNDeployRole.Arn'} } resources['CFNDeployRole'] = { 'Type': 'AWS::IAM::Role', 'Properties': { "Policies": [ { "PolicyName": "DeployAccess", "PolicyDocument": { "Version": "2012-10-17", "Statement": [ { "Action": "*", "Resource": "*", "Effect": "Allow" } ] } } ], "AssumeRolePolicyDocument": { "Version": "2012-10-17", "Statement": [ { "Action": [ "sts:AssumeRole" ], "Effect": "Allow", "Principal": { "Service": [ {'Fn::Sub': 'cloudformation.${AWS::URLSuffix}'} ] } } ] } } } def _add_pipeline(self, resources, pipeline_params): # type: (Dict[str, Any], PipelineParameters) -> None properties = { 'Name': { 'Fn::Sub': '${ApplicationName}Pipeline' }, 'ArtifactStore': { 'Type': 'S3', 'Location': {'Ref': 'ArtifactBucketStore'}, }, 'RoleArn': { 'Fn::GetAtt': 'CodePipelineRole.Arn', }, 'Stages': self._create_pipeline_stages(pipeline_params), } resources['AppPipeline'] = { 'Type': 'AWS::CodePipeline::Pipeline', 'Properties': properties } def _create_pipeline_stages(self, pipeline_params): # type: (PipelineParameters) -> List[Dict[str, Any]] # The goal is to eventually allow a user to configure # the various stages they want created. For now, there's # a fixed list. stages = [] source = self._create_source_stage(pipeline_params) if source: stages.append(source) stages.extend([self._create_build_stage(), self._create_beta_stage()]) return stages def _code_commit_source(self): # type: () -> Dict[str, Any] return { "Name": "Source", "Actions": [ { "ActionTypeId": { "Category": "Source", "Owner": "AWS", "Version": 1, "Provider": "CodeCommit" }, "Configuration": { "BranchName": "master", "RepositoryName": { "Fn::GetAtt": "SourceRepository.Name" } }, "OutputArtifacts": [ { "Name": "SourceRepo" } ], "RunOrder": 1, "Name": "Source" } ] } def _create_source_stage(self, pipeline_params): # type: (PipelineParameters) -> Dict[str, Any] if pipeline_params.code_source == 'codecommit': return self._code_commit_source() return self._github_source(pipeline_params.pipeline_version) def _github_source(self, pipeline_version): # type: (str) -> Dict[str, Any] oauth_token = {'Ref': 'GithubPersonalToken'} # type: Dict[str, Any] if pipeline_version == 'v2': oauth_token = { "Fn::Join": [ "", ["{{resolve:secretsmanager:", {"Ref": "GithubRepoSecretId"}, ":SecretString:", {"Ref": "GithubRepoSecretJSONKey"}, "}}"] ] } return { 'Name': 'Source', 'Actions': [{ "Name": "Source", "ActionTypeId": { "Category": "Source", "Owner": "ThirdParty", "Version": "1", "Provider": "GitHub" }, 'RunOrder': 1, 'OutputArtifacts': [{ 'Name': 'SourceRepo', }], 'Configuration': { 'Owner': {'Ref': 'GithubOwner'}, 'Repo': {'Ref': 'GithubRepoName'}, 'OAuthToken': oauth_token, 'Branch': 'master', 'PollForSourceChanges': True, } }], } def _create_build_stage(self): # type: () -> Dict[str, Any] return { "Name": "Build", "Actions": [ { "InputArtifacts": [ { "Name": "SourceRepo" } ], "Name": "CodeBuild", "ActionTypeId": { "Category": "Build", "Owner": "AWS", "Version": "1", "Provider": "CodeBuild" }, "OutputArtifacts": [ { "Name": "CompiledCFNTemplate" } ], "Configuration": { "ProjectName": { "Ref": "AppPackageBuild" } }, "RunOrder": 1 } ] } def _create_beta_stage(self): # type: () -> Dict[str, Any] return { "Name": "Beta", "Actions": [ { "ActionTypeId": { "Category": "Deploy", "Owner": "AWS", "Version": "1", "Provider": "CloudFormation" }, "InputArtifacts": [ { "Name": "CompiledCFNTemplate" } ], "Name": "CreateBetaChangeSet", "Configuration": { "ActionMode": "CHANGE_SET_REPLACE", "ChangeSetName": { "Fn::Sub": "${ApplicationName}ChangeSet" }, "RoleArn": { "Fn::GetAtt": "CFNDeployRole.Arn" }, "Capabilities": "CAPABILITY_IAM", "StackName": { "Fn::Sub": "${ApplicationName}BetaStack" }, "TemplatePath": "CompiledCFNTemplate::transformed.yaml" }, "RunOrder": 1 }, { "RunOrder": 2, "ActionTypeId": { "Category": "Deploy", "Owner": "AWS", "Version": "1", "Provider": "CloudFormation" }, "Configuration": { "StackName": { "Fn::Sub": "${ApplicationName}BetaStack" }, "ActionMode": "CHANGE_SET_EXECUTE", "ChangeSetName": { "Fn::Sub": "${ApplicationName}ChangeSet" }, "OutputFileName": "StackOutputs.json" }, "Name": "ExecuteChangeSet", "OutputArtifacts": [ { "Name": "AppDeploymentValues" } ] } ] } def _add_bucket_store(self, resources, outputs): # type: (Dict[str, Any], Dict[str, Any]) -> None resources['ArtifactBucketStore'] = { 'Type': 'AWS::S3::Bucket', 'Properties': { 'VersioningConfiguration': { 'Status': 'Enabled' } } } outputs['S3PipelineBucket'] = { 'Value': {'Ref': 'ArtifactBucketStore'} } def _add_codepipeline_role(self, resources, outputs): # type: (Dict[str, Any], Dict[str, Any]) -> None outputs['CodePipelineRoleArn'] = { 'Value': {'Fn::GetAtt': 'CodePipelineRole.Arn'} } resources['CodePipelineRole'] = { "Type": "AWS::IAM::Role", "Properties": { "Policies": [ { "PolicyName": "DefaultPolicy", "PolicyDocument": constants.CODEPIPELINE_POLICY, } ], "AssumeRolePolicyDocument": { "Version": "2012-10-17", "Statement": [ { "Action": [ "sts:AssumeRole" ], "Effect": "Allow", "Principal": { "Service": [ {'Fn::Sub': 'codepipeline' '.${AWS::URLSuffix}'} ] } } ] } } } class BuildSpecExtractor(object): def extract_buildspec(self, template): # type: (Dict[str, Any]) -> str source = template['Resources']['AppPackageBuild'][ 'Properties']['Source'] buildspec = source.pop('BuildSpec') return buildspec ================================================ FILE: chalice/policies-extra.json ================================================ { "s3": { "upload_file": ["s3:PutObject", "s3:AbortMultipartUpload"], "upload_fileobj": ["s3:PutObject", "s3: AbortMultipartUpload"], "download_file": ["s3:GetObject"], "download_fileobj": ["s3:GetObject"], "copy": ["s3:PutObject", "s3:AbortMultipartUpload"] } } ================================================ FILE: chalice/policies.json ================================================ { "account": { "DeleteAlternateContact": "account:DeleteAlternateContact", "GetAlternateContact": "account:GetAlternateContact", "PutAlternateContact": "account:PutAlternateContact" }, "acm": { "AddTagsToCertificate": "acm:AddTagsToCertificate", "DeleteCertificate": "acm:DeleteCertificate", "DescribeCertificate": "acm:DescribeCertificate", "ExportCertificate": "acm:ExportCertificate", "GetAccountConfiguration": "acm:GetAccountConfiguration", "GetCertificate": "acm:GetCertificate", "ImportCertificate": "acm:ImportCertificate", "ListCertificates": "acm:ListCertificates", "ListTagsForCertificate": "acm:ListTagsForCertificate", "PutAccountConfiguration": "acm:PutAccountConfiguration", "RemoveTagsFromCertificate": "acm:RemoveTagsFromCertificate", "RenewCertificate": "acm:RenewCertificate", "RequestCertificate": "acm:RequestCertificate", "ResendValidationEmail": "acm:ResendValidationEmail", "UpdateCertificateOptions": "acm:UpdateCertificateOptions" }, "acm-pca": { "CreateCertificateAuthority": "acm-pca:CreateCertificateAuthority", "CreateCertificateAuthorityAuditReport": "acm-pca:CreateCertificateAuthorityAuditReport", "CreatePermission": "acm-pca:CreatePermission", "DeleteCertificateAuthority": "acm-pca:DeleteCertificateAuthority", "DeletePermission": "acm-pca:DeletePermission", "DeletePolicy": "acm-pca:DeletePolicy", "DescribeCertificateAuthority": "acm-pca:DescribeCertificateAuthority", "DescribeCertificateAuthorityAuditReport": "acm-pca:DescribeCertificateAuthorityAuditReport", "GetCertificate": "acm-pca:GetCertificate", "GetCertificateAuthorityCertificate": "acm-pca:GetCertificateAuthorityCertificate", "GetCertificateAuthorityCsr": "acm-pca:GetCertificateAuthorityCsr", "GetPolicy": "acm-pca:GetPolicy", "ImportCertificateAuthorityCertificate": "acm-pca:ImportCertificateAuthorityCertificate", "IssueCertificate": "acm-pca:IssueCertificate", "ListCertificateAuthorities": "acm-pca:ListCertificateAuthorities", "ListPermissions": "acm-pca:ListPermissions", "ListTags": "acm-pca:ListTags", "PutPolicy": "acm-pca:PutPolicy", "RestoreCertificateAuthority": "acm-pca:RestoreCertificateAuthority", "RevokeCertificate": "acm-pca:RevokeCertificate", "TagCertificateAuthority": "acm-pca:TagCertificateAuthority", "UntagCertificateAuthority": "acm-pca:UntagCertificateAuthority", "UpdateCertificateAuthority": "acm-pca:UpdateCertificateAuthority" }, "alexaforbusiness": { "ApproveSkill": "a4b:ApproveSkill", "AssociateContactWithAddressBook": "a4b:AssociateContactWithAddressBook", "AssociateDeviceWithNetworkProfile": "a4b:AssociateDeviceWithNetworkProfile", "AssociateDeviceWithRoom": "a4b:AssociateDeviceWithRoom", "AssociateSkillGroupWithRoom": "a4b:AssociateSkillGroupWithRoom", "AssociateSkillWithSkillGroup": "a4b:AssociateSkillWithSkillGroup", "AssociateSkillWithUsers": "a4b:AssociateSkillWithUsers", "CreateAddressBook": "a4b:CreateAddressBook", "CreateBusinessReportSchedule": "a4b:CreateBusinessReportSchedule", "CreateConferenceProvider": "a4b:CreateConferenceProvider", "CreateContact": "a4b:CreateContact", "CreateGatewayGroup": "a4b:CreateGatewayGroup", "CreateNetworkProfile": "a4b:CreateNetworkProfile", "CreateProfile": "a4b:CreateProfile", "CreateRoom": "a4b:CreateRoom", "CreateSkillGroup": "a4b:CreateSkillGroup", "CreateUser": "a4b:CreateUser", "DeleteAddressBook": "a4b:DeleteAddressBook", "DeleteBusinessReportSchedule": "a4b:DeleteBusinessReportSchedule", "DeleteConferenceProvider": "a4b:DeleteConferenceProvider", "DeleteContact": "a4b:DeleteContact", "DeleteDevice": "a4b:DeleteDevice", "DeleteDeviceUsageData": "a4b:DeleteDeviceUsageData", "DeleteGatewayGroup": "a4b:DeleteGatewayGroup", "DeleteNetworkProfile": "a4b:DeleteNetworkProfile", "DeleteProfile": "a4b:DeleteProfile", "DeleteRoom": "a4b:DeleteRoom", "DeleteRoomSkillParameter": "a4b:DeleteRoomSkillParameter", "DeleteSkillAuthorization": "a4b:DeleteSkillAuthorization", "DeleteSkillGroup": "a4b:DeleteSkillGroup", "DeleteUser": "a4b:DeleteUser", "DisassociateContactFromAddressBook": "a4b:DisassociateContactFromAddressBook", "DisassociateDeviceFromRoom": "a4b:DisassociateDeviceFromRoom", "DisassociateSkillFromSkillGroup": "a4b:DisassociateSkillFromSkillGroup", "DisassociateSkillFromUsers": "a4b:DisassociateSkillFromUsers", "DisassociateSkillGroupFromRoom": "a4b:DisassociateSkillGroupFromRoom", "ForgetSmartHomeAppliances": "a4b:ForgetSmartHomeAppliances", "GetAddressBook": "a4b:GetAddressBook", "GetConferencePreference": "a4b:GetConferencePreference", "GetConferenceProvider": "a4b:GetConferenceProvider", "GetContact": "a4b:GetContact", "GetDevice": "a4b:GetDevice", "GetGateway": "a4b:GetGateway", "GetGatewayGroup": "a4b:GetGatewayGroup", "GetInvitationConfiguration": "a4b:GetInvitationConfiguration", "GetNetworkProfile": "a4b:GetNetworkProfile", "GetProfile": "a4b:GetProfile", "GetRoom": "a4b:GetRoom", "GetRoomSkillParameter": "a4b:GetRoomSkillParameter", "GetSkillGroup": "a4b:GetSkillGroup", "ListBusinessReportSchedules": "a4b:ListBusinessReportSchedules", "ListConferenceProviders": "a4b:ListConferenceProviders", "ListDeviceEvents": "a4b:ListDeviceEvents", "ListGatewayGroups": "a4b:ListGatewayGroups", "ListGateways": "a4b:ListGateways", "ListSkills": "a4b:ListSkills", "ListSkillsStoreCategories": "a4b:ListSkillsStoreCategories", "ListSkillsStoreSkillsByCategory": "a4b:ListSkillsStoreSkillsByCategory", "ListSmartHomeAppliances": "a4b:ListSmartHomeAppliances", "ListTags": "a4b:ListTags", "PutConferencePreference": "a4b:PutConferencePreference", "PutInvitationConfiguration": "a4b:PutInvitationConfiguration", "PutRoomSkillParameter": "a4b:PutRoomSkillParameter", "PutSkillAuthorization": "a4b:PutSkillAuthorization", "RegisterAVSDevice": "a4b:RegisterAVSDevice", "RejectSkill": "a4b:RejectSkill", "ResolveRoom": "a4b:ResolveRoom", "RevokeInvitation": "a4b:RevokeInvitation", "SearchAddressBooks": "a4b:SearchAddressBooks", "SearchContacts": "a4b:SearchContacts", "SearchDevices": "a4b:SearchDevices", "SearchNetworkProfiles": "a4b:SearchNetworkProfiles", "SearchProfiles": "a4b:SearchProfiles", "SearchRooms": "a4b:SearchRooms", "SearchSkillGroups": "a4b:SearchSkillGroups", "SearchUsers": "a4b:SearchUsers", "SendAnnouncement": "a4b:SendAnnouncement", "SendInvitation": "a4b:SendInvitation", "StartDeviceSync": "a4b:StartDeviceSync", "StartSmartHomeApplianceDiscovery": "a4b:StartSmartHomeApplianceDiscovery", "TagResource": "a4b:TagResource", "UntagResource": "a4b:UntagResource", "UpdateAddressBook": "a4b:UpdateAddressBook", "UpdateBusinessReportSchedule": "a4b:UpdateBusinessReportSchedule", "UpdateConferenceProvider": "a4b:UpdateConferenceProvider", "UpdateContact": "a4b:UpdateContact", "UpdateDevice": "a4b:UpdateDevice", "UpdateGateway": "a4b:UpdateGateway", "UpdateGatewayGroup": "a4b:UpdateGatewayGroup", "UpdateNetworkProfile": "a4b:UpdateNetworkProfile", "UpdateProfile": "a4b:UpdateProfile", "UpdateRoom": "a4b:UpdateRoom", "UpdateSkillGroup": "a4b:UpdateSkillGroup" }, "amplify": { "CreateApp": "amplify:CreateApp", "CreateBackendEnvironment": "amplify:CreateBackendEnvironment", "CreateBranch": "amplify:CreateBranch", "CreateDeployment": "amplify:CreateDeployment", "CreateDomainAssociation": "amplify:CreateDomainAssociation", "DeleteApp": "amplify:DeleteApp", "DeleteBackendEnvironment": "amplify:DeleteBackendEnvironment", "DeleteBranch": "amplify:DeleteBranch", "DeleteDomainAssociation": "amplify:DeleteDomainAssociation", "DeleteJob": "amplify:DeleteJob", "GenerateAccessLogs": "amplify:GenerateAccessLogs", "GetApp": "amplify:GetApp", "GetArtifactUrl": "amplify:GetArtifactUrl", "GetBackendEnvironment": "amplify:GetBackendEnvironment", "GetBranch": "amplify:GetBranch", "GetDomainAssociation": "amplify:GetDomainAssociation", "GetJob": "amplify:GetJob", "ListApps": "amplify:ListApps", "ListArtifacts": "amplify:ListArtifacts", "ListBackendEnvironments": "amplify:ListBackendEnvironments", "ListBranches": "amplify:ListBranches", "ListDomainAssociations": "amplify:ListDomainAssociations", "ListJobs": "amplify:ListJobs", "ListTagsForResource": "amplify:ListTagsForResource", "StartDeployment": "amplify:StartDeployment", "StartJob": "amplify:StartJob", "StopJob": "amplify:StopJob", "TagResource": "amplify:TagResource", "UntagResource": "amplify:UntagResource", "UpdateApp": "amplify:UpdateApp", "UpdateBranch": "amplify:UpdateBranch", "UpdateDomainAssociation": "amplify:UpdateDomainAssociation" }, "amplifybackend": { "CloneBackend": "amplifybackend:CloneBackend", "CreateBackend": "amplifybackend:CreateBackend", "CreateBackendAPI": "amplifybackend:CreateBackendAPI", "CreateBackendAuth": "amplifybackend:CreateBackendAuth", "CreateBackendConfig": "amplifybackend:CreateBackendConfig", "CreateToken": "amplifybackend:CreateToken", "DeleteBackend": "amplifybackend:DeleteBackend", "DeleteBackendAPI": "amplifybackend:DeleteBackendAPI", "DeleteBackendAuth": "amplifybackend:DeleteBackendAuth", "DeleteToken": "amplifybackend:DeleteToken", "GenerateBackendAPIModels": "amplifybackend:GenerateBackendAPIModels", "GetBackend": "amplifybackend:GetBackend", "GetBackendAPI": "amplifybackend:GetBackendAPI", "GetBackendAPIModels": "amplifybackend:GetBackendAPIModels", "GetBackendAuth": "amplifybackend:GetBackendAuth", "GetBackendJob": "amplifybackend:GetBackendJob", "GetToken": "amplifybackend:GetToken", "ImportBackendAuth": "amplifybackend:ImportBackendAuth", "ListBackendJobs": "amplifybackend:ListBackendJobs", "RemoveAllBackends": "amplifybackend:RemoveAllBackends", "RemoveBackendConfig": "amplifybackend:RemoveBackendConfig", "UpdateBackendAPI": "amplifybackend:UpdateBackendAPI", "UpdateBackendAuth": "amplifybackend:UpdateBackendAuth", "UpdateBackendConfig": "amplifybackend:UpdateBackendConfig", "UpdateBackendJob": "amplifybackend:UpdateBackendJob" }, "amplifyuibuilder": { "CreateComponent": "amplifyuibuilder:CreateComponent", "CreateTheme": "amplifyuibuilder:CreateTheme", "DeleteComponent": "amplifyuibuilder:DeleteComponent", "DeleteTheme": "amplifyuibuilder:DeleteTheme", "ExchangeCodeForToken": "amplifyuibuilder:ExchangeCodeForToken", "ExportComponents": "amplifyuibuilder:ExportComponents", "ExportThemes": "amplifyuibuilder:ExportThemes", "GetComponent": "amplifyuibuilder:GetComponent", "GetTheme": "amplifyuibuilder:GetTheme", "ListComponents": "amplifyuibuilder:ListComponents", "ListThemes": "amplifyuibuilder:ListThemes", "RefreshToken": "amplifyuibuilder:RefreshToken", "UpdateComponent": "amplifyuibuilder:UpdateComponent", "UpdateTheme": "amplifyuibuilder:UpdateTheme" }, "appconfig": { "CreateApplication": "appconfig:CreateApplication", "CreateConfigurationProfile": "appconfig:CreateConfigurationProfile", "CreateDeploymentStrategy": "appconfig:CreateDeploymentStrategy", "CreateEnvironment": "appconfig:CreateEnvironment", "CreateHostedConfigurationVersion": "appconfig:CreateHostedConfigurationVersion", "DeleteApplication": "appconfig:DeleteApplication", "DeleteConfigurationProfile": "appconfig:DeleteConfigurationProfile", "DeleteDeploymentStrategy": "appconfig:DeleteDeploymentStrategy", "DeleteEnvironment": "appconfig:DeleteEnvironment", "DeleteHostedConfigurationVersion": "appconfig:DeleteHostedConfigurationVersion", "GetApplication": "appconfig:GetApplication", "GetConfiguration": "appconfig:GetConfiguration", "GetConfigurationProfile": "appconfig:GetConfigurationProfile", "GetDeployment": "appconfig:GetDeployment", "GetDeploymentStrategy": "appconfig:GetDeploymentStrategy", "GetEnvironment": "appconfig:GetEnvironment", "GetHostedConfigurationVersion": "appconfig:GetHostedConfigurationVersion", "ListApplications": "appconfig:ListApplications", "ListConfigurationProfiles": "appconfig:ListConfigurationProfiles", "ListDeploymentStrategies": "appconfig:ListDeploymentStrategies", "ListDeployments": "appconfig:ListDeployments", "ListEnvironments": "appconfig:ListEnvironments", "ListHostedConfigurationVersions": "appconfig:ListHostedConfigurationVersions", "ListTagsForResource": "appconfig:ListTagsForResource", "StartDeployment": "appconfig:StartDeployment", "StopDeployment": "appconfig:StopDeployment", "TagResource": "appconfig:TagResource", "UntagResource": "appconfig:UntagResource", "UpdateApplication": "appconfig:UpdateApplication", "UpdateConfigurationProfile": "appconfig:UpdateConfigurationProfile", "UpdateDeploymentStrategy": "appconfig:UpdateDeploymentStrategy", "UpdateEnvironment": "appconfig:UpdateEnvironment", "ValidateConfiguration": "appconfig:ValidateConfiguration" }, "appflow": { "CreateConnectorProfile": "appflow:CreateConnectorProfile", "CreateFlow": "appflow:CreateFlow", "DeleteConnectorProfile": "appflow:DeleteConnectorProfile", "DeleteFlow": "appflow:DeleteFlow", "DescribeConnectorEntity": "appflow:DescribeConnectorEntity", "DescribeConnectorProfiles": "appflow:DescribeConnectorProfiles", "DescribeConnectors": "appflow:DescribeConnectors", "DescribeFlow": "appflow:DescribeFlow", "DescribeFlowExecutionRecords": "appflow:DescribeFlowExecutionRecords", "ListConnectorEntities": "appflow:ListConnectorEntities", "ListFlows": "appflow:ListFlows", "ListTagsForResource": "appflow:ListTagsForResource", "StartFlow": "appflow:StartFlow", "StopFlow": "appflow:StopFlow", "TagResource": "appflow:TagResource", "UntagResource": "appflow:UntagResource", "UpdateConnectorProfile": "appflow:UpdateConnectorProfile", "UpdateFlow": "appflow:UpdateFlow" }, "application-autoscaling": { "DeleteScalingPolicy": "application-autoscaling:DeleteScalingPolicy", "DeleteScheduledAction": "application-autoscaling:DeleteScheduledAction", "DeregisterScalableTarget": "application-autoscaling:DeregisterScalableTarget", "DescribeScalableTargets": "application-autoscaling:DescribeScalableTargets", "DescribeScalingActivities": "application-autoscaling:DescribeScalingActivities", "DescribeScalingPolicies": "application-autoscaling:DescribeScalingPolicies", "DescribeScheduledActions": "application-autoscaling:DescribeScheduledActions", "PutScalingPolicy": "application-autoscaling:PutScalingPolicy", "PutScheduledAction": "application-autoscaling:PutScheduledAction", "RegisterScalableTarget": "application-autoscaling:RegisterScalableTarget" }, "appmesh": { "CreateGatewayRoute": "appmesh:CreateGatewayRoute", "CreateMesh": "appmesh:CreateMesh", "CreateRoute": "appmesh:CreateRoute", "CreateVirtualGateway": "appmesh:CreateVirtualGateway", "CreateVirtualNode": "appmesh:CreateVirtualNode", "CreateVirtualRouter": "appmesh:CreateVirtualRouter", "CreateVirtualService": "appmesh:CreateVirtualService", "DeleteGatewayRoute": "appmesh:DeleteGatewayRoute", "DeleteMesh": "appmesh:DeleteMesh", "DeleteRoute": "appmesh:DeleteRoute", "DeleteVirtualGateway": "appmesh:DeleteVirtualGateway", "DeleteVirtualNode": "appmesh:DeleteVirtualNode", "DeleteVirtualRouter": "appmesh:DeleteVirtualRouter", "DeleteVirtualService": "appmesh:DeleteVirtualService", "DescribeGatewayRoute": "appmesh:DescribeGatewayRoute", "DescribeMesh": "appmesh:DescribeMesh", "DescribeRoute": "appmesh:DescribeRoute", "DescribeVirtualGateway": "appmesh:DescribeVirtualGateway", "DescribeVirtualNode": "appmesh:DescribeVirtualNode", "DescribeVirtualRouter": "appmesh:DescribeVirtualRouter", "DescribeVirtualService": "appmesh:DescribeVirtualService", "ListGatewayRoutes": "appmesh:ListGatewayRoutes", "ListMeshes": "appmesh:ListMeshes", "ListRoutes": "appmesh:ListRoutes", "ListTagsForResource": "appmesh:ListTagsForResource", "ListVirtualGateways": "appmesh:ListVirtualGateways", "ListVirtualNodes": "appmesh:ListVirtualNodes", "ListVirtualRouters": "appmesh:ListVirtualRouters", "ListVirtualServices": "appmesh:ListVirtualServices", "TagResource": "appmesh:TagResource", "UntagResource": "appmesh:UntagResource", "UpdateGatewayRoute": "appmesh:UpdateGatewayRoute", "UpdateMesh": "appmesh:UpdateMesh", "UpdateRoute": "appmesh:UpdateRoute", "UpdateVirtualGateway": "appmesh:UpdateVirtualGateway", "UpdateVirtualNode": "appmesh:UpdateVirtualNode", "UpdateVirtualRouter": "appmesh:UpdateVirtualRouter", "UpdateVirtualService": "appmesh:UpdateVirtualService" }, "apprunner": { "AssociateCustomDomain": "apprunner:AssociateCustomDomain", "CreateAutoScalingConfiguration": "apprunner:CreateAutoScalingConfiguration", "CreateConnection": "apprunner:CreateConnection", "CreateService": "apprunner:CreateService", "DeleteAutoScalingConfiguration": "apprunner:DeleteAutoScalingConfiguration", "DeleteConnection": "apprunner:DeleteConnection", "DeleteService": "apprunner:DeleteService", "DescribeAutoScalingConfiguration": "apprunner:DescribeAutoScalingConfiguration", "DescribeCustomDomains": "apprunner:DescribeCustomDomains", "DescribeService": "apprunner:DescribeService", "DisassociateCustomDomain": "apprunner:DisassociateCustomDomain", "ListAutoScalingConfigurations": "apprunner:ListAutoScalingConfigurations", "ListConnections": "apprunner:ListConnections", "ListOperations": "apprunner:ListOperations", "ListServices": "apprunner:ListServices", "ListTagsForResource": "apprunner:ListTagsForResource", "PauseService": "apprunner:PauseService", "ResumeService": "apprunner:ResumeService", "StartDeployment": "apprunner:StartDeployment", "TagResource": "apprunner:TagResource", "UntagResource": "apprunner:UntagResource", "UpdateService": "apprunner:UpdateService" }, "appstream": { "AssociateApplicationFleet": "appstream:AssociateApplicationFleet", "AssociateFleet": "appstream:AssociateFleet", "BatchAssociateUserStack": "appstream:BatchAssociateUserStack", "BatchDisassociateUserStack": "appstream:BatchDisassociateUserStack", "CopyImage": "appstream:CopyImage", "CreateAppBlock": "appstream:CreateAppBlock", "CreateApplication": "appstream:CreateApplication", "CreateDirectoryConfig": "appstream:CreateDirectoryConfig", "CreateEntitlement": "appstream:CreateEntitlement", "CreateFleet": "appstream:CreateFleet", "CreateImageBuilder": "appstream:CreateImageBuilder", "CreateImageBuilderStreamingURL": "appstream:CreateImageBuilderStreamingURL", "CreateStack": "appstream:CreateStack", "CreateStreamingURL": "appstream:CreateStreamingURL", "CreateUpdatedImage": "appstream:CreateUpdatedImage", "CreateUsageReportSubscription": "appstream:CreateUsageReportSubscription", "CreateUser": "appstream:CreateUser", "DeleteAppBlock": "appstream:DeleteAppBlock", "DeleteApplication": "appstream:DeleteApplication", "DeleteDirectoryConfig": "appstream:DeleteDirectoryConfig", "DeleteEntitlement": "appstream:DeleteEntitlement", "DeleteFleet": "appstream:DeleteFleet", "DeleteImage": "appstream:DeleteImage", "DeleteImageBuilder": "appstream:DeleteImageBuilder", "DeleteImagePermissions": "appstream:DeleteImagePermissions", "DeleteStack": "appstream:DeleteStack", "DeleteUsageReportSubscription": "appstream:DeleteUsageReportSubscription", "DeleteUser": "appstream:DeleteUser", "DescribeAppBlocks": "appstream:DescribeAppBlocks", "DescribeApplicationFleetAssociations": "appstream:DescribeApplicationFleetAssociations", "DescribeApplications": "appstream:DescribeApplications", "DescribeDirectoryConfigs": "appstream:DescribeDirectoryConfigs", "DescribeEntitlements": "appstream:DescribeEntitlements", "DescribeFleets": "appstream:DescribeFleets", "DescribeImageBuilders": "appstream:DescribeImageBuilders", "DescribeImagePermissions": "appstream:DescribeImagePermissions", "DescribeImages": "appstream:DescribeImages", "DescribeSessions": "appstream:DescribeSessions", "DescribeStacks": "appstream:DescribeStacks", "DescribeUsageReportSubscriptions": "appstream:DescribeUsageReportSubscriptions", "DescribeUserStackAssociations": "appstream:DescribeUserStackAssociations", "DescribeUsers": "appstream:DescribeUsers", "DisableUser": "appstream:DisableUser", "DisassociateApplicationFleet": "appstream:DisassociateApplicationFleet", "DisassociateFleet": "appstream:DisassociateFleet", "EnableUser": "appstream:EnableUser", "ExpireSession": "appstream:ExpireSession", "ListAssociatedFleets": "appstream:ListAssociatedFleets", "ListAssociatedStacks": "appstream:ListAssociatedStacks", "ListEntitledApplications": "appstream:ListEntitledApplications", "ListTagsForResource": "appstream:ListTagsForResource", "StartFleet": "appstream:StartFleet", "StartImageBuilder": "appstream:StartImageBuilder", "StopFleet": "appstream:StopFleet", "StopImageBuilder": "appstream:StopImageBuilder", "TagResource": "appstream:TagResource", "UntagResource": "appstream:UntagResource", "UpdateApplication": "appstream:UpdateApplication", "UpdateDirectoryConfig": "appstream:UpdateDirectoryConfig", "UpdateEntitlement": "appstream:UpdateEntitlement", "UpdateFleet": "appstream:UpdateFleet", "UpdateImagePermissions": "appstream:UpdateImagePermissions", "UpdateStack": "appstream:UpdateStack" }, "athena": { "BatchGetNamedQuery": "athena:BatchGetNamedQuery", "BatchGetQueryExecution": "athena:BatchGetQueryExecution", "CreateDataCatalog": "athena:CreateDataCatalog", "CreateNamedQuery": "athena:CreateNamedQuery", "CreatePreparedStatement": "athena:CreatePreparedStatement", "CreateWorkGroup": "athena:CreateWorkGroup", "DeleteDataCatalog": "athena:DeleteDataCatalog", "DeleteNamedQuery": "athena:DeleteNamedQuery", "DeletePreparedStatement": "athena:DeletePreparedStatement", "DeleteWorkGroup": "athena:DeleteWorkGroup", "GetDataCatalog": "athena:GetDataCatalog", "GetDatabase": "athena:GetDatabase", "GetNamedQuery": "athena:GetNamedQuery", "GetPreparedStatement": "athena:GetPreparedStatement", "GetQueryExecution": "athena:GetQueryExecution", "GetQueryResults": "athena:GetQueryResults", "GetTableMetadata": "athena:GetTableMetadata", "GetWorkGroup": "athena:GetWorkGroup", "ListDataCatalogs": "athena:ListDataCatalogs", "ListDatabases": "athena:ListDatabases", "ListEngineVersions": "athena:ListEngineVersions", "ListNamedQueries": "athena:ListNamedQueries", "ListPreparedStatements": "athena:ListPreparedStatements", "ListQueryExecutions": "athena:ListQueryExecutions", "ListTableMetadata": "athena:ListTableMetadata", "ListTagsForResource": "athena:ListTagsForResource", "ListWorkGroups": "athena:ListWorkGroups", "StartQueryExecution": "athena:StartQueryExecution", "StopQueryExecution": "athena:StopQueryExecution", "TagResource": "athena:TagResource", "UntagResource": "athena:UntagResource", "UpdateDataCatalog": "athena:UpdateDataCatalog", "UpdatePreparedStatement": "athena:UpdatePreparedStatement", "UpdateWorkGroup": "athena:UpdateWorkGroup" }, "auditmanager": { "AssociateAssessmentReportEvidenceFolder": "auditmanager:AssociateAssessmentReportEvidenceFolder", "BatchAssociateAssessmentReportEvidence": "auditmanager:BatchAssociateAssessmentReportEvidence", "BatchCreateDelegationByAssessment": "auditmanager:BatchCreateDelegationByAssessment", "BatchDeleteDelegationByAssessment": "auditmanager:BatchDeleteDelegationByAssessment", "BatchDisassociateAssessmentReportEvidence": "auditmanager:BatchDisassociateAssessmentReportEvidence", "BatchImportEvidenceToAssessmentControl": "auditmanager:BatchImportEvidenceToAssessmentControl", "CreateAssessment": "auditmanager:CreateAssessment", "CreateAssessmentFramework": "auditmanager:CreateAssessmentFramework", "CreateAssessmentReport": "auditmanager:CreateAssessmentReport", "CreateControl": "auditmanager:CreateControl", "DeleteAssessment": "auditmanager:DeleteAssessment", "DeleteAssessmentFramework": "auditmanager:DeleteAssessmentFramework", "DeleteAssessmentFrameworkShare": "auditmanager:DeleteAssessmentFrameworkShare", "DeleteAssessmentReport": "auditmanager:DeleteAssessmentReport", "DeleteControl": "auditmanager:DeleteControl", "DeregisterAccount": "auditmanager:DeregisterAccount", "DeregisterOrganizationAdminAccount": "auditmanager:DeregisterOrganizationAdminAccount", "DisassociateAssessmentReportEvidenceFolder": "auditmanager:DisassociateAssessmentReportEvidenceFolder", "GetAccountStatus": "auditmanager:GetAccountStatus", "GetAssessment": "auditmanager:GetAssessment", "GetAssessmentFramework": "auditmanager:GetAssessmentFramework", "GetAssessmentReportUrl": "auditmanager:GetAssessmentReportUrl", "GetChangeLogs": "auditmanager:GetChangeLogs", "GetControl": "auditmanager:GetControl", "GetDelegations": "auditmanager:GetDelegations", "GetEvidence": "auditmanager:GetEvidence", "GetEvidenceByEvidenceFolder": "auditmanager:GetEvidenceByEvidenceFolder", "GetEvidenceFolder": "auditmanager:GetEvidenceFolder", "GetEvidenceFoldersByAssessment": "auditmanager:GetEvidenceFoldersByAssessment", "GetEvidenceFoldersByAssessmentControl": "auditmanager:GetEvidenceFoldersByAssessmentControl", "GetInsights": "auditmanager:GetInsights", "GetInsightsByAssessment": "auditmanager:GetInsightsByAssessment", "GetOrganizationAdminAccount": "auditmanager:GetOrganizationAdminAccount", "GetServicesInScope": "auditmanager:GetServicesInScope", "GetSettings": "auditmanager:GetSettings", "ListAssessmentControlInsightsByControlDomain": "auditmanager:ListAssessmentControlInsightsByControlDomain", "ListAssessmentFrameworkShareRequests": "auditmanager:ListAssessmentFrameworkShareRequests", "ListAssessmentFrameworks": "auditmanager:ListAssessmentFrameworks", "ListAssessmentReports": "auditmanager:ListAssessmentReports", "ListAssessments": "auditmanager:ListAssessments", "ListControlDomainInsights": "auditmanager:ListControlDomainInsights", "ListControlDomainInsightsByAssessment": "auditmanager:ListControlDomainInsightsByAssessment", "ListControlInsightsByControlDomain": "auditmanager:ListControlInsightsByControlDomain", "ListControls": "auditmanager:ListControls", "ListKeywordsForDataSource": "auditmanager:ListKeywordsForDataSource", "ListNotifications": "auditmanager:ListNotifications", "ListTagsForResource": "auditmanager:ListTagsForResource", "RegisterAccount": "auditmanager:RegisterAccount", "RegisterOrganizationAdminAccount": "auditmanager:RegisterOrganizationAdminAccount", "StartAssessmentFrameworkShare": "auditmanager:StartAssessmentFrameworkShare", "TagResource": "auditmanager:TagResource", "UntagResource": "auditmanager:UntagResource", "UpdateAssessment": "auditmanager:UpdateAssessment", "UpdateAssessmentControl": "auditmanager:UpdateAssessmentControl", "UpdateAssessmentControlSetStatus": "auditmanager:UpdateAssessmentControlSetStatus", "UpdateAssessmentFramework": "auditmanager:UpdateAssessmentFramework", "UpdateAssessmentFrameworkShare": "auditmanager:UpdateAssessmentFrameworkShare", "UpdateAssessmentStatus": "auditmanager:UpdateAssessmentStatus", "UpdateControl": "auditmanager:UpdateControl", "UpdateSettings": "auditmanager:UpdateSettings", "ValidateAssessmentReportIntegrity": "auditmanager:ValidateAssessmentReportIntegrity" }, "autoscaling": { "AttachInstances": "autoscaling:AttachInstances", "AttachLoadBalancerTargetGroups": "autoscaling:AttachLoadBalancerTargetGroups", "AttachLoadBalancers": "autoscaling:AttachLoadBalancers", "BatchDeleteScheduledAction": "autoscaling:BatchDeleteScheduledAction", "BatchPutScheduledUpdateGroupAction": "autoscaling:BatchPutScheduledUpdateGroupAction", "CancelInstanceRefresh": "autoscaling:CancelInstanceRefresh", "CompleteLifecycleAction": "autoscaling:CompleteLifecycleAction", "CreateAutoScalingGroup": "autoscaling:CreateAutoScalingGroup", "CreateLaunchConfiguration": "autoscaling:CreateLaunchConfiguration", "CreateOrUpdateTags": "autoscaling:CreateOrUpdateTags", "DeleteAutoScalingGroup": "autoscaling:DeleteAutoScalingGroup", "DeleteLaunchConfiguration": "autoscaling:DeleteLaunchConfiguration", "DeleteLifecycleHook": "autoscaling:DeleteLifecycleHook", "DeleteNotificationConfiguration": "autoscaling:DeleteNotificationConfiguration", "DeletePolicy": "autoscaling:DeletePolicy", "DeleteScheduledAction": "autoscaling:DeleteScheduledAction", "DeleteTags": "autoscaling:DeleteTags", "DeleteWarmPool": "autoscaling:DeleteWarmPool", "DescribeAccountLimits": "autoscaling:DescribeAccountLimits", "DescribeAdjustmentTypes": "autoscaling:DescribeAdjustmentTypes", "DescribeAutoScalingGroups": "autoscaling:DescribeAutoScalingGroups", "DescribeAutoScalingInstances": "autoscaling:DescribeAutoScalingInstances", "DescribeAutoScalingNotificationTypes": "autoscaling:DescribeAutoScalingNotificationTypes", "DescribeInstanceRefreshes": "autoscaling:DescribeInstanceRefreshes", "DescribeLaunchConfigurations": "autoscaling:DescribeLaunchConfigurations", "DescribeLifecycleHookTypes": "autoscaling:DescribeLifecycleHookTypes", "DescribeLifecycleHooks": "autoscaling:DescribeLifecycleHooks", "DescribeLoadBalancerTargetGroups": "autoscaling:DescribeLoadBalancerTargetGroups", "DescribeLoadBalancers": "autoscaling:DescribeLoadBalancers", "DescribeMetricCollectionTypes": "autoscaling:DescribeMetricCollectionTypes", "DescribeNotificationConfigurations": "autoscaling:DescribeNotificationConfigurations", "DescribePolicies": "autoscaling:DescribePolicies", "DescribeScalingActivities": "autoscaling:DescribeScalingActivities", "DescribeScalingProcessTypes": "autoscaling:DescribeScalingProcessTypes", "DescribeScheduledActions": "autoscaling:DescribeScheduledActions", "DescribeTags": "autoscaling:DescribeTags", "DescribeTerminationPolicyTypes": "autoscaling:DescribeTerminationPolicyTypes", "DescribeWarmPool": "autoscaling:DescribeWarmPool", "DetachInstances": "autoscaling:DetachInstances", "DetachLoadBalancerTargetGroups": "autoscaling:DetachLoadBalancerTargetGroups", "DetachLoadBalancers": "autoscaling:DetachLoadBalancers", "DisableMetricsCollection": "autoscaling:DisableMetricsCollection", "EnableMetricsCollection": "autoscaling:EnableMetricsCollection", "EnterStandby": "autoscaling:EnterStandby", "ExecutePolicy": "autoscaling:ExecutePolicy", "ExitStandby": "autoscaling:ExitStandby", "GetPredictiveScalingForecast": "autoscaling:GetPredictiveScalingForecast", "PutLifecycleHook": "autoscaling:PutLifecycleHook", "PutNotificationConfiguration": "autoscaling:PutNotificationConfiguration", "PutScalingPolicy": "autoscaling:PutScalingPolicy", "PutScheduledUpdateGroupAction": "autoscaling:PutScheduledUpdateGroupAction", "PutWarmPool": "autoscaling:PutWarmPool", "RecordLifecycleActionHeartbeat": "autoscaling:RecordLifecycleActionHeartbeat", "ResumeProcesses": "autoscaling:ResumeProcesses", "SetDesiredCapacity": "autoscaling:SetDesiredCapacity", "SetInstanceHealth": "autoscaling:SetInstanceHealth", "SetInstanceProtection": "autoscaling:SetInstanceProtection", "StartInstanceRefresh": "autoscaling:StartInstanceRefresh", "SuspendProcesses": "autoscaling:SuspendProcesses", "TerminateInstanceInAutoScalingGroup": "autoscaling:TerminateInstanceInAutoScalingGroup", "UpdateAutoScalingGroup": "autoscaling:UpdateAutoScalingGroup" }, "autoscaling-plans": { "CreateScalingPlan": "autoscaling-plans:CreateScalingPlan", "DeleteScalingPlan": "autoscaling-plans:DeleteScalingPlan", "DescribeScalingPlanResources": "autoscaling-plans:DescribeScalingPlanResources", "DescribeScalingPlans": "autoscaling-plans:DescribeScalingPlans", "GetScalingPlanResourceForecastData": "autoscaling-plans:GetScalingPlanResourceForecastData", "UpdateScalingPlan": "autoscaling-plans:UpdateScalingPlan" }, "backup": { "CreateBackupPlan": "backup:CreateBackupPlan", "CreateBackupSelection": "backup:CreateBackupSelection", "CreateBackupVault": "backup:CreateBackupVault", "CreateFramework": "backup:CreateFramework", "CreateReportPlan": "backup:CreateReportPlan", "DeleteBackupPlan": "backup:DeleteBackupPlan", "DeleteBackupSelection": "backup:DeleteBackupSelection", "DeleteBackupVault": "backup:DeleteBackupVault", "DeleteBackupVaultAccessPolicy": "backup:DeleteBackupVaultAccessPolicy", "DeleteBackupVaultLockConfiguration": "backup:DeleteBackupVaultLockConfiguration", "DeleteBackupVaultNotifications": "backup:DeleteBackupVaultNotifications", "DeleteFramework": "backup:DeleteFramework", "DeleteRecoveryPoint": "backup:DeleteRecoveryPoint", "DeleteReportPlan": "backup:DeleteReportPlan", "DescribeBackupJob": "backup:DescribeBackupJob", "DescribeBackupVault": "backup:DescribeBackupVault", "DescribeCopyJob": "backup:DescribeCopyJob", "DescribeFramework": "backup:DescribeFramework", "DescribeGlobalSettings": "backup:DescribeGlobalSettings", "DescribeProtectedResource": "backup:DescribeProtectedResource", "DescribeRecoveryPoint": "backup:DescribeRecoveryPoint", "DescribeRegionSettings": "backup:DescribeRegionSettings", "DescribeReportJob": "backup:DescribeReportJob", "DescribeReportPlan": "backup:DescribeReportPlan", "DescribeRestoreJob": "backup:DescribeRestoreJob", "DisassociateRecoveryPoint": "backup:DisassociateRecoveryPoint", "ExportBackupPlanTemplate": "backup:ExportBackupPlanTemplate", "GetBackupPlan": "backup:GetBackupPlan", "GetBackupPlanFromJSON": "backup:GetBackupPlanFromJSON", "GetBackupPlanFromTemplate": "backup:GetBackupPlanFromTemplate", "GetBackupSelection": "backup:GetBackupSelection", "GetBackupVaultAccessPolicy": "backup:GetBackupVaultAccessPolicy", "GetBackupVaultNotifications": "backup:GetBackupVaultNotifications", "GetRecoveryPointRestoreMetadata": "backup:GetRecoveryPointRestoreMetadata", "GetSupportedResourceTypes": "backup:GetSupportedResourceTypes", "ListBackupJobs": "backup:ListBackupJobs", "ListBackupPlanTemplates": "backup:ListBackupPlanTemplates", "ListBackupPlanVersions": "backup:ListBackupPlanVersions", "ListBackupPlans": "backup:ListBackupPlans", "ListBackupSelections": "backup:ListBackupSelections", "ListBackupVaults": "backup:ListBackupVaults", "ListCopyJobs": "backup:ListCopyJobs", "ListFrameworks": "backup:ListFrameworks", "ListProtectedResources": "backup:ListProtectedResources", "ListRecoveryPointsByBackupVault": "backup:ListRecoveryPointsByBackupVault", "ListRecoveryPointsByResource": "backup:ListRecoveryPointsByResource", "ListReportJobs": "backup:ListReportJobs", "ListReportPlans": "backup:ListReportPlans", "ListRestoreJobs": "backup:ListRestoreJobs", "ListTags": "backup:ListTags", "PutBackupVaultAccessPolicy": "backup:PutBackupVaultAccessPolicy", "PutBackupVaultLockConfiguration": "backup:PutBackupVaultLockConfiguration", "PutBackupVaultNotifications": "backup:PutBackupVaultNotifications", "StartBackupJob": "backup:StartBackupJob", "StartCopyJob": "backup:StartCopyJob", "StartReportJob": "backup:StartReportJob", "StartRestoreJob": "backup:StartRestoreJob", "StopBackupJob": "backup:StopBackupJob", "TagResource": "backup:TagResource", "UntagResource": "backup:UntagResource", "UpdateBackupPlan": "backup:UpdateBackupPlan", "UpdateFramework": "backup:UpdateFramework", "UpdateGlobalSettings": "backup:UpdateGlobalSettings", "UpdateRecoveryPointLifecycle": "backup:UpdateRecoveryPointLifecycle", "UpdateRegionSettings": "backup:UpdateRegionSettings", "UpdateReportPlan": "backup:UpdateReportPlan" }, "backup-gateway": { "AssociateGatewayToServer": "backup-gateway:AssociateGatewayToServer", "CreateGateway": "backup-gateway:CreateGateway", "DeleteGateway": "backup-gateway:DeleteGateway", "DeleteHypervisor": "backup-gateway:DeleteHypervisor", "DisassociateGatewayFromServer": "backup-gateway:DisassociateGatewayFromServer", "ImportHypervisorConfiguration": "backup-gateway:ImportHypervisorConfiguration", "ListGateways": "backup-gateway:ListGateways", "ListHypervisors": "backup-gateway:ListHypervisors", "ListTagsForResource": "backup-gateway:ListTagsForResource", "ListVirtualMachines": "backup-gateway:ListVirtualMachines", "PutMaintenanceStartTime": "backup-gateway:PutMaintenanceStartTime", "TagResource": "backup-gateway:TagResource", "TestHypervisorConfiguration": "backup-gateway:TestHypervisorConfiguration", "UntagResource": "backup-gateway:UntagResource", "UpdateGatewayInformation": "backup-gateway:UpdateGatewayInformation", "UpdateHypervisor": "backup-gateway:UpdateHypervisor" }, "batch": { "CancelJob": "batch:CancelJob", "CreateComputeEnvironment": "batch:CreateComputeEnvironment", "CreateJobQueue": "batch:CreateJobQueue", "CreateSchedulingPolicy": "batch:CreateSchedulingPolicy", "DeleteComputeEnvironment": "batch:DeleteComputeEnvironment", "DeleteJobQueue": "batch:DeleteJobQueue", "DeleteSchedulingPolicy": "batch:DeleteSchedulingPolicy", "DeregisterJobDefinition": "batch:DeregisterJobDefinition", "DescribeComputeEnvironments": "batch:DescribeComputeEnvironments", "DescribeJobDefinitions": "batch:DescribeJobDefinitions", "DescribeJobQueues": "batch:DescribeJobQueues", "DescribeJobs": "batch:DescribeJobs", "DescribeSchedulingPolicies": "batch:DescribeSchedulingPolicies", "ListJobs": "batch:ListJobs", "ListSchedulingPolicies": "batch:ListSchedulingPolicies", "ListTagsForResource": "batch:ListTagsForResource", "RegisterJobDefinition": "batch:RegisterJobDefinition", "SubmitJob": "batch:SubmitJob", "TagResource": "batch:TagResource", "TerminateJob": "batch:TerminateJob", "UntagResource": "batch:UntagResource", "UpdateComputeEnvironment": "batch:UpdateComputeEnvironment", "UpdateJobQueue": "batch:UpdateJobQueue", "UpdateSchedulingPolicy": "batch:UpdateSchedulingPolicy" }, "braket": { "CancelJob": "braket:CancelJob", "CancelQuantumTask": "braket:CancelQuantumTask", "CreateJob": "braket:CreateJob", "CreateQuantumTask": "braket:CreateQuantumTask", "GetDevice": "braket:GetDevice", "GetJob": "braket:GetJob", "GetQuantumTask": "braket:GetQuantumTask", "ListTagsForResource": "braket:ListTagsForResource", "SearchDevices": "braket:SearchDevices", "SearchJobs": "braket:SearchJobs", "SearchQuantumTasks": "braket:SearchQuantumTasks", "TagResource": "braket:TagResource", "UntagResource": "braket:UntagResource" }, "budgets": { "CreateBudgetAction": "budgets:CreateBudgetAction", "DeleteBudgetAction": "budgets:DeleteBudgetAction", "DescribeBudgetAction": "budgets:DescribeBudgetAction", "DescribeBudgetActionHistories": "budgets:DescribeBudgetActionHistories", "DescribeBudgetActionsForAccount": "budgets:DescribeBudgetActionsForAccount", "DescribeBudgetActionsForBudget": "budgets:DescribeBudgetActionsForBudget", "ExecuteBudgetAction": "budgets:ExecuteBudgetAction", "UpdateBudgetAction": "budgets:UpdateBudgetAction" }, "chime": { "AssociatePhoneNumberWithUser": "chime:AssociatePhoneNumberWithUser", "AssociatePhoneNumbersWithVoiceConnector": "chime:AssociatePhoneNumbersWithVoiceConnector", "AssociatePhoneNumbersWithVoiceConnectorGroup": "chime:AssociatePhoneNumbersWithVoiceConnectorGroup", "AssociateSigninDelegateGroupsWithAccount": "chime:AssociateSigninDelegateGroupsWithAccount", "BatchCreateAttendee": "chime:BatchCreateAttendee", "BatchCreateChannelMembership": "chime:BatchCreateChannelMembership", "BatchCreateRoomMembership": "chime:BatchCreateRoomMembership", "BatchDeletePhoneNumber": "chime:BatchDeletePhoneNumber", "BatchSuspendUser": "chime:BatchSuspendUser", "BatchUnsuspendUser": "chime:BatchUnsuspendUser", "BatchUpdatePhoneNumber": "chime:BatchUpdatePhoneNumber", "BatchUpdateUser": "chime:BatchUpdateUser", "CreateAccount": "chime:CreateAccount", "CreateAppInstance": "chime:CreateAppInstance", "CreateAppInstanceAdmin": "chime:CreateAppInstanceAdmin", "CreateAppInstanceUser": "chime:CreateAppInstanceUser", "CreateAttendee": "chime:CreateAttendee", "CreateBot": "chime:CreateBot", "CreateChannel": "chime:CreateChannel", "CreateChannelBan": "chime:CreateChannelBan", "CreateChannelMembership": "chime:CreateChannelMembership", "CreateChannelModerator": "chime:CreateChannelModerator", "CreateMediaCapturePipeline": "chime:CreateMediaCapturePipeline", "CreateMeeting": "chime:CreateMeeting", "CreateMeetingDialOut": "chime:CreateMeetingDialOut", "CreateMeetingWithAttendees": "chime:CreateMeetingWithAttendees", "CreatePhoneNumberOrder": "chime:CreatePhoneNumberOrder", "CreateProxySession": "chime:CreateProxySession", "CreateRoom": "chime:CreateRoom", "CreateRoomMembership": "chime:CreateRoomMembership", "CreateSipMediaApplication": "chime:CreateSipMediaApplication", "CreateSipMediaApplicationCall": "chime:CreateSipMediaApplicationCall", "CreateSipRule": "chime:CreateSipRule", "CreateUser": "chime:CreateUser", "CreateVoiceConnector": "chime:CreateVoiceConnector", "CreateVoiceConnectorGroup": "chime:CreateVoiceConnectorGroup", "DeleteAccount": "chime:DeleteAccount", "DeleteAppInstance": "chime:DeleteAppInstance", "DeleteAppInstanceAdmin": "chime:DeleteAppInstanceAdmin", "DeleteAppInstanceStreamingConfigurations": "chime:DeleteAppInstanceStreamingConfigurations", "DeleteAppInstanceUser": "chime:DeleteAppInstanceUser", "DeleteAttendee": "chime:DeleteAttendee", "DeleteChannel": "chime:DeleteChannel", "DeleteChannelBan": "chime:DeleteChannelBan", "DeleteChannelMembership": "chime:DeleteChannelMembership", "DeleteChannelMessage": "chime:DeleteChannelMessage", "DeleteChannelModerator": "chime:DeleteChannelModerator", "DeleteEventsConfiguration": "chime:DeleteEventsConfiguration", "DeleteMediaCapturePipeline": "chime:DeleteMediaCapturePipeline", "DeleteMeeting": "chime:DeleteMeeting", "DeletePhoneNumber": "chime:DeletePhoneNumber", "DeleteProxySession": "chime:DeleteProxySession", "DeleteRoom": "chime:DeleteRoom", "DeleteRoomMembership": "chime:DeleteRoomMembership", "DeleteSipMediaApplication": "chime:DeleteSipMediaApplication", "DeleteSipRule": "chime:DeleteSipRule", "DeleteVoiceConnector": "chime:DeleteVoiceConnector", "DeleteVoiceConnectorEmergencyCallingConfiguration": "chime:DeleteVoiceConnectorEmergencyCallingConfiguration", "DeleteVoiceConnectorGroup": "chime:DeleteVoiceConnectorGroup", "DeleteVoiceConnectorOrigination": "chime:DeleteVoiceConnectorOrigination", "DeleteVoiceConnectorProxy": "chime:DeleteVoiceConnectorProxy", "DeleteVoiceConnectorStreamingConfiguration": "chime:DeleteVoiceConnectorStreamingConfiguration", "DeleteVoiceConnectorTermination": "chime:DeleteVoiceConnectorTermination", "DeleteVoiceConnectorTerminationCredentials": "chime:DeleteVoiceConnectorTerminationCredentials", "DescribeAppInstance": "chime:DescribeAppInstance", "DescribeAppInstanceAdmin": "chime:DescribeAppInstanceAdmin", "DescribeAppInstanceUser": "chime:DescribeAppInstanceUser", "DescribeChannel": "chime:DescribeChannel", "DescribeChannelBan": "chime:DescribeChannelBan", "DescribeChannelMembership": "chime:DescribeChannelMembership", "DescribeChannelMembershipForAppInstanceUser": "chime:DescribeChannelMembershipForAppInstanceUser", "DescribeChannelModeratedByAppInstanceUser": "chime:DescribeChannelModeratedByAppInstanceUser", "DescribeChannelModerator": "chime:DescribeChannelModerator", "DisassociatePhoneNumberFromUser": "chime:DisassociatePhoneNumberFromUser", "DisassociatePhoneNumbersFromVoiceConnector": "chime:DisassociatePhoneNumbersFromVoiceConnector", "DisassociatePhoneNumbersFromVoiceConnectorGroup": "chime:DisassociatePhoneNumbersFromVoiceConnectorGroup", "DisassociateSigninDelegateGroupsFromAccount": "chime:DisassociateSigninDelegateGroupsFromAccount", "GetAccount": "chime:GetAccount", "GetAccountSettings": "chime:GetAccountSettings", "GetAppInstanceRetentionSettings": "chime:GetAppInstanceRetentionSettings", "GetAppInstanceStreamingConfigurations": "chime:GetAppInstanceStreamingConfigurations", "GetAttendee": "chime:GetAttendee", "GetBot": "chime:GetBot", "GetChannelMessage": "chime:GetChannelMessage", "GetEventsConfiguration": "chime:GetEventsConfiguration", "GetGlobalSettings": "chime:GetGlobalSettings", "GetMediaCapturePipeline": "chime:GetMediaCapturePipeline", "GetMeeting": "chime:GetMeeting", "GetMessagingSessionEndpoint": "chime:GetMessagingSessionEndpoint", "GetPhoneNumber": "chime:GetPhoneNumber", "GetPhoneNumberOrder": "chime:GetPhoneNumberOrder", "GetPhoneNumberSettings": "chime:GetPhoneNumberSettings", "GetProxySession": "chime:GetProxySession", "GetRetentionSettings": "chime:GetRetentionSettings", "GetRoom": "chime:GetRoom", "GetSipMediaApplication": "chime:GetSipMediaApplication", "GetSipMediaApplicationLoggingConfiguration": "chime:GetSipMediaApplicationLoggingConfiguration", "GetSipRule": "chime:GetSipRule", "GetUser": "chime:GetUser", "GetUserSettings": "chime:GetUserSettings", "GetVoiceConnector": "chime:GetVoiceConnector", "GetVoiceConnectorEmergencyCallingConfiguration": "chime:GetVoiceConnectorEmergencyCallingConfiguration", "GetVoiceConnectorGroup": "chime:GetVoiceConnectorGroup", "GetVoiceConnectorLoggingConfiguration": "chime:GetVoiceConnectorLoggingConfiguration", "GetVoiceConnectorOrigination": "chime:GetVoiceConnectorOrigination", "GetVoiceConnectorProxy": "chime:GetVoiceConnectorProxy", "GetVoiceConnectorStreamingConfiguration": "chime:GetVoiceConnectorStreamingConfiguration", "GetVoiceConnectorTermination": "chime:GetVoiceConnectorTermination", "GetVoiceConnectorTerminationHealth": "chime:GetVoiceConnectorTerminationHealth", "InviteUsers": "chime:InviteUsers", "ListAccounts": "chime:ListAccounts", "ListAppInstanceAdmins": "chime:ListAppInstanceAdmins", "ListAppInstanceUsers": "chime:ListAppInstanceUsers", "ListAppInstances": "chime:ListAppInstances", "ListAttendeeTags": "chime:ListAttendeeTags", "ListAttendees": "chime:ListAttendees", "ListBots": "chime:ListBots", "ListChannelBans": "chime:ListChannelBans", "ListChannelMemberships": "chime:ListChannelMemberships", "ListChannelMembershipsForAppInstanceUser": "chime:ListChannelMembershipsForAppInstanceUser", "ListChannelMessages": "chime:ListChannelMessages", "ListChannelModerators": "chime:ListChannelModerators", "ListChannels": "chime:ListChannels", "ListChannelsModeratedByAppInstanceUser": "chime:ListChannelsModeratedByAppInstanceUser", "ListMediaCapturePipelines": "chime:ListMediaCapturePipelines", "ListMeetingTags": "chime:ListMeetingTags", "ListMeetings": "chime:ListMeetings", "ListPhoneNumberOrders": "chime:ListPhoneNumberOrders", "ListPhoneNumbers": "chime:ListPhoneNumbers", "ListProxySessions": "chime:ListProxySessions", "ListRoomMemberships": "chime:ListRoomMemberships", "ListRooms": "chime:ListRooms", "ListSipMediaApplications": "chime:ListSipMediaApplications", "ListSipRules": "chime:ListSipRules", "ListSupportedPhoneNumberCountries": "chime:ListSupportedPhoneNumberCountries", "ListTagsForResource": "chime:ListTagsForResource", "ListUsers": "chime:ListUsers", "ListVoiceConnectorGroups": "chime:ListVoiceConnectorGroups", "ListVoiceConnectorTerminationCredentials": "chime:ListVoiceConnectorTerminationCredentials", "ListVoiceConnectors": "chime:ListVoiceConnectors", "LogoutUser": "chime:LogoutUser", "PutAppInstanceRetentionSettings": "chime:PutAppInstanceRetentionSettings", "PutAppInstanceStreamingConfigurations": "chime:PutAppInstanceStreamingConfigurations", "PutEventsConfiguration": "chime:PutEventsConfiguration", "PutRetentionSettings": "chime:PutRetentionSettings", "PutSipMediaApplicationLoggingConfiguration": "chime:PutSipMediaApplicationLoggingConfiguration", "PutVoiceConnectorEmergencyCallingConfiguration": "chime:PutVoiceConnectorEmergencyCallingConfiguration", "PutVoiceConnectorLoggingConfiguration": "chime:PutVoiceConnectorLoggingConfiguration", "PutVoiceConnectorOrigination": "chime:PutVoiceConnectorOrigination", "PutVoiceConnectorProxy": "chime:PutVoiceConnectorProxy", "PutVoiceConnectorStreamingConfiguration": "chime:PutVoiceConnectorStreamingConfiguration", "PutVoiceConnectorTermination": "chime:PutVoiceConnectorTermination", "PutVoiceConnectorTerminationCredentials": "chime:PutVoiceConnectorTerminationCredentials", "RedactChannelMessage": "chime:RedactChannelMessage", "RedactConversationMessage": "chime:RedactConversationMessage", "RedactRoomMessage": "chime:RedactRoomMessage", "RegenerateSecurityToken": "chime:RegenerateSecurityToken", "ResetPersonalPIN": "chime:ResetPersonalPIN", "RestorePhoneNumber": "chime:RestorePhoneNumber", "SearchAvailablePhoneNumbers": "chime:SearchAvailablePhoneNumbers", "SendChannelMessage": "chime:SendChannelMessage", "StartMeetingTranscription": "chime:StartMeetingTranscription", "StopMeetingTranscription": "chime:StopMeetingTranscription", "TagAttendee": "chime:TagAttendee", "TagMeeting": "chime:TagMeeting", "TagResource": "chime:TagResource", "UntagAttendee": "chime:UntagAttendee", "UntagMeeting": "chime:UntagMeeting", "UntagResource": "chime:UntagResource", "UpdateAccount": "chime:UpdateAccount", "UpdateAccountSettings": "chime:UpdateAccountSettings", "UpdateAppInstance": "chime:UpdateAppInstance", "UpdateAppInstanceUser": "chime:UpdateAppInstanceUser", "UpdateBot": "chime:UpdateBot", "UpdateChannel": "chime:UpdateChannel", "UpdateChannelMessage": "chime:UpdateChannelMessage", "UpdateChannelReadMarker": "chime:UpdateChannelReadMarker", "UpdateGlobalSettings": "chime:UpdateGlobalSettings", "UpdatePhoneNumber": "chime:UpdatePhoneNumber", "UpdatePhoneNumberSettings": "chime:UpdatePhoneNumberSettings", "UpdateProxySession": "chime:UpdateProxySession", "UpdateRoom": "chime:UpdateRoom", "UpdateRoomMembership": "chime:UpdateRoomMembership", "UpdateSipMediaApplication": "chime:UpdateSipMediaApplication", "UpdateSipMediaApplicationCall": "chime:UpdateSipMediaApplicationCall", "UpdateSipRule": "chime:UpdateSipRule", "UpdateUser": "chime:UpdateUser", "UpdateUserSettings": "chime:UpdateUserSettings", "UpdateVoiceConnector": "chime:UpdateVoiceConnector", "UpdateVoiceConnectorGroup": "chime:UpdateVoiceConnectorGroup" }, "clouddirectory": { "AddFacetToObject": "clouddirectory:AddFacetToObject", "ApplySchema": "clouddirectory:ApplySchema", "AttachObject": "clouddirectory:AttachObject", "AttachPolicy": "clouddirectory:AttachPolicy", "AttachToIndex": "clouddirectory:AttachToIndex", "AttachTypedLink": "clouddirectory:AttachTypedLink", "BatchRead": "clouddirectory:BatchRead", "BatchWrite": "clouddirectory:BatchWrite", "CreateDirectory": "clouddirectory:CreateDirectory", "CreateFacet": "clouddirectory:CreateFacet", "CreateIndex": "clouddirectory:CreateIndex", "CreateObject": "clouddirectory:CreateObject", "CreateSchema": "clouddirectory:CreateSchema", "CreateTypedLinkFacet": "clouddirectory:CreateTypedLinkFacet", "DeleteDirectory": "clouddirectory:DeleteDirectory", "DeleteFacet": "clouddirectory:DeleteFacet", "DeleteObject": "clouddirectory:DeleteObject", "DeleteSchema": "clouddirectory:DeleteSchema", "DeleteTypedLinkFacet": "clouddirectory:DeleteTypedLinkFacet", "DetachFromIndex": "clouddirectory:DetachFromIndex", "DetachObject": "clouddirectory:DetachObject", "DetachPolicy": "clouddirectory:DetachPolicy", "DetachTypedLink": "clouddirectory:DetachTypedLink", "DisableDirectory": "clouddirectory:DisableDirectory", "EnableDirectory": "clouddirectory:EnableDirectory", "GetDirectory": "clouddirectory:GetDirectory", "GetFacet": "clouddirectory:GetFacet", "GetLinkAttributes": "clouddirectory:GetLinkAttributes", "GetObjectAttributes": "clouddirectory:GetObjectAttributes", "GetObjectInformation": "clouddirectory:GetObjectInformation", "GetSchemaAsJson": "clouddirectory:GetSchemaAsJson", "GetTypedLinkFacetInformation": "clouddirectory:GetTypedLinkFacetInformation", "ListAppliedSchemaArns": "clouddirectory:ListAppliedSchemaArns", "ListAttachedIndices": "clouddirectory:ListAttachedIndices", "ListDevelopmentSchemaArns": "clouddirectory:ListDevelopmentSchemaArns", "ListDirectories": "clouddirectory:ListDirectories", "ListFacetAttributes": "clouddirectory:ListFacetAttributes", "ListFacetNames": "clouddirectory:ListFacetNames", "ListIncomingTypedLinks": "clouddirectory:ListIncomingTypedLinks", "ListIndex": "clouddirectory:ListIndex", "ListManagedSchemaArns": "clouddirectory:ListManagedSchemaArns", "ListObjectAttributes": "clouddirectory:ListObjectAttributes", "ListObjectChildren": "clouddirectory:ListObjectChildren", "ListObjectParentPaths": "clouddirectory:ListObjectParentPaths", "ListObjectParents": "clouddirectory:ListObjectParents", "ListObjectPolicies": "clouddirectory:ListObjectPolicies", "ListOutgoingTypedLinks": "clouddirectory:ListOutgoingTypedLinks", "ListPolicyAttachments": "clouddirectory:ListPolicyAttachments", "ListPublishedSchemaArns": "clouddirectory:ListPublishedSchemaArns", "ListTagsForResource": "clouddirectory:ListTagsForResource", "ListTypedLinkFacetAttributes": "clouddirectory:ListTypedLinkFacetAttributes", "ListTypedLinkFacetNames": "clouddirectory:ListTypedLinkFacetNames", "LookupPolicy": "clouddirectory:LookupPolicy", "PublishSchema": "clouddirectory:PublishSchema", "PutSchemaFromJson": "clouddirectory:PutSchemaFromJson", "RemoveFacetFromObject": "clouddirectory:RemoveFacetFromObject", "TagResource": "clouddirectory:TagResource", "UntagResource": "clouddirectory:UntagResource", "UpdateFacet": "clouddirectory:UpdateFacet", "UpdateLinkAttributes": "clouddirectory:UpdateLinkAttributes", "UpdateObjectAttributes": "clouddirectory:UpdateObjectAttributes", "UpdateSchema": "clouddirectory:UpdateSchema", "UpdateTypedLinkFacet": "clouddirectory:UpdateTypedLinkFacet" }, "cloudformation": { "ActivateType": "cloudformation:ActivateType", "BatchDescribeTypeConfigurations": "cloudformation:BatchDescribeTypeConfigurations", "CancelUpdateStack": "cloudformation:CancelUpdateStack", "ContinueUpdateRollback": "cloudformation:ContinueUpdateRollback", "CreateChangeSet": "cloudformation:CreateChangeSet", "CreateStack": "cloudformation:CreateStack", "CreateStackInstances": "cloudformation:CreateStackInstances", "CreateStackSet": "cloudformation:CreateStackSet", "DeactivateType": "cloudformation:DeactivateType", "DeleteChangeSet": "cloudformation:DeleteChangeSet", "DeleteStack": "cloudformation:DeleteStack", "DeleteStackInstances": "cloudformation:DeleteStackInstances", "DeleteStackSet": "cloudformation:DeleteStackSet", "DeregisterType": "cloudformation:DeregisterType", "DescribeAccountLimits": "cloudformation:DescribeAccountLimits", "DescribeChangeSet": "cloudformation:DescribeChangeSet", "DescribePublisher": "cloudformation:DescribePublisher", "DescribeStackDriftDetectionStatus": "cloudformation:DescribeStackDriftDetectionStatus", "DescribeStackEvents": "cloudformation:DescribeStackEvents", "DescribeStackInstance": "cloudformation:DescribeStackInstance", "DescribeStackResource": "cloudformation:DescribeStackResource", "DescribeStackResourceDrifts": "cloudformation:DescribeStackResourceDrifts", "DescribeStackResources": "cloudformation:DescribeStackResources", "DescribeStackSet": "cloudformation:DescribeStackSet", "DescribeStackSetOperation": "cloudformation:DescribeStackSetOperation", "DescribeStacks": "cloudformation:DescribeStacks", "DescribeType": "cloudformation:DescribeType", "DescribeTypeRegistration": "cloudformation:DescribeTypeRegistration", "DetectStackDrift": "cloudformation:DetectStackDrift", "DetectStackResourceDrift": "cloudformation:DetectStackResourceDrift", "DetectStackSetDrift": "cloudformation:DetectStackSetDrift", "EstimateTemplateCost": "cloudformation:EstimateTemplateCost", "ExecuteChangeSet": "cloudformation:ExecuteChangeSet", "GetStackPolicy": "cloudformation:GetStackPolicy", "GetTemplate": "cloudformation:GetTemplate", "GetTemplateSummary": "cloudformation:GetTemplateSummary", "ImportStacksToStackSet": "cloudformation:ImportStacksToStackSet", "ListChangeSets": "cloudformation:ListChangeSets", "ListExports": "cloudformation:ListExports", "ListImports": "cloudformation:ListImports", "ListStackInstances": "cloudformation:ListStackInstances", "ListStackResources": "cloudformation:ListStackResources", "ListStackSetOperationResults": "cloudformation:ListStackSetOperationResults", "ListStackSetOperations": "cloudformation:ListStackSetOperations", "ListStackSets": "cloudformation:ListStackSets", "ListStacks": "cloudformation:ListStacks", "ListTypeRegistrations": "cloudformation:ListTypeRegistrations", "ListTypeVersions": "cloudformation:ListTypeVersions", "ListTypes": "cloudformation:ListTypes", "PublishType": "cloudformation:PublishType", "RecordHandlerProgress": "cloudformation:RecordHandlerProgress", "RegisterPublisher": "cloudformation:RegisterPublisher", "RegisterType": "cloudformation:RegisterType", "SetStackPolicy": "cloudformation:SetStackPolicy", "SetTypeConfiguration": "cloudformation:SetTypeConfiguration", "SetTypeDefaultVersion": "cloudformation:SetTypeDefaultVersion", "SignalResource": "cloudformation:SignalResource", "StopStackSetOperation": "cloudformation:StopStackSetOperation", "TestType": "cloudformation:TestType", "UpdateStack": "cloudformation:UpdateStack", "UpdateStackInstances": "cloudformation:UpdateStackInstances", "UpdateStackSet": "cloudformation:UpdateStackSet", "UpdateTerminationProtection": "cloudformation:UpdateTerminationProtection", "ValidateTemplate": "cloudformation:ValidateTemplate" }, "cloudfront": { "AssociateAlias": "cloudfront:AssociateAlias", "CreateCachePolicy": "cloudfront:CreateCachePolicy", "CreateCloudFrontOriginAccessIdentity": "cloudfront:CreateCloudFrontOriginAccessIdentity", "CreateDistribution": "cloudfront:CreateDistribution", "CreateDistributionWithTags": "cloudfront:CreateDistributionWithTags", "CreateFieldLevelEncryptionConfig": "cloudfront:CreateFieldLevelEncryptionConfig", "CreateFieldLevelEncryptionProfile": "cloudfront:CreateFieldLevelEncryptionProfile", "CreateFunction": "cloudfront:CreateFunction", "CreateInvalidation": "cloudfront:CreateInvalidation", "CreateKeyGroup": "cloudfront:CreateKeyGroup", "CreateMonitoringSubscription": "cloudfront:CreateMonitoringSubscription", "CreateOriginRequestPolicy": "cloudfront:CreateOriginRequestPolicy", "CreatePublicKey": "cloudfront:CreatePublicKey", "CreateRealtimeLogConfig": "cloudfront:CreateRealtimeLogConfig", "CreateResponseHeadersPolicy": "cloudfront:CreateResponseHeadersPolicy", "CreateStreamingDistribution": "cloudfront:CreateStreamingDistribution", "CreateStreamingDistributionWithTags": "cloudfront:CreateStreamingDistributionWithTags", "DeleteCachePolicy": "cloudfront:DeleteCachePolicy", "DeleteCloudFrontOriginAccessIdentity": "cloudfront:DeleteCloudFrontOriginAccessIdentity", "DeleteDistribution": "cloudfront:DeleteDistribution", "DeleteFieldLevelEncryptionConfig": "cloudfront:DeleteFieldLevelEncryptionConfig", "DeleteFieldLevelEncryptionProfile": "cloudfront:DeleteFieldLevelEncryptionProfile", "DeleteFunction": "cloudfront:DeleteFunction", "DeleteKeyGroup": "cloudfront:DeleteKeyGroup", "DeleteMonitoringSubscription": "cloudfront:DeleteMonitoringSubscription", "DeleteOriginRequestPolicy": "cloudfront:DeleteOriginRequestPolicy", "DeletePublicKey": "cloudfront:DeletePublicKey", "DeleteRealtimeLogConfig": "cloudfront:DeleteRealtimeLogConfig", "DeleteResponseHeadersPolicy": "cloudfront:DeleteResponseHeadersPolicy", "DeleteStreamingDistribution": "cloudfront:DeleteStreamingDistribution", "DescribeFunction": "cloudfront:DescribeFunction", "GetCachePolicy": "cloudfront:GetCachePolicy", "GetCachePolicyConfig": "cloudfront:GetCachePolicyConfig", "GetCloudFrontOriginAccessIdentity": "cloudfront:GetCloudFrontOriginAccessIdentity", "GetCloudFrontOriginAccessIdentityConfig": "cloudfront:GetCloudFrontOriginAccessIdentityConfig", "GetDistribution": "cloudfront:GetDistribution", "GetDistributionConfig": "cloudfront:GetDistributionConfig", "GetFieldLevelEncryption": "cloudfront:GetFieldLevelEncryption", "GetFieldLevelEncryptionConfig": "cloudfront:GetFieldLevelEncryptionConfig", "GetFieldLevelEncryptionProfile": "cloudfront:GetFieldLevelEncryptionProfile", "GetFieldLevelEncryptionProfileConfig": "cloudfront:GetFieldLevelEncryptionProfileConfig", "GetFunction": "cloudfront:GetFunction", "GetInvalidation": "cloudfront:GetInvalidation", "GetKeyGroup": "cloudfront:GetKeyGroup", "GetKeyGroupConfig": "cloudfront:GetKeyGroupConfig", "GetMonitoringSubscription": "cloudfront:GetMonitoringSubscription", "GetOriginRequestPolicy": "cloudfront:GetOriginRequestPolicy", "GetOriginRequestPolicyConfig": "cloudfront:GetOriginRequestPolicyConfig", "GetPublicKey": "cloudfront:GetPublicKey", "GetPublicKeyConfig": "cloudfront:GetPublicKeyConfig", "GetRealtimeLogConfig": "cloudfront:GetRealtimeLogConfig", "GetResponseHeadersPolicy": "cloudfront:GetResponseHeadersPolicy", "GetResponseHeadersPolicyConfig": "cloudfront:GetResponseHeadersPolicyConfig", "GetStreamingDistribution": "cloudfront:GetStreamingDistribution", "GetStreamingDistributionConfig": "cloudfront:GetStreamingDistributionConfig", "ListCachePolicies": "cloudfront:ListCachePolicies", "ListCloudFrontOriginAccessIdentities": "cloudfront:ListCloudFrontOriginAccessIdentities", "ListConflictingAliases": "cloudfront:ListConflictingAliases", "ListDistributions": "cloudfront:ListDistributions", "ListDistributionsByCachePolicyId": "cloudfront:ListDistributionsByCachePolicyId", "ListDistributionsByKeyGroup": "cloudfront:ListDistributionsByKeyGroup", "ListDistributionsByOriginRequestPolicyId": "cloudfront:ListDistributionsByOriginRequestPolicyId", "ListDistributionsByRealtimeLogConfig": "cloudfront:ListDistributionsByRealtimeLogConfig", "ListDistributionsByResponseHeadersPolicyId": "cloudfront:ListDistributionsByResponseHeadersPolicyId", "ListDistributionsByWebACLId": "cloudfront:ListDistributionsByWebACLId", "ListFieldLevelEncryptionConfigs": "cloudfront:ListFieldLevelEncryptionConfigs", "ListFieldLevelEncryptionProfiles": "cloudfront:ListFieldLevelEncryptionProfiles", "ListFunctions": "cloudfront:ListFunctions", "ListInvalidations": "cloudfront:ListInvalidations", "ListKeyGroups": "cloudfront:ListKeyGroups", "ListOriginRequestPolicies": "cloudfront:ListOriginRequestPolicies", "ListPublicKeys": "cloudfront:ListPublicKeys", "ListRealtimeLogConfigs": "cloudfront:ListRealtimeLogConfigs", "ListResponseHeadersPolicies": "cloudfront:ListResponseHeadersPolicies", "ListStreamingDistributions": "cloudfront:ListStreamingDistributions", "ListTagsForResource": "cloudfront:ListTagsForResource", "PublishFunction": "cloudfront:PublishFunction", "TagResource": "cloudfront:TagResource", "TestFunction": "cloudfront:TestFunction", "UntagResource": "cloudfront:UntagResource", "UpdateCachePolicy": "cloudfront:UpdateCachePolicy", "UpdateCloudFrontOriginAccessIdentity": "cloudfront:UpdateCloudFrontOriginAccessIdentity", "UpdateDistribution": "cloudfront:UpdateDistribution", "UpdateFieldLevelEncryptionConfig": "cloudfront:UpdateFieldLevelEncryptionConfig", "UpdateFieldLevelEncryptionProfile": "cloudfront:UpdateFieldLevelEncryptionProfile", "UpdateFunction": "cloudfront:UpdateFunction", "UpdateKeyGroup": "cloudfront:UpdateKeyGroup", "UpdateOriginRequestPolicy": "cloudfront:UpdateOriginRequestPolicy", "UpdatePublicKey": "cloudfront:UpdatePublicKey", "UpdateRealtimeLogConfig": "cloudfront:UpdateRealtimeLogConfig", "UpdateResponseHeadersPolicy": "cloudfront:UpdateResponseHeadersPolicy", "UpdateStreamingDistribution": "cloudfront:UpdateStreamingDistribution" }, "cloudhsm": { "AddTagsToResource": "cloudhsm:AddTagsToResource", "CreateHapg": "cloudhsm:CreateHapg", "CreateHsm": "cloudhsm:CreateHsm", "CreateLunaClient": "cloudhsm:CreateLunaClient", "DeleteHapg": "cloudhsm:DeleteHapg", "DeleteHsm": "cloudhsm:DeleteHsm", "DeleteLunaClient": "cloudhsm:DeleteLunaClient", "DescribeHapg": "cloudhsm:DescribeHapg", "DescribeHsm": "cloudhsm:DescribeHsm", "DescribeLunaClient": "cloudhsm:DescribeLunaClient", "GetConfig": "cloudhsm:GetConfig", "ListAvailableZones": "cloudhsm:ListAvailableZones", "ListHapgs": "cloudhsm:ListHapgs", "ListHsms": "cloudhsm:ListHsms", "ListLunaClients": "cloudhsm:ListLunaClients", "ListTagsForResource": "cloudhsm:ListTagsForResource", "ModifyHapg": "cloudhsm:ModifyHapg", "ModifyHsm": "cloudhsm:ModifyHsm", "ModifyLunaClient": "cloudhsm:ModifyLunaClient", "RemoveTagsFromResource": "cloudhsm:RemoveTagsFromResource" }, "cloudhsmv2": { "CopyBackupToRegion": "cloudhsm:CopyBackupToRegion", "CreateCluster": "cloudhsm:CreateCluster", "CreateHsm": "cloudhsm:CreateHsm", "DeleteBackup": "cloudhsm:DeleteBackup", "DeleteCluster": "cloudhsm:DeleteCluster", "DeleteHsm": "cloudhsm:DeleteHsm", "DescribeBackups": "cloudhsm:DescribeBackups", "DescribeClusters": "cloudhsm:DescribeClusters", "InitializeCluster": "cloudhsm:InitializeCluster", "ListTags": "cloudhsm:ListTags", "ModifyBackupAttributes": "cloudhsm:ModifyBackupAttributes", "ModifyCluster": "cloudhsm:ModifyCluster", "RestoreBackup": "cloudhsm:RestoreBackup", "TagResource": "cloudhsm:TagResource", "UntagResource": "cloudhsm:UntagResource" }, "cloudsearch": { "BuildSuggesters": "cloudsearch:BuildSuggesters", "CreateDomain": "cloudsearch:CreateDomain", "DefineAnalysisScheme": "cloudsearch:DefineAnalysisScheme", "DefineExpression": "cloudsearch:DefineExpression", "DefineIndexField": "cloudsearch:DefineIndexField", "DefineSuggester": "cloudsearch:DefineSuggester", "DeleteAnalysisScheme": "cloudsearch:DeleteAnalysisScheme", "DeleteDomain": "cloudsearch:DeleteDomain", "DeleteExpression": "cloudsearch:DeleteExpression", "DeleteIndexField": "cloudsearch:DeleteIndexField", "DeleteSuggester": "cloudsearch:DeleteSuggester", "DescribeAnalysisSchemes": "cloudsearch:DescribeAnalysisSchemes", "DescribeAvailabilityOptions": "cloudsearch:DescribeAvailabilityOptions", "DescribeDomainEndpointOptions": "cloudsearch:DescribeDomainEndpointOptions", "DescribeDomains": "cloudsearch:DescribeDomains", "DescribeExpressions": "cloudsearch:DescribeExpressions", "DescribeIndexFields": "cloudsearch:DescribeIndexFields", "DescribeScalingParameters": "cloudsearch:DescribeScalingParameters", "DescribeServiceAccessPolicies": "cloudsearch:DescribeServiceAccessPolicies", "DescribeSuggesters": "cloudsearch:DescribeSuggesters", "IndexDocuments": "cloudsearch:IndexDocuments", "ListDomainNames": "cloudsearch:ListDomainNames", "UpdateAvailabilityOptions": "cloudsearch:UpdateAvailabilityOptions", "UpdateDomainEndpointOptions": "cloudsearch:UpdateDomainEndpointOptions", "UpdateScalingParameters": "cloudsearch:UpdateScalingParameters", "UpdateServiceAccessPolicies": "cloudsearch:UpdateServiceAccessPolicies" }, "cloudtrail": { "AddTags": "cloudtrail:AddTags", "CreateTrail": "cloudtrail:CreateTrail", "DeleteTrail": "cloudtrail:DeleteTrail", "DescribeTrails": "cloudtrail:DescribeTrails", "GetEventSelectors": "cloudtrail:GetEventSelectors", "GetInsightSelectors": "cloudtrail:GetInsightSelectors", "GetTrail": "cloudtrail:GetTrail", "GetTrailStatus": "cloudtrail:GetTrailStatus", "ListPublicKeys": "cloudtrail:ListPublicKeys", "ListTags": "cloudtrail:ListTags", "ListTrails": "cloudtrail:ListTrails", "LookupEvents": "cloudtrail:LookupEvents", "PutEventSelectors": "cloudtrail:PutEventSelectors", "PutInsightSelectors": "cloudtrail:PutInsightSelectors", "RemoveTags": "cloudtrail:RemoveTags", "StartLogging": "cloudtrail:StartLogging", "StopLogging": "cloudtrail:StopLogging", "UpdateTrail": "cloudtrail:UpdateTrail" }, "cloudwatch": { "DeleteAlarms": "cloudwatch:DeleteAlarms", "DeleteAnomalyDetector": "cloudwatch:DeleteAnomalyDetector", "DeleteDashboards": "cloudwatch:DeleteDashboards", "DeleteInsightRules": "cloudwatch:DeleteInsightRules", "DeleteMetricStream": "cloudwatch:DeleteMetricStream", "DescribeAlarmHistory": "cloudwatch:DescribeAlarmHistory", "DescribeAlarms": "cloudwatch:DescribeAlarms", "DescribeAlarmsForMetric": "cloudwatch:DescribeAlarmsForMetric", "DescribeAnomalyDetectors": "cloudwatch:DescribeAnomalyDetectors", "DescribeInsightRules": "cloudwatch:DescribeInsightRules", "DisableAlarmActions": "cloudwatch:DisableAlarmActions", "DisableInsightRules": "cloudwatch:DisableInsightRules", "EnableAlarmActions": "cloudwatch:EnableAlarmActions", "EnableInsightRules": "cloudwatch:EnableInsightRules", "GetDashboard": "cloudwatch:GetDashboard", "GetInsightRuleReport": "cloudwatch:GetInsightRuleReport", "GetMetricData": "cloudwatch:GetMetricData", "GetMetricStatistics": "cloudwatch:GetMetricStatistics", "GetMetricStream": "cloudwatch:GetMetricStream", "GetMetricWidgetImage": "cloudwatch:GetMetricWidgetImage", "ListDashboards": "cloudwatch:ListDashboards", "ListMetricStreams": "cloudwatch:ListMetricStreams", "ListMetrics": "cloudwatch:ListMetrics", "ListTagsForResource": "cloudwatch:ListTagsForResource", "PutAnomalyDetector": "cloudwatch:PutAnomalyDetector", "PutCompositeAlarm": "cloudwatch:PutCompositeAlarm", "PutDashboard": "cloudwatch:PutDashboard", "PutInsightRule": "cloudwatch:PutInsightRule", "PutMetricAlarm": "cloudwatch:PutMetricAlarm", "PutMetricData": "cloudwatch:PutMetricData", "PutMetricStream": "cloudwatch:PutMetricStream", "SetAlarmState": "cloudwatch:SetAlarmState", "StartMetricStreams": "cloudwatch:StartMetricStreams", "StopMetricStreams": "cloudwatch:StopMetricStreams", "TagResource": "cloudwatch:TagResource", "UntagResource": "cloudwatch:UntagResource" }, "codeartifact": { "AssociateExternalConnection": "codeartifact:AssociateExternalConnection", "CopyPackageVersions": "codeartifact:CopyPackageVersions", "CreateDomain": "codeartifact:CreateDomain", "CreateRepository": "codeartifact:CreateRepository", "DeleteDomain": "codeartifact:DeleteDomain", "DeleteDomainPermissionsPolicy": "codeartifact:DeleteDomainPermissionsPolicy", "DeletePackageVersions": "codeartifact:DeletePackageVersions", "DeleteRepository": "codeartifact:DeleteRepository", "DeleteRepositoryPermissionsPolicy": "codeartifact:DeleteRepositoryPermissionsPolicy", "DescribeDomain": "codeartifact:DescribeDomain", "DescribePackageVersion": "codeartifact:DescribePackageVersion", "DescribeRepository": "codeartifact:DescribeRepository", "DisassociateExternalConnection": "codeartifact:DisassociateExternalConnection", "DisposePackageVersions": "codeartifact:DisposePackageVersions", "GetAuthorizationToken": "codeartifact:GetAuthorizationToken", "GetDomainPermissionsPolicy": "codeartifact:GetDomainPermissionsPolicy", "GetPackageVersionAsset": "codeartifact:GetPackageVersionAsset", "GetPackageVersionReadme": "codeartifact:GetPackageVersionReadme", "GetRepositoryEndpoint": "codeartifact:GetRepositoryEndpoint", "GetRepositoryPermissionsPolicy": "codeartifact:GetRepositoryPermissionsPolicy", "ListDomains": "codeartifact:ListDomains", "ListPackageVersionAssets": "codeartifact:ListPackageVersionAssets", "ListPackageVersionDependencies": "codeartifact:ListPackageVersionDependencies", "ListPackageVersions": "codeartifact:ListPackageVersions", "ListPackages": "codeartifact:ListPackages", "ListRepositories": "codeartifact:ListRepositories", "ListRepositoriesInDomain": "codeartifact:ListRepositoriesInDomain", "ListTagsForResource": "codeartifact:ListTagsForResource", "PutDomainPermissionsPolicy": "codeartifact:PutDomainPermissionsPolicy", "PutRepositoryPermissionsPolicy": "codeartifact:PutRepositoryPermissionsPolicy", "TagResource": "codeartifact:TagResource", "UntagResource": "codeartifact:UntagResource", "UpdatePackageVersionsStatus": "codeartifact:UpdatePackageVersionsStatus", "UpdateRepository": "codeartifact:UpdateRepository" }, "codebuild": { "BatchDeleteBuilds": "codebuild:BatchDeleteBuilds", "BatchGetBuildBatches": "codebuild:BatchGetBuildBatches", "BatchGetBuilds": "codebuild:BatchGetBuilds", "BatchGetProjects": "codebuild:BatchGetProjects", "BatchGetReportGroups": "codebuild:BatchGetReportGroups", "BatchGetReports": "codebuild:BatchGetReports", "CreateProject": "codebuild:CreateProject", "CreateReportGroup": "codebuild:CreateReportGroup", "CreateWebhook": "codebuild:CreateWebhook", "DeleteBuildBatch": "codebuild:DeleteBuildBatch", "DeleteProject": "codebuild:DeleteProject", "DeleteReport": "codebuild:DeleteReport", "DeleteReportGroup": "codebuild:DeleteReportGroup", "DeleteResourcePolicy": "codebuild:DeleteResourcePolicy", "DeleteSourceCredentials": "codebuild:DeleteSourceCredentials", "DeleteWebhook": "codebuild:DeleteWebhook", "DescribeCodeCoverages": "codebuild:DescribeCodeCoverages", "DescribeTestCases": "codebuild:DescribeTestCases", "GetReportGroupTrend": "codebuild:GetReportGroupTrend", "GetResourcePolicy": "codebuild:GetResourcePolicy", "ImportSourceCredentials": "codebuild:ImportSourceCredentials", "InvalidateProjectCache": "codebuild:InvalidateProjectCache", "ListBuildBatches": "codebuild:ListBuildBatches", "ListBuildBatchesForProject": "codebuild:ListBuildBatchesForProject", "ListBuilds": "codebuild:ListBuilds", "ListBuildsForProject": "codebuild:ListBuildsForProject", "ListCuratedEnvironmentImages": "codebuild:ListCuratedEnvironmentImages", "ListProjects": "codebuild:ListProjects", "ListReportGroups": "codebuild:ListReportGroups", "ListReports": "codebuild:ListReports", "ListReportsForReportGroup": "codebuild:ListReportsForReportGroup", "ListSharedProjects": "codebuild:ListSharedProjects", "ListSharedReportGroups": "codebuild:ListSharedReportGroups", "ListSourceCredentials": "codebuild:ListSourceCredentials", "PutResourcePolicy": "codebuild:PutResourcePolicy", "RetryBuild": "codebuild:RetryBuild", "RetryBuildBatch": "codebuild:RetryBuildBatch", "StartBuild": "codebuild:StartBuild", "StartBuildBatch": "codebuild:StartBuildBatch", "StopBuild": "codebuild:StopBuild", "StopBuildBatch": "codebuild:StopBuildBatch", "UpdateProject": "codebuild:UpdateProject", "UpdateProjectVisibility": "codebuild:UpdateProjectVisibility", "UpdateReportGroup": "codebuild:UpdateReportGroup", "UpdateWebhook": "codebuild:UpdateWebhook" }, "codecommit": { "AssociateApprovalRuleTemplateWithRepository": "codecommit:AssociateApprovalRuleTemplateWithRepository", "BatchAssociateApprovalRuleTemplateWithRepositories": "codecommit:BatchAssociateApprovalRuleTemplateWithRepositories", "BatchDescribeMergeConflicts": "codecommit:BatchDescribeMergeConflicts", "BatchDisassociateApprovalRuleTemplateFromRepositories": "codecommit:BatchDisassociateApprovalRuleTemplateFromRepositories", "BatchGetCommits": "codecommit:BatchGetCommits", "BatchGetRepositories": "codecommit:BatchGetRepositories", "CreateApprovalRuleTemplate": "codecommit:CreateApprovalRuleTemplate", "CreateBranch": "codecommit:CreateBranch", "CreateCommit": "codecommit:CreateCommit", "CreatePullRequest": "codecommit:CreatePullRequest", "CreatePullRequestApprovalRule": "codecommit:CreatePullRequestApprovalRule", "CreateRepository": "codecommit:CreateRepository", "CreateUnreferencedMergeCommit": "codecommit:CreateUnreferencedMergeCommit", "DeleteApprovalRuleTemplate": "codecommit:DeleteApprovalRuleTemplate", "DeleteBranch": "codecommit:DeleteBranch", "DeleteCommentContent": "codecommit:DeleteCommentContent", "DeleteFile": "codecommit:DeleteFile", "DeletePullRequestApprovalRule": "codecommit:DeletePullRequestApprovalRule", "DeleteRepository": "codecommit:DeleteRepository", "DescribeMergeConflicts": "codecommit:DescribeMergeConflicts", "DescribePullRequestEvents": "codecommit:DescribePullRequestEvents", "DisassociateApprovalRuleTemplateFromRepository": "codecommit:DisassociateApprovalRuleTemplateFromRepository", "EvaluatePullRequestApprovalRules": "codecommit:EvaluatePullRequestApprovalRules", "GetApprovalRuleTemplate": "codecommit:GetApprovalRuleTemplate", "GetBlob": "codecommit:GetBlob", "GetBranch": "codecommit:GetBranch", "GetComment": "codecommit:GetComment", "GetCommentReactions": "codecommit:GetCommentReactions", "GetCommentsForComparedCommit": "codecommit:GetCommentsForComparedCommit", "GetCommentsForPullRequest": "codecommit:GetCommentsForPullRequest", "GetCommit": "codecommit:GetCommit", "GetDifferences": "codecommit:GetDifferences", "GetFile": "codecommit:GetFile", "GetFolder": "codecommit:GetFolder", "GetMergeCommit": "codecommit:GetMergeCommit", "GetMergeConflicts": "codecommit:GetMergeConflicts", "GetMergeOptions": "codecommit:GetMergeOptions", "GetPullRequest": "codecommit:GetPullRequest", "GetPullRequestApprovalStates": "codecommit:GetPullRequestApprovalStates", "GetPullRequestOverrideState": "codecommit:GetPullRequestOverrideState", "GetRepository": "codecommit:GetRepository", "GetRepositoryTriggers": "codecommit:GetRepositoryTriggers", "ListApprovalRuleTemplates": "codecommit:ListApprovalRuleTemplates", "ListAssociatedApprovalRuleTemplatesForRepository": "codecommit:ListAssociatedApprovalRuleTemplatesForRepository", "ListBranches": "codecommit:ListBranches", "ListPullRequests": "codecommit:ListPullRequests", "ListRepositories": "codecommit:ListRepositories", "ListRepositoriesForApprovalRuleTemplate": "codecommit:ListRepositoriesForApprovalRuleTemplate", "ListTagsForResource": "codecommit:ListTagsForResource", "MergeBranchesByFastForward": "codecommit:MergeBranchesByFastForward", "MergeBranchesBySquash": "codecommit:MergeBranchesBySquash", "MergeBranchesByThreeWay": "codecommit:MergeBranchesByThreeWay", "MergePullRequestByFastForward": "codecommit:MergePullRequestByFastForward", "MergePullRequestBySquash": "codecommit:MergePullRequestBySquash", "MergePullRequestByThreeWay": "codecommit:MergePullRequestByThreeWay", "OverridePullRequestApprovalRules": "codecommit:OverridePullRequestApprovalRules", "PostCommentForComparedCommit": "codecommit:PostCommentForComparedCommit", "PostCommentForPullRequest": "codecommit:PostCommentForPullRequest", "PostCommentReply": "codecommit:PostCommentReply", "PutCommentReaction": "codecommit:PutCommentReaction", "PutFile": "codecommit:PutFile", "PutRepositoryTriggers": "codecommit:PutRepositoryTriggers", "TagResource": "codecommit:TagResource", "TestRepositoryTriggers": "codecommit:TestRepositoryTriggers", "UntagResource": "codecommit:UntagResource", "UpdateApprovalRuleTemplateContent": "codecommit:UpdateApprovalRuleTemplateContent", "UpdateApprovalRuleTemplateDescription": "codecommit:UpdateApprovalRuleTemplateDescription", "UpdateApprovalRuleTemplateName": "codecommit:UpdateApprovalRuleTemplateName", "UpdateComment": "codecommit:UpdateComment", "UpdateDefaultBranch": "codecommit:UpdateDefaultBranch", "UpdatePullRequestApprovalRuleContent": "codecommit:UpdatePullRequestApprovalRuleContent", "UpdatePullRequestApprovalState": "codecommit:UpdatePullRequestApprovalState", "UpdatePullRequestDescription": "codecommit:UpdatePullRequestDescription", "UpdatePullRequestStatus": "codecommit:UpdatePullRequestStatus", "UpdatePullRequestTitle": "codecommit:UpdatePullRequestTitle", "UpdateRepositoryDescription": "codecommit:UpdateRepositoryDescription", "UpdateRepositoryName": "codecommit:UpdateRepositoryName" }, "codedeploy": { "AddTagsToOnPremisesInstances": "codedeploy:AddTagsToOnPremisesInstances", "BatchGetApplicationRevisions": "codedeploy:BatchGetApplicationRevisions", "BatchGetApplications": "codedeploy:BatchGetApplications", "BatchGetDeploymentGroups": "codedeploy:BatchGetDeploymentGroups", "BatchGetDeploymentInstances": "codedeploy:BatchGetDeploymentInstances", "BatchGetDeploymentTargets": "codedeploy:BatchGetDeploymentTargets", "BatchGetDeployments": "codedeploy:BatchGetDeployments", "BatchGetOnPremisesInstances": "codedeploy:BatchGetOnPremisesInstances", "ContinueDeployment": "codedeploy:ContinueDeployment", "CreateApplication": "codedeploy:CreateApplication", "CreateDeployment": "codedeploy:CreateDeployment", "CreateDeploymentConfig": "codedeploy:CreateDeploymentConfig", "CreateDeploymentGroup": "codedeploy:CreateDeploymentGroup", "DeleteApplication": "codedeploy:DeleteApplication", "DeleteDeploymentConfig": "codedeploy:DeleteDeploymentConfig", "DeleteDeploymentGroup": "codedeploy:DeleteDeploymentGroup", "DeleteGitHubAccountToken": "codedeploy:DeleteGitHubAccountToken", "DeleteResourcesByExternalId": "codedeploy:DeleteResourcesByExternalId", "DeregisterOnPremisesInstance": "codedeploy:DeregisterOnPremisesInstance", "GetApplication": "codedeploy:GetApplication", "GetApplicationRevision": "codedeploy:GetApplicationRevision", "GetDeployment": "codedeploy:GetDeployment", "GetDeploymentConfig": "codedeploy:GetDeploymentConfig", "GetDeploymentGroup": "codedeploy:GetDeploymentGroup", "GetDeploymentInstance": "codedeploy:GetDeploymentInstance", "GetDeploymentTarget": "codedeploy:GetDeploymentTarget", "GetOnPremisesInstance": "codedeploy:GetOnPremisesInstance", "ListApplicationRevisions": "codedeploy:ListApplicationRevisions", "ListApplications": "codedeploy:ListApplications", "ListDeploymentConfigs": "codedeploy:ListDeploymentConfigs", "ListDeploymentGroups": "codedeploy:ListDeploymentGroups", "ListDeploymentInstances": "codedeploy:ListDeploymentInstances", "ListDeploymentTargets": "codedeploy:ListDeploymentTargets", "ListDeployments": "codedeploy:ListDeployments", "ListGitHubAccountTokenNames": "codedeploy:ListGitHubAccountTokenNames", "ListOnPremisesInstances": "codedeploy:ListOnPremisesInstances", "ListTagsForResource": "codedeploy:ListTagsForResource", "PutLifecycleEventHookExecutionStatus": "codedeploy:PutLifecycleEventHookExecutionStatus", "RegisterApplicationRevision": "codedeploy:RegisterApplicationRevision", "RegisterOnPremisesInstance": "codedeploy:RegisterOnPremisesInstance", "RemoveTagsFromOnPremisesInstances": "codedeploy:RemoveTagsFromOnPremisesInstances", "SkipWaitTimeForInstanceTermination": "codedeploy:SkipWaitTimeForInstanceTermination", "StopDeployment": "codedeploy:StopDeployment", "TagResource": "codedeploy:TagResource", "UntagResource": "codedeploy:UntagResource", "UpdateApplication": "codedeploy:UpdateApplication", "UpdateDeploymentGroup": "codedeploy:UpdateDeploymentGroup" }, "codeguru-reviewer": { "AssociateRepository": "codeguru-reviewer:AssociateRepository", "CreateCodeReview": "codeguru-reviewer:CreateCodeReview", "DescribeCodeReview": "codeguru-reviewer:DescribeCodeReview", "DescribeRecommendationFeedback": "codeguru-reviewer:DescribeRecommendationFeedback", "DescribeRepositoryAssociation": "codeguru-reviewer:DescribeRepositoryAssociation", "DisassociateRepository": "codeguru-reviewer:DisassociateRepository", "ListCodeReviews": "codeguru-reviewer:ListCodeReviews", "ListRecommendationFeedback": "codeguru-reviewer:ListRecommendationFeedback", "ListRecommendations": "codeguru-reviewer:ListRecommendations", "ListRepositoryAssociations": "codeguru-reviewer:ListRepositoryAssociations", "ListTagsForResource": "codeguru-reviewer:ListTagsForResource", "PutRecommendationFeedback": "codeguru-reviewer:PutRecommendationFeedback", "TagResource": "codeguru-reviewer:TagResource" }, "codepipeline": { "AcknowledgeJob": "codepipeline:AcknowledgeJob", "AcknowledgeThirdPartyJob": "codepipeline:AcknowledgeThirdPartyJob", "CreateCustomActionType": "codepipeline:CreateCustomActionType", "CreatePipeline": "codepipeline:CreatePipeline", "DeleteCustomActionType": "codepipeline:DeleteCustomActionType", "DeletePipeline": "codepipeline:DeletePipeline", "DeleteWebhook": "codepipeline:DeleteWebhook", "DeregisterWebhookWithThirdParty": "codepipeline:DeregisterWebhookWithThirdParty", "DisableStageTransition": "codepipeline:DisableStageTransition", "EnableStageTransition": "codepipeline:EnableStageTransition", "GetActionType": "codepipeline:GetActionType", "GetJobDetails": "codepipeline:GetJobDetails", "GetPipeline": "codepipeline:GetPipeline", "GetPipelineExecution": "codepipeline:GetPipelineExecution", "GetPipelineState": "codepipeline:GetPipelineState", "GetThirdPartyJobDetails": "codepipeline:GetThirdPartyJobDetails", "ListActionExecutions": "codepipeline:ListActionExecutions", "ListActionTypes": "codepipeline:ListActionTypes", "ListPipelineExecutions": "codepipeline:ListPipelineExecutions", "ListPipelines": "codepipeline:ListPipelines", "ListTagsForResource": "codepipeline:ListTagsForResource", "ListWebhooks": "codepipeline:ListWebhooks", "PollForJobs": "codepipeline:PollForJobs", "PollForThirdPartyJobs": "codepipeline:PollForThirdPartyJobs", "PutActionRevision": "codepipeline:PutActionRevision", "PutApprovalResult": "codepipeline:PutApprovalResult", "PutJobFailureResult": "codepipeline:PutJobFailureResult", "PutJobSuccessResult": "codepipeline:PutJobSuccessResult", "PutThirdPartyJobFailureResult": "codepipeline:PutThirdPartyJobFailureResult", "PutThirdPartyJobSuccessResult": "codepipeline:PutThirdPartyJobSuccessResult", "PutWebhook": "codepipeline:PutWebhook", "RegisterWebhookWithThirdParty": "codepipeline:RegisterWebhookWithThirdParty", "RetryStageExecution": "codepipeline:RetryStageExecution", "StartPipelineExecution": "codepipeline:StartPipelineExecution", "StopPipelineExecution": "codepipeline:StopPipelineExecution", "TagResource": "codepipeline:TagResource", "UntagResource": "codepipeline:UntagResource", "UpdateActionType": "codepipeline:UpdateActionType", "UpdatePipeline": "codepipeline:UpdatePipeline" }, "codestar": { "AssociateTeamMember": "codestar:AssociateTeamMember", "CreateProject": "codestar:CreateProject", "CreateUserProfile": "codestar:CreateUserProfile", "DeleteProject": "codestar:DeleteProject", "DeleteUserProfile": "codestar:DeleteUserProfile", "DescribeProject": "codestar:DescribeProject", "DescribeUserProfile": "codestar:DescribeUserProfile", "DisassociateTeamMember": "codestar:DisassociateTeamMember", "ListProjects": "codestar:ListProjects", "ListResources": "codestar:ListResources", "ListTagsForProject": "codestar:ListTagsForProject", "ListTeamMembers": "codestar:ListTeamMembers", "ListUserProfiles": "codestar:ListUserProfiles", "TagProject": "codestar:TagProject", "UntagProject": "codestar:UntagProject", "UpdateProject": "codestar:UpdateProject", "UpdateTeamMember": "codestar:UpdateTeamMember", "UpdateUserProfile": "codestar:UpdateUserProfile" }, "codestar-connections": { "CreateConnection": "codestar-connections:CreateConnection", "CreateHost": "codestar-connections:CreateHost", "DeleteConnection": "codestar-connections:DeleteConnection", "DeleteHost": "codestar-connections:DeleteHost", "GetConnection": "codestar-connections:GetConnection", "GetHost": "codestar-connections:GetHost", "ListConnections": "codestar-connections:ListConnections", "ListHosts": "codestar-connections:ListHosts", "ListTagsForResource": "codestar-connections:ListTagsForResource", "TagResource": "codestar-connections:TagResource", "UntagResource": "codestar-connections:UntagResource", "UpdateHost": "codestar-connections:UpdateHost" }, "codestar-notifications": { "CreateNotificationRule": "codestar-notifications:CreateNotificationRule", "DeleteNotificationRule": "codestar-notifications:DeleteNotificationRule", "DeleteTarget": "codestar-notifications:DeleteTarget", "DescribeNotificationRule": "codestar-notifications:DescribeNotificationRule", "ListEventTypes": "codestar-notifications:ListEventTypes", "ListNotificationRules": "codestar-notifications:ListNotificationRules", "ListTagsForResource": "codestar-notifications:ListTagsForResource", "ListTargets": "codestar-notifications:ListTargets", "Subscribe": "codestar-notifications:Subscribe", "TagResource": "codestar-notifications:TagResource", "Unsubscribe": "codestar-notifications:Unsubscribe", "UntagResource": "codestar-notifications:UntagResource", "UpdateNotificationRule": "codestar-notifications:UpdateNotificationRule" }, "cognito-identity": { "CreateIdentityPool": "cognito-identity:CreateIdentityPool", "DeleteIdentities": "cognito-identity:DeleteIdentities", "DeleteIdentityPool": "cognito-identity:DeleteIdentityPool", "DescribeIdentity": "cognito-identity:DescribeIdentity", "DescribeIdentityPool": "cognito-identity:DescribeIdentityPool", "GetCredentialsForIdentity": "cognito-identity:GetCredentialsForIdentity", "GetId": "cognito-identity:GetId", "GetIdentityPoolRoles": "cognito-identity:GetIdentityPoolRoles", "GetOpenIdToken": "cognito-identity:GetOpenIdToken", "GetOpenIdTokenForDeveloperIdentity": "cognito-identity:GetOpenIdTokenForDeveloperIdentity", "GetPrincipalTagAttributeMap": "cognito-identity:GetPrincipalTagAttributeMap", "ListIdentities": "cognito-identity:ListIdentities", "ListIdentityPools": "cognito-identity:ListIdentityPools", "ListTagsForResource": "cognito-identity:ListTagsForResource", "LookupDeveloperIdentity": "cognito-identity:LookupDeveloperIdentity", "MergeDeveloperIdentities": "cognito-identity:MergeDeveloperIdentities", "SetIdentityPoolRoles": "cognito-identity:SetIdentityPoolRoles", "SetPrincipalTagAttributeMap": "cognito-identity:SetPrincipalTagAttributeMap", "TagResource": "cognito-identity:TagResource", "UnlinkDeveloperIdentity": "cognito-identity:UnlinkDeveloperIdentity", "UnlinkIdentity": "cognito-identity:UnlinkIdentity", "UntagResource": "cognito-identity:UntagResource", "UpdateIdentityPool": "cognito-identity:UpdateIdentityPool" }, "cognito-idp": { "AddCustomAttributes": "cognito-idp:AddCustomAttributes", "AdminAddUserToGroup": "cognito-idp:AdminAddUserToGroup", "AdminConfirmSignUp": "cognito-idp:AdminConfirmSignUp", "AdminCreateUser": "cognito-idp:AdminCreateUser", "AdminDeleteUser": "cognito-idp:AdminDeleteUser", "AdminDeleteUserAttributes": "cognito-idp:AdminDeleteUserAttributes", "AdminDisableProviderForUser": "cognito-idp:AdminDisableProviderForUser", "AdminDisableUser": "cognito-idp:AdminDisableUser", "AdminEnableUser": "cognito-idp:AdminEnableUser", "AdminForgetDevice": "cognito-idp:AdminForgetDevice", "AdminGetDevice": "cognito-idp:AdminGetDevice", "AdminGetUser": "cognito-idp:AdminGetUser", "AdminInitiateAuth": "cognito-idp:AdminInitiateAuth", "AdminLinkProviderForUser": "cognito-idp:AdminLinkProviderForUser", "AdminListDevices": "cognito-idp:AdminListDevices", "AdminListGroupsForUser": "cognito-idp:AdminListGroupsForUser", "AdminListUserAuthEvents": "cognito-idp:AdminListUserAuthEvents", "AdminRemoveUserFromGroup": "cognito-idp:AdminRemoveUserFromGroup", "AdminResetUserPassword": "cognito-idp:AdminResetUserPassword", "AdminRespondToAuthChallenge": "cognito-idp:AdminRespondToAuthChallenge", "AdminSetUserMFAPreference": "cognito-idp:AdminSetUserMFAPreference", "AdminSetUserPassword": "cognito-idp:AdminSetUserPassword", "AdminSetUserSettings": "cognito-idp:AdminSetUserSettings", "AdminUpdateAuthEventFeedback": "cognito-idp:AdminUpdateAuthEventFeedback", "AdminUpdateDeviceStatus": "cognito-idp:AdminUpdateDeviceStatus", "AdminUpdateUserAttributes": "cognito-idp:AdminUpdateUserAttributes", "AdminUserGlobalSignOut": "cognito-idp:AdminUserGlobalSignOut", "AssociateSoftwareToken": "cognito-idp:AssociateSoftwareToken", "ChangePassword": "cognito-idp:ChangePassword", "ConfirmDevice": "cognito-idp:ConfirmDevice", "ConfirmForgotPassword": "cognito-idp:ConfirmForgotPassword", "ConfirmSignUp": "cognito-idp:ConfirmSignUp", "CreateGroup": "cognito-idp:CreateGroup", "CreateIdentityProvider": "cognito-idp:CreateIdentityProvider", "CreateResourceServer": "cognito-idp:CreateResourceServer", "CreateUserImportJob": "cognito-idp:CreateUserImportJob", "CreateUserPool": "cognito-idp:CreateUserPool", "CreateUserPoolClient": "cognito-idp:CreateUserPoolClient", "CreateUserPoolDomain": "cognito-idp:CreateUserPoolDomain", "DeleteGroup": "cognito-idp:DeleteGroup", "DeleteIdentityProvider": "cognito-idp:DeleteIdentityProvider", "DeleteResourceServer": "cognito-idp:DeleteResourceServer", "DeleteUser": "cognito-idp:DeleteUser", "DeleteUserAttributes": "cognito-idp:DeleteUserAttributes", "DeleteUserPool": "cognito-idp:DeleteUserPool", "DeleteUserPoolClient": "cognito-idp:DeleteUserPoolClient", "DeleteUserPoolDomain": "cognito-idp:DeleteUserPoolDomain", "DescribeIdentityProvider": "cognito-idp:DescribeIdentityProvider", "DescribeResourceServer": "cognito-idp:DescribeResourceServer", "DescribeRiskConfiguration": "cognito-idp:DescribeRiskConfiguration", "DescribeUserImportJob": "cognito-idp:DescribeUserImportJob", "DescribeUserPool": "cognito-idp:DescribeUserPool", "DescribeUserPoolClient": "cognito-idp:DescribeUserPoolClient", "DescribeUserPoolDomain": "cognito-idp:DescribeUserPoolDomain", "ForgetDevice": "cognito-idp:ForgetDevice", "ForgotPassword": "cognito-idp:ForgotPassword", "GetCSVHeader": "cognito-idp:GetCSVHeader", "GetDevice": "cognito-idp:GetDevice", "GetGroup": "cognito-idp:GetGroup", "GetIdentityProviderByIdentifier": "cognito-idp:GetIdentityProviderByIdentifier", "GetSigningCertificate": "cognito-idp:GetSigningCertificate", "GetUICustomization": "cognito-idp:GetUICustomization", "GetUser": "cognito-idp:GetUser", "GetUserAttributeVerificationCode": "cognito-idp:GetUserAttributeVerificationCode", "GetUserPoolMfaConfig": "cognito-idp:GetUserPoolMfaConfig", "GlobalSignOut": "cognito-idp:GlobalSignOut", "InitiateAuth": "cognito-idp:InitiateAuth", "ListDevices": "cognito-idp:ListDevices", "ListGroups": "cognito-idp:ListGroups", "ListIdentityProviders": "cognito-idp:ListIdentityProviders", "ListResourceServers": "cognito-idp:ListResourceServers", "ListTagsForResource": "cognito-idp:ListTagsForResource", "ListUserImportJobs": "cognito-idp:ListUserImportJobs", "ListUserPoolClients": "cognito-idp:ListUserPoolClients", "ListUserPools": "cognito-idp:ListUserPools", "ListUsers": "cognito-idp:ListUsers", "ListUsersInGroup": "cognito-idp:ListUsersInGroup", "ResendConfirmationCode": "cognito-idp:ResendConfirmationCode", "RespondToAuthChallenge": "cognito-idp:RespondToAuthChallenge", "SetRiskConfiguration": "cognito-idp:SetRiskConfiguration", "SetUICustomization": "cognito-idp:SetUICustomization", "SetUserMFAPreference": "cognito-idp:SetUserMFAPreference", "SetUserPoolMfaConfig": "cognito-idp:SetUserPoolMfaConfig", "SetUserSettings": "cognito-idp:SetUserSettings", "SignUp": "cognito-idp:SignUp", "StartUserImportJob": "cognito-idp:StartUserImportJob", "StopUserImportJob": "cognito-idp:StopUserImportJob", "TagResource": "cognito-idp:TagResource", "UntagResource": "cognito-idp:UntagResource", "UpdateAuthEventFeedback": "cognito-idp:UpdateAuthEventFeedback", "UpdateDeviceStatus": "cognito-idp:UpdateDeviceStatus", "UpdateGroup": "cognito-idp:UpdateGroup", "UpdateIdentityProvider": "cognito-idp:UpdateIdentityProvider", "UpdateResourceServer": "cognito-idp:UpdateResourceServer", "UpdateUserAttributes": "cognito-idp:UpdateUserAttributes", "UpdateUserPool": "cognito-idp:UpdateUserPool", "UpdateUserPoolClient": "cognito-idp:UpdateUserPoolClient", "UpdateUserPoolDomain": "cognito-idp:UpdateUserPoolDomain", "VerifySoftwareToken": "cognito-idp:VerifySoftwareToken", "VerifyUserAttribute": "cognito-idp:VerifyUserAttribute" }, "cognito-sync": { "BulkPublish": "cognito-sync:BulkPublish", "DeleteDataset": "cognito-sync:DeleteDataset", "DescribeDataset": "cognito-sync:DescribeDataset", "DescribeIdentityPoolUsage": "cognito-sync:DescribeIdentityPoolUsage", "DescribeIdentityUsage": "cognito-sync:DescribeIdentityUsage", "GetBulkPublishDetails": "cognito-sync:GetBulkPublishDetails", "GetCognitoEvents": "cognito-sync:GetCognitoEvents", "GetIdentityPoolConfiguration": "cognito-sync:GetIdentityPoolConfiguration", "ListDatasets": "cognito-sync:ListDatasets", "ListIdentityPoolUsage": "cognito-sync:ListIdentityPoolUsage", "ListRecords": "cognito-sync:ListRecords", "RegisterDevice": "cognito-sync:RegisterDevice", "SetCognitoEvents": "cognito-sync:SetCognitoEvents", "SetIdentityPoolConfiguration": "cognito-sync:SetIdentityPoolConfiguration", "SubscribeToDataset": "cognito-sync:SubscribeToDataset", "UnsubscribeFromDataset": "cognito-sync:UnsubscribeFromDataset", "UpdateRecords": "cognito-sync:UpdateRecords" }, "comprehend": { "BatchDetectDominantLanguage": "comprehend:BatchDetectDominantLanguage", "BatchDetectEntities": "comprehend:BatchDetectEntities", "BatchDetectKeyPhrases": "comprehend:BatchDetectKeyPhrases", "BatchDetectSentiment": "comprehend:BatchDetectSentiment", "BatchDetectSyntax": "comprehend:BatchDetectSyntax", "ClassifyDocument": "comprehend:ClassifyDocument", "ContainsPiiEntities": "comprehend:ContainsPiiEntities", "CreateDocumentClassifier": "comprehend:CreateDocumentClassifier", "CreateEndpoint": "comprehend:CreateEndpoint", "CreateEntityRecognizer": "comprehend:CreateEntityRecognizer", "DeleteDocumentClassifier": "comprehend:DeleteDocumentClassifier", "DeleteEndpoint": "comprehend:DeleteEndpoint", "DeleteEntityRecognizer": "comprehend:DeleteEntityRecognizer", "DescribeDocumentClassificationJob": "comprehend:DescribeDocumentClassificationJob", "DescribeDocumentClassifier": "comprehend:DescribeDocumentClassifier", "DescribeDominantLanguageDetectionJob": "comprehend:DescribeDominantLanguageDetectionJob", "DescribeEndpoint": "comprehend:DescribeEndpoint", "DescribeEntitiesDetectionJob": "comprehend:DescribeEntitiesDetectionJob", "DescribeEntityRecognizer": "comprehend:DescribeEntityRecognizer", "DescribeEventsDetectionJob": "comprehend:DescribeEventsDetectionJob", "DescribeKeyPhrasesDetectionJob": "comprehend:DescribeKeyPhrasesDetectionJob", "DescribePiiEntitiesDetectionJob": "comprehend:DescribePiiEntitiesDetectionJob", "DescribeSentimentDetectionJob": "comprehend:DescribeSentimentDetectionJob", "DescribeTopicsDetectionJob": "comprehend:DescribeTopicsDetectionJob", "DetectDominantLanguage": "comprehend:DetectDominantLanguage", "DetectEntities": "comprehend:DetectEntities", "DetectKeyPhrases": "comprehend:DetectKeyPhrases", "DetectPiiEntities": "comprehend:DetectPiiEntities", "DetectSentiment": "comprehend:DetectSentiment", "DetectSyntax": "comprehend:DetectSyntax", "ListDocumentClassificationJobs": "comprehend:ListDocumentClassificationJobs", "ListDocumentClassifierSummaries": "comprehend:ListDocumentClassifierSummaries", "ListDocumentClassifiers": "comprehend:ListDocumentClassifiers", "ListDominantLanguageDetectionJobs": "comprehend:ListDominantLanguageDetectionJobs", "ListEndpoints": "comprehend:ListEndpoints", "ListEntitiesDetectionJobs": "comprehend:ListEntitiesDetectionJobs", "ListEntityRecognizerSummaries": "comprehend:ListEntityRecognizerSummaries", "ListEntityRecognizers": "comprehend:ListEntityRecognizers", "ListEventsDetectionJobs": "comprehend:ListEventsDetectionJobs", "ListKeyPhrasesDetectionJobs": "comprehend:ListKeyPhrasesDetectionJobs", "ListPiiEntitiesDetectionJobs": "comprehend:ListPiiEntitiesDetectionJobs", "ListSentimentDetectionJobs": "comprehend:ListSentimentDetectionJobs", "ListTagsForResource": "comprehend:ListTagsForResource", "ListTopicsDetectionJobs": "comprehend:ListTopicsDetectionJobs", "StartDocumentClassificationJob": "comprehend:StartDocumentClassificationJob", "StartDominantLanguageDetectionJob": "comprehend:StartDominantLanguageDetectionJob", "StartEntitiesDetectionJob": "comprehend:StartEntitiesDetectionJob", "StartEventsDetectionJob": "comprehend:StartEventsDetectionJob", "StartKeyPhrasesDetectionJob": "comprehend:StartKeyPhrasesDetectionJob", "StartPiiEntitiesDetectionJob": "comprehend:StartPiiEntitiesDetectionJob", "StartSentimentDetectionJob": "comprehend:StartSentimentDetectionJob", "StartTopicsDetectionJob": "comprehend:StartTopicsDetectionJob", "StopDominantLanguageDetectionJob": "comprehend:StopDominantLanguageDetectionJob", "StopEntitiesDetectionJob": "comprehend:StopEntitiesDetectionJob", "StopEventsDetectionJob": "comprehend:StopEventsDetectionJob", "StopKeyPhrasesDetectionJob": "comprehend:StopKeyPhrasesDetectionJob", "StopPiiEntitiesDetectionJob": "comprehend:StopPiiEntitiesDetectionJob", "StopSentimentDetectionJob": "comprehend:StopSentimentDetectionJob", "StopTrainingDocumentClassifier": "comprehend:StopTrainingDocumentClassifier", "StopTrainingEntityRecognizer": "comprehend:StopTrainingEntityRecognizer", "TagResource": "comprehend:TagResource", "UntagResource": "comprehend:UntagResource", "UpdateEndpoint": "comprehend:UpdateEndpoint" }, "comprehendmedical": { "DescribeEntitiesDetectionV2Job": "comprehendmedical:DescribeEntitiesDetectionV2Job", "DescribeICD10CMInferenceJob": "comprehendmedical:DescribeICD10CMInferenceJob", "DescribePHIDetectionJob": "comprehendmedical:DescribePHIDetectionJob", "DescribeRxNormInferenceJob": "comprehendmedical:DescribeRxNormInferenceJob", "DetectEntitiesV2": "comprehendmedical:DetectEntitiesV2", "DetectPHI": "comprehendmedical:DetectPHI", "InferICD10CM": "comprehendmedical:InferICD10CM", "InferRxNorm": "comprehendmedical:InferRxNorm", "ListEntitiesDetectionV2Jobs": "comprehendmedical:ListEntitiesDetectionV2Jobs", "ListICD10CMInferenceJobs": "comprehendmedical:ListICD10CMInferenceJobs", "ListPHIDetectionJobs": "comprehendmedical:ListPHIDetectionJobs", "ListRxNormInferenceJobs": "comprehendmedical:ListRxNormInferenceJobs", "StartEntitiesDetectionV2Job": "comprehendmedical:StartEntitiesDetectionV2Job", "StartICD10CMInferenceJob": "comprehendmedical:StartICD10CMInferenceJob", "StartPHIDetectionJob": "comprehendmedical:StartPHIDetectionJob", "StartRxNormInferenceJob": "comprehendmedical:StartRxNormInferenceJob", "StopEntitiesDetectionV2Job": "comprehendmedical:StopEntitiesDetectionV2Job", "StopICD10CMInferenceJob": "comprehendmedical:StopICD10CMInferenceJob", "StopPHIDetectionJob": "comprehendmedical:StopPHIDetectionJob", "StopRxNormInferenceJob": "comprehendmedical:StopRxNormInferenceJob" }, "compute-optimizer": { "DeleteRecommendationPreferences": "compute-optimizer:DeleteRecommendationPreferences", "DescribeRecommendationExportJobs": "compute-optimizer:DescribeRecommendationExportJobs", "ExportAutoScalingGroupRecommendations": "compute-optimizer:ExportAutoScalingGroupRecommendations", "ExportEBSVolumeRecommendations": "compute-optimizer:ExportEBSVolumeRecommendations", "ExportEC2InstanceRecommendations": "compute-optimizer:ExportEC2InstanceRecommendations", "ExportLambdaFunctionRecommendations": "compute-optimizer:ExportLambdaFunctionRecommendations", "GetAutoScalingGroupRecommendations": "compute-optimizer:GetAutoScalingGroupRecommendations", "GetEBSVolumeRecommendations": "compute-optimizer:GetEBSVolumeRecommendations", "GetEC2InstanceRecommendations": "compute-optimizer:GetEC2InstanceRecommendations", "GetEC2RecommendationProjectedMetrics": "compute-optimizer:GetEC2RecommendationProjectedMetrics", "GetEffectiveRecommendationPreferences": "compute-optimizer:GetEffectiveRecommendationPreferences", "GetEnrollmentStatus": "compute-optimizer:GetEnrollmentStatus", "GetEnrollmentStatusesForOrganization": "compute-optimizer:GetEnrollmentStatusesForOrganization", "GetLambdaFunctionRecommendations": "compute-optimizer:GetLambdaFunctionRecommendations", "GetRecommendationPreferences": "compute-optimizer:GetRecommendationPreferences", "GetRecommendationSummaries": "compute-optimizer:GetRecommendationSummaries", "PutRecommendationPreferences": "compute-optimizer:PutRecommendationPreferences", "UpdateEnrollmentStatus": "compute-optimizer:UpdateEnrollmentStatus" }, "config": { "BatchGetAggregateResourceConfig": "config:BatchGetAggregateResourceConfig", "BatchGetResourceConfig": "config:BatchGetResourceConfig", "DeleteAggregationAuthorization": "config:DeleteAggregationAuthorization", "DeleteConfigRule": "config:DeleteConfigRule", "DeleteConfigurationAggregator": "config:DeleteConfigurationAggregator", "DeleteConfigurationRecorder": "config:DeleteConfigurationRecorder", "DeleteConformancePack": "config:DeleteConformancePack", "DeleteDeliveryChannel": "config:DeleteDeliveryChannel", "DeleteEvaluationResults": "config:DeleteEvaluationResults", "DeleteOrganizationConfigRule": "config:DeleteOrganizationConfigRule", "DeleteOrganizationConformancePack": "config:DeleteOrganizationConformancePack", "DeletePendingAggregationRequest": "config:DeletePendingAggregationRequest", "DeleteRemediationConfiguration": "config:DeleteRemediationConfiguration", "DeleteRemediationExceptions": "config:DeleteRemediationExceptions", "DeleteResourceConfig": "config:DeleteResourceConfig", "DeleteRetentionConfiguration": "config:DeleteRetentionConfiguration", "DeleteStoredQuery": "config:DeleteStoredQuery", "DeliverConfigSnapshot": "config:DeliverConfigSnapshot", "DescribeAggregateComplianceByConfigRules": "config:DescribeAggregateComplianceByConfigRules", "DescribeAggregateComplianceByConformancePacks": "config:DescribeAggregateComplianceByConformancePacks", "DescribeAggregationAuthorizations": "config:DescribeAggregationAuthorizations", "DescribeComplianceByConfigRule": "config:DescribeComplianceByConfigRule", "DescribeComplianceByResource": "config:DescribeComplianceByResource", "DescribeConfigRuleEvaluationStatus": "config:DescribeConfigRuleEvaluationStatus", "DescribeConfigRules": "config:DescribeConfigRules", "DescribeConfigurationAggregatorSourcesStatus": "config:DescribeConfigurationAggregatorSourcesStatus", "DescribeConfigurationAggregators": "config:DescribeConfigurationAggregators", "DescribeConfigurationRecorderStatus": "config:DescribeConfigurationRecorderStatus", "DescribeConfigurationRecorders": "config:DescribeConfigurationRecorders", "DescribeConformancePackCompliance": "config:DescribeConformancePackCompliance", "DescribeConformancePackStatus": "config:DescribeConformancePackStatus", "DescribeConformancePacks": "config:DescribeConformancePacks", "DescribeDeliveryChannelStatus": "config:DescribeDeliveryChannelStatus", "DescribeDeliveryChannels": "config:DescribeDeliveryChannels", "DescribeOrganizationConfigRuleStatuses": "config:DescribeOrganizationConfigRuleStatuses", "DescribeOrganizationConfigRules": "config:DescribeOrganizationConfigRules", "DescribeOrganizationConformancePackStatuses": "config:DescribeOrganizationConformancePackStatuses", "DescribeOrganizationConformancePacks": "config:DescribeOrganizationConformancePacks", "DescribePendingAggregationRequests": "config:DescribePendingAggregationRequests", "DescribeRemediationConfigurations": "config:DescribeRemediationConfigurations", "DescribeRemediationExceptions": "config:DescribeRemediationExceptions", "DescribeRemediationExecutionStatus": "config:DescribeRemediationExecutionStatus", "DescribeRetentionConfigurations": "config:DescribeRetentionConfigurations", "GetAggregateComplianceDetailsByConfigRule": "config:GetAggregateComplianceDetailsByConfigRule", "GetAggregateConfigRuleComplianceSummary": "config:GetAggregateConfigRuleComplianceSummary", "GetAggregateConformancePackComplianceSummary": "config:GetAggregateConformancePackComplianceSummary", "GetAggregateDiscoveredResourceCounts": "config:GetAggregateDiscoveredResourceCounts", "GetAggregateResourceConfig": "config:GetAggregateResourceConfig", "GetComplianceDetailsByConfigRule": "config:GetComplianceDetailsByConfigRule", "GetComplianceDetailsByResource": "config:GetComplianceDetailsByResource", "GetComplianceSummaryByConfigRule": "config:GetComplianceSummaryByConfigRule", "GetComplianceSummaryByResourceType": "config:GetComplianceSummaryByResourceType", "GetConformancePackComplianceDetails": "config:GetConformancePackComplianceDetails", "GetConformancePackComplianceSummary": "config:GetConformancePackComplianceSummary", "GetDiscoveredResourceCounts": "config:GetDiscoveredResourceCounts", "GetOrganizationConfigRuleDetailedStatus": "config:GetOrganizationConfigRuleDetailedStatus", "GetOrganizationConformancePackDetailedStatus": "config:GetOrganizationConformancePackDetailedStatus", "GetResourceConfigHistory": "config:GetResourceConfigHistory", "GetStoredQuery": "config:GetStoredQuery", "ListAggregateDiscoveredResources": "config:ListAggregateDiscoveredResources", "ListDiscoveredResources": "config:ListDiscoveredResources", "ListStoredQueries": "config:ListStoredQueries", "ListTagsForResource": "config:ListTagsForResource", "PutAggregationAuthorization": "config:PutAggregationAuthorization", "PutConfigRule": "config:PutConfigRule", "PutConfigurationAggregator": "config:PutConfigurationAggregator", "PutConfigurationRecorder": "config:PutConfigurationRecorder", "PutConformancePack": "config:PutConformancePack", "PutDeliveryChannel": "config:PutDeliveryChannel", "PutEvaluations": "config:PutEvaluations", "PutExternalEvaluation": "config:PutExternalEvaluation", "PutOrganizationConfigRule": "config:PutOrganizationConfigRule", "PutOrganizationConformancePack": "config:PutOrganizationConformancePack", "PutRemediationConfigurations": "config:PutRemediationConfigurations", "PutRemediationExceptions": "config:PutRemediationExceptions", "PutResourceConfig": "config:PutResourceConfig", "PutRetentionConfiguration": "config:PutRetentionConfiguration", "PutStoredQuery": "config:PutStoredQuery", "SelectAggregateResourceConfig": "config:SelectAggregateResourceConfig", "SelectResourceConfig": "config:SelectResourceConfig", "StartConfigRulesEvaluation": "config:StartConfigRulesEvaluation", "StartConfigurationRecorder": "config:StartConfigurationRecorder", "StartRemediationExecution": "config:StartRemediationExecution", "StopConfigurationRecorder": "config:StopConfigurationRecorder", "TagResource": "config:TagResource", "UntagResource": "config:UntagResource" }, "connect": { "AssociateApprovedOrigin": "connect:AssociateApprovedOrigin", "AssociateBot": "connect:AssociateBot", "AssociateInstanceStorageConfig": "connect:AssociateInstanceStorageConfig", "AssociateLambdaFunction": "connect:AssociateLambdaFunction", "AssociateLexBot": "connect:AssociateLexBot", "AssociateQueueQuickConnects": "connect:AssociateQueueQuickConnects", "AssociateRoutingProfileQueues": "connect:AssociateRoutingProfileQueues", "AssociateSecurityKey": "connect:AssociateSecurityKey", "CreateAgentStatus": "connect:CreateAgentStatus", "CreateContactFlow": "connect:CreateContactFlow", "CreateContactFlowModule": "connect:CreateContactFlowModule", "CreateHoursOfOperation": "connect:CreateHoursOfOperation", "CreateInstance": "connect:CreateInstance", "CreateIntegrationAssociation": "connect:CreateIntegrationAssociation", "CreateQueue": "connect:CreateQueue", "CreateQuickConnect": "connect:CreateQuickConnect", "CreateRoutingProfile": "connect:CreateRoutingProfile", "CreateSecurityProfile": "connect:CreateSecurityProfile", "CreateUseCase": "connect:CreateUseCase", "CreateUser": "connect:CreateUser", "CreateUserHierarchyGroup": "connect:CreateUserHierarchyGroup", "DeleteContactFlow": "connect:DeleteContactFlow", "DeleteContactFlowModule": "connect:DeleteContactFlowModule", "DeleteHoursOfOperation": "connect:DeleteHoursOfOperation", "DeleteInstance": "connect:DeleteInstance", "DeleteIntegrationAssociation": "connect:DeleteIntegrationAssociation", "DeleteQuickConnect": "connect:DeleteQuickConnect", "DeleteSecurityProfile": "connect:DeleteSecurityProfile", "DeleteUseCase": "connect:DeleteUseCase", "DeleteUser": "connect:DeleteUser", "DeleteUserHierarchyGroup": "connect:DeleteUserHierarchyGroup", "DescribeAgentStatus": "connect:DescribeAgentStatus", "DescribeContact": "connect:DescribeContact", "DescribeContactFlow": "connect:DescribeContactFlow", "DescribeContactFlowModule": "connect:DescribeContactFlowModule", "DescribeHoursOfOperation": "connect:DescribeHoursOfOperation", "DescribeInstance": "connect:DescribeInstance", "DescribeInstanceAttribute": "connect:DescribeInstanceAttribute", "DescribeInstanceStorageConfig": "connect:DescribeInstanceStorageConfig", "DescribeQueue": "connect:DescribeQueue", "DescribeQuickConnect": "connect:DescribeQuickConnect", "DescribeRoutingProfile": "connect:DescribeRoutingProfile", "DescribeSecurityProfile": "connect:DescribeSecurityProfile", "DescribeUser": "connect:DescribeUser", "DescribeUserHierarchyGroup": "connect:DescribeUserHierarchyGroup", "DescribeUserHierarchyStructure": "connect:DescribeUserHierarchyStructure", "DisassociateApprovedOrigin": "connect:DisassociateApprovedOrigin", "DisassociateBot": "connect:DisassociateBot", "DisassociateInstanceStorageConfig": "connect:DisassociateInstanceStorageConfig", "DisassociateLambdaFunction": "connect:DisassociateLambdaFunction", "DisassociateLexBot": "connect:DisassociateLexBot", "DisassociateQueueQuickConnects": "connect:DisassociateQueueQuickConnects", "DisassociateRoutingProfileQueues": "connect:DisassociateRoutingProfileQueues", "DisassociateSecurityKey": "connect:DisassociateSecurityKey", "GetContactAttributes": "connect:GetContactAttributes", "GetCurrentMetricData": "connect:GetCurrentMetricData", "GetFederationToken": "connect:GetFederationToken", "GetMetricData": "connect:GetMetricData", "ListAgentStatuses": "connect:ListAgentStatuses", "ListApprovedOrigins": "connect:ListApprovedOrigins", "ListBots": "connect:ListBots", "ListContactFlowModules": "connect:ListContactFlowModules", "ListContactFlows": "connect:ListContactFlows", "ListContactReferences": "connect:ListContactReferences", "ListHoursOfOperations": "connect:ListHoursOfOperations", "ListInstanceAttributes": "connect:ListInstanceAttributes", "ListInstanceStorageConfigs": "connect:ListInstanceStorageConfigs", "ListInstances": "connect:ListInstances", "ListIntegrationAssociations": "connect:ListIntegrationAssociations", "ListLambdaFunctions": "connect:ListLambdaFunctions", "ListLexBots": "connect:ListLexBots", "ListPhoneNumbers": "connect:ListPhoneNumbers", "ListPrompts": "connect:ListPrompts", "ListQueueQuickConnects": "connect:ListQueueQuickConnects", "ListQueues": "connect:ListQueues", "ListQuickConnects": "connect:ListQuickConnects", "ListRoutingProfileQueues": "connect:ListRoutingProfileQueues", "ListRoutingProfiles": "connect:ListRoutingProfiles", "ListSecurityKeys": "connect:ListSecurityKeys", "ListSecurityProfilePermissions": "connect:ListSecurityProfilePermissions", "ListSecurityProfiles": "connect:ListSecurityProfiles", "ListTagsForResource": "connect:ListTagsForResource", "ListUseCases": "connect:ListUseCases", "ListUserHierarchyGroups": "connect:ListUserHierarchyGroups", "ListUsers": "connect:ListUsers", "ResumeContactRecording": "connect:ResumeContactRecording", "StartChatContact": "connect:StartChatContact", "StartContactRecording": "connect:StartContactRecording", "StartOutboundVoiceContact": "connect:StartOutboundVoiceContact", "StartTaskContact": "connect:StartTaskContact", "StopContact": "connect:StopContact", "StopContactRecording": "connect:StopContactRecording", "SuspendContactRecording": "connect:SuspendContactRecording", "TagResource": "connect:TagResource", "UntagResource": "connect:UntagResource", "UpdateAgentStatus": "connect:UpdateAgentStatus", "UpdateContact": "connect:UpdateContact", "UpdateContactAttributes": "connect:UpdateContactAttributes", "UpdateContactFlowContent": "connect:UpdateContactFlowContent", "UpdateContactFlowMetadata": "connect:UpdateContactFlowMetadata", "UpdateContactFlowModuleMetadata": "connect:UpdateContactFlowModuleMetadata", "UpdateContactFlowName": "connect:UpdateContactFlowName", "UpdateContactSchedule": "connect:UpdateContactSchedule", "UpdateHoursOfOperation": "connect:UpdateHoursOfOperation", "UpdateInstanceAttribute": "connect:UpdateInstanceAttribute", "UpdateInstanceStorageConfig": "connect:UpdateInstanceStorageConfig", "UpdateQueueHoursOfOperation": "connect:UpdateQueueHoursOfOperation", "UpdateQueueMaxContacts": "connect:UpdateQueueMaxContacts", "UpdateQueueName": "connect:UpdateQueueName", "UpdateQueueOutboundCallerConfig": "connect:UpdateQueueOutboundCallerConfig", "UpdateQueueStatus": "connect:UpdateQueueStatus", "UpdateQuickConnectConfig": "connect:UpdateQuickConnectConfig", "UpdateQuickConnectName": "connect:UpdateQuickConnectName", "UpdateRoutingProfileConcurrency": "connect:UpdateRoutingProfileConcurrency", "UpdateRoutingProfileDefaultOutboundQueue": "connect:UpdateRoutingProfileDefaultOutboundQueue", "UpdateRoutingProfileName": "connect:UpdateRoutingProfileName", "UpdateRoutingProfileQueues": "connect:UpdateRoutingProfileQueues", "UpdateSecurityProfile": "connect:UpdateSecurityProfile", "UpdateUserHierarchy": "connect:UpdateUserHierarchy", "UpdateUserHierarchyGroupName": "connect:UpdateUserHierarchyGroupName", "UpdateUserHierarchyStructure": "connect:UpdateUserHierarchyStructure", "UpdateUserIdentityInfo": "connect:UpdateUserIdentityInfo", "UpdateUserPhoneConfig": "connect:UpdateUserPhoneConfig", "UpdateUserRoutingProfile": "connect:UpdateUserRoutingProfile", "UpdateUserSecurityProfiles": "connect:UpdateUserSecurityProfiles" }, "cur": { "DeleteReportDefinition": "cur:DeleteReportDefinition", "DescribeReportDefinitions": "cur:DescribeReportDefinitions", "ModifyReportDefinition": "cur:ModifyReportDefinition", "PutReportDefinition": "cur:PutReportDefinition" }, "databrew": { "BatchDeleteRecipeVersion": "databrew:BatchDeleteRecipeVersion", "CreateDataset": "databrew:CreateDataset", "CreateProfileJob": "databrew:CreateProfileJob", "CreateProject": "databrew:CreateProject", "CreateRecipe": "databrew:CreateRecipe", "CreateRecipeJob": "databrew:CreateRecipeJob", "CreateRuleset": "databrew:CreateRuleset", "CreateSchedule": "databrew:CreateSchedule", "DeleteDataset": "databrew:DeleteDataset", "DeleteJob": "databrew:DeleteJob", "DeleteProject": "databrew:DeleteProject", "DeleteRecipeVersion": "databrew:DeleteRecipeVersion", "DeleteRuleset": "databrew:DeleteRuleset", "DeleteSchedule": "databrew:DeleteSchedule", "DescribeDataset": "databrew:DescribeDataset", "DescribeJob": "databrew:DescribeJob", "DescribeJobRun": "databrew:DescribeJobRun", "DescribeProject": "databrew:DescribeProject", "DescribeRecipe": "databrew:DescribeRecipe", "DescribeRuleset": "databrew:DescribeRuleset", "DescribeSchedule": "databrew:DescribeSchedule", "ListDatasets": "databrew:ListDatasets", "ListJobRuns": "databrew:ListJobRuns", "ListJobs": "databrew:ListJobs", "ListProjects": "databrew:ListProjects", "ListRecipeVersions": "databrew:ListRecipeVersions", "ListRecipes": "databrew:ListRecipes", "ListRulesets": "databrew:ListRulesets", "ListSchedules": "databrew:ListSchedules", "ListTagsForResource": "databrew:ListTagsForResource", "PublishRecipe": "databrew:PublishRecipe", "SendProjectSessionAction": "databrew:SendProjectSessionAction", "StartJobRun": "databrew:StartJobRun", "StartProjectSession": "databrew:StartProjectSession", "StopJobRun": "databrew:StopJobRun", "TagResource": "databrew:TagResource", "UntagResource": "databrew:UntagResource", "UpdateDataset": "databrew:UpdateDataset", "UpdateProfileJob": "databrew:UpdateProfileJob", "UpdateProject": "databrew:UpdateProject", "UpdateRecipe": "databrew:UpdateRecipe", "UpdateRecipeJob": "databrew:UpdateRecipeJob", "UpdateRuleset": "databrew:UpdateRuleset", "UpdateSchedule": "databrew:UpdateSchedule" }, "dataexchange": { "CancelJob": "dataexchange:CancelJob", "CreateDataSet": "dataexchange:CreateDataSet", "CreateEventAction": "dataexchange:CreateEventAction", "CreateJob": "dataexchange:CreateJob", "CreateRevision": "dataexchange:CreateRevision", "DeleteAsset": "dataexchange:DeleteAsset", "DeleteDataSet": "dataexchange:DeleteDataSet", "DeleteEventAction": "dataexchange:DeleteEventAction", "DeleteRevision": "dataexchange:DeleteRevision", "GetAsset": "dataexchange:GetAsset", "GetDataSet": "dataexchange:GetDataSet", "GetEventAction": "dataexchange:GetEventAction", "GetJob": "dataexchange:GetJob", "GetRevision": "dataexchange:GetRevision", "ListDataSetRevisions": "dataexchange:ListDataSetRevisions", "ListDataSets": "dataexchange:ListDataSets", "ListEventActions": "dataexchange:ListEventActions", "ListJobs": "dataexchange:ListJobs", "ListRevisionAssets": "dataexchange:ListRevisionAssets", "ListTagsForResource": "dataexchange:ListTagsForResource", "SendApiAsset": "dataexchange:SendApiAsset", "StartJob": "dataexchange:StartJob", "TagResource": "dataexchange:TagResource", "UntagResource": "dataexchange:UntagResource", "UpdateAsset": "dataexchange:UpdateAsset", "UpdateDataSet": "dataexchange:UpdateDataSet", "UpdateEventAction": "dataexchange:UpdateEventAction", "UpdateRevision": "dataexchange:UpdateRevision" }, "datapipeline": { "ActivatePipeline": "datapipeline:ActivatePipeline", "AddTags": "datapipeline:AddTags", "CreatePipeline": "datapipeline:CreatePipeline", "DeactivatePipeline": "datapipeline:DeactivatePipeline", "DeletePipeline": "datapipeline:DeletePipeline", "DescribeObjects": "datapipeline:DescribeObjects", "DescribePipelines": "datapipeline:DescribePipelines", "EvaluateExpression": "datapipeline:EvaluateExpression", "GetPipelineDefinition": "datapipeline:GetPipelineDefinition", "ListPipelines": "datapipeline:ListPipelines", "PollForTask": "datapipeline:PollForTask", "PutPipelineDefinition": "datapipeline:PutPipelineDefinition", "QueryObjects": "datapipeline:QueryObjects", "RemoveTags": "datapipeline:RemoveTags", "ReportTaskProgress": "datapipeline:ReportTaskProgress", "ReportTaskRunnerHeartbeat": "datapipeline:ReportTaskRunnerHeartbeat", "SetStatus": "datapipeline:SetStatus", "SetTaskStatus": "datapipeline:SetTaskStatus", "ValidatePipelineDefinition": "datapipeline:ValidatePipelineDefinition" }, "datasync": { "CancelTaskExecution": "datasync:CancelTaskExecution", "CreateAgent": "datasync:CreateAgent", "CreateLocationEfs": "datasync:CreateLocationEfs", "CreateLocationFsxWindows": "datasync:CreateLocationFsxWindows", "CreateLocationNfs": "datasync:CreateLocationNfs", "CreateLocationObjectStorage": "datasync:CreateLocationObjectStorage", "CreateLocationS3": "datasync:CreateLocationS3", "CreateLocationSmb": "datasync:CreateLocationSmb", "CreateTask": "datasync:CreateTask", "DeleteAgent": "datasync:DeleteAgent", "DeleteLocation": "datasync:DeleteLocation", "DeleteTask": "datasync:DeleteTask", "DescribeAgent": "datasync:DescribeAgent", "DescribeLocationEfs": "datasync:DescribeLocationEfs", "DescribeLocationFsxWindows": "datasync:DescribeLocationFsxWindows", "DescribeLocationNfs": "datasync:DescribeLocationNfs", "DescribeLocationObjectStorage": "datasync:DescribeLocationObjectStorage", "DescribeLocationS3": "datasync:DescribeLocationS3", "DescribeLocationSmb": "datasync:DescribeLocationSmb", "DescribeTask": "datasync:DescribeTask", "DescribeTaskExecution": "datasync:DescribeTaskExecution", "ListAgents": "datasync:ListAgents", "ListLocations": "datasync:ListLocations", "ListTagsForResource": "datasync:ListTagsForResource", "ListTaskExecutions": "datasync:ListTaskExecutions", "ListTasks": "datasync:ListTasks", "StartTaskExecution": "datasync:StartTaskExecution", "TagResource": "datasync:TagResource", "UntagResource": "datasync:UntagResource", "UpdateAgent": "datasync:UpdateAgent", "UpdateLocationNfs": "datasync:UpdateLocationNfs", "UpdateLocationObjectStorage": "datasync:UpdateLocationObjectStorage", "UpdateLocationSmb": "datasync:UpdateLocationSmb", "UpdateTask": "datasync:UpdateTask", "UpdateTaskExecution": "datasync:UpdateTaskExecution" }, "dax": { "CreateCluster": "dax:CreateCluster", "CreateParameterGroup": "dax:CreateParameterGroup", "CreateSubnetGroup": "dax:CreateSubnetGroup", "DecreaseReplicationFactor": "dax:DecreaseReplicationFactor", "DeleteCluster": "dax:DeleteCluster", "DeleteParameterGroup": "dax:DeleteParameterGroup", "DeleteSubnetGroup": "dax:DeleteSubnetGroup", "DescribeClusters": "dax:DescribeClusters", "DescribeDefaultParameters": "dax:DescribeDefaultParameters", "DescribeEvents": "dax:DescribeEvents", "DescribeParameterGroups": "dax:DescribeParameterGroups", "DescribeParameters": "dax:DescribeParameters", "DescribeSubnetGroups": "dax:DescribeSubnetGroups", "IncreaseReplicationFactor": "dax:IncreaseReplicationFactor", "ListTags": "dax:ListTags", "RebootNode": "dax:RebootNode", "TagResource": "dax:TagResource", "UntagResource": "dax:UntagResource", "UpdateCluster": "dax:UpdateCluster", "UpdateParameterGroup": "dax:UpdateParameterGroup", "UpdateSubnetGroup": "dax:UpdateSubnetGroup" }, "detective": { "AcceptInvitation": "detective:AcceptInvitation", "CreateGraph": "detective:CreateGraph", "CreateMembers": "detective:CreateMembers", "DeleteGraph": "detective:DeleteGraph", "DeleteMembers": "detective:DeleteMembers", "DescribeOrganizationConfiguration": "detective:DescribeOrganizationConfiguration", "DisableOrganizationAdminAccount": "detective:DisableOrganizationAdminAccount", "DisassociateMembership": "detective:DisassociateMembership", "EnableOrganizationAdminAccount": "detective:EnableOrganizationAdminAccount", "GetMembers": "detective:GetMembers", "ListGraphs": "detective:ListGraphs", "ListInvitations": "detective:ListInvitations", "ListMembers": "detective:ListMembers", "ListOrganizationAdminAccounts": "detective:ListOrganizationAdminAccounts", "ListTagsForResource": "detective:ListTagsForResource", "RejectInvitation": "detective:RejectInvitation", "StartMonitoringMember": "detective:StartMonitoringMember", "TagResource": "detective:TagResource", "UntagResource": "detective:UntagResource", "UpdateOrganizationConfiguration": "detective:UpdateOrganizationConfiguration" }, "devicefarm": { "CreateDevicePool": "devicefarm:CreateDevicePool", "CreateInstanceProfile": "devicefarm:CreateInstanceProfile", "CreateNetworkProfile": "devicefarm:CreateNetworkProfile", "CreateProject": "devicefarm:CreateProject", "CreateRemoteAccessSession": "devicefarm:CreateRemoteAccessSession", "CreateTestGridProject": "devicefarm:CreateTestGridProject", "CreateTestGridUrl": "devicefarm:CreateTestGridUrl", "CreateUpload": "devicefarm:CreateUpload", "CreateVPCEConfiguration": "devicefarm:CreateVPCEConfiguration", "DeleteDevicePool": "devicefarm:DeleteDevicePool", "DeleteInstanceProfile": "devicefarm:DeleteInstanceProfile", "DeleteNetworkProfile": "devicefarm:DeleteNetworkProfile", "DeleteProject": "devicefarm:DeleteProject", "DeleteRemoteAccessSession": "devicefarm:DeleteRemoteAccessSession", "DeleteRun": "devicefarm:DeleteRun", "DeleteTestGridProject": "devicefarm:DeleteTestGridProject", "DeleteUpload": "devicefarm:DeleteUpload", "DeleteVPCEConfiguration": "devicefarm:DeleteVPCEConfiguration", "GetAccountSettings": "devicefarm:GetAccountSettings", "GetDevice": "devicefarm:GetDevice", "GetDeviceInstance": "devicefarm:GetDeviceInstance", "GetDevicePool": "devicefarm:GetDevicePool", "GetDevicePoolCompatibility": "devicefarm:GetDevicePoolCompatibility", "GetInstanceProfile": "devicefarm:GetInstanceProfile", "GetJob": "devicefarm:GetJob", "GetNetworkProfile": "devicefarm:GetNetworkProfile", "GetOfferingStatus": "devicefarm:GetOfferingStatus", "GetProject": "devicefarm:GetProject", "GetRemoteAccessSession": "devicefarm:GetRemoteAccessSession", "GetRun": "devicefarm:GetRun", "GetSuite": "devicefarm:GetSuite", "GetTest": "devicefarm:GetTest", "GetTestGridProject": "devicefarm:GetTestGridProject", "GetTestGridSession": "devicefarm:GetTestGridSession", "GetUpload": "devicefarm:GetUpload", "GetVPCEConfiguration": "devicefarm:GetVPCEConfiguration", "InstallToRemoteAccessSession": "devicefarm:InstallToRemoteAccessSession", "ListArtifacts": "devicefarm:ListArtifacts", "ListDeviceInstances": "devicefarm:ListDeviceInstances", "ListDevicePools": "devicefarm:ListDevicePools", "ListDevices": "devicefarm:ListDevices", "ListInstanceProfiles": "devicefarm:ListInstanceProfiles", "ListJobs": "devicefarm:ListJobs", "ListNetworkProfiles": "devicefarm:ListNetworkProfiles", "ListOfferingPromotions": "devicefarm:ListOfferingPromotions", "ListOfferingTransactions": "devicefarm:ListOfferingTransactions", "ListOfferings": "devicefarm:ListOfferings", "ListProjects": "devicefarm:ListProjects", "ListRemoteAccessSessions": "devicefarm:ListRemoteAccessSessions", "ListRuns": "devicefarm:ListRuns", "ListSamples": "devicefarm:ListSamples", "ListSuites": "devicefarm:ListSuites", "ListTagsForResource": "devicefarm:ListTagsForResource", "ListTestGridProjects": "devicefarm:ListTestGridProjects", "ListTestGridSessionActions": "devicefarm:ListTestGridSessionActions", "ListTestGridSessionArtifacts": "devicefarm:ListTestGridSessionArtifacts", "ListTestGridSessions": "devicefarm:ListTestGridSessions", "ListTests": "devicefarm:ListTests", "ListUniqueProblems": "devicefarm:ListUniqueProblems", "ListUploads": "devicefarm:ListUploads", "ListVPCEConfigurations": "devicefarm:ListVPCEConfigurations", "PurchaseOffering": "devicefarm:PurchaseOffering", "RenewOffering": "devicefarm:RenewOffering", "ScheduleRun": "devicefarm:ScheduleRun", "StopJob": "devicefarm:StopJob", "StopRemoteAccessSession": "devicefarm:StopRemoteAccessSession", "StopRun": "devicefarm:StopRun", "TagResource": "devicefarm:TagResource", "UntagResource": "devicefarm:UntagResource", "UpdateDeviceInstance": "devicefarm:UpdateDeviceInstance", "UpdateDevicePool": "devicefarm:UpdateDevicePool", "UpdateInstanceProfile": "devicefarm:UpdateInstanceProfile", "UpdateNetworkProfile": "devicefarm:UpdateNetworkProfile", "UpdateProject": "devicefarm:UpdateProject", "UpdateTestGridProject": "devicefarm:UpdateTestGridProject", "UpdateUpload": "devicefarm:UpdateUpload", "UpdateVPCEConfiguration": "devicefarm:UpdateVPCEConfiguration" }, "devops-guru": { "AddNotificationChannel": "devops-guru:AddNotificationChannel", "DescribeAccountHealth": "devops-guru:DescribeAccountHealth", "DescribeAccountOverview": "devops-guru:DescribeAccountOverview", "DescribeAnomaly": "devops-guru:DescribeAnomaly", "DescribeFeedback": "devops-guru:DescribeFeedback", "DescribeInsight": "devops-guru:DescribeInsight", "DescribeOrganizationHealth": "devops-guru:DescribeOrganizationHealth", "DescribeOrganizationOverview": "devops-guru:DescribeOrganizationOverview", "DescribeOrganizationResourceCollectionHealth": "devops-guru:DescribeOrganizationResourceCollectionHealth", "DescribeResourceCollectionHealth": "devops-guru:DescribeResourceCollectionHealth", "DescribeServiceIntegration": "devops-guru:DescribeServiceIntegration", "GetCostEstimation": "devops-guru:GetCostEstimation", "GetResourceCollection": "devops-guru:GetResourceCollection", "ListAnomaliesForInsight": "devops-guru:ListAnomaliesForInsight", "ListEvents": "devops-guru:ListEvents", "ListInsights": "devops-guru:ListInsights", "ListNotificationChannels": "devops-guru:ListNotificationChannels", "ListOrganizationInsights": "devops-guru:ListOrganizationInsights", "ListRecommendations": "devops-guru:ListRecommendations", "PutFeedback": "devops-guru:PutFeedback", "RemoveNotificationChannel": "devops-guru:RemoveNotificationChannel", "SearchInsights": "devops-guru:SearchInsights", "SearchOrganizationInsights": "devops-guru:SearchOrganizationInsights", "StartCostEstimation": "devops-guru:StartCostEstimation", "UpdateResourceCollection": "devops-guru:UpdateResourceCollection", "UpdateServiceIntegration": "devops-guru:UpdateServiceIntegration" }, "directconnect": { "AcceptDirectConnectGatewayAssociationProposal": "directconnect:AcceptDirectConnectGatewayAssociationProposal", "AllocateConnectionOnInterconnect": "directconnect:AllocateConnectionOnInterconnect", "AllocateHostedConnection": "directconnect:AllocateHostedConnection", "AllocatePrivateVirtualInterface": "directconnect:AllocatePrivateVirtualInterface", "AllocatePublicVirtualInterface": "directconnect:AllocatePublicVirtualInterface", "AllocateTransitVirtualInterface": "directconnect:AllocateTransitVirtualInterface", "AssociateConnectionWithLag": "directconnect:AssociateConnectionWithLag", "AssociateHostedConnection": "directconnect:AssociateHostedConnection", "AssociateMacSecKey": "directconnect:AssociateMacSecKey", "AssociateVirtualInterface": "directconnect:AssociateVirtualInterface", "ConfirmConnection": "directconnect:ConfirmConnection", "ConfirmCustomerAgreement": "directconnect:ConfirmCustomerAgreement", "ConfirmPrivateVirtualInterface": "directconnect:ConfirmPrivateVirtualInterface", "ConfirmPublicVirtualInterface": "directconnect:ConfirmPublicVirtualInterface", "ConfirmTransitVirtualInterface": "directconnect:ConfirmTransitVirtualInterface", "CreateBGPPeer": "directconnect:CreateBGPPeer", "CreateConnection": "directconnect:CreateConnection", "CreateDirectConnectGateway": "directconnect:CreateDirectConnectGateway", "CreateDirectConnectGatewayAssociation": "directconnect:CreateDirectConnectGatewayAssociation", "CreateDirectConnectGatewayAssociationProposal": "directconnect:CreateDirectConnectGatewayAssociationProposal", "CreateInterconnect": "directconnect:CreateInterconnect", "CreateLag": "directconnect:CreateLag", "CreatePrivateVirtualInterface": "directconnect:CreatePrivateVirtualInterface", "CreatePublicVirtualInterface": "directconnect:CreatePublicVirtualInterface", "CreateTransitVirtualInterface": "directconnect:CreateTransitVirtualInterface", "DeleteBGPPeer": "directconnect:DeleteBGPPeer", "DeleteConnection": "directconnect:DeleteConnection", "DeleteDirectConnectGateway": "directconnect:DeleteDirectConnectGateway", "DeleteDirectConnectGatewayAssociation": "directconnect:DeleteDirectConnectGatewayAssociation", "DeleteDirectConnectGatewayAssociationProposal": "directconnect:DeleteDirectConnectGatewayAssociationProposal", "DeleteInterconnect": "directconnect:DeleteInterconnect", "DeleteLag": "directconnect:DeleteLag", "DeleteVirtualInterface": "directconnect:DeleteVirtualInterface", "DescribeConnectionLoa": "directconnect:DescribeConnectionLoa", "DescribeConnections": "directconnect:DescribeConnections", "DescribeConnectionsOnInterconnect": "directconnect:DescribeConnectionsOnInterconnect", "DescribeCustomerMetadata": "directconnect:DescribeCustomerMetadata", "DescribeDirectConnectGatewayAssociationProposals": "directconnect:DescribeDirectConnectGatewayAssociationProposals", "DescribeDirectConnectGatewayAssociations": "directconnect:DescribeDirectConnectGatewayAssociations", "DescribeDirectConnectGatewayAttachments": "directconnect:DescribeDirectConnectGatewayAttachments", "DescribeDirectConnectGateways": "directconnect:DescribeDirectConnectGateways", "DescribeHostedConnections": "directconnect:DescribeHostedConnections", "DescribeInterconnectLoa": "directconnect:DescribeInterconnectLoa", "DescribeInterconnects": "directconnect:DescribeInterconnects", "DescribeLags": "directconnect:DescribeLags", "DescribeLoa": "directconnect:DescribeLoa", "DescribeLocations": "directconnect:DescribeLocations", "DescribeRouterConfiguration": "directconnect:DescribeRouterConfiguration", "DescribeTags": "directconnect:DescribeTags", "DescribeVirtualGateways": "directconnect:DescribeVirtualGateways", "DescribeVirtualInterfaces": "directconnect:DescribeVirtualInterfaces", "DisassociateConnectionFromLag": "directconnect:DisassociateConnectionFromLag", "DisassociateMacSecKey": "directconnect:DisassociateMacSecKey", "ListVirtualInterfaceTestHistory": "directconnect:ListVirtualInterfaceTestHistory", "StartBgpFailoverTest": "directconnect:StartBgpFailoverTest", "StopBgpFailoverTest": "directconnect:StopBgpFailoverTest", "TagResource": "directconnect:TagResource", "UntagResource": "directconnect:UntagResource", "UpdateConnection": "directconnect:UpdateConnection", "UpdateDirectConnectGateway": "directconnect:UpdateDirectConnectGateway", "UpdateDirectConnectGatewayAssociation": "directconnect:UpdateDirectConnectGatewayAssociation", "UpdateLag": "directconnect:UpdateLag", "UpdateVirtualInterfaceAttributes": "directconnect:UpdateVirtualInterfaceAttributes" }, "discovery": { "AssociateConfigurationItemsToApplication": "discovery:AssociateConfigurationItemsToApplication", "BatchDeleteImportData": "discovery:BatchDeleteImportData", "CreateApplication": "discovery:CreateApplication", "CreateTags": "discovery:CreateTags", "DeleteApplications": "discovery:DeleteApplications", "DeleteTags": "discovery:DeleteTags", "DescribeAgents": "discovery:DescribeAgents", "DescribeConfigurations": "discovery:DescribeConfigurations", "DescribeContinuousExports": "discovery:DescribeContinuousExports", "DescribeExportConfigurations": "discovery:DescribeExportConfigurations", "DescribeExportTasks": "discovery:DescribeExportTasks", "DescribeImportTasks": "discovery:DescribeImportTasks", "DescribeTags": "discovery:DescribeTags", "DisassociateConfigurationItemsFromApplication": "discovery:DisassociateConfigurationItemsFromApplication", "ExportConfigurations": "discovery:ExportConfigurations", "GetDiscoverySummary": "discovery:GetDiscoverySummary", "ListConfigurations": "discovery:ListConfigurations", "ListServerNeighbors": "discovery:ListServerNeighbors", "StartContinuousExport": "discovery:StartContinuousExport", "StartDataCollectionByAgentIds": "discovery:StartDataCollectionByAgentIds", "StartExportTask": "discovery:StartExportTask", "StartImportTask": "discovery:StartImportTask", "StopContinuousExport": "discovery:StopContinuousExport", "StopDataCollectionByAgentIds": "discovery:StopDataCollectionByAgentIds", "UpdateApplication": "discovery:UpdateApplication" }, "dlm": { "CreateLifecyclePolicy": "dlm:CreateLifecyclePolicy", "DeleteLifecyclePolicy": "dlm:DeleteLifecyclePolicy", "GetLifecyclePolicies": "dlm:GetLifecyclePolicies", "GetLifecyclePolicy": "dlm:GetLifecyclePolicy", "ListTagsForResource": "dlm:ListTagsForResource", "TagResource": "dlm:TagResource", "UntagResource": "dlm:UntagResource", "UpdateLifecyclePolicy": "dlm:UpdateLifecyclePolicy" }, "dms": { "AddTagsToResource": "dms:AddTagsToResource", "ApplyPendingMaintenanceAction": "dms:ApplyPendingMaintenanceAction", "CancelReplicationTaskAssessmentRun": "dms:CancelReplicationTaskAssessmentRun", "CreateEndpoint": "dms:CreateEndpoint", "CreateEventSubscription": "dms:CreateEventSubscription", "CreateReplicationInstance": "dms:CreateReplicationInstance", "CreateReplicationSubnetGroup": "dms:CreateReplicationSubnetGroup", "CreateReplicationTask": "dms:CreateReplicationTask", "DeleteCertificate": "dms:DeleteCertificate", "DeleteConnection": "dms:DeleteConnection", "DeleteEndpoint": "dms:DeleteEndpoint", "DeleteEventSubscription": "dms:DeleteEventSubscription", "DeleteReplicationInstance": "dms:DeleteReplicationInstance", "DeleteReplicationSubnetGroup": "dms:DeleteReplicationSubnetGroup", "DeleteReplicationTask": "dms:DeleteReplicationTask", "DeleteReplicationTaskAssessmentRun": "dms:DeleteReplicationTaskAssessmentRun", "DescribeAccountAttributes": "dms:DescribeAccountAttributes", "DescribeApplicableIndividualAssessments": "dms:DescribeApplicableIndividualAssessments", "DescribeCertificates": "dms:DescribeCertificates", "DescribeConnections": "dms:DescribeConnections", "DescribeEndpointSettings": "dms:DescribeEndpointSettings", "DescribeEndpointTypes": "dms:DescribeEndpointTypes", "DescribeEndpoints": "dms:DescribeEndpoints", "DescribeEventCategories": "dms:DescribeEventCategories", "DescribeEventSubscriptions": "dms:DescribeEventSubscriptions", "DescribeEvents": "dms:DescribeEvents", "DescribeOrderableReplicationInstances": "dms:DescribeOrderableReplicationInstances", "DescribeRefreshSchemasStatus": "dms:DescribeRefreshSchemasStatus", "DescribeReplicationInstanceTaskLogs": "dms:DescribeReplicationInstanceTaskLogs", "DescribeReplicationInstances": "dms:DescribeReplicationInstances", "DescribeReplicationSubnetGroups": "dms:DescribeReplicationSubnetGroups", "DescribeReplicationTaskAssessmentResults": "dms:DescribeReplicationTaskAssessmentResults", "DescribeReplicationTaskAssessmentRuns": "dms:DescribeReplicationTaskAssessmentRuns", "DescribeReplicationTaskIndividualAssessments": "dms:DescribeReplicationTaskIndividualAssessments", "DescribeReplicationTasks": "dms:DescribeReplicationTasks", "DescribeSchemas": "dms:DescribeSchemas", "DescribeTableStatistics": "dms:DescribeTableStatistics", "ImportCertificate": "dms:ImportCertificate", "ListTagsForResource": "dms:ListTagsForResource", "ModifyEndpoint": "dms:ModifyEndpoint", "ModifyEventSubscription": "dms:ModifyEventSubscription", "ModifyReplicationInstance": "dms:ModifyReplicationInstance", "ModifyReplicationSubnetGroup": "dms:ModifyReplicationSubnetGroup", "ModifyReplicationTask": "dms:ModifyReplicationTask", "MoveReplicationTask": "dms:MoveReplicationTask", "RebootReplicationInstance": "dms:RebootReplicationInstance", "RefreshSchemas": "dms:RefreshSchemas", "ReloadTables": "dms:ReloadTables", "RemoveTagsFromResource": "dms:RemoveTagsFromResource", "StartReplicationTask": "dms:StartReplicationTask", "StartReplicationTaskAssessment": "dms:StartReplicationTaskAssessment", "StartReplicationTaskAssessmentRun": "dms:StartReplicationTaskAssessmentRun", "StopReplicationTask": "dms:StopReplicationTask", "TestConnection": "dms:TestConnection" }, "drs": { "CreateReplicationConfigurationTemplate": "drs:CreateReplicationConfigurationTemplate", "DeleteJob": "drs:DeleteJob", "DeleteRecoveryInstance": "drs:DeleteRecoveryInstance", "DeleteReplicationConfigurationTemplate": "drs:DeleteReplicationConfigurationTemplate", "DeleteSourceServer": "drs:DeleteSourceServer", "DescribeJobLogItems": "drs:DescribeJobLogItems", "DescribeJobs": "drs:DescribeJobs", "DescribeRecoveryInstances": "drs:DescribeRecoveryInstances", "DescribeRecoverySnapshots": "drs:DescribeRecoverySnapshots", "DescribeReplicationConfigurationTemplates": "drs:DescribeReplicationConfigurationTemplates", "DescribeSourceServers": "drs:DescribeSourceServers", "DisconnectRecoveryInstance": "drs:DisconnectRecoveryInstance", "DisconnectSourceServer": "drs:DisconnectSourceServer", "GetFailbackReplicationConfiguration": "drs:GetFailbackReplicationConfiguration", "GetLaunchConfiguration": "drs:GetLaunchConfiguration", "GetReplicationConfiguration": "drs:GetReplicationConfiguration", "InitializeService": "drs:InitializeService", "ListTagsForResource": "drs:ListTagsForResource", "RetryDataReplication": "drs:RetryDataReplication", "StartFailbackLaunch": "drs:StartFailbackLaunch", "StartRecovery": "drs:StartRecovery", "StopFailback": "drs:StopFailback", "TagResource": "drs:TagResource", "TerminateRecoveryInstances": "drs:TerminateRecoveryInstances", "UntagResource": "drs:UntagResource", "UpdateFailbackReplicationConfiguration": "drs:UpdateFailbackReplicationConfiguration", "UpdateLaunchConfiguration": "drs:UpdateLaunchConfiguration", "UpdateReplicationConfiguration": "drs:UpdateReplicationConfiguration", "UpdateReplicationConfigurationTemplate": "drs:UpdateReplicationConfigurationTemplate" }, "ds": { "AcceptSharedDirectory": "ds:AcceptSharedDirectory", "AddIpRoutes": "ds:AddIpRoutes", "AddRegion": "ds:AddRegion", "AddTagsToResource": "ds:AddTagsToResource", "CancelSchemaExtension": "ds:CancelSchemaExtension", "ConnectDirectory": "ds:ConnectDirectory", "CreateAlias": "ds:CreateAlias", "CreateComputer": "ds:CreateComputer", "CreateConditionalForwarder": "ds:CreateConditionalForwarder", "CreateDirectory": "ds:CreateDirectory", "CreateLogSubscription": "ds:CreateLogSubscription", "CreateMicrosoftAD": "ds:CreateMicrosoftAD", "CreateSnapshot": "ds:CreateSnapshot", "CreateTrust": "ds:CreateTrust", "DeleteConditionalForwarder": "ds:DeleteConditionalForwarder", "DeleteDirectory": "ds:DeleteDirectory", "DeleteLogSubscription": "ds:DeleteLogSubscription", "DeleteSnapshot": "ds:DeleteSnapshot", "DeleteTrust": "ds:DeleteTrust", "DeregisterCertificate": "ds:DeregisterCertificate", "DeregisterEventTopic": "ds:DeregisterEventTopic", "DescribeCertificate": "ds:DescribeCertificate", "DescribeConditionalForwarders": "ds:DescribeConditionalForwarders", "DescribeDirectories": "ds:DescribeDirectories", "DescribeDomainControllers": "ds:DescribeDomainControllers", "DescribeEventTopics": "ds:DescribeEventTopics", "DescribeLDAPSSettings": "ds:DescribeLDAPSSettings", "DescribeRegions": "ds:DescribeRegions", "DescribeSharedDirectories": "ds:DescribeSharedDirectories", "DescribeSnapshots": "ds:DescribeSnapshots", "DescribeTrusts": "ds:DescribeTrusts", "DisableClientAuthentication": "ds:DisableClientAuthentication", "DisableLDAPS": "ds:DisableLDAPS", "DisableRadius": "ds:DisableRadius", "DisableSso": "ds:DisableSso", "EnableClientAuthentication": "ds:EnableClientAuthentication", "EnableLDAPS": "ds:EnableLDAPS", "EnableRadius": "ds:EnableRadius", "EnableSso": "ds:EnableSso", "GetDirectoryLimits": "ds:GetDirectoryLimits", "GetSnapshotLimits": "ds:GetSnapshotLimits", "ListCertificates": "ds:ListCertificates", "ListIpRoutes": "ds:ListIpRoutes", "ListLogSubscriptions": "ds:ListLogSubscriptions", "ListSchemaExtensions": "ds:ListSchemaExtensions", "ListTagsForResource": "ds:ListTagsForResource", "RegisterCertificate": "ds:RegisterCertificate", "RegisterEventTopic": "ds:RegisterEventTopic", "RejectSharedDirectory": "ds:RejectSharedDirectory", "RemoveIpRoutes": "ds:RemoveIpRoutes", "RemoveRegion": "ds:RemoveRegion", "RemoveTagsFromResource": "ds:RemoveTagsFromResource", "ResetUserPassword": "ds:ResetUserPassword", "RestoreFromSnapshot": "ds:RestoreFromSnapshot", "ShareDirectory": "ds:ShareDirectory", "StartSchemaExtension": "ds:StartSchemaExtension", "UnshareDirectory": "ds:UnshareDirectory", "UpdateConditionalForwarder": "ds:UpdateConditionalForwarder", "UpdateNumberOfDomainControllers": "ds:UpdateNumberOfDomainControllers", "UpdateRadius": "ds:UpdateRadius", "UpdateTrust": "ds:UpdateTrust", "VerifyTrust": "ds:VerifyTrust" }, "dynamodb": { "BatchGetItem": "dynamodb:BatchGetItem", "BatchWriteItem": "dynamodb:BatchWriteItem", "CreateBackup": "dynamodb:CreateBackup", "CreateGlobalTable": "dynamodb:CreateGlobalTable", "CreateTable": "dynamodb:CreateTable", "DeleteBackup": "dynamodb:DeleteBackup", "DeleteItem": "dynamodb:DeleteItem", "DeleteTable": "dynamodb:DeleteTable", "DescribeBackup": "dynamodb:DescribeBackup", "DescribeContinuousBackups": "dynamodb:DescribeContinuousBackups", "DescribeContributorInsights": "dynamodb:DescribeContributorInsights", "DescribeExport": "dynamodb:DescribeExport", "DescribeGlobalTable": "dynamodb:DescribeGlobalTable", "DescribeGlobalTableSettings": "dynamodb:DescribeGlobalTableSettings", "DescribeKinesisStreamingDestination": "dynamodb:DescribeKinesisStreamingDestination", "DescribeLimits": "dynamodb:DescribeLimits", "DescribeTable": "dynamodb:DescribeTable", "DescribeTableReplicaAutoScaling": "dynamodb:DescribeTableReplicaAutoScaling", "DescribeTimeToLive": "dynamodb:DescribeTimeToLive", "DisableKinesisStreamingDestination": "dynamodb:DisableKinesisStreamingDestination", "EnableKinesisStreamingDestination": "dynamodb:EnableKinesisStreamingDestination", "ExportTableToPointInTime": "dynamodb:ExportTableToPointInTime", "GetItem": "dynamodb:GetItem", "ListBackups": "dynamodb:ListBackups", "ListContributorInsights": "dynamodb:ListContributorInsights", "ListExports": "dynamodb:ListExports", "ListGlobalTables": "dynamodb:ListGlobalTables", "ListTables": "dynamodb:ListTables", "ListTagsOfResource": "dynamodb:ListTagsOfResource", "PutItem": "dynamodb:PutItem", "Query": "dynamodb:Query", "RestoreTableFromBackup": "dynamodb:RestoreTableFromBackup", "RestoreTableToPointInTime": "dynamodb:RestoreTableToPointInTime", "Scan": "dynamodb:Scan", "TagResource": "dynamodb:TagResource", "UntagResource": "dynamodb:UntagResource", "UpdateContinuousBackups": "dynamodb:UpdateContinuousBackups", "UpdateContributorInsights": "dynamodb:UpdateContributorInsights", "UpdateGlobalTable": "dynamodb:UpdateGlobalTable", "UpdateGlobalTableSettings": "dynamodb:UpdateGlobalTableSettings", "UpdateItem": "dynamodb:UpdateItem", "UpdateTable": "dynamodb:UpdateTable", "UpdateTableReplicaAutoScaling": "dynamodb:UpdateTableReplicaAutoScaling", "UpdateTimeToLive": "dynamodb:UpdateTimeToLive" }, "dynamodbstreams": { "DescribeStream": "dynamodb:DescribeStream", "GetRecords": "dynamodb:GetRecords", "GetShardIterator": "dynamodb:GetShardIterator", "ListStreams": "dynamodb:ListStreams" }, "ebs": { "CompleteSnapshot": "ebs:CompleteSnapshot", "GetSnapshotBlock": "ebs:GetSnapshotBlock", "ListChangedBlocks": "ebs:ListChangedBlocks", "ListSnapshotBlocks": "ebs:ListSnapshotBlocks", "PutSnapshotBlock": "ebs:PutSnapshotBlock", "StartSnapshot": "ebs:StartSnapshot" }, "ec2": { "AcceptReservedInstancesExchangeQuote": "ec2:AcceptReservedInstancesExchangeQuote", "AcceptTransitGatewayMulticastDomainAssociations": "ec2:AcceptTransitGatewayMulticastDomainAssociations", "AcceptTransitGatewayPeeringAttachment": "ec2:AcceptTransitGatewayPeeringAttachment", "AcceptTransitGatewayVpcAttachment": "ec2:AcceptTransitGatewayVpcAttachment", "AcceptVpcEndpointConnections": "ec2:AcceptVpcEndpointConnections", "AcceptVpcPeeringConnection": "ec2:AcceptVpcPeeringConnection", "AdvertiseByoipCidr": "ec2:AdvertiseByoipCidr", "AllocateAddress": "ec2:AllocateAddress", "AllocateHosts": "ec2:AllocateHosts", "AllocateIpamPoolCidr": "ec2:AllocateIpamPoolCidr", "ApplySecurityGroupsToClientVpnTargetNetwork": "ec2:ApplySecurityGroupsToClientVpnTargetNetwork", "AssignIpv6Addresses": "ec2:AssignIpv6Addresses", "AssignPrivateIpAddresses": "ec2:AssignPrivateIpAddresses", "AssociateAddress": "ec2:AssociateAddress", "AssociateClientVpnTargetNetwork": "ec2:AssociateClientVpnTargetNetwork", "AssociateDhcpOptions": "ec2:AssociateDhcpOptions", "AssociateEnclaveCertificateIamRole": "ec2:AssociateEnclaveCertificateIamRole", "AssociateIamInstanceProfile": "ec2:AssociateIamInstanceProfile", "AssociateInstanceEventWindow": "ec2:AssociateInstanceEventWindow", "AssociateRouteTable": "ec2:AssociateRouteTable", "AssociateSubnetCidrBlock": "ec2:AssociateSubnetCidrBlock", "AssociateTransitGatewayMulticastDomain": "ec2:AssociateTransitGatewayMulticastDomain", "AssociateTransitGatewayRouteTable": "ec2:AssociateTransitGatewayRouteTable", "AssociateTrunkInterface": "ec2:AssociateTrunkInterface", "AssociateVpcCidrBlock": "ec2:AssociateVpcCidrBlock", "AttachClassicLinkVpc": "ec2:AttachClassicLinkVpc", "AttachInternetGateway": "ec2:AttachInternetGateway", "AttachNetworkInterface": "ec2:AttachNetworkInterface", "AttachVolume": "ec2:AttachVolume", "AttachVpnGateway": "ec2:AttachVpnGateway", "AuthorizeClientVpnIngress": "ec2:AuthorizeClientVpnIngress", "AuthorizeSecurityGroupEgress": "ec2:AuthorizeSecurityGroupEgress", "AuthorizeSecurityGroupIngress": "ec2:AuthorizeSecurityGroupIngress", "BundleInstance": "ec2:BundleInstance", "CancelBundleTask": "ec2:CancelBundleTask", "CancelCapacityReservation": "ec2:CancelCapacityReservation", "CancelCapacityReservationFleets": "ec2:CancelCapacityReservationFleets", "CancelConversionTask": "ec2:CancelConversionTask", "CancelExportTask": "ec2:CancelExportTask", "CancelImportTask": "ec2:CancelImportTask", "CancelReservedInstancesListing": "ec2:CancelReservedInstancesListing", "CancelSpotFleetRequests": "ec2:CancelSpotFleetRequests", "CancelSpotInstanceRequests": "ec2:CancelSpotInstanceRequests", "ConfirmProductInstance": "ec2:ConfirmProductInstance", "CopyFpgaImage": "ec2:CopyFpgaImage", "CopyImage": "ec2:CopyImage", "CopySnapshot": "ec2:CopySnapshot", "CreateCapacityReservation": "ec2:CreateCapacityReservation", "CreateCapacityReservationFleet": "ec2:CreateCapacityReservationFleet", "CreateCarrierGateway": "ec2:CreateCarrierGateway", "CreateClientVpnEndpoint": "ec2:CreateClientVpnEndpoint", "CreateClientVpnRoute": "ec2:CreateClientVpnRoute", "CreateCustomerGateway": "ec2:CreateCustomerGateway", "CreateDefaultSubnet": "ec2:CreateDefaultSubnet", "CreateDefaultVpc": "ec2:CreateDefaultVpc", "CreateDhcpOptions": "ec2:CreateDhcpOptions", "CreateEgressOnlyInternetGateway": "ec2:CreateEgressOnlyInternetGateway", "CreateFleet": "ec2:CreateFleet", "CreateFlowLogs": "ec2:CreateFlowLogs", "CreateFpgaImage": "ec2:CreateFpgaImage", "CreateImage": "ec2:CreateImage", "CreateInstanceEventWindow": "ec2:CreateInstanceEventWindow", "CreateInstanceExportTask": "ec2:CreateInstanceExportTask", "CreateInternetGateway": "ec2:CreateInternetGateway", "CreateIpam": "ec2:CreateIpam", "CreateIpamPool": "ec2:CreateIpamPool", "CreateIpamScope": "ec2:CreateIpamScope", "CreateKeyPair": "ec2:CreateKeyPair", "CreateLaunchTemplate": "ec2:CreateLaunchTemplate", "CreateLaunchTemplateVersion": "ec2:CreateLaunchTemplateVersion", "CreateLocalGatewayRoute": "ec2:CreateLocalGatewayRoute", "CreateLocalGatewayRouteTableVpcAssociation": "ec2:CreateLocalGatewayRouteTableVpcAssociation", "CreateManagedPrefixList": "ec2:CreateManagedPrefixList", "CreateNatGateway": "ec2:CreateNatGateway", "CreateNetworkAcl": "ec2:CreateNetworkAcl", "CreateNetworkAclEntry": "ec2:CreateNetworkAclEntry", "CreateNetworkInsightsAccessScope": "ec2:CreateNetworkInsightsAccessScope", "CreateNetworkInsightsPath": "ec2:CreateNetworkInsightsPath", "CreateNetworkInterface": "ec2:CreateNetworkInterface", "CreateNetworkInterfacePermission": "ec2:CreateNetworkInterfacePermission", "CreatePlacementGroup": "ec2:CreatePlacementGroup", "CreatePublicIpv4Pool": "ec2:CreatePublicIpv4Pool", "CreateReplaceRootVolumeTask": "ec2:CreateReplaceRootVolumeTask", "CreateReservedInstancesListing": "ec2:CreateReservedInstancesListing", "CreateRestoreImageTask": "ec2:CreateRestoreImageTask", "CreateRoute": "ec2:CreateRoute", "CreateRouteTable": "ec2:CreateRouteTable", "CreateSecurityGroup": "ec2:CreateSecurityGroup", "CreateSnapshot": "ec2:CreateSnapshot", "CreateSnapshots": "ec2:CreateSnapshots", "CreateSpotDatafeedSubscription": "ec2:CreateSpotDatafeedSubscription", "CreateStoreImageTask": "ec2:CreateStoreImageTask", "CreateSubnet": "ec2:CreateSubnet", "CreateSubnetCidrReservation": "ec2:CreateSubnetCidrReservation", "CreateTags": "ec2:CreateTags", "CreateTrafficMirrorFilter": "ec2:CreateTrafficMirrorFilter", "CreateTrafficMirrorFilterRule": "ec2:CreateTrafficMirrorFilterRule", "CreateTrafficMirrorSession": "ec2:CreateTrafficMirrorSession", "CreateTrafficMirrorTarget": "ec2:CreateTrafficMirrorTarget", "CreateTransitGateway": "ec2:CreateTransitGateway", "CreateTransitGatewayConnect": "ec2:CreateTransitGatewayConnect", "CreateTransitGatewayConnectPeer": "ec2:CreateTransitGatewayConnectPeer", "CreateTransitGatewayMulticastDomain": "ec2:CreateTransitGatewayMulticastDomain", "CreateTransitGatewayPeeringAttachment": "ec2:CreateTransitGatewayPeeringAttachment", "CreateTransitGatewayPrefixListReference": "ec2:CreateTransitGatewayPrefixListReference", "CreateTransitGatewayRoute": "ec2:CreateTransitGatewayRoute", "CreateTransitGatewayRouteTable": "ec2:CreateTransitGatewayRouteTable", "CreateTransitGatewayVpcAttachment": "ec2:CreateTransitGatewayVpcAttachment", "CreateVolume": "ec2:CreateVolume", "CreateVpc": "ec2:CreateVpc", "CreateVpcEndpoint": "ec2:CreateVpcEndpoint", "CreateVpcEndpointConnectionNotification": "ec2:CreateVpcEndpointConnectionNotification", "CreateVpcEndpointServiceConfiguration": "ec2:CreateVpcEndpointServiceConfiguration", "CreateVpcPeeringConnection": "ec2:CreateVpcPeeringConnection", "CreateVpnConnection": "ec2:CreateVpnConnection", "CreateVpnConnectionRoute": "ec2:CreateVpnConnectionRoute", "CreateVpnGateway": "ec2:CreateVpnGateway", "DeleteCarrierGateway": "ec2:DeleteCarrierGateway", "DeleteClientVpnEndpoint": "ec2:DeleteClientVpnEndpoint", "DeleteClientVpnRoute": "ec2:DeleteClientVpnRoute", "DeleteCustomerGateway": "ec2:DeleteCustomerGateway", "DeleteDhcpOptions": "ec2:DeleteDhcpOptions", "DeleteEgressOnlyInternetGateway": "ec2:DeleteEgressOnlyInternetGateway", "DeleteFleets": "ec2:DeleteFleets", "DeleteFlowLogs": "ec2:DeleteFlowLogs", "DeleteFpgaImage": "ec2:DeleteFpgaImage", "DeleteInstanceEventWindow": "ec2:DeleteInstanceEventWindow", "DeleteInternetGateway": "ec2:DeleteInternetGateway", "DeleteIpam": "ec2:DeleteIpam", "DeleteIpamPool": "ec2:DeleteIpamPool", "DeleteIpamScope": "ec2:DeleteIpamScope", "DeleteKeyPair": "ec2:DeleteKeyPair", "DeleteLaunchTemplate": "ec2:DeleteLaunchTemplate", "DeleteLaunchTemplateVersions": "ec2:DeleteLaunchTemplateVersions", "DeleteLocalGatewayRoute": "ec2:DeleteLocalGatewayRoute", "DeleteLocalGatewayRouteTableVpcAssociation": "ec2:DeleteLocalGatewayRouteTableVpcAssociation", "DeleteManagedPrefixList": "ec2:DeleteManagedPrefixList", "DeleteNatGateway": "ec2:DeleteNatGateway", "DeleteNetworkAcl": "ec2:DeleteNetworkAcl", "DeleteNetworkAclEntry": "ec2:DeleteNetworkAclEntry", "DeleteNetworkInsightsAccessScope": "ec2:DeleteNetworkInsightsAccessScope", "DeleteNetworkInsightsAccessScopeAnalysis": "ec2:DeleteNetworkInsightsAccessScopeAnalysis", "DeleteNetworkInsightsAnalysis": "ec2:DeleteNetworkInsightsAnalysis", "DeleteNetworkInsightsPath": "ec2:DeleteNetworkInsightsPath", "DeleteNetworkInterface": "ec2:DeleteNetworkInterface", "DeleteNetworkInterfacePermission": "ec2:DeleteNetworkInterfacePermission", "DeletePlacementGroup": "ec2:DeletePlacementGroup", "DeletePublicIpv4Pool": "ec2:DeletePublicIpv4Pool", "DeleteQueuedReservedInstances": "ec2:DeleteQueuedReservedInstances", "DeleteRoute": "ec2:DeleteRoute", "DeleteRouteTable": "ec2:DeleteRouteTable", "DeleteSecurityGroup": "ec2:DeleteSecurityGroup", "DeleteSnapshot": "ec2:DeleteSnapshot", "DeleteSpotDatafeedSubscription": "ec2:DeleteSpotDatafeedSubscription", "DeleteSubnet": "ec2:DeleteSubnet", "DeleteSubnetCidrReservation": "ec2:DeleteSubnetCidrReservation", "DeleteTags": "ec2:DeleteTags", "DeleteTrafficMirrorFilter": "ec2:DeleteTrafficMirrorFilter", "DeleteTrafficMirrorFilterRule": "ec2:DeleteTrafficMirrorFilterRule", "DeleteTrafficMirrorSession": "ec2:DeleteTrafficMirrorSession", "DeleteTrafficMirrorTarget": "ec2:DeleteTrafficMirrorTarget", "DeleteTransitGateway": "ec2:DeleteTransitGateway", "DeleteTransitGatewayConnect": "ec2:DeleteTransitGatewayConnect", "DeleteTransitGatewayConnectPeer": "ec2:DeleteTransitGatewayConnectPeer", "DeleteTransitGatewayMulticastDomain": "ec2:DeleteTransitGatewayMulticastDomain", "DeleteTransitGatewayPeeringAttachment": "ec2:DeleteTransitGatewayPeeringAttachment", "DeleteTransitGatewayPrefixListReference": "ec2:DeleteTransitGatewayPrefixListReference", "DeleteTransitGatewayRoute": "ec2:DeleteTransitGatewayRoute", "DeleteTransitGatewayRouteTable": "ec2:DeleteTransitGatewayRouteTable", "DeleteTransitGatewayVpcAttachment": "ec2:DeleteTransitGatewayVpcAttachment", "DeleteVolume": "ec2:DeleteVolume", "DeleteVpc": "ec2:DeleteVpc", "DeleteVpcEndpointConnectionNotifications": "ec2:DeleteVpcEndpointConnectionNotifications", "DeleteVpcEndpointServiceConfigurations": "ec2:DeleteVpcEndpointServiceConfigurations", "DeleteVpcEndpoints": "ec2:DeleteVpcEndpoints", "DeleteVpcPeeringConnection": "ec2:DeleteVpcPeeringConnection", "DeleteVpnConnection": "ec2:DeleteVpnConnection", "DeleteVpnConnectionRoute": "ec2:DeleteVpnConnectionRoute", "DeleteVpnGateway": "ec2:DeleteVpnGateway", "DeprovisionByoipCidr": "ec2:DeprovisionByoipCidr", "DeprovisionIpamPoolCidr": "ec2:DeprovisionIpamPoolCidr", "DeprovisionPublicIpv4PoolCidr": "ec2:DeprovisionPublicIpv4PoolCidr", "DeregisterImage": "ec2:DeregisterImage", "DeregisterInstanceEventNotificationAttributes": "ec2:DeregisterInstanceEventNotificationAttributes", "DeregisterTransitGatewayMulticastGroupMembers": "ec2:DeregisterTransitGatewayMulticastGroupMembers", "DeregisterTransitGatewayMulticastGroupSources": "ec2:DeregisterTransitGatewayMulticastGroupSources", "DescribeAccountAttributes": "ec2:DescribeAccountAttributes", "DescribeAddresses": "ec2:DescribeAddresses", "DescribeAddressesAttribute": "ec2:DescribeAddressesAttribute", "DescribeAggregateIdFormat": "ec2:DescribeAggregateIdFormat", "DescribeAvailabilityZones": "ec2:DescribeAvailabilityZones", "DescribeBundleTasks": "ec2:DescribeBundleTasks", "DescribeByoipCidrs": "ec2:DescribeByoipCidrs", "DescribeCapacityReservationFleets": "ec2:DescribeCapacityReservationFleets", "DescribeCapacityReservations": "ec2:DescribeCapacityReservations", "DescribeCarrierGateways": "ec2:DescribeCarrierGateways", "DescribeClassicLinkInstances": "ec2:DescribeClassicLinkInstances", "DescribeClientVpnAuthorizationRules": "ec2:DescribeClientVpnAuthorizationRules", "DescribeClientVpnConnections": "ec2:DescribeClientVpnConnections", "DescribeClientVpnEndpoints": "ec2:DescribeClientVpnEndpoints", "DescribeClientVpnRoutes": "ec2:DescribeClientVpnRoutes", "DescribeClientVpnTargetNetworks": "ec2:DescribeClientVpnTargetNetworks", "DescribeCoipPools": "ec2:DescribeCoipPools", "DescribeConversionTasks": "ec2:DescribeConversionTasks", "DescribeCustomerGateways": "ec2:DescribeCustomerGateways", "DescribeDhcpOptions": "ec2:DescribeDhcpOptions", "DescribeEgressOnlyInternetGateways": "ec2:DescribeEgressOnlyInternetGateways", "DescribeElasticGpus": "ec2:DescribeElasticGpus", "DescribeExportImageTasks": "ec2:DescribeExportImageTasks", "DescribeExportTasks": "ec2:DescribeExportTasks", "DescribeFastSnapshotRestores": "ec2:DescribeFastSnapshotRestores", "DescribeFleetHistory": "ec2:DescribeFleetHistory", "DescribeFleetInstances": "ec2:DescribeFleetInstances", "DescribeFleets": "ec2:DescribeFleets", "DescribeFlowLogs": "ec2:DescribeFlowLogs", "DescribeFpgaImageAttribute": "ec2:DescribeFpgaImageAttribute", "DescribeFpgaImages": "ec2:DescribeFpgaImages", "DescribeHostReservationOfferings": "ec2:DescribeHostReservationOfferings", "DescribeHostReservations": "ec2:DescribeHostReservations", "DescribeHosts": "ec2:DescribeHosts", "DescribeIamInstanceProfileAssociations": "ec2:DescribeIamInstanceProfileAssociations", "DescribeIdFormat": "ec2:DescribeIdFormat", "DescribeIdentityIdFormat": "ec2:DescribeIdentityIdFormat", "DescribeImageAttribute": "ec2:DescribeImageAttribute", "DescribeImages": "ec2:DescribeImages", "DescribeImportImageTasks": "ec2:DescribeImportImageTasks", "DescribeImportSnapshotTasks": "ec2:DescribeImportSnapshotTasks", "DescribeInstanceAttribute": "ec2:DescribeInstanceAttribute", "DescribeInstanceCreditSpecifications": "ec2:DescribeInstanceCreditSpecifications", "DescribeInstanceEventNotificationAttributes": "ec2:DescribeInstanceEventNotificationAttributes", "DescribeInstanceEventWindows": "ec2:DescribeInstanceEventWindows", "DescribeInstanceStatus": "ec2:DescribeInstanceStatus", "DescribeInstanceTypeOfferings": "ec2:DescribeInstanceTypeOfferings", "DescribeInstanceTypes": "ec2:DescribeInstanceTypes", "DescribeInstances": "ec2:DescribeInstances", "DescribeInternetGateways": "ec2:DescribeInternetGateways", "DescribeIpamPools": "ec2:DescribeIpamPools", "DescribeIpamScopes": "ec2:DescribeIpamScopes", "DescribeIpams": "ec2:DescribeIpams", "DescribeIpv6Pools": "ec2:DescribeIpv6Pools", "DescribeKeyPairs": "ec2:DescribeKeyPairs", "DescribeLaunchTemplateVersions": "ec2:DescribeLaunchTemplateVersions", "DescribeLaunchTemplates": "ec2:DescribeLaunchTemplates", "DescribeLocalGatewayRouteTableVirtualInterfaceGroupAssociations": "ec2:DescribeLocalGatewayRouteTableVirtualInterfaceGroupAssociations", "DescribeLocalGatewayRouteTableVpcAssociations": "ec2:DescribeLocalGatewayRouteTableVpcAssociations", "DescribeLocalGatewayRouteTables": "ec2:DescribeLocalGatewayRouteTables", "DescribeLocalGatewayVirtualInterfaceGroups": "ec2:DescribeLocalGatewayVirtualInterfaceGroups", "DescribeLocalGatewayVirtualInterfaces": "ec2:DescribeLocalGatewayVirtualInterfaces", "DescribeLocalGateways": "ec2:DescribeLocalGateways", "DescribeManagedPrefixLists": "ec2:DescribeManagedPrefixLists", "DescribeMovingAddresses": "ec2:DescribeMovingAddresses", "DescribeNatGateways": "ec2:DescribeNatGateways", "DescribeNetworkAcls": "ec2:DescribeNetworkAcls", "DescribeNetworkInsightsAccessScopeAnalyses": "ec2:DescribeNetworkInsightsAccessScopeAnalyses", "DescribeNetworkInsightsAccessScopes": "ec2:DescribeNetworkInsightsAccessScopes", "DescribeNetworkInsightsAnalyses": "ec2:DescribeNetworkInsightsAnalyses", "DescribeNetworkInsightsPaths": "ec2:DescribeNetworkInsightsPaths", "DescribeNetworkInterfaceAttribute": "ec2:DescribeNetworkInterfaceAttribute", "DescribeNetworkInterfacePermissions": "ec2:DescribeNetworkInterfacePermissions", "DescribeNetworkInterfaces": "ec2:DescribeNetworkInterfaces", "DescribePlacementGroups": "ec2:DescribePlacementGroups", "DescribePrefixLists": "ec2:DescribePrefixLists", "DescribePrincipalIdFormat": "ec2:DescribePrincipalIdFormat", "DescribePublicIpv4Pools": "ec2:DescribePublicIpv4Pools", "DescribeRegions": "ec2:DescribeRegions", "DescribeReplaceRootVolumeTasks": "ec2:DescribeReplaceRootVolumeTasks", "DescribeReservedInstances": "ec2:DescribeReservedInstances", "DescribeReservedInstancesListings": "ec2:DescribeReservedInstancesListings", "DescribeReservedInstancesModifications": "ec2:DescribeReservedInstancesModifications", "DescribeReservedInstancesOfferings": "ec2:DescribeReservedInstancesOfferings", "DescribeRouteTables": "ec2:DescribeRouteTables", "DescribeScheduledInstanceAvailability": "ec2:DescribeScheduledInstanceAvailability", "DescribeScheduledInstances": "ec2:DescribeScheduledInstances", "DescribeSecurityGroupReferences": "ec2:DescribeSecurityGroupReferences", "DescribeSecurityGroupRules": "ec2:DescribeSecurityGroupRules", "DescribeSecurityGroups": "ec2:DescribeSecurityGroups", "DescribeSnapshotAttribute": "ec2:DescribeSnapshotAttribute", "DescribeSnapshotTierStatus": "ec2:DescribeSnapshotTierStatus", "DescribeSnapshots": "ec2:DescribeSnapshots", "DescribeSpotDatafeedSubscription": "ec2:DescribeSpotDatafeedSubscription", "DescribeSpotFleetInstances": "ec2:DescribeSpotFleetInstances", "DescribeSpotFleetRequestHistory": "ec2:DescribeSpotFleetRequestHistory", "DescribeSpotFleetRequests": "ec2:DescribeSpotFleetRequests", "DescribeSpotInstanceRequests": "ec2:DescribeSpotInstanceRequests", "DescribeSpotPriceHistory": "ec2:DescribeSpotPriceHistory", "DescribeStaleSecurityGroups": "ec2:DescribeStaleSecurityGroups", "DescribeStoreImageTasks": "ec2:DescribeStoreImageTasks", "DescribeSubnets": "ec2:DescribeSubnets", "DescribeTags": "ec2:DescribeTags", "DescribeTrafficMirrorFilters": "ec2:DescribeTrafficMirrorFilters", "DescribeTrafficMirrorSessions": "ec2:DescribeTrafficMirrorSessions", "DescribeTrafficMirrorTargets": "ec2:DescribeTrafficMirrorTargets", "DescribeTransitGatewayAttachments": "ec2:DescribeTransitGatewayAttachments", "DescribeTransitGatewayConnectPeers": "ec2:DescribeTransitGatewayConnectPeers", "DescribeTransitGatewayConnects": "ec2:DescribeTransitGatewayConnects", "DescribeTransitGatewayMulticastDomains": "ec2:DescribeTransitGatewayMulticastDomains", "DescribeTransitGatewayPeeringAttachments": "ec2:DescribeTransitGatewayPeeringAttachments", "DescribeTransitGatewayRouteTables": "ec2:DescribeTransitGatewayRouteTables", "DescribeTransitGatewayVpcAttachments": "ec2:DescribeTransitGatewayVpcAttachments", "DescribeTransitGateways": "ec2:DescribeTransitGateways", "DescribeTrunkInterfaceAssociations": "ec2:DescribeTrunkInterfaceAssociations", "DescribeVolumeAttribute": "ec2:DescribeVolumeAttribute", "DescribeVolumeStatus": "ec2:DescribeVolumeStatus", "DescribeVolumes": "ec2:DescribeVolumes", "DescribeVolumesModifications": "ec2:DescribeVolumesModifications", "DescribeVpcAttribute": "ec2:DescribeVpcAttribute", "DescribeVpcClassicLink": "ec2:DescribeVpcClassicLink", "DescribeVpcClassicLinkDnsSupport": "ec2:DescribeVpcClassicLinkDnsSupport", "DescribeVpcEndpointConnectionNotifications": "ec2:DescribeVpcEndpointConnectionNotifications", "DescribeVpcEndpointConnections": "ec2:DescribeVpcEndpointConnections", "DescribeVpcEndpointServiceConfigurations": "ec2:DescribeVpcEndpointServiceConfigurations", "DescribeVpcEndpointServicePermissions": "ec2:DescribeVpcEndpointServicePermissions", "DescribeVpcEndpointServices": "ec2:DescribeVpcEndpointServices", "DescribeVpcEndpoints": "ec2:DescribeVpcEndpoints", "DescribeVpcPeeringConnections": "ec2:DescribeVpcPeeringConnections", "DescribeVpcs": "ec2:DescribeVpcs", "DescribeVpnConnections": "ec2:DescribeVpnConnections", "DescribeVpnGateways": "ec2:DescribeVpnGateways", "DetachClassicLinkVpc": "ec2:DetachClassicLinkVpc", "DetachInternetGateway": "ec2:DetachInternetGateway", "DetachNetworkInterface": "ec2:DetachNetworkInterface", "DetachVolume": "ec2:DetachVolume", "DetachVpnGateway": "ec2:DetachVpnGateway", "DisableEbsEncryptionByDefault": "ec2:DisableEbsEncryptionByDefault", "DisableFastSnapshotRestores": "ec2:DisableFastSnapshotRestores", "DisableImageDeprecation": "ec2:DisableImageDeprecation", "DisableIpamOrganizationAdminAccount": "ec2:DisableIpamOrganizationAdminAccount", "DisableSerialConsoleAccess": "ec2:DisableSerialConsoleAccess", "DisableTransitGatewayRouteTablePropagation": "ec2:DisableTransitGatewayRouteTablePropagation", "DisableVgwRoutePropagation": "ec2:DisableVgwRoutePropagation", "DisableVpcClassicLink": "ec2:DisableVpcClassicLink", "DisableVpcClassicLinkDnsSupport": "ec2:DisableVpcClassicLinkDnsSupport", "DisassociateAddress": "ec2:DisassociateAddress", "DisassociateClientVpnTargetNetwork": "ec2:DisassociateClientVpnTargetNetwork", "DisassociateEnclaveCertificateIamRole": "ec2:DisassociateEnclaveCertificateIamRole", "DisassociateIamInstanceProfile": "ec2:DisassociateIamInstanceProfile", "DisassociateInstanceEventWindow": "ec2:DisassociateInstanceEventWindow", "DisassociateRouteTable": "ec2:DisassociateRouteTable", "DisassociateSubnetCidrBlock": "ec2:DisassociateSubnetCidrBlock", "DisassociateTransitGatewayMulticastDomain": "ec2:DisassociateTransitGatewayMulticastDomain", "DisassociateTransitGatewayRouteTable": "ec2:DisassociateTransitGatewayRouteTable", "DisassociateTrunkInterface": "ec2:DisassociateTrunkInterface", "DisassociateVpcCidrBlock": "ec2:DisassociateVpcCidrBlock", "EnableEbsEncryptionByDefault": "ec2:EnableEbsEncryptionByDefault", "EnableFastSnapshotRestores": "ec2:EnableFastSnapshotRestores", "EnableImageDeprecation": "ec2:EnableImageDeprecation", "EnableIpamOrganizationAdminAccount": "ec2:EnableIpamOrganizationAdminAccount", "EnableSerialConsoleAccess": "ec2:EnableSerialConsoleAccess", "EnableTransitGatewayRouteTablePropagation": "ec2:EnableTransitGatewayRouteTablePropagation", "EnableVgwRoutePropagation": "ec2:EnableVgwRoutePropagation", "EnableVolumeIO": "ec2:EnableVolumeIO", "EnableVpcClassicLink": "ec2:EnableVpcClassicLink", "EnableVpcClassicLinkDnsSupport": "ec2:EnableVpcClassicLinkDnsSupport", "ExportClientVpnClientCertificateRevocationList": "ec2:ExportClientVpnClientCertificateRevocationList", "ExportClientVpnClientConfiguration": "ec2:ExportClientVpnClientConfiguration", "ExportImage": "ec2:ExportImage", "ExportTransitGatewayRoutes": "ec2:ExportTransitGatewayRoutes", "GetAssociatedEnclaveCertificateIamRoles": "ec2:GetAssociatedEnclaveCertificateIamRoles", "GetAssociatedIpv6PoolCidrs": "ec2:GetAssociatedIpv6PoolCidrs", "GetCapacityReservationUsage": "ec2:GetCapacityReservationUsage", "GetCoipPoolUsage": "ec2:GetCoipPoolUsage", "GetConsoleOutput": "ec2:GetConsoleOutput", "GetConsoleScreenshot": "ec2:GetConsoleScreenshot", "GetDefaultCreditSpecification": "ec2:GetDefaultCreditSpecification", "GetEbsDefaultKmsKeyId": "ec2:GetEbsDefaultKmsKeyId", "GetEbsEncryptionByDefault": "ec2:GetEbsEncryptionByDefault", "GetFlowLogsIntegrationTemplate": "ec2:GetFlowLogsIntegrationTemplate", "GetGroupsForCapacityReservation": "ec2:GetGroupsForCapacityReservation", "GetHostReservationPurchasePreview": "ec2:GetHostReservationPurchasePreview", "GetInstanceTypesFromInstanceRequirements": "ec2:GetInstanceTypesFromInstanceRequirements", "GetIpamAddressHistory": "ec2:GetIpamAddressHistory", "GetIpamPoolAllocations": "ec2:GetIpamPoolAllocations", "GetIpamPoolCidrs": "ec2:GetIpamPoolCidrs", "GetIpamResourceCidrs": "ec2:GetIpamResourceCidrs", "GetLaunchTemplateData": "ec2:GetLaunchTemplateData", "GetManagedPrefixListAssociations": "ec2:GetManagedPrefixListAssociations", "GetManagedPrefixListEntries": "ec2:GetManagedPrefixListEntries", "GetNetworkInsightsAccessScopeAnalysisFindings": "ec2:GetNetworkInsightsAccessScopeAnalysisFindings", "GetNetworkInsightsAccessScopeContent": "ec2:GetNetworkInsightsAccessScopeContent", "GetPasswordData": "ec2:GetPasswordData", "GetReservedInstancesExchangeQuote": "ec2:GetReservedInstancesExchangeQuote", "GetSerialConsoleAccessStatus": "ec2:GetSerialConsoleAccessStatus", "GetSpotPlacementScores": "ec2:GetSpotPlacementScores", "GetSubnetCidrReservations": "ec2:GetSubnetCidrReservations", "GetTransitGatewayAttachmentPropagations": "ec2:GetTransitGatewayAttachmentPropagations", "GetTransitGatewayMulticastDomainAssociations": "ec2:GetTransitGatewayMulticastDomainAssociations", "GetTransitGatewayPrefixListReferences": "ec2:GetTransitGatewayPrefixListReferences", "GetTransitGatewayRouteTableAssociations": "ec2:GetTransitGatewayRouteTableAssociations", "GetTransitGatewayRouteTablePropagations": "ec2:GetTransitGatewayRouteTablePropagations", "GetVpnConnectionDeviceSampleConfiguration": "ec2:GetVpnConnectionDeviceSampleConfiguration", "GetVpnConnectionDeviceTypes": "ec2:GetVpnConnectionDeviceTypes", "ImportClientVpnClientCertificateRevocationList": "ec2:ImportClientVpnClientCertificateRevocationList", "ImportImage": "ec2:ImportImage", "ImportInstance": "ec2:ImportInstance", "ImportKeyPair": "ec2:ImportKeyPair", "ImportSnapshot": "ec2:ImportSnapshot", "ImportVolume": "ec2:ImportVolume", "ListSnapshotsInRecycleBin": "ec2:ListSnapshotsInRecycleBin", "ModifyAddressAttribute": "ec2:ModifyAddressAttribute", "ModifyAvailabilityZoneGroup": "ec2:ModifyAvailabilityZoneGroup", "ModifyCapacityReservation": "ec2:ModifyCapacityReservation", "ModifyCapacityReservationFleet": "ec2:ModifyCapacityReservationFleet", "ModifyClientVpnEndpoint": "ec2:ModifyClientVpnEndpoint", "ModifyDefaultCreditSpecification": "ec2:ModifyDefaultCreditSpecification", "ModifyEbsDefaultKmsKeyId": "ec2:ModifyEbsDefaultKmsKeyId", "ModifyFleet": "ec2:ModifyFleet", "ModifyFpgaImageAttribute": "ec2:ModifyFpgaImageAttribute", "ModifyHosts": "ec2:ModifyHosts", "ModifyIdFormat": "ec2:ModifyIdFormat", "ModifyIdentityIdFormat": "ec2:ModifyIdentityIdFormat", "ModifyImageAttribute": "ec2:ModifyImageAttribute", "ModifyInstanceAttribute": "ec2:ModifyInstanceAttribute", "ModifyInstanceCapacityReservationAttributes": "ec2:ModifyInstanceCapacityReservationAttributes", "ModifyInstanceCreditSpecification": "ec2:ModifyInstanceCreditSpecification", "ModifyInstanceEventStartTime": "ec2:ModifyInstanceEventStartTime", "ModifyInstanceEventWindow": "ec2:ModifyInstanceEventWindow", "ModifyInstanceMetadataOptions": "ec2:ModifyInstanceMetadataOptions", "ModifyInstancePlacement": "ec2:ModifyInstancePlacement", "ModifyIpam": "ec2:ModifyIpam", "ModifyIpamPool": "ec2:ModifyIpamPool", "ModifyIpamResourceCidr": "ec2:ModifyIpamResourceCidr", "ModifyIpamScope": "ec2:ModifyIpamScope", "ModifyLaunchTemplate": "ec2:ModifyLaunchTemplate", "ModifyManagedPrefixList": "ec2:ModifyManagedPrefixList", "ModifyNetworkInterfaceAttribute": "ec2:ModifyNetworkInterfaceAttribute", "ModifyReservedInstances": "ec2:ModifyReservedInstances", "ModifySecurityGroupRules": "ec2:ModifySecurityGroupRules", "ModifySnapshotAttribute": "ec2:ModifySnapshotAttribute", "ModifySnapshotTier": "ec2:ModifySnapshotTier", "ModifySpotFleetRequest": "ec2:ModifySpotFleetRequest", "ModifySubnetAttribute": "ec2:ModifySubnetAttribute", "ModifyTrafficMirrorFilterNetworkServices": "ec2:ModifyTrafficMirrorFilterNetworkServices", "ModifyTrafficMirrorFilterRule": "ec2:ModifyTrafficMirrorFilterRule", "ModifyTrafficMirrorSession": "ec2:ModifyTrafficMirrorSession", "ModifyTransitGateway": "ec2:ModifyTransitGateway", "ModifyTransitGatewayPrefixListReference": "ec2:ModifyTransitGatewayPrefixListReference", "ModifyTransitGatewayVpcAttachment": "ec2:ModifyTransitGatewayVpcAttachment", "ModifyVolume": "ec2:ModifyVolume", "ModifyVolumeAttribute": "ec2:ModifyVolumeAttribute", "ModifyVpcAttribute": "ec2:ModifyVpcAttribute", "ModifyVpcEndpoint": "ec2:ModifyVpcEndpoint", "ModifyVpcEndpointConnectionNotification": "ec2:ModifyVpcEndpointConnectionNotification", "ModifyVpcEndpointServiceConfiguration": "ec2:ModifyVpcEndpointServiceConfiguration", "ModifyVpcEndpointServicePermissions": "ec2:ModifyVpcEndpointServicePermissions", "ModifyVpcPeeringConnectionOptions": "ec2:ModifyVpcPeeringConnectionOptions", "ModifyVpcTenancy": "ec2:ModifyVpcTenancy", "ModifyVpnConnection": "ec2:ModifyVpnConnection", "ModifyVpnConnectionOptions": "ec2:ModifyVpnConnectionOptions", "ModifyVpnTunnelCertificate": "ec2:ModifyVpnTunnelCertificate", "ModifyVpnTunnelOptions": "ec2:ModifyVpnTunnelOptions", "MonitorInstances": "ec2:MonitorInstances", "MoveAddressToVpc": "ec2:MoveAddressToVpc", "MoveByoipCidrToIpam": "ec2:MoveByoipCidrToIpam", "ProvisionByoipCidr": "ec2:ProvisionByoipCidr", "ProvisionIpamPoolCidr": "ec2:ProvisionIpamPoolCidr", "ProvisionPublicIpv4PoolCidr": "ec2:ProvisionPublicIpv4PoolCidr", "PurchaseHostReservation": "ec2:PurchaseHostReservation", "PurchaseReservedInstancesOffering": "ec2:PurchaseReservedInstancesOffering", "PurchaseScheduledInstances": "ec2:PurchaseScheduledInstances", "RebootInstances": "ec2:RebootInstances", "RegisterImage": "ec2:RegisterImage", "RegisterInstanceEventNotificationAttributes": "ec2:RegisterInstanceEventNotificationAttributes", "RegisterTransitGatewayMulticastGroupMembers": "ec2:RegisterTransitGatewayMulticastGroupMembers", "RegisterTransitGatewayMulticastGroupSources": "ec2:RegisterTransitGatewayMulticastGroupSources", "RejectTransitGatewayMulticastDomainAssociations": "ec2:RejectTransitGatewayMulticastDomainAssociations", "RejectTransitGatewayPeeringAttachment": "ec2:RejectTransitGatewayPeeringAttachment", "RejectTransitGatewayVpcAttachment": "ec2:RejectTransitGatewayVpcAttachment", "RejectVpcEndpointConnections": "ec2:RejectVpcEndpointConnections", "RejectVpcPeeringConnection": "ec2:RejectVpcPeeringConnection", "ReleaseAddress": "ec2:ReleaseAddress", "ReleaseHosts": "ec2:ReleaseHosts", "ReleaseIpamPoolAllocation": "ec2:ReleaseIpamPoolAllocation", "ReplaceIamInstanceProfileAssociation": "ec2:ReplaceIamInstanceProfileAssociation", "ReplaceNetworkAclAssociation": "ec2:ReplaceNetworkAclAssociation", "ReplaceNetworkAclEntry": "ec2:ReplaceNetworkAclEntry", "ReplaceRoute": "ec2:ReplaceRoute", "ReplaceRouteTableAssociation": "ec2:ReplaceRouteTableAssociation", "ReplaceTransitGatewayRoute": "ec2:ReplaceTransitGatewayRoute", "ReportInstanceStatus": "ec2:ReportInstanceStatus", "RequestSpotFleet": "ec2:RequestSpotFleet", "RequestSpotInstances": "ec2:RequestSpotInstances", "ResetAddressAttribute": "ec2:ResetAddressAttribute", "ResetEbsDefaultKmsKeyId": "ec2:ResetEbsDefaultKmsKeyId", "ResetFpgaImageAttribute": "ec2:ResetFpgaImageAttribute", "ResetImageAttribute": "ec2:ResetImageAttribute", "ResetInstanceAttribute": "ec2:ResetInstanceAttribute", "ResetNetworkInterfaceAttribute": "ec2:ResetNetworkInterfaceAttribute", "ResetSnapshotAttribute": "ec2:ResetSnapshotAttribute", "RestoreAddressToClassic": "ec2:RestoreAddressToClassic", "RestoreManagedPrefixListVersion": "ec2:RestoreManagedPrefixListVersion", "RestoreSnapshotFromRecycleBin": "ec2:RestoreSnapshotFromRecycleBin", "RestoreSnapshotTier": "ec2:RestoreSnapshotTier", "RevokeClientVpnIngress": "ec2:RevokeClientVpnIngress", "RevokeSecurityGroupEgress": "ec2:RevokeSecurityGroupEgress", "RevokeSecurityGroupIngress": "ec2:RevokeSecurityGroupIngress", "RunInstances": "ec2:RunInstances", "RunScheduledInstances": "ec2:RunScheduledInstances", "SearchLocalGatewayRoutes": "ec2:SearchLocalGatewayRoutes", "SearchTransitGatewayMulticastGroups": "ec2:SearchTransitGatewayMulticastGroups", "SearchTransitGatewayRoutes": "ec2:SearchTransitGatewayRoutes", "SendDiagnosticInterrupt": "ec2:SendDiagnosticInterrupt", "StartInstances": "ec2:StartInstances", "StartNetworkInsightsAccessScopeAnalysis": "ec2:StartNetworkInsightsAccessScopeAnalysis", "StartNetworkInsightsAnalysis": "ec2:StartNetworkInsightsAnalysis", "StartVpcEndpointServicePrivateDnsVerification": "ec2:StartVpcEndpointServicePrivateDnsVerification", "StopInstances": "ec2:StopInstances", "TerminateClientVpnConnections": "ec2:TerminateClientVpnConnections", "TerminateInstances": "ec2:TerminateInstances", "UnassignIpv6Addresses": "ec2:UnassignIpv6Addresses", "UnassignPrivateIpAddresses": "ec2:UnassignPrivateIpAddresses", "UnmonitorInstances": "ec2:UnmonitorInstances", "UpdateSecurityGroupRuleDescriptionsEgress": "ec2:UpdateSecurityGroupRuleDescriptionsEgress", "UpdateSecurityGroupRuleDescriptionsIngress": "ec2:UpdateSecurityGroupRuleDescriptionsIngress", "WithdrawByoipCidr": "ec2:WithdrawByoipCidr" }, "ec2-instance-connect": { "SendSSHPublicKey": "ec2-instance-connect:SendSSHPublicKey", "SendSerialConsoleSSHPublicKey": "ec2-instance-connect:SendSerialConsoleSSHPublicKey" }, "ecr": { "BatchCheckLayerAvailability": "ecr:BatchCheckLayerAvailability", "BatchDeleteImage": "ecr:BatchDeleteImage", "BatchGetImage": "ecr:BatchGetImage", "BatchGetRepositoryScanningConfiguration": "ecr:BatchGetRepositoryScanningConfiguration", "CompleteLayerUpload": "ecr:CompleteLayerUpload", "CreatePullThroughCacheRule": "ecr:CreatePullThroughCacheRule", "CreateRepository": "ecr:CreateRepository", "DeleteLifecyclePolicy": "ecr:DeleteLifecyclePolicy", "DeletePullThroughCacheRule": "ecr:DeletePullThroughCacheRule", "DeleteRegistryPolicy": "ecr:DeleteRegistryPolicy", "DeleteRepository": "ecr:DeleteRepository", "DeleteRepositoryPolicy": "ecr:DeleteRepositoryPolicy", "DescribeImageReplicationStatus": "ecr:DescribeImageReplicationStatus", "DescribeImageScanFindings": "ecr:DescribeImageScanFindings", "DescribeImages": "ecr:DescribeImages", "DescribePullThroughCacheRules": "ecr:DescribePullThroughCacheRules", "DescribeRegistry": "ecr:DescribeRegistry", "DescribeRepositories": "ecr:DescribeRepositories", "GetAuthorizationToken": "ecr:GetAuthorizationToken", "GetDownloadUrlForLayer": "ecr:GetDownloadUrlForLayer", "GetLifecyclePolicy": "ecr:GetLifecyclePolicy", "GetLifecyclePolicyPreview": "ecr:GetLifecyclePolicyPreview", "GetRegistryPolicy": "ecr:GetRegistryPolicy", "GetRegistryScanningConfiguration": "ecr:GetRegistryScanningConfiguration", "GetRepositoryPolicy": "ecr:GetRepositoryPolicy", "InitiateLayerUpload": "ecr:InitiateLayerUpload", "ListImages": "ecr:ListImages", "ListTagsForResource": "ecr:ListTagsForResource", "PutImage": "ecr:PutImage", "PutImageScanningConfiguration": "ecr:PutImageScanningConfiguration", "PutImageTagMutability": "ecr:PutImageTagMutability", "PutLifecyclePolicy": "ecr:PutLifecyclePolicy", "PutRegistryPolicy": "ecr:PutRegistryPolicy", "PutRegistryScanningConfiguration": "ecr:PutRegistryScanningConfiguration", "PutReplicationConfiguration": "ecr:PutReplicationConfiguration", "SetRepositoryPolicy": "ecr:SetRepositoryPolicy", "StartImageScan": "ecr:StartImageScan", "StartLifecyclePolicyPreview": "ecr:StartLifecyclePolicyPreview", "TagResource": "ecr:TagResource", "UntagResource": "ecr:UntagResource", "UploadLayerPart": "ecr:UploadLayerPart" }, "ecr-public": { "BatchCheckLayerAvailability": "ecr-public:BatchCheckLayerAvailability", "BatchDeleteImage": "ecr-public:BatchDeleteImage", "CompleteLayerUpload": "ecr-public:CompleteLayerUpload", "CreateRepository": "ecr-public:CreateRepository", "DeleteRepository": "ecr-public:DeleteRepository", "DeleteRepositoryPolicy": "ecr-public:DeleteRepositoryPolicy", "DescribeImageTags": "ecr-public:DescribeImageTags", "DescribeImages": "ecr-public:DescribeImages", "DescribeRegistries": "ecr-public:DescribeRegistries", "DescribeRepositories": "ecr-public:DescribeRepositories", "GetAuthorizationToken": "ecr-public:GetAuthorizationToken", "GetRegistryCatalogData": "ecr-public:GetRegistryCatalogData", "GetRepositoryCatalogData": "ecr-public:GetRepositoryCatalogData", "GetRepositoryPolicy": "ecr-public:GetRepositoryPolicy", "InitiateLayerUpload": "ecr-public:InitiateLayerUpload", "ListTagsForResource": "ecr-public:ListTagsForResource", "PutImage": "ecr-public:PutImage", "PutRegistryCatalogData": "ecr-public:PutRegistryCatalogData", "PutRepositoryCatalogData": "ecr-public:PutRepositoryCatalogData", "SetRepositoryPolicy": "ecr-public:SetRepositoryPolicy", "TagResource": "ecr-public:TagResource", "UntagResource": "ecr-public:UntagResource", "UploadLayerPart": "ecr-public:UploadLayerPart" }, "ecs": { "CreateCapacityProvider": "ecs:CreateCapacityProvider", "CreateCluster": "ecs:CreateCluster", "CreateService": "ecs:CreateService", "CreateTaskSet": "ecs:CreateTaskSet", "DeleteAccountSetting": "ecs:DeleteAccountSetting", "DeleteAttributes": "ecs:DeleteAttributes", "DeleteCapacityProvider": "ecs:DeleteCapacityProvider", "DeleteCluster": "ecs:DeleteCluster", "DeleteService": "ecs:DeleteService", "DeleteTaskSet": "ecs:DeleteTaskSet", "DeregisterContainerInstance": "ecs:DeregisterContainerInstance", "DeregisterTaskDefinition": "ecs:DeregisterTaskDefinition", "DescribeCapacityProviders": "ecs:DescribeCapacityProviders", "DescribeClusters": "ecs:DescribeClusters", "DescribeContainerInstances": "ecs:DescribeContainerInstances", "DescribeServices": "ecs:DescribeServices", "DescribeTaskDefinition": "ecs:DescribeTaskDefinition", "DescribeTaskSets": "ecs:DescribeTaskSets", "DescribeTasks": "ecs:DescribeTasks", "DiscoverPollEndpoint": "ecs:DiscoverPollEndpoint", "ExecuteCommand": "ecs:ExecuteCommand", "ListAccountSettings": "ecs:ListAccountSettings", "ListAttributes": "ecs:ListAttributes", "ListClusters": "ecs:ListClusters", "ListContainerInstances": "ecs:ListContainerInstances", "ListServices": "ecs:ListServices", "ListTagsForResource": "ecs:ListTagsForResource", "ListTaskDefinitionFamilies": "ecs:ListTaskDefinitionFamilies", "ListTaskDefinitions": "ecs:ListTaskDefinitions", "ListTasks": "ecs:ListTasks", "PutAccountSetting": "ecs:PutAccountSetting", "PutAccountSettingDefault": "ecs:PutAccountSettingDefault", "PutAttributes": "ecs:PutAttributes", "PutClusterCapacityProviders": "ecs:PutClusterCapacityProviders", "RegisterContainerInstance": "ecs:RegisterContainerInstance", "RegisterTaskDefinition": "ecs:RegisterTaskDefinition", "RunTask": "ecs:RunTask", "StartTask": "ecs:StartTask", "StopTask": "ecs:StopTask", "SubmitAttachmentStateChanges": "ecs:SubmitAttachmentStateChanges", "SubmitContainerStateChange": "ecs:SubmitContainerStateChange", "SubmitTaskStateChange": "ecs:SubmitTaskStateChange", "TagResource": "ecs:TagResource", "UntagResource": "ecs:UntagResource", "UpdateCapacityProvider": "ecs:UpdateCapacityProvider", "UpdateCluster": "ecs:UpdateCluster", "UpdateClusterSettings": "ecs:UpdateClusterSettings", "UpdateContainerAgent": "ecs:UpdateContainerAgent", "UpdateContainerInstancesState": "ecs:UpdateContainerInstancesState", "UpdateService": "ecs:UpdateService", "UpdateServicePrimaryTaskSet": "ecs:UpdateServicePrimaryTaskSet", "UpdateTaskSet": "ecs:UpdateTaskSet" }, "efs": { "CreateAccessPoint": "elasticfilesystem:CreateAccessPoint", "CreateFileSystem": "elasticfilesystem:CreateFileSystem", "CreateMountTarget": "elasticfilesystem:CreateMountTarget", "CreateTags": "elasticfilesystem:CreateTags", "DeleteAccessPoint": "elasticfilesystem:DeleteAccessPoint", "DeleteFileSystem": "elasticfilesystem:DeleteFileSystem", "DeleteFileSystemPolicy": "elasticfilesystem:DeleteFileSystemPolicy", "DeleteMountTarget": "elasticfilesystem:DeleteMountTarget", "DeleteTags": "elasticfilesystem:DeleteTags", "DescribeAccessPoints": "elasticfilesystem:DescribeAccessPoints", "DescribeAccountPreferences": "elasticfilesystem:DescribeAccountPreferences", "DescribeBackupPolicy": "elasticfilesystem:DescribeBackupPolicy", "DescribeFileSystemPolicy": "elasticfilesystem:DescribeFileSystemPolicy", "DescribeFileSystems": "elasticfilesystem:DescribeFileSystems", "DescribeLifecycleConfiguration": "elasticfilesystem:DescribeLifecycleConfiguration", "DescribeMountTargetSecurityGroups": "elasticfilesystem:DescribeMountTargetSecurityGroups", "DescribeMountTargets": "elasticfilesystem:DescribeMountTargets", "DescribeTags": "elasticfilesystem:DescribeTags", "ListTagsForResource": "elasticfilesystem:ListTagsForResource", "ModifyMountTargetSecurityGroups": "elasticfilesystem:ModifyMountTargetSecurityGroups", "PutAccountPreferences": "elasticfilesystem:PutAccountPreferences", "PutBackupPolicy": "elasticfilesystem:PutBackupPolicy", "PutFileSystemPolicy": "elasticfilesystem:PutFileSystemPolicy", "PutLifecycleConfiguration": "elasticfilesystem:PutLifecycleConfiguration", "TagResource": "elasticfilesystem:TagResource", "UntagResource": "elasticfilesystem:UntagResource", "UpdateFileSystem": "elasticfilesystem:UpdateFileSystem" }, "eks": { "AssociateEncryptionConfig": "eks:AssociateEncryptionConfig", "AssociateIdentityProviderConfig": "eks:AssociateIdentityProviderConfig", "CreateAddon": "eks:CreateAddon", "CreateCluster": "eks:CreateCluster", "CreateFargateProfile": "eks:CreateFargateProfile", "CreateNodegroup": "eks:CreateNodegroup", "DeleteAddon": "eks:DeleteAddon", "DeleteCluster": "eks:DeleteCluster", "DeleteFargateProfile": "eks:DeleteFargateProfile", "DeleteNodegroup": "eks:DeleteNodegroup", "DescribeAddon": "eks:DescribeAddon", "DescribeAddonVersions": "eks:DescribeAddonVersions", "DescribeCluster": "eks:DescribeCluster", "DescribeFargateProfile": "eks:DescribeFargateProfile", "DescribeIdentityProviderConfig": "eks:DescribeIdentityProviderConfig", "DescribeNodegroup": "eks:DescribeNodegroup", "DescribeUpdate": "eks:DescribeUpdate", "DisassociateIdentityProviderConfig": "eks:DisassociateIdentityProviderConfig", "ListAddons": "eks:ListAddons", "ListClusters": "eks:ListClusters", "ListFargateProfiles": "eks:ListFargateProfiles", "ListIdentityProviderConfigs": "eks:ListIdentityProviderConfigs", "ListNodegroups": "eks:ListNodegroups", "ListTagsForResource": "eks:ListTagsForResource", "ListUpdates": "eks:ListUpdates", "TagResource": "eks:TagResource", "UntagResource": "eks:UntagResource", "UpdateAddon": "eks:UpdateAddon", "UpdateClusterConfig": "eks:UpdateClusterConfig", "UpdateClusterVersion": "eks:UpdateClusterVersion", "UpdateNodegroupConfig": "eks:UpdateNodegroupConfig", "UpdateNodegroupVersion": "eks:UpdateNodegroupVersion" }, "elastic-inference": { "DescribeAcceleratorOfferings": "elastic-inference:DescribeAcceleratorOfferings", "DescribeAcceleratorTypes": "elastic-inference:DescribeAcceleratorTypes", "DescribeAccelerators": "elastic-inference:DescribeAccelerators", "ListTagsForResource": "elastic-inference:ListTagsForResource", "TagResource": "elastic-inference:TagResource", "UntagResource": "elastic-inference:UntagResource" }, "elasticache": { "AddTagsToResource": "elasticache:AddTagsToResource", "AuthorizeCacheSecurityGroupIngress": "elasticache:AuthorizeCacheSecurityGroupIngress", "BatchApplyUpdateAction": "elasticache:BatchApplyUpdateAction", "BatchStopUpdateAction": "elasticache:BatchStopUpdateAction", "CompleteMigration": "elasticache:CompleteMigration", "CopySnapshot": "elasticache:CopySnapshot", "CreateCacheCluster": "elasticache:CreateCacheCluster", "CreateCacheParameterGroup": "elasticache:CreateCacheParameterGroup", "CreateCacheSecurityGroup": "elasticache:CreateCacheSecurityGroup", "CreateCacheSubnetGroup": "elasticache:CreateCacheSubnetGroup", "CreateGlobalReplicationGroup": "elasticache:CreateGlobalReplicationGroup", "CreateReplicationGroup": "elasticache:CreateReplicationGroup", "CreateSnapshot": "elasticache:CreateSnapshot", "CreateUser": "elasticache:CreateUser", "CreateUserGroup": "elasticache:CreateUserGroup", "DecreaseNodeGroupsInGlobalReplicationGroup": "elasticache:DecreaseNodeGroupsInGlobalReplicationGroup", "DecreaseReplicaCount": "elasticache:DecreaseReplicaCount", "DeleteCacheCluster": "elasticache:DeleteCacheCluster", "DeleteCacheParameterGroup": "elasticache:DeleteCacheParameterGroup", "DeleteCacheSecurityGroup": "elasticache:DeleteCacheSecurityGroup", "DeleteCacheSubnetGroup": "elasticache:DeleteCacheSubnetGroup", "DeleteGlobalReplicationGroup": "elasticache:DeleteGlobalReplicationGroup", "DeleteReplicationGroup": "elasticache:DeleteReplicationGroup", "DeleteSnapshot": "elasticache:DeleteSnapshot", "DeleteUser": "elasticache:DeleteUser", "DeleteUserGroup": "elasticache:DeleteUserGroup", "DescribeCacheClusters": "elasticache:DescribeCacheClusters", "DescribeCacheEngineVersions": "elasticache:DescribeCacheEngineVersions", "DescribeCacheParameterGroups": "elasticache:DescribeCacheParameterGroups", "DescribeCacheParameters": "elasticache:DescribeCacheParameters", "DescribeCacheSecurityGroups": "elasticache:DescribeCacheSecurityGroups", "DescribeCacheSubnetGroups": "elasticache:DescribeCacheSubnetGroups", "DescribeEngineDefaultParameters": "elasticache:DescribeEngineDefaultParameters", "DescribeEvents": "elasticache:DescribeEvents", "DescribeGlobalReplicationGroups": "elasticache:DescribeGlobalReplicationGroups", "DescribeReplicationGroups": "elasticache:DescribeReplicationGroups", "DescribeReservedCacheNodes": "elasticache:DescribeReservedCacheNodes", "DescribeReservedCacheNodesOfferings": "elasticache:DescribeReservedCacheNodesOfferings", "DescribeServiceUpdates": "elasticache:DescribeServiceUpdates", "DescribeSnapshots": "elasticache:DescribeSnapshots", "DescribeUpdateActions": "elasticache:DescribeUpdateActions", "DescribeUserGroups": "elasticache:DescribeUserGroups", "DescribeUsers": "elasticache:DescribeUsers", "DisassociateGlobalReplicationGroup": "elasticache:DisassociateGlobalReplicationGroup", "FailoverGlobalReplicationGroup": "elasticache:FailoverGlobalReplicationGroup", "IncreaseNodeGroupsInGlobalReplicationGroup": "elasticache:IncreaseNodeGroupsInGlobalReplicationGroup", "IncreaseReplicaCount": "elasticache:IncreaseReplicaCount", "ListAllowedNodeTypeModifications": "elasticache:ListAllowedNodeTypeModifications", "ListTagsForResource": "elasticache:ListTagsForResource", "ModifyCacheCluster": "elasticache:ModifyCacheCluster", "ModifyCacheParameterGroup": "elasticache:ModifyCacheParameterGroup", "ModifyCacheSubnetGroup": "elasticache:ModifyCacheSubnetGroup", "ModifyGlobalReplicationGroup": "elasticache:ModifyGlobalReplicationGroup", "ModifyReplicationGroup": "elasticache:ModifyReplicationGroup", "ModifyReplicationGroupShardConfiguration": "elasticache:ModifyReplicationGroupShardConfiguration", "ModifyUser": "elasticache:ModifyUser", "ModifyUserGroup": "elasticache:ModifyUserGroup", "PurchaseReservedCacheNodesOffering": "elasticache:PurchaseReservedCacheNodesOffering", "RebalanceSlotsInGlobalReplicationGroup": "elasticache:RebalanceSlotsInGlobalReplicationGroup", "RebootCacheCluster": "elasticache:RebootCacheCluster", "RemoveTagsFromResource": "elasticache:RemoveTagsFromResource", "ResetCacheParameterGroup": "elasticache:ResetCacheParameterGroup", "RevokeCacheSecurityGroupIngress": "elasticache:RevokeCacheSecurityGroupIngress", "StartMigration": "elasticache:StartMigration", "TestFailover": "elasticache:TestFailover" }, "elasticbeanstalk": { "AbortEnvironmentUpdate": "elasticbeanstalk:AbortEnvironmentUpdate", "ApplyEnvironmentManagedAction": "elasticbeanstalk:ApplyEnvironmentManagedAction", "AssociateEnvironmentOperationsRole": "elasticbeanstalk:AssociateEnvironmentOperationsRole", "CheckDNSAvailability": "elasticbeanstalk:CheckDNSAvailability", "ComposeEnvironments": "elasticbeanstalk:ComposeEnvironments", "CreateApplication": "elasticbeanstalk:CreateApplication", "CreateApplicationVersion": "elasticbeanstalk:CreateApplicationVersion", "CreateConfigurationTemplate": "elasticbeanstalk:CreateConfigurationTemplate", "CreateEnvironment": "elasticbeanstalk:CreateEnvironment", "CreatePlatformVersion": "elasticbeanstalk:CreatePlatformVersion", "CreateStorageLocation": "elasticbeanstalk:CreateStorageLocation", "DeleteApplication": "elasticbeanstalk:DeleteApplication", "DeleteApplicationVersion": "elasticbeanstalk:DeleteApplicationVersion", "DeleteConfigurationTemplate": "elasticbeanstalk:DeleteConfigurationTemplate", "DeleteEnvironmentConfiguration": "elasticbeanstalk:DeleteEnvironmentConfiguration", "DeletePlatformVersion": "elasticbeanstalk:DeletePlatformVersion", "DescribeAccountAttributes": "elasticbeanstalk:DescribeAccountAttributes", "DescribeApplicationVersions": "elasticbeanstalk:DescribeApplicationVersions", "DescribeApplications": "elasticbeanstalk:DescribeApplications", "DescribeConfigurationOptions": "elasticbeanstalk:DescribeConfigurationOptions", "DescribeConfigurationSettings": "elasticbeanstalk:DescribeConfigurationSettings", "DescribeEnvironmentHealth": "elasticbeanstalk:DescribeEnvironmentHealth", "DescribeEnvironmentManagedActionHistory": "elasticbeanstalk:DescribeEnvironmentManagedActionHistory", "DescribeEnvironmentManagedActions": "elasticbeanstalk:DescribeEnvironmentManagedActions", "DescribeEnvironmentResources": "elasticbeanstalk:DescribeEnvironmentResources", "DescribeEnvironments": "elasticbeanstalk:DescribeEnvironments", "DescribeEvents": "elasticbeanstalk:DescribeEvents", "DescribeInstancesHealth": "elasticbeanstalk:DescribeInstancesHealth", "DescribePlatformVersion": "elasticbeanstalk:DescribePlatformVersion", "DisassociateEnvironmentOperationsRole": "elasticbeanstalk:DisassociateEnvironmentOperationsRole", "ListAvailableSolutionStacks": "elasticbeanstalk:ListAvailableSolutionStacks", "ListPlatformBranches": "elasticbeanstalk:ListPlatformBranches", "ListPlatformVersions": "elasticbeanstalk:ListPlatformVersions", "ListTagsForResource": "elasticbeanstalk:ListTagsForResource", "RebuildEnvironment": "elasticbeanstalk:RebuildEnvironment", "RequestEnvironmentInfo": "elasticbeanstalk:RequestEnvironmentInfo", "RestartAppServer": "elasticbeanstalk:RestartAppServer", "RetrieveEnvironmentInfo": "elasticbeanstalk:RetrieveEnvironmentInfo", "SwapEnvironmentCNAMEs": "elasticbeanstalk:SwapEnvironmentCNAMEs", "TerminateEnvironment": "elasticbeanstalk:TerminateEnvironment", "UpdateApplication": "elasticbeanstalk:UpdateApplication", "UpdateApplicationResourceLifecycle": "elasticbeanstalk:UpdateApplicationResourceLifecycle", "UpdateApplicationVersion": "elasticbeanstalk:UpdateApplicationVersion", "UpdateConfigurationTemplate": "elasticbeanstalk:UpdateConfigurationTemplate", "UpdateEnvironment": "elasticbeanstalk:UpdateEnvironment", "UpdateTagsForResource": "elasticbeanstalk:UpdateTagsForResource", "ValidateConfigurationSettings": "elasticbeanstalk:ValidateConfigurationSettings" }, "elastictranscoder": { "CancelJob": "elastictranscoder:CancelJob", "CreateJob": "elastictranscoder:CreateJob", "CreatePipeline": "elastictranscoder:CreatePipeline", "CreatePreset": "elastictranscoder:CreatePreset", "DeletePipeline": "elastictranscoder:DeletePipeline", "DeletePreset": "elastictranscoder:DeletePreset", "ListJobsByPipeline": "elastictranscoder:ListJobsByPipeline", "ListJobsByStatus": "elastictranscoder:ListJobsByStatus", "ListPipelines": "elastictranscoder:ListPipelines", "ListPresets": "elastictranscoder:ListPresets", "ReadJob": "elastictranscoder:ReadJob", "ReadPipeline": "elastictranscoder:ReadPipeline", "ReadPreset": "elastictranscoder:ReadPreset", "TestRole": "elastictranscoder:TestRole", "UpdatePipeline": "elastictranscoder:UpdatePipeline", "UpdatePipelineNotifications": "elastictranscoder:UpdatePipelineNotifications", "UpdatePipelineStatus": "elastictranscoder:UpdatePipelineStatus" }, "elb": { "AddTags": "elasticloadbalancing:AddTags", "ApplySecurityGroupsToLoadBalancer": "elasticloadbalancing:ApplySecurityGroupsToLoadBalancer", "AttachLoadBalancerToSubnets": "elasticloadbalancing:AttachLoadBalancerToSubnets", "ConfigureHealthCheck": "elasticloadbalancing:ConfigureHealthCheck", "CreateAppCookieStickinessPolicy": "elasticloadbalancing:CreateAppCookieStickinessPolicy", "CreateLBCookieStickinessPolicy": "elasticloadbalancing:CreateLBCookieStickinessPolicy", "CreateLoadBalancer": "elasticloadbalancing:CreateLoadBalancer", "CreateLoadBalancerListeners": "elasticloadbalancing:CreateLoadBalancerListeners", "CreateLoadBalancerPolicy": "elasticloadbalancing:CreateLoadBalancerPolicy", "DeleteLoadBalancer": "elasticloadbalancing:DeleteLoadBalancer", "DeleteLoadBalancerListeners": "elasticloadbalancing:DeleteLoadBalancerListeners", "DeleteLoadBalancerPolicy": "elasticloadbalancing:DeleteLoadBalancerPolicy", "DeregisterInstancesFromLoadBalancer": "elasticloadbalancing:DeregisterInstancesFromLoadBalancer", "DescribeAccountLimits": "elasticloadbalancing:DescribeAccountLimits", "DescribeInstanceHealth": "elasticloadbalancing:DescribeInstanceHealth", "DescribeLoadBalancerAttributes": "elasticloadbalancing:DescribeLoadBalancerAttributes", "DescribeLoadBalancerPolicies": "elasticloadbalancing:DescribeLoadBalancerPolicies", "DescribeLoadBalancerPolicyTypes": "elasticloadbalancing:DescribeLoadBalancerPolicyTypes", "DescribeLoadBalancers": "elasticloadbalancing:DescribeLoadBalancers", "DescribeTags": "elasticloadbalancing:DescribeTags", "DetachLoadBalancerFromSubnets": "elasticloadbalancing:DetachLoadBalancerFromSubnets", "DisableAvailabilityZonesForLoadBalancer": "elasticloadbalancing:DisableAvailabilityZonesForLoadBalancer", "EnableAvailabilityZonesForLoadBalancer": "elasticloadbalancing:EnableAvailabilityZonesForLoadBalancer", "ModifyLoadBalancerAttributes": "elasticloadbalancing:ModifyLoadBalancerAttributes", "RegisterInstancesWithLoadBalancer": "elasticloadbalancing:RegisterInstancesWithLoadBalancer", "RemoveTags": "elasticloadbalancing:RemoveTags", "SetLoadBalancerListenerSSLCertificate": "elasticloadbalancing:SetLoadBalancerListenerSSLCertificate", "SetLoadBalancerPoliciesForBackendServer": "elasticloadbalancing:SetLoadBalancerPoliciesForBackendServer", "SetLoadBalancerPoliciesOfListener": "elasticloadbalancing:SetLoadBalancerPoliciesOfListener" }, "elbv2": { "AddListenerCertificates": "elasticloadbalancing:AddListenerCertificates", "AddTags": "elasticloadbalancing:AddTags", "CreateListener": "elasticloadbalancing:CreateListener", "CreateLoadBalancer": "elasticloadbalancing:CreateLoadBalancer", "CreateRule": "elasticloadbalancing:CreateRule", "CreateTargetGroup": "elasticloadbalancing:CreateTargetGroup", "DeleteListener": "elasticloadbalancing:DeleteListener", "DeleteLoadBalancer": "elasticloadbalancing:DeleteLoadBalancer", "DeleteRule": "elasticloadbalancing:DeleteRule", "DeleteTargetGroup": "elasticloadbalancing:DeleteTargetGroup", "DeregisterTargets": "elasticloadbalancing:DeregisterTargets", "DescribeAccountLimits": "elasticloadbalancing:DescribeAccountLimits", "DescribeListenerCertificates": "elasticloadbalancing:DescribeListenerCertificates", "DescribeListeners": "elasticloadbalancing:DescribeListeners", "DescribeLoadBalancerAttributes": "elasticloadbalancing:DescribeLoadBalancerAttributes", "DescribeLoadBalancers": "elasticloadbalancing:DescribeLoadBalancers", "DescribeRules": "elasticloadbalancing:DescribeRules", "DescribeSSLPolicies": "elasticloadbalancing:DescribeSSLPolicies", "DescribeTags": "elasticloadbalancing:DescribeTags", "DescribeTargetGroupAttributes": "elasticloadbalancing:DescribeTargetGroupAttributes", "DescribeTargetGroups": "elasticloadbalancing:DescribeTargetGroups", "DescribeTargetHealth": "elasticloadbalancing:DescribeTargetHealth", "ModifyListener": "elasticloadbalancing:ModifyListener", "ModifyLoadBalancerAttributes": "elasticloadbalancing:ModifyLoadBalancerAttributes", "ModifyRule": "elasticloadbalancing:ModifyRule", "ModifyTargetGroup": "elasticloadbalancing:ModifyTargetGroup", "ModifyTargetGroupAttributes": "elasticloadbalancing:ModifyTargetGroupAttributes", "RegisterTargets": "elasticloadbalancing:RegisterTargets", "RemoveListenerCertificates": "elasticloadbalancing:RemoveListenerCertificates", "RemoveTags": "elasticloadbalancing:RemoveTags", "SetIpAddressType": "elasticloadbalancing:SetIpAddressType", "SetRulePriorities": "elasticloadbalancing:SetRulePriorities", "SetSecurityGroups": "elasticloadbalancing:SetSecurityGroups", "SetSubnets": "elasticloadbalancing:SetSubnets" }, "emr": { "AddInstanceFleet": "elasticmapreduce:AddInstanceFleet", "AddInstanceGroups": "elasticmapreduce:AddInstanceGroups", "AddJobFlowSteps": "elasticmapreduce:AddJobFlowSteps", "AddTags": "elasticmapreduce:AddTags", "CancelSteps": "elasticmapreduce:CancelSteps", "CreateSecurityConfiguration": "elasticmapreduce:CreateSecurityConfiguration", "CreateStudio": "elasticmapreduce:CreateStudio", "CreateStudioSessionMapping": "elasticmapreduce:CreateStudioSessionMapping", "DeleteSecurityConfiguration": "elasticmapreduce:DeleteSecurityConfiguration", "DeleteStudio": "elasticmapreduce:DeleteStudio", "DeleteStudioSessionMapping": "elasticmapreduce:DeleteStudioSessionMapping", "DescribeCluster": "elasticmapreduce:DescribeCluster", "DescribeJobFlows": "elasticmapreduce:DescribeJobFlows", "DescribeNotebookExecution": "elasticmapreduce:DescribeNotebookExecution", "DescribeReleaseLabel": "elasticmapreduce:DescribeReleaseLabel", "DescribeSecurityConfiguration": "elasticmapreduce:DescribeSecurityConfiguration", "DescribeStep": "elasticmapreduce:DescribeStep", "DescribeStudio": "elasticmapreduce:DescribeStudio", "GetAutoTerminationPolicy": "elasticmapreduce:GetAutoTerminationPolicy", "GetBlockPublicAccessConfiguration": "elasticmapreduce:GetBlockPublicAccessConfiguration", "GetManagedScalingPolicy": "elasticmapreduce:GetManagedScalingPolicy", "GetStudioSessionMapping": "elasticmapreduce:GetStudioSessionMapping", "ListBootstrapActions": "elasticmapreduce:ListBootstrapActions", "ListClusters": "elasticmapreduce:ListClusters", "ListInstanceFleets": "elasticmapreduce:ListInstanceFleets", "ListInstanceGroups": "elasticmapreduce:ListInstanceGroups", "ListInstances": "elasticmapreduce:ListInstances", "ListNotebookExecutions": "elasticmapreduce:ListNotebookExecutions", "ListReleaseLabels": "elasticmapreduce:ListReleaseLabels", "ListSecurityConfigurations": "elasticmapreduce:ListSecurityConfigurations", "ListSteps": "elasticmapreduce:ListSteps", "ListStudioSessionMappings": "elasticmapreduce:ListStudioSessionMappings", "ListStudios": "elasticmapreduce:ListStudios", "ModifyCluster": "elasticmapreduce:ModifyCluster", "ModifyInstanceFleet": "elasticmapreduce:ModifyInstanceFleet", "ModifyInstanceGroups": "elasticmapreduce:ModifyInstanceGroups", "PutAutoScalingPolicy": "elasticmapreduce:PutAutoScalingPolicy", "PutAutoTerminationPolicy": "elasticmapreduce:PutAutoTerminationPolicy", "PutBlockPublicAccessConfiguration": "elasticmapreduce:PutBlockPublicAccessConfiguration", "PutManagedScalingPolicy": "elasticmapreduce:PutManagedScalingPolicy", "RemoveAutoScalingPolicy": "elasticmapreduce:RemoveAutoScalingPolicy", "RemoveAutoTerminationPolicy": "elasticmapreduce:RemoveAutoTerminationPolicy", "RemoveManagedScalingPolicy": "elasticmapreduce:RemoveManagedScalingPolicy", "RemoveTags": "elasticmapreduce:RemoveTags", "RunJobFlow": "elasticmapreduce:RunJobFlow", "SetTerminationProtection": "elasticmapreduce:SetTerminationProtection", "StartNotebookExecution": "elasticmapreduce:StartNotebookExecution", "StopNotebookExecution": "elasticmapreduce:StopNotebookExecution", "TerminateJobFlows": "elasticmapreduce:TerminateJobFlows", "UpdateStudio": "elasticmapreduce:UpdateStudio", "UpdateStudioSessionMapping": "elasticmapreduce:UpdateStudioSessionMapping" }, "emr-containers": { "CancelJobRun": "emr-containers:CancelJobRun", "CreateManagedEndpoint": "emr-containers:CreateManagedEndpoint", "CreateVirtualCluster": "emr-containers:CreateVirtualCluster", "DeleteManagedEndpoint": "emr-containers:DeleteManagedEndpoint", "DeleteVirtualCluster": "emr-containers:DeleteVirtualCluster", "DescribeJobRun": "emr-containers:DescribeJobRun", "DescribeManagedEndpoint": "emr-containers:DescribeManagedEndpoint", "DescribeVirtualCluster": "emr-containers:DescribeVirtualCluster", "ListJobRuns": "emr-containers:ListJobRuns", "ListManagedEndpoints": "emr-containers:ListManagedEndpoints", "ListTagsForResource": "emr-containers:ListTagsForResource", "ListVirtualClusters": "emr-containers:ListVirtualClusters", "StartJobRun": "emr-containers:StartJobRun", "TagResource": "emr-containers:TagResource", "UntagResource": "emr-containers:UntagResource" }, "es": { "AcceptInboundCrossClusterSearchConnection": "es:AcceptInboundCrossClusterSearchConnection", "AddTags": "es:AddTags", "AssociatePackage": "es:AssociatePackage", "CancelElasticsearchServiceSoftwareUpdate": "es:CancelElasticsearchServiceSoftwareUpdate", "CreateElasticsearchDomain": "es:CreateElasticsearchDomain", "CreateOutboundCrossClusterSearchConnection": "es:CreateOutboundCrossClusterSearchConnection", "CreatePackage": "es:CreatePackage", "DeleteElasticsearchDomain": "es:DeleteElasticsearchDomain", "DeleteElasticsearchServiceRole": "es:DeleteElasticsearchServiceRole", "DeleteInboundCrossClusterSearchConnection": "es:DeleteInboundCrossClusterSearchConnection", "DeleteOutboundCrossClusterSearchConnection": "es:DeleteOutboundCrossClusterSearchConnection", "DeletePackage": "es:DeletePackage", "DescribeDomainAutoTunes": "es:DescribeDomainAutoTunes", "DescribeElasticsearchDomain": "es:DescribeElasticsearchDomain", "DescribeElasticsearchDomainConfig": "es:DescribeElasticsearchDomainConfig", "DescribeElasticsearchDomains": "es:DescribeElasticsearchDomains", "DescribeElasticsearchInstanceTypeLimits": "es:DescribeElasticsearchInstanceTypeLimits", "DescribeInboundCrossClusterSearchConnections": "es:DescribeInboundCrossClusterSearchConnections", "DescribeOutboundCrossClusterSearchConnections": "es:DescribeOutboundCrossClusterSearchConnections", "DescribePackages": "es:DescribePackages", "DescribeReservedElasticsearchInstanceOfferings": "es:DescribeReservedElasticsearchInstanceOfferings", "DescribeReservedElasticsearchInstances": "es:DescribeReservedElasticsearchInstances", "DissociatePackage": "es:DissociatePackage", "GetCompatibleElasticsearchVersions": "es:GetCompatibleElasticsearchVersions", "GetPackageVersionHistory": "es:GetPackageVersionHistory", "GetUpgradeHistory": "es:GetUpgradeHistory", "GetUpgradeStatus": "es:GetUpgradeStatus", "ListDomainNames": "es:ListDomainNames", "ListDomainsForPackage": "es:ListDomainsForPackage", "ListElasticsearchInstanceTypes": "es:ListElasticsearchInstanceTypes", "ListElasticsearchVersions": "es:ListElasticsearchVersions", "ListPackagesForDomain": "es:ListPackagesForDomain", "ListTags": "es:ListTags", "PurchaseReservedElasticsearchInstanceOffering": "es:PurchaseReservedElasticsearchInstanceOffering", "RejectInboundCrossClusterSearchConnection": "es:RejectInboundCrossClusterSearchConnection", "RemoveTags": "es:RemoveTags", "StartElasticsearchServiceSoftwareUpdate": "es:StartElasticsearchServiceSoftwareUpdate", "UpdateElasticsearchDomainConfig": "es:UpdateElasticsearchDomainConfig", "UpdatePackage": "es:UpdatePackage", "UpgradeElasticsearchDomain": "es:UpgradeElasticsearchDomain" }, "events": { "ActivateEventSource": "events:ActivateEventSource", "CancelReplay": "events:CancelReplay", "CreateApiDestination": "events:CreateApiDestination", "CreateArchive": "events:CreateArchive", "CreateConnection": "events:CreateConnection", "CreateEventBus": "events:CreateEventBus", "CreatePartnerEventSource": "events:CreatePartnerEventSource", "DeactivateEventSource": "events:DeactivateEventSource", "DeauthorizeConnection": "events:DeauthorizeConnection", "DeleteApiDestination": "events:DeleteApiDestination", "DeleteArchive": "events:DeleteArchive", "DeleteConnection": "events:DeleteConnection", "DeleteEventBus": "events:DeleteEventBus", "DeletePartnerEventSource": "events:DeletePartnerEventSource", "DeleteRule": "events:DeleteRule", "DescribeApiDestination": "events:DescribeApiDestination", "DescribeArchive": "events:DescribeArchive", "DescribeConnection": "events:DescribeConnection", "DescribeEventBus": "events:DescribeEventBus", "DescribeEventSource": "events:DescribeEventSource", "DescribePartnerEventSource": "events:DescribePartnerEventSource", "DescribeReplay": "events:DescribeReplay", "DescribeRule": "events:DescribeRule", "DisableRule": "events:DisableRule", "EnableRule": "events:EnableRule", "ListApiDestinations": "events:ListApiDestinations", "ListArchives": "events:ListArchives", "ListConnections": "events:ListConnections", "ListEventBuses": "events:ListEventBuses", "ListEventSources": "events:ListEventSources", "ListPartnerEventSourceAccounts": "events:ListPartnerEventSourceAccounts", "ListPartnerEventSources": "events:ListPartnerEventSources", "ListReplays": "events:ListReplays", "ListRuleNamesByTarget": "events:ListRuleNamesByTarget", "ListRules": "events:ListRules", "ListTagsForResource": "events:ListTagsForResource", "ListTargetsByRule": "events:ListTargetsByRule", "PutEvents": "events:PutEvents", "PutPartnerEvents": "events:PutPartnerEvents", "PutPermission": "events:PutPermission", "PutRule": "events:PutRule", "PutTargets": "events:PutTargets", "RemovePermission": "events:RemovePermission", "RemoveTargets": "events:RemoveTargets", "StartReplay": "events:StartReplay", "TagResource": "events:TagResource", "TestEventPattern": "events:TestEventPattern", "UntagResource": "events:UntagResource", "UpdateApiDestination": "events:UpdateApiDestination", "UpdateArchive": "events:UpdateArchive", "UpdateConnection": "events:UpdateConnection" }, "evidently": { "CreateExperiment": "evidently:CreateExperiment", "CreateFeature": "evidently:CreateFeature", "CreateLaunch": "evidently:CreateLaunch", "CreateProject": "evidently:CreateProject", "DeleteExperiment": "evidently:DeleteExperiment", "DeleteFeature": "evidently:DeleteFeature", "DeleteLaunch": "evidently:DeleteLaunch", "DeleteProject": "evidently:DeleteProject", "GetExperiment": "evidently:GetExperiment", "GetExperimentResults": "evidently:GetExperimentResults", "GetFeature": "evidently:GetFeature", "GetLaunch": "evidently:GetLaunch", "GetProject": "evidently:GetProject", "ListExperiments": "evidently:ListExperiments", "ListFeatures": "evidently:ListFeatures", "ListLaunches": "evidently:ListLaunches", "ListProjects": "evidently:ListProjects", "StartExperiment": "evidently:StartExperiment", "StartLaunch": "evidently:StartLaunch", "StopExperiment": "evidently:StopExperiment", "StopLaunch": "evidently:StopLaunch", "UpdateExperiment": "evidently:UpdateExperiment", "UpdateFeature": "evidently:UpdateFeature", "UpdateLaunch": "evidently:UpdateLaunch", "UpdateProject": "evidently:UpdateProject", "UpdateProjectDataDelivery": "evidently:UpdateProjectDataDelivery" }, "finspace": { "CreateEnvironment": "finspace:CreateEnvironment", "DeleteEnvironment": "finspace:DeleteEnvironment", "GetEnvironment": "finspace:GetEnvironment", "ListEnvironments": "finspace:ListEnvironments", "ListTagsForResource": "finspace:ListTagsForResource", "TagResource": "finspace:TagResource", "UntagResource": "finspace:UntagResource", "UpdateEnvironment": "finspace:UpdateEnvironment" }, "firehose": { "CreateDeliveryStream": "firehose:CreateDeliveryStream", "DeleteDeliveryStream": "firehose:DeleteDeliveryStream", "DescribeDeliveryStream": "firehose:DescribeDeliveryStream", "ListDeliveryStreams": "firehose:ListDeliveryStreams", "ListTagsForDeliveryStream": "firehose:ListTagsForDeliveryStream", "PutRecord": "firehose:PutRecord", "PutRecordBatch": "firehose:PutRecordBatch", "StartDeliveryStreamEncryption": "firehose:StartDeliveryStreamEncryption", "StopDeliveryStreamEncryption": "firehose:StopDeliveryStreamEncryption", "TagDeliveryStream": "firehose:TagDeliveryStream", "UntagDeliveryStream": "firehose:UntagDeliveryStream", "UpdateDestination": "firehose:UpdateDestination" }, "fis": { "CreateExperimentTemplate": "fis:CreateExperimentTemplate", "DeleteExperimentTemplate": "fis:DeleteExperimentTemplate", "GetAction": "fis:GetAction", "GetExperiment": "fis:GetExperiment", "GetExperimentTemplate": "fis:GetExperimentTemplate", "ListActions": "fis:ListActions", "ListExperimentTemplates": "fis:ListExperimentTemplates", "ListExperiments": "fis:ListExperiments", "ListTagsForResource": "fis:ListTagsForResource", "StartExperiment": "fis:StartExperiment", "StopExperiment": "fis:StopExperiment", "TagResource": "fis:TagResource", "UntagResource": "fis:UntagResource", "UpdateExperimentTemplate": "fis:UpdateExperimentTemplate" }, "fms": { "AssociateAdminAccount": "fms:AssociateAdminAccount", "DeleteAppsList": "fms:DeleteAppsList", "DeleteNotificationChannel": "fms:DeleteNotificationChannel", "DeletePolicy": "fms:DeletePolicy", "DeleteProtocolsList": "fms:DeleteProtocolsList", "DisassociateAdminAccount": "fms:DisassociateAdminAccount", "GetAdminAccount": "fms:GetAdminAccount", "GetAppsList": "fms:GetAppsList", "GetComplianceDetail": "fms:GetComplianceDetail", "GetNotificationChannel": "fms:GetNotificationChannel", "GetPolicy": "fms:GetPolicy", "GetProtectionStatus": "fms:GetProtectionStatus", "GetProtocolsList": "fms:GetProtocolsList", "GetViolationDetails": "fms:GetViolationDetails", "ListAppsLists": "fms:ListAppsLists", "ListComplianceStatus": "fms:ListComplianceStatus", "ListMemberAccounts": "fms:ListMemberAccounts", "ListPolicies": "fms:ListPolicies", "ListProtocolsLists": "fms:ListProtocolsLists", "ListTagsForResource": "fms:ListTagsForResource", "PutAppsList": "fms:PutAppsList", "PutNotificationChannel": "fms:PutNotificationChannel", "PutPolicy": "fms:PutPolicy", "PutProtocolsList": "fms:PutProtocolsList", "TagResource": "fms:TagResource", "UntagResource": "fms:UntagResource" }, "forecast": { "CreateAutoPredictor": "forecast:CreateAutoPredictor", "CreateDataset": "forecast:CreateDataset", "CreateDatasetGroup": "forecast:CreateDatasetGroup", "CreateDatasetImportJob": "forecast:CreateDatasetImportJob", "CreateExplainability": "forecast:CreateExplainability", "CreateExplainabilityExport": "forecast:CreateExplainabilityExport", "CreateForecast": "forecast:CreateForecast", "CreateForecastExportJob": "forecast:CreateForecastExportJob", "CreatePredictor": "forecast:CreatePredictor", "CreatePredictorBacktestExportJob": "forecast:CreatePredictorBacktestExportJob", "DeleteDataset": "forecast:DeleteDataset", "DeleteDatasetGroup": "forecast:DeleteDatasetGroup", "DeleteDatasetImportJob": "forecast:DeleteDatasetImportJob", "DeleteExplainability": "forecast:DeleteExplainability", "DeleteExplainabilityExport": "forecast:DeleteExplainabilityExport", "DeleteForecast": "forecast:DeleteForecast", "DeleteForecastExportJob": "forecast:DeleteForecastExportJob", "DeletePredictor": "forecast:DeletePredictor", "DeletePredictorBacktestExportJob": "forecast:DeletePredictorBacktestExportJob", "DeleteResourceTree": "forecast:DeleteResourceTree", "DescribeAutoPredictor": "forecast:DescribeAutoPredictor", "DescribeDataset": "forecast:DescribeDataset", "DescribeDatasetGroup": "forecast:DescribeDatasetGroup", "DescribeDatasetImportJob": "forecast:DescribeDatasetImportJob", "DescribeExplainabilityExport": "forecast:DescribeExplainabilityExport", "DescribeForecast": "forecast:DescribeForecast", "DescribeForecastExportJob": "forecast:DescribeForecastExportJob", "DescribePredictor": "forecast:DescribePredictor", "DescribePredictorBacktestExportJob": "forecast:DescribePredictorBacktestExportJob", "GetAccuracyMetrics": "forecast:GetAccuracyMetrics", "ListDatasetGroups": "forecast:ListDatasetGroups", "ListDatasetImportJobs": "forecast:ListDatasetImportJobs", "ListDatasets": "forecast:ListDatasets", "ListExplainabilities": "forecast:ListExplainabilities", "ListExplainabilityExports": "forecast:ListExplainabilityExports", "ListForecastExportJobs": "forecast:ListForecastExportJobs", "ListForecasts": "forecast:ListForecasts", "ListPredictorBacktestExportJobs": "forecast:ListPredictorBacktestExportJobs", "ListPredictors": "forecast:ListPredictors", "ListTagsForResource": "forecast:ListTagsForResource", "StopResource": "forecast:StopResource", "TagResource": "forecast:TagResource", "UntagResource": "forecast:UntagResource", "UpdateDatasetGroup": "forecast:UpdateDatasetGroup" }, "frauddetector": { "BatchCreateVariable": "frauddetector:BatchCreateVariable", "BatchGetVariable": "frauddetector:BatchGetVariable", "CancelBatchImportJob": "frauddetector:CancelBatchImportJob", "CancelBatchPredictionJob": "frauddetector:CancelBatchPredictionJob", "CreateBatchImportJob": "frauddetector:CreateBatchImportJob", "CreateBatchPredictionJob": "frauddetector:CreateBatchPredictionJob", "CreateDetectorVersion": "frauddetector:CreateDetectorVersion", "CreateModel": "frauddetector:CreateModel", "CreateModelVersion": "frauddetector:CreateModelVersion", "CreateRule": "frauddetector:CreateRule", "CreateVariable": "frauddetector:CreateVariable", "DeleteBatchImportJob": "frauddetector:DeleteBatchImportJob", "DeleteBatchPredictionJob": "frauddetector:DeleteBatchPredictionJob", "DeleteDetector": "frauddetector:DeleteDetector", "DeleteDetectorVersion": "frauddetector:DeleteDetectorVersion", "DeleteEntityType": "frauddetector:DeleteEntityType", "DeleteEvent": "frauddetector:DeleteEvent", "DeleteEventType": "frauddetector:DeleteEventType", "DeleteEventsByEventType": "frauddetector:DeleteEventsByEventType", "DeleteExternalModel": "frauddetector:DeleteExternalModel", "DeleteLabel": "frauddetector:DeleteLabel", "DeleteModel": "frauddetector:DeleteModel", "DeleteModelVersion": "frauddetector:DeleteModelVersion", "DeleteOutcome": "frauddetector:DeleteOutcome", "DeleteRule": "frauddetector:DeleteRule", "DeleteVariable": "frauddetector:DeleteVariable", "DescribeDetector": "frauddetector:DescribeDetector", "DescribeModelVersions": "frauddetector:DescribeModelVersions", "GetBatchImportJobs": "frauddetector:GetBatchImportJobs", "GetBatchPredictionJobs": "frauddetector:GetBatchPredictionJobs", "GetDeleteEventsByEventTypeStatus": "frauddetector:GetDeleteEventsByEventTypeStatus", "GetDetectorVersion": "frauddetector:GetDetectorVersion", "GetDetectors": "frauddetector:GetDetectors", "GetEntityTypes": "frauddetector:GetEntityTypes", "GetEvent": "frauddetector:GetEvent", "GetEventPrediction": "frauddetector:GetEventPrediction", "GetEventTypes": "frauddetector:GetEventTypes", "GetExternalModels": "frauddetector:GetExternalModels", "GetKMSEncryptionKey": "frauddetector:GetKMSEncryptionKey", "GetLabels": "frauddetector:GetLabels", "GetModelVersion": "frauddetector:GetModelVersion", "GetModels": "frauddetector:GetModels", "GetOutcomes": "frauddetector:GetOutcomes", "GetRules": "frauddetector:GetRules", "GetVariables": "frauddetector:GetVariables", "ListTagsForResource": "frauddetector:ListTagsForResource", "PutDetector": "frauddetector:PutDetector", "PutEntityType": "frauddetector:PutEntityType", "PutEventType": "frauddetector:PutEventType", "PutExternalModel": "frauddetector:PutExternalModel", "PutKMSEncryptionKey": "frauddetector:PutKMSEncryptionKey", "PutLabel": "frauddetector:PutLabel", "PutOutcome": "frauddetector:PutOutcome", "SendEvent": "frauddetector:SendEvent", "TagResource": "frauddetector:TagResource", "UntagResource": "frauddetector:UntagResource", "UpdateDetectorVersion": "frauddetector:UpdateDetectorVersion", "UpdateDetectorVersionMetadata": "frauddetector:UpdateDetectorVersionMetadata", "UpdateDetectorVersionStatus": "frauddetector:UpdateDetectorVersionStatus", "UpdateEventLabel": "frauddetector:UpdateEventLabel", "UpdateModel": "frauddetector:UpdateModel", "UpdateModelVersion": "frauddetector:UpdateModelVersion", "UpdateModelVersionStatus": "frauddetector:UpdateModelVersionStatus", "UpdateRuleMetadata": "frauddetector:UpdateRuleMetadata", "UpdateRuleVersion": "frauddetector:UpdateRuleVersion", "UpdateVariable": "frauddetector:UpdateVariable" }, "fsx": { "AssociateFileSystemAliases": "fsx:AssociateFileSystemAliases", "CancelDataRepositoryTask": "fsx:CancelDataRepositoryTask", "CopyBackup": "fsx:CopyBackup", "CreateBackup": "fsx:CreateBackup", "CreateDataRepositoryAssociation": "fsx:CreateDataRepositoryAssociation", "CreateDataRepositoryTask": "fsx:CreateDataRepositoryTask", "CreateFileSystem": "fsx:CreateFileSystem", "CreateFileSystemFromBackup": "fsx:CreateFileSystemFromBackup", "CreateSnapshot": "fsx:CreateSnapshot", "CreateStorageVirtualMachine": "fsx:CreateStorageVirtualMachine", "CreateVolume": "fsx:CreateVolume", "CreateVolumeFromBackup": "fsx:CreateVolumeFromBackup", "DeleteBackup": "fsx:DeleteBackup", "DeleteDataRepositoryAssociation": "fsx:DeleteDataRepositoryAssociation", "DeleteFileSystem": "fsx:DeleteFileSystem", "DeleteSnapshot": "fsx:DeleteSnapshot", "DeleteStorageVirtualMachine": "fsx:DeleteStorageVirtualMachine", "DeleteVolume": "fsx:DeleteVolume", "DescribeBackups": "fsx:DescribeBackups", "DescribeDataRepositoryAssociations": "fsx:DescribeDataRepositoryAssociations", "DescribeDataRepositoryTasks": "fsx:DescribeDataRepositoryTasks", "DescribeFileSystemAliases": "fsx:DescribeFileSystemAliases", "DescribeFileSystems": "fsx:DescribeFileSystems", "DescribeSnapshots": "fsx:DescribeSnapshots", "DescribeStorageVirtualMachines": "fsx:DescribeStorageVirtualMachines", "DescribeVolumes": "fsx:DescribeVolumes", "DisassociateFileSystemAliases": "fsx:DisassociateFileSystemAliases", "ListTagsForResource": "fsx:ListTagsForResource", "RestoreVolumeFromSnapshot": "fsx:RestoreVolumeFromSnapshot", "TagResource": "fsx:TagResource", "UntagResource": "fsx:UntagResource", "UpdateDataRepositoryAssociation": "fsx:UpdateDataRepositoryAssociation", "UpdateFileSystem": "fsx:UpdateFileSystem", "UpdateSnapshot": "fsx:UpdateSnapshot", "UpdateStorageVirtualMachine": "fsx:UpdateStorageVirtualMachine", "UpdateVolume": "fsx:UpdateVolume" }, "gamelift": { "AcceptMatch": "gamelift:AcceptMatch", "ClaimGameServer": "gamelift:ClaimGameServer", "CreateAlias": "gamelift:CreateAlias", "CreateBuild": "gamelift:CreateBuild", "CreateFleet": "gamelift:CreateFleet", "CreateFleetLocations": "gamelift:CreateFleetLocations", "CreateGameServerGroup": "gamelift:CreateGameServerGroup", "CreateGameSession": "gamelift:CreateGameSession", "CreateGameSessionQueue": "gamelift:CreateGameSessionQueue", "CreateMatchmakingConfiguration": "gamelift:CreateMatchmakingConfiguration", "CreateMatchmakingRuleSet": "gamelift:CreateMatchmakingRuleSet", "CreatePlayerSession": "gamelift:CreatePlayerSession", "CreatePlayerSessions": "gamelift:CreatePlayerSessions", "CreateScript": "gamelift:CreateScript", "CreateVpcPeeringAuthorization": "gamelift:CreateVpcPeeringAuthorization", "CreateVpcPeeringConnection": "gamelift:CreateVpcPeeringConnection", "DeleteAlias": "gamelift:DeleteAlias", "DeleteBuild": "gamelift:DeleteBuild", "DeleteFleet": "gamelift:DeleteFleet", "DeleteFleetLocations": "gamelift:DeleteFleetLocations", "DeleteGameServerGroup": "gamelift:DeleteGameServerGroup", "DeleteGameSessionQueue": "gamelift:DeleteGameSessionQueue", "DeleteMatchmakingConfiguration": "gamelift:DeleteMatchmakingConfiguration", "DeleteMatchmakingRuleSet": "gamelift:DeleteMatchmakingRuleSet", "DeleteScalingPolicy": "gamelift:DeleteScalingPolicy", "DeleteScript": "gamelift:DeleteScript", "DeleteVpcPeeringAuthorization": "gamelift:DeleteVpcPeeringAuthorization", "DeleteVpcPeeringConnection": "gamelift:DeleteVpcPeeringConnection", "DeregisterGameServer": "gamelift:DeregisterGameServer", "DescribeAlias": "gamelift:DescribeAlias", "DescribeBuild": "gamelift:DescribeBuild", "DescribeEC2InstanceLimits": "gamelift:DescribeEC2InstanceLimits", "DescribeFleetAttributes": "gamelift:DescribeFleetAttributes", "DescribeFleetCapacity": "gamelift:DescribeFleetCapacity", "DescribeFleetEvents": "gamelift:DescribeFleetEvents", "DescribeFleetLocationAttributes": "gamelift:DescribeFleetLocationAttributes", "DescribeFleetLocationCapacity": "gamelift:DescribeFleetLocationCapacity", "DescribeFleetLocationUtilization": "gamelift:DescribeFleetLocationUtilization", "DescribeFleetPortSettings": "gamelift:DescribeFleetPortSettings", "DescribeFleetUtilization": "gamelift:DescribeFleetUtilization", "DescribeGameServer": "gamelift:DescribeGameServer", "DescribeGameServerGroup": "gamelift:DescribeGameServerGroup", "DescribeGameServerInstances": "gamelift:DescribeGameServerInstances", "DescribeGameSessionDetails": "gamelift:DescribeGameSessionDetails", "DescribeGameSessionPlacement": "gamelift:DescribeGameSessionPlacement", "DescribeGameSessionQueues": "gamelift:DescribeGameSessionQueues", "DescribeGameSessions": "gamelift:DescribeGameSessions", "DescribeInstances": "gamelift:DescribeInstances", "DescribeMatchmaking": "gamelift:DescribeMatchmaking", "DescribeMatchmakingConfigurations": "gamelift:DescribeMatchmakingConfigurations", "DescribeMatchmakingRuleSets": "gamelift:DescribeMatchmakingRuleSets", "DescribePlayerSessions": "gamelift:DescribePlayerSessions", "DescribeRuntimeConfiguration": "gamelift:DescribeRuntimeConfiguration", "DescribeScalingPolicies": "gamelift:DescribeScalingPolicies", "DescribeScript": "gamelift:DescribeScript", "DescribeVpcPeeringAuthorizations": "gamelift:DescribeVpcPeeringAuthorizations", "DescribeVpcPeeringConnections": "gamelift:DescribeVpcPeeringConnections", "GetGameSessionLogUrl": "gamelift:GetGameSessionLogUrl", "GetInstanceAccess": "gamelift:GetInstanceAccess", "ListAliases": "gamelift:ListAliases", "ListBuilds": "gamelift:ListBuilds", "ListFleets": "gamelift:ListFleets", "ListGameServerGroups": "gamelift:ListGameServerGroups", "ListGameServers": "gamelift:ListGameServers", "ListScripts": "gamelift:ListScripts", "ListTagsForResource": "gamelift:ListTagsForResource", "PutScalingPolicy": "gamelift:PutScalingPolicy", "RegisterGameServer": "gamelift:RegisterGameServer", "RequestUploadCredentials": "gamelift:RequestUploadCredentials", "ResolveAlias": "gamelift:ResolveAlias", "ResumeGameServerGroup": "gamelift:ResumeGameServerGroup", "SearchGameSessions": "gamelift:SearchGameSessions", "StartFleetActions": "gamelift:StartFleetActions", "StartGameSessionPlacement": "gamelift:StartGameSessionPlacement", "StartMatchBackfill": "gamelift:StartMatchBackfill", "StartMatchmaking": "gamelift:StartMatchmaking", "StopFleetActions": "gamelift:StopFleetActions", "StopGameSessionPlacement": "gamelift:StopGameSessionPlacement", "StopMatchmaking": "gamelift:StopMatchmaking", "SuspendGameServerGroup": "gamelift:SuspendGameServerGroup", "TagResource": "gamelift:TagResource", "UntagResource": "gamelift:UntagResource", "UpdateAlias": "gamelift:UpdateAlias", "UpdateBuild": "gamelift:UpdateBuild", "UpdateFleetAttributes": "gamelift:UpdateFleetAttributes", "UpdateFleetCapacity": "gamelift:UpdateFleetCapacity", "UpdateFleetPortSettings": "gamelift:UpdateFleetPortSettings", "UpdateGameServer": "gamelift:UpdateGameServer", "UpdateGameServerGroup": "gamelift:UpdateGameServerGroup", "UpdateGameSession": "gamelift:UpdateGameSession", "UpdateGameSessionQueue": "gamelift:UpdateGameSessionQueue", "UpdateMatchmakingConfiguration": "gamelift:UpdateMatchmakingConfiguration", "UpdateRuntimeConfiguration": "gamelift:UpdateRuntimeConfiguration", "UpdateScript": "gamelift:UpdateScript", "ValidateMatchmakingRuleSet": "gamelift:ValidateMatchmakingRuleSet" }, "glacier": { "AbortMultipartUpload": "glacier:AbortMultipartUpload", "AbortVaultLock": "glacier:AbortVaultLock", "AddTagsToVault": "glacier:AddTagsToVault", "CompleteMultipartUpload": "glacier:CompleteMultipartUpload", "CompleteVaultLock": "glacier:CompleteVaultLock", "CreateVault": "glacier:CreateVault", "DeleteArchive": "glacier:DeleteArchive", "DeleteVault": "glacier:DeleteVault", "DeleteVaultAccessPolicy": "glacier:DeleteVaultAccessPolicy", "DeleteVaultNotifications": "glacier:DeleteVaultNotifications", "DescribeJob": "glacier:DescribeJob", "DescribeVault": "glacier:DescribeVault", "GetDataRetrievalPolicy": "glacier:GetDataRetrievalPolicy", "GetJobOutput": "glacier:GetJobOutput", "GetVaultAccessPolicy": "glacier:GetVaultAccessPolicy", "GetVaultLock": "glacier:GetVaultLock", "GetVaultNotifications": "glacier:GetVaultNotifications", "InitiateJob": "glacier:InitiateJob", "InitiateMultipartUpload": "glacier:InitiateMultipartUpload", "InitiateVaultLock": "glacier:InitiateVaultLock", "ListJobs": "glacier:ListJobs", "ListMultipartUploads": "glacier:ListMultipartUploads", "ListParts": "glacier:ListParts", "ListProvisionedCapacity": "glacier:ListProvisionedCapacity", "ListTagsForVault": "glacier:ListTagsForVault", "ListVaults": "glacier:ListVaults", "PurchaseProvisionedCapacity": "glacier:PurchaseProvisionedCapacity", "RemoveTagsFromVault": "glacier:RemoveTagsFromVault", "SetDataRetrievalPolicy": "glacier:SetDataRetrievalPolicy", "SetVaultAccessPolicy": "glacier:SetVaultAccessPolicy", "SetVaultNotifications": "glacier:SetVaultNotifications", "UploadArchive": "glacier:UploadArchive", "UploadMultipartPart": "glacier:UploadMultipartPart" }, "globalaccelerator": { "AddCustomRoutingEndpoints": "globalaccelerator:AddCustomRoutingEndpoints", "AdvertiseByoipCidr": "globalaccelerator:AdvertiseByoipCidr", "AllowCustomRoutingTraffic": "globalaccelerator:AllowCustomRoutingTraffic", "CreateAccelerator": "globalaccelerator:CreateAccelerator", "CreateCustomRoutingAccelerator": "globalaccelerator:CreateCustomRoutingAccelerator", "CreateCustomRoutingEndpointGroup": "globalaccelerator:CreateCustomRoutingEndpointGroup", "CreateCustomRoutingListener": "globalaccelerator:CreateCustomRoutingListener", "CreateEndpointGroup": "globalaccelerator:CreateEndpointGroup", "CreateListener": "globalaccelerator:CreateListener", "DeleteAccelerator": "globalaccelerator:DeleteAccelerator", "DeleteCustomRoutingAccelerator": "globalaccelerator:DeleteCustomRoutingAccelerator", "DeleteCustomRoutingEndpointGroup": "globalaccelerator:DeleteCustomRoutingEndpointGroup", "DeleteCustomRoutingListener": "globalaccelerator:DeleteCustomRoutingListener", "DeleteEndpointGroup": "globalaccelerator:DeleteEndpointGroup", "DeleteListener": "globalaccelerator:DeleteListener", "DenyCustomRoutingTraffic": "globalaccelerator:DenyCustomRoutingTraffic", "DeprovisionByoipCidr": "globalaccelerator:DeprovisionByoipCidr", "DescribeAccelerator": "globalaccelerator:DescribeAccelerator", "DescribeAcceleratorAttributes": "globalaccelerator:DescribeAcceleratorAttributes", "DescribeCustomRoutingAccelerator": "globalaccelerator:DescribeCustomRoutingAccelerator", "DescribeCustomRoutingAcceleratorAttributes": "globalaccelerator:DescribeCustomRoutingAcceleratorAttributes", "DescribeCustomRoutingEndpointGroup": "globalaccelerator:DescribeCustomRoutingEndpointGroup", "DescribeCustomRoutingListener": "globalaccelerator:DescribeCustomRoutingListener", "DescribeEndpointGroup": "globalaccelerator:DescribeEndpointGroup", "DescribeListener": "globalaccelerator:DescribeListener", "ListAccelerators": "globalaccelerator:ListAccelerators", "ListByoipCidrs": "globalaccelerator:ListByoipCidrs", "ListCustomRoutingAccelerators": "globalaccelerator:ListCustomRoutingAccelerators", "ListCustomRoutingEndpointGroups": "globalaccelerator:ListCustomRoutingEndpointGroups", "ListCustomRoutingListeners": "globalaccelerator:ListCustomRoutingListeners", "ListCustomRoutingPortMappings": "globalaccelerator:ListCustomRoutingPortMappings", "ListCustomRoutingPortMappingsByDestination": "globalaccelerator:ListCustomRoutingPortMappingsByDestination", "ListEndpointGroups": "globalaccelerator:ListEndpointGroups", "ListListeners": "globalaccelerator:ListListeners", "ListTagsForResource": "globalaccelerator:ListTagsForResource", "ProvisionByoipCidr": "globalaccelerator:ProvisionByoipCidr", "RemoveCustomRoutingEndpoints": "globalaccelerator:RemoveCustomRoutingEndpoints", "TagResource": "globalaccelerator:TagResource", "UntagResource": "globalaccelerator:UntagResource", "UpdateAccelerator": "globalaccelerator:UpdateAccelerator", "UpdateAcceleratorAttributes": "globalaccelerator:UpdateAcceleratorAttributes", "UpdateCustomRoutingAccelerator": "globalaccelerator:UpdateCustomRoutingAccelerator", "UpdateCustomRoutingAcceleratorAttributes": "globalaccelerator:UpdateCustomRoutingAcceleratorAttributes", "UpdateCustomRoutingListener": "globalaccelerator:UpdateCustomRoutingListener", "UpdateEndpointGroup": "globalaccelerator:UpdateEndpointGroup", "UpdateListener": "globalaccelerator:UpdateListener", "WithdrawByoipCidr": "globalaccelerator:WithdrawByoipCidr" }, "grafana": { "AssociateLicense": "grafana:AssociateLicense", "CreateWorkspace": "grafana:CreateWorkspace", "DeleteWorkspace": "grafana:DeleteWorkspace", "DescribeWorkspace": "grafana:DescribeWorkspace", "DescribeWorkspaceAuthentication": "grafana:DescribeWorkspaceAuthentication", "DisassociateLicense": "grafana:DisassociateLicense", "ListPermissions": "grafana:ListPermissions", "ListWorkspaces": "grafana:ListWorkspaces", "UpdatePermissions": "grafana:UpdatePermissions", "UpdateWorkspace": "grafana:UpdateWorkspace", "UpdateWorkspaceAuthentication": "grafana:UpdateWorkspaceAuthentication" }, "groundstation": { "CancelContact": "groundstation:CancelContact", "CreateConfig": "groundstation:CreateConfig", "CreateDataflowEndpointGroup": "groundstation:CreateDataflowEndpointGroup", "CreateMissionProfile": "groundstation:CreateMissionProfile", "DeleteConfig": "groundstation:DeleteConfig", "DeleteDataflowEndpointGroup": "groundstation:DeleteDataflowEndpointGroup", "DeleteMissionProfile": "groundstation:DeleteMissionProfile", "DescribeContact": "groundstation:DescribeContact", "GetConfig": "groundstation:GetConfig", "GetDataflowEndpointGroup": "groundstation:GetDataflowEndpointGroup", "GetMinuteUsage": "groundstation:GetMinuteUsage", "GetMissionProfile": "groundstation:GetMissionProfile", "GetSatellite": "groundstation:GetSatellite", "ListConfigs": "groundstation:ListConfigs", "ListContacts": "groundstation:ListContacts", "ListDataflowEndpointGroups": "groundstation:ListDataflowEndpointGroups", "ListGroundStations": "groundstation:ListGroundStations", "ListMissionProfiles": "groundstation:ListMissionProfiles", "ListSatellites": "groundstation:ListSatellites", "ListTagsForResource": "groundstation:ListTagsForResource", "ReserveContact": "groundstation:ReserveContact", "TagResource": "groundstation:TagResource", "UntagResource": "groundstation:UntagResource", "UpdateConfig": "groundstation:UpdateConfig", "UpdateMissionProfile": "groundstation:UpdateMissionProfile" }, "guardduty": { "AcceptInvitation": "guardduty:AcceptInvitation", "ArchiveFindings": "guardduty:ArchiveFindings", "CreateDetector": "guardduty:CreateDetector", "CreateFilter": "guardduty:CreateFilter", "CreateIPSet": "guardduty:CreateIPSet", "CreateMembers": "guardduty:CreateMembers", "CreatePublishingDestination": "guardduty:CreatePublishingDestination", "CreateSampleFindings": "guardduty:CreateSampleFindings", "CreateThreatIntelSet": "guardduty:CreateThreatIntelSet", "DeclineInvitations": "guardduty:DeclineInvitations", "DeleteDetector": "guardduty:DeleteDetector", "DeleteFilter": "guardduty:DeleteFilter", "DeleteIPSet": "guardduty:DeleteIPSet", "DeleteInvitations": "guardduty:DeleteInvitations", "DeleteMembers": "guardduty:DeleteMembers", "DeletePublishingDestination": "guardduty:DeletePublishingDestination", "DeleteThreatIntelSet": "guardduty:DeleteThreatIntelSet", "DescribeOrganizationConfiguration": "guardduty:DescribeOrganizationConfiguration", "DescribePublishingDestination": "guardduty:DescribePublishingDestination", "DisableOrganizationAdminAccount": "guardduty:DisableOrganizationAdminAccount", "DisassociateFromMasterAccount": "guardduty:DisassociateFromMasterAccount", "DisassociateMembers": "guardduty:DisassociateMembers", "EnableOrganizationAdminAccount": "guardduty:EnableOrganizationAdminAccount", "GetDetector": "guardduty:GetDetector", "GetFilter": "guardduty:GetFilter", "GetFindings": "guardduty:GetFindings", "GetFindingsStatistics": "guardduty:GetFindingsStatistics", "GetIPSet": "guardduty:GetIPSet", "GetInvitationsCount": "guardduty:GetInvitationsCount", "GetMasterAccount": "guardduty:GetMasterAccount", "GetMemberDetectors": "guardduty:GetMemberDetectors", "GetMembers": "guardduty:GetMembers", "GetThreatIntelSet": "guardduty:GetThreatIntelSet", "GetUsageStatistics": "guardduty:GetUsageStatistics", "InviteMembers": "guardduty:InviteMembers", "ListDetectors": "guardduty:ListDetectors", "ListFilters": "guardduty:ListFilters", "ListFindings": "guardduty:ListFindings", "ListIPSets": "guardduty:ListIPSets", "ListInvitations": "guardduty:ListInvitations", "ListMembers": "guardduty:ListMembers", "ListOrganizationAdminAccounts": "guardduty:ListOrganizationAdminAccounts", "ListPublishingDestinations": "guardduty:ListPublishingDestinations", "ListTagsForResource": "guardduty:ListTagsForResource", "ListThreatIntelSets": "guardduty:ListThreatIntelSets", "StartMonitoringMembers": "guardduty:StartMonitoringMembers", "StopMonitoringMembers": "guardduty:StopMonitoringMembers", "TagResource": "guardduty:TagResource", "UnarchiveFindings": "guardduty:UnarchiveFindings", "UntagResource": "guardduty:UntagResource", "UpdateDetector": "guardduty:UpdateDetector", "UpdateFilter": "guardduty:UpdateFilter", "UpdateFindingsFeedback": "guardduty:UpdateFindingsFeedback", "UpdateIPSet": "guardduty:UpdateIPSet", "UpdateMemberDetectors": "guardduty:UpdateMemberDetectors", "UpdateOrganizationConfiguration": "guardduty:UpdateOrganizationConfiguration", "UpdatePublishingDestination": "guardduty:UpdatePublishingDestination", "UpdateThreatIntelSet": "guardduty:UpdateThreatIntelSet" }, "health": { "DescribeAffectedAccountsForOrganization": "health:DescribeAffectedAccountsForOrganization", "DescribeAffectedEntities": "health:DescribeAffectedEntities", "DescribeAffectedEntitiesForOrganization": "health:DescribeAffectedEntitiesForOrganization", "DescribeEntityAggregates": "health:DescribeEntityAggregates", "DescribeEventAggregates": "health:DescribeEventAggregates", "DescribeEventDetails": "health:DescribeEventDetails", "DescribeEventDetailsForOrganization": "health:DescribeEventDetailsForOrganization", "DescribeEventTypes": "health:DescribeEventTypes", "DescribeEvents": "health:DescribeEvents", "DescribeEventsForOrganization": "health:DescribeEventsForOrganization", "DescribeHealthServiceStatusForOrganization": "health:DescribeHealthServiceStatusForOrganization", "DisableHealthServiceAccessForOrganization": "health:DisableHealthServiceAccessForOrganization", "EnableHealthServiceAccessForOrganization": "health:EnableHealthServiceAccessForOrganization" }, "healthlake": { "CreateFHIRDatastore": "healthlake:CreateFHIRDatastore", "DeleteFHIRDatastore": "healthlake:DeleteFHIRDatastore", "DescribeFHIRDatastore": "healthlake:DescribeFHIRDatastore", "DescribeFHIRExportJob": "healthlake:DescribeFHIRExportJob", "DescribeFHIRImportJob": "healthlake:DescribeFHIRImportJob", "ListFHIRDatastores": "healthlake:ListFHIRDatastores", "ListFHIRExportJobs": "healthlake:ListFHIRExportJobs", "ListFHIRImportJobs": "healthlake:ListFHIRImportJobs", "ListTagsForResource": "healthlake:ListTagsForResource", "StartFHIRExportJob": "healthlake:StartFHIRExportJob", "StartFHIRImportJob": "healthlake:StartFHIRImportJob", "TagResource": "healthlake:TagResource", "UntagResource": "healthlake:UntagResource" }, "honeycode": { "BatchCreateTableRows": "honeycode:BatchCreateTableRows", "BatchDeleteTableRows": "honeycode:BatchDeleteTableRows", "BatchUpdateTableRows": "honeycode:BatchUpdateTableRows", "BatchUpsertTableRows": "honeycode:BatchUpsertTableRows", "DescribeTableDataImportJob": "honeycode:DescribeTableDataImportJob", "GetScreenData": "honeycode:GetScreenData", "InvokeScreenAutomation": "honeycode:InvokeScreenAutomation", "ListTableColumns": "honeycode:ListTableColumns", "ListTableRows": "honeycode:ListTableRows", "ListTables": "honeycode:ListTables", "QueryTableRows": "honeycode:QueryTableRows", "StartTableDataImportJob": "honeycode:StartTableDataImportJob" }, "iam": { "AddClientIDToOpenIDConnectProvider": "iam:AddClientIDToOpenIDConnectProvider", "AddRoleToInstanceProfile": "iam:AddRoleToInstanceProfile", "AddUserToGroup": "iam:AddUserToGroup", "AttachGroupPolicy": "iam:AttachGroupPolicy", "AttachRolePolicy": "iam:AttachRolePolicy", "AttachUserPolicy": "iam:AttachUserPolicy", "ChangePassword": "iam:ChangePassword", "CreateAccessKey": "iam:CreateAccessKey", "CreateAccountAlias": "iam:CreateAccountAlias", "CreateGroup": "iam:CreateGroup", "CreateInstanceProfile": "iam:CreateInstanceProfile", "CreateLoginProfile": "iam:CreateLoginProfile", "CreateOpenIDConnectProvider": "iam:CreateOpenIDConnectProvider", "CreatePolicy": "iam:CreatePolicy", "CreatePolicyVersion": "iam:CreatePolicyVersion", "CreateRole": "iam:CreateRole", "CreateSAMLProvider": "iam:CreateSAMLProvider", "CreateServiceLinkedRole": "iam:CreateServiceLinkedRole", "CreateServiceSpecificCredential": "iam:CreateServiceSpecificCredential", "CreateUser": "iam:CreateUser", "CreateVirtualMFADevice": "iam:CreateVirtualMFADevice", "DeactivateMFADevice": "iam:DeactivateMFADevice", "DeleteAccessKey": "iam:DeleteAccessKey", "DeleteAccountAlias": "iam:DeleteAccountAlias", "DeleteAccountPasswordPolicy": "iam:DeleteAccountPasswordPolicy", "DeleteGroup": "iam:DeleteGroup", "DeleteGroupPolicy": "iam:DeleteGroupPolicy", "DeleteInstanceProfile": "iam:DeleteInstanceProfile", "DeleteLoginProfile": "iam:DeleteLoginProfile", "DeleteOpenIDConnectProvider": "iam:DeleteOpenIDConnectProvider", "DeletePolicy": "iam:DeletePolicy", "DeletePolicyVersion": "iam:DeletePolicyVersion", "DeleteRole": "iam:DeleteRole", "DeleteRolePermissionsBoundary": "iam:DeleteRolePermissionsBoundary", "DeleteRolePolicy": "iam:DeleteRolePolicy", "DeleteSAMLProvider": "iam:DeleteSAMLProvider", "DeleteSSHPublicKey": "iam:DeleteSSHPublicKey", "DeleteServerCertificate": "iam:DeleteServerCertificate", "DeleteServiceLinkedRole": "iam:DeleteServiceLinkedRole", "DeleteServiceSpecificCredential": "iam:DeleteServiceSpecificCredential", "DeleteSigningCertificate": "iam:DeleteSigningCertificate", "DeleteUser": "iam:DeleteUser", "DeleteUserPermissionsBoundary": "iam:DeleteUserPermissionsBoundary", "DeleteUserPolicy": "iam:DeleteUserPolicy", "DeleteVirtualMFADevice": "iam:DeleteVirtualMFADevice", "DetachGroupPolicy": "iam:DetachGroupPolicy", "DetachRolePolicy": "iam:DetachRolePolicy", "DetachUserPolicy": "iam:DetachUserPolicy", "EnableMFADevice": "iam:EnableMFADevice", "GenerateCredentialReport": "iam:GenerateCredentialReport", "GenerateOrganizationsAccessReport": "iam:GenerateOrganizationsAccessReport", "GenerateServiceLastAccessedDetails": "iam:GenerateServiceLastAccessedDetails", "GetAccessKeyLastUsed": "iam:GetAccessKeyLastUsed", "GetAccountAuthorizationDetails": "iam:GetAccountAuthorizationDetails", "GetAccountPasswordPolicy": "iam:GetAccountPasswordPolicy", "GetAccountSummary": "iam:GetAccountSummary", "GetContextKeysForCustomPolicy": "iam:GetContextKeysForCustomPolicy", "GetContextKeysForPrincipalPolicy": "iam:GetContextKeysForPrincipalPolicy", "GetCredentialReport": "iam:GetCredentialReport", "GetGroup": "iam:GetGroup", "GetGroupPolicy": "iam:GetGroupPolicy", "GetInstanceProfile": "iam:GetInstanceProfile", "GetLoginProfile": "iam:GetLoginProfile", "GetOpenIDConnectProvider": "iam:GetOpenIDConnectProvider", "GetOrganizationsAccessReport": "iam:GetOrganizationsAccessReport", "GetPolicy": "iam:GetPolicy", "GetPolicyVersion": "iam:GetPolicyVersion", "GetRole": "iam:GetRole", "GetRolePolicy": "iam:GetRolePolicy", "GetSAMLProvider": "iam:GetSAMLProvider", "GetSSHPublicKey": "iam:GetSSHPublicKey", "GetServerCertificate": "iam:GetServerCertificate", "GetServiceLastAccessedDetails": "iam:GetServiceLastAccessedDetails", "GetServiceLastAccessedDetailsWithEntities": "iam:GetServiceLastAccessedDetailsWithEntities", "GetServiceLinkedRoleDeletionStatus": "iam:GetServiceLinkedRoleDeletionStatus", "GetUser": "iam:GetUser", "GetUserPolicy": "iam:GetUserPolicy", "ListAccessKeys": "iam:ListAccessKeys", "ListAccountAliases": "iam:ListAccountAliases", "ListAttachedGroupPolicies": "iam:ListAttachedGroupPolicies", "ListAttachedRolePolicies": "iam:ListAttachedRolePolicies", "ListAttachedUserPolicies": "iam:ListAttachedUserPolicies", "ListEntitiesForPolicy": "iam:ListEntitiesForPolicy", "ListGroupPolicies": "iam:ListGroupPolicies", "ListGroups": "iam:ListGroups", "ListGroupsForUser": "iam:ListGroupsForUser", "ListInstanceProfileTags": "iam:ListInstanceProfileTags", "ListInstanceProfiles": "iam:ListInstanceProfiles", "ListInstanceProfilesForRole": "iam:ListInstanceProfilesForRole", "ListMFADeviceTags": "iam:ListMFADeviceTags", "ListMFADevices": "iam:ListMFADevices", "ListOpenIDConnectProviderTags": "iam:ListOpenIDConnectProviderTags", "ListOpenIDConnectProviders": "iam:ListOpenIDConnectProviders", "ListPolicies": "iam:ListPolicies", "ListPoliciesGrantingServiceAccess": "iam:ListPoliciesGrantingServiceAccess", "ListPolicyTags": "iam:ListPolicyTags", "ListPolicyVersions": "iam:ListPolicyVersions", "ListRolePolicies": "iam:ListRolePolicies", "ListRoleTags": "iam:ListRoleTags", "ListRoles": "iam:ListRoles", "ListSAMLProviderTags": "iam:ListSAMLProviderTags", "ListSAMLProviders": "iam:ListSAMLProviders", "ListSSHPublicKeys": "iam:ListSSHPublicKeys", "ListServerCertificateTags": "iam:ListServerCertificateTags", "ListServerCertificates": "iam:ListServerCertificates", "ListServiceSpecificCredentials": "iam:ListServiceSpecificCredentials", "ListSigningCertificates": "iam:ListSigningCertificates", "ListUserPolicies": "iam:ListUserPolicies", "ListUserTags": "iam:ListUserTags", "ListUsers": "iam:ListUsers", "ListVirtualMFADevices": "iam:ListVirtualMFADevices", "PutGroupPolicy": "iam:PutGroupPolicy", "PutRolePermissionsBoundary": "iam:PutRolePermissionsBoundary", "PutRolePolicy": "iam:PutRolePolicy", "PutUserPermissionsBoundary": "iam:PutUserPermissionsBoundary", "PutUserPolicy": "iam:PutUserPolicy", "RemoveClientIDFromOpenIDConnectProvider": "iam:RemoveClientIDFromOpenIDConnectProvider", "RemoveRoleFromInstanceProfile": "iam:RemoveRoleFromInstanceProfile", "RemoveUserFromGroup": "iam:RemoveUserFromGroup", "ResetServiceSpecificCredential": "iam:ResetServiceSpecificCredential", "ResyncMFADevice": "iam:ResyncMFADevice", "SetDefaultPolicyVersion": "iam:SetDefaultPolicyVersion", "SetSecurityTokenServicePreferences": "iam:SetSecurityTokenServicePreferences", "SimulateCustomPolicy": "iam:SimulateCustomPolicy", "SimulatePrincipalPolicy": "iam:SimulatePrincipalPolicy", "TagInstanceProfile": "iam:TagInstanceProfile", "TagMFADevice": "iam:TagMFADevice", "TagOpenIDConnectProvider": "iam:TagOpenIDConnectProvider", "TagPolicy": "iam:TagPolicy", "TagRole": "iam:TagRole", "TagSAMLProvider": "iam:TagSAMLProvider", "TagServerCertificate": "iam:TagServerCertificate", "TagUser": "iam:TagUser", "UntagInstanceProfile": "iam:UntagInstanceProfile", "UntagMFADevice": "iam:UntagMFADevice", "UntagOpenIDConnectProvider": "iam:UntagOpenIDConnectProvider", "UntagPolicy": "iam:UntagPolicy", "UntagRole": "iam:UntagRole", "UntagSAMLProvider": "iam:UntagSAMLProvider", "UntagServerCertificate": "iam:UntagServerCertificate", "UntagUser": "iam:UntagUser", "UpdateAccessKey": "iam:UpdateAccessKey", "UpdateAccountPasswordPolicy": "iam:UpdateAccountPasswordPolicy", "UpdateAssumeRolePolicy": "iam:UpdateAssumeRolePolicy", "UpdateGroup": "iam:UpdateGroup", "UpdateLoginProfile": "iam:UpdateLoginProfile", "UpdateOpenIDConnectProviderThumbprint": "iam:UpdateOpenIDConnectProviderThumbprint", "UpdateRole": "iam:UpdateRole", "UpdateRoleDescription": "iam:UpdateRoleDescription", "UpdateSAMLProvider": "iam:UpdateSAMLProvider", "UpdateSSHPublicKey": "iam:UpdateSSHPublicKey", "UpdateServerCertificate": "iam:UpdateServerCertificate", "UpdateServiceSpecificCredential": "iam:UpdateServiceSpecificCredential", "UpdateSigningCertificate": "iam:UpdateSigningCertificate", "UpdateUser": "iam:UpdateUser", "UploadSSHPublicKey": "iam:UploadSSHPublicKey", "UploadServerCertificate": "iam:UploadServerCertificate", "UploadSigningCertificate": "iam:UploadSigningCertificate" }, "identitystore": { "DescribeGroup": "identitystore:DescribeGroup", "DescribeUser": "identitystore:DescribeUser", "ListGroups": "identitystore:ListGroups", "ListUsers": "identitystore:ListUsers" }, "imagebuilder": { "CancelImageCreation": "imagebuilder:CancelImageCreation", "CreateComponent": "imagebuilder:CreateComponent", "CreateContainerRecipe": "imagebuilder:CreateContainerRecipe", "CreateDistributionConfiguration": "imagebuilder:CreateDistributionConfiguration", "CreateImage": "imagebuilder:CreateImage", "CreateImagePipeline": "imagebuilder:CreateImagePipeline", "CreateImageRecipe": "imagebuilder:CreateImageRecipe", "CreateInfrastructureConfiguration": "imagebuilder:CreateInfrastructureConfiguration", "DeleteComponent": "imagebuilder:DeleteComponent", "DeleteContainerRecipe": "imagebuilder:DeleteContainerRecipe", "DeleteDistributionConfiguration": "imagebuilder:DeleteDistributionConfiguration", "DeleteImage": "imagebuilder:DeleteImage", "DeleteImagePipeline": "imagebuilder:DeleteImagePipeline", "DeleteImageRecipe": "imagebuilder:DeleteImageRecipe", "DeleteInfrastructureConfiguration": "imagebuilder:DeleteInfrastructureConfiguration", "GetComponent": "imagebuilder:GetComponent", "GetComponentPolicy": "imagebuilder:GetComponentPolicy", "GetContainerRecipe": "imagebuilder:GetContainerRecipe", "GetContainerRecipePolicy": "imagebuilder:GetContainerRecipePolicy", "GetDistributionConfiguration": "imagebuilder:GetDistributionConfiguration", "GetImage": "imagebuilder:GetImage", "GetImagePipeline": "imagebuilder:GetImagePipeline", "GetImagePolicy": "imagebuilder:GetImagePolicy", "GetImageRecipe": "imagebuilder:GetImageRecipe", "GetImageRecipePolicy": "imagebuilder:GetImageRecipePolicy", "GetInfrastructureConfiguration": "imagebuilder:GetInfrastructureConfiguration", "ImportComponent": "imagebuilder:ImportComponent", "ListComponentBuildVersions": "imagebuilder:ListComponentBuildVersions", "ListComponents": "imagebuilder:ListComponents", "ListContainerRecipes": "imagebuilder:ListContainerRecipes", "ListDistributionConfigurations": "imagebuilder:ListDistributionConfigurations", "ListImageBuildVersions": "imagebuilder:ListImageBuildVersions", "ListImagePackages": "imagebuilder:ListImagePackages", "ListImagePipelineImages": "imagebuilder:ListImagePipelineImages", "ListImagePipelines": "imagebuilder:ListImagePipelines", "ListImageRecipes": "imagebuilder:ListImageRecipes", "ListImages": "imagebuilder:ListImages", "ListInfrastructureConfigurations": "imagebuilder:ListInfrastructureConfigurations", "ListTagsForResource": "imagebuilder:ListTagsForResource", "PutComponentPolicy": "imagebuilder:PutComponentPolicy", "PutContainerRecipePolicy": "imagebuilder:PutContainerRecipePolicy", "PutImagePolicy": "imagebuilder:PutImagePolicy", "PutImageRecipePolicy": "imagebuilder:PutImageRecipePolicy", "StartImagePipelineExecution": "imagebuilder:StartImagePipelineExecution", "TagResource": "imagebuilder:TagResource", "UntagResource": "imagebuilder:UntagResource", "UpdateDistributionConfiguration": "imagebuilder:UpdateDistributionConfiguration", "UpdateImagePipeline": "imagebuilder:UpdateImagePipeline", "UpdateInfrastructureConfiguration": "imagebuilder:UpdateInfrastructureConfiguration" }, "importexport": { "CancelJob": "importexport:CancelJob", "CreateJob": "importexport:CreateJob", "GetShippingLabel": "importexport:GetShippingLabel", "GetStatus": "importexport:GetStatus", "ListJobs": "importexport:ListJobs", "UpdateJob": "importexport:UpdateJob" }, "inspector": { "AddAttributesToFindings": "inspector:AddAttributesToFindings", "CreateAssessmentTarget": "inspector:CreateAssessmentTarget", "CreateAssessmentTemplate": "inspector:CreateAssessmentTemplate", "CreateExclusionsPreview": "inspector:CreateExclusionsPreview", "CreateResourceGroup": "inspector:CreateResourceGroup", "DeleteAssessmentRun": "inspector:DeleteAssessmentRun", "DeleteAssessmentTarget": "inspector:DeleteAssessmentTarget", "DeleteAssessmentTemplate": "inspector:DeleteAssessmentTemplate", "DescribeAssessmentRuns": "inspector:DescribeAssessmentRuns", "DescribeAssessmentTargets": "inspector:DescribeAssessmentTargets", "DescribeAssessmentTemplates": "inspector:DescribeAssessmentTemplates", "DescribeCrossAccountAccessRole": "inspector:DescribeCrossAccountAccessRole", "DescribeExclusions": "inspector:DescribeExclusions", "DescribeFindings": "inspector:DescribeFindings", "DescribeResourceGroups": "inspector:DescribeResourceGroups", "DescribeRulesPackages": "inspector:DescribeRulesPackages", "GetAssessmentReport": "inspector:GetAssessmentReport", "GetExclusionsPreview": "inspector:GetExclusionsPreview", "GetTelemetryMetadata": "inspector:GetTelemetryMetadata", "ListAssessmentRunAgents": "inspector:ListAssessmentRunAgents", "ListAssessmentRuns": "inspector:ListAssessmentRuns", "ListAssessmentTargets": "inspector:ListAssessmentTargets", "ListAssessmentTemplates": "inspector:ListAssessmentTemplates", "ListEventSubscriptions": "inspector:ListEventSubscriptions", "ListExclusions": "inspector:ListExclusions", "ListFindings": "inspector:ListFindings", "ListRulesPackages": "inspector:ListRulesPackages", "ListTagsForResource": "inspector:ListTagsForResource", "PreviewAgents": "inspector:PreviewAgents", "RegisterCrossAccountAccessRole": "inspector:RegisterCrossAccountAccessRole", "RemoveAttributesFromFindings": "inspector:RemoveAttributesFromFindings", "SetTagsForResource": "inspector:SetTagsForResource", "StartAssessmentRun": "inspector:StartAssessmentRun", "StopAssessmentRun": "inspector:StopAssessmentRun", "SubscribeToEvent": "inspector:SubscribeToEvent", "UnsubscribeFromEvent": "inspector:UnsubscribeFromEvent", "UpdateAssessmentTarget": "inspector:UpdateAssessmentTarget" }, "inspector2": { "AssociateMember": "inspector2:AssociateMember", "BatchGetAccountStatus": "inspector2:BatchGetAccountStatus", "BatchGetFreeTrialInfo": "inspector2:BatchGetFreeTrialInfo", "CancelFindingsReport": "inspector2:CancelFindingsReport", "CreateFilter": "inspector2:CreateFilter", "CreateFindingsReport": "inspector2:CreateFindingsReport", "DeleteFilter": "inspector2:DeleteFilter", "DescribeOrganizationConfiguration": "inspector2:DescribeOrganizationConfiguration", "Disable": "inspector2:Disable", "DisableDelegatedAdminAccount": "inspector2:DisableDelegatedAdminAccount", "DisassociateMember": "inspector2:DisassociateMember", "Enable": "inspector2:Enable", "EnableDelegatedAdminAccount": "inspector2:EnableDelegatedAdminAccount", "GetDelegatedAdminAccount": "inspector2:GetDelegatedAdminAccount", "GetFindingsReportStatus": "inspector2:GetFindingsReportStatus", "GetMember": "inspector2:GetMember", "ListAccountPermissions": "inspector2:ListAccountPermissions", "ListCoverage": "inspector2:ListCoverage", "ListCoverageStatistics": "inspector2:ListCoverageStatistics", "ListDelegatedAdminAccounts": "inspector2:ListDelegatedAdminAccounts", "ListFilters": "inspector2:ListFilters", "ListFindingAggregations": "inspector2:ListFindingAggregations", "ListFindings": "inspector2:ListFindings", "ListMembers": "inspector2:ListMembers", "ListTagsForResource": "inspector2:ListTagsForResource", "ListUsageTotals": "inspector2:ListUsageTotals", "TagResource": "inspector2:TagResource", "UntagResource": "inspector2:UntagResource", "UpdateFilter": "inspector2:UpdateFilter", "UpdateOrganizationConfiguration": "inspector2:UpdateOrganizationConfiguration" }, "iot": { "AcceptCertificateTransfer": "iot:AcceptCertificateTransfer", "AddThingToBillingGroup": "iot:AddThingToBillingGroup", "AddThingToThingGroup": "iot:AddThingToThingGroup", "AssociateTargetsWithJob": "iot:AssociateTargetsWithJob", "AttachPolicy": "iot:AttachPolicy", "AttachPrincipalPolicy": "iot:AttachPrincipalPolicy", "AttachSecurityProfile": "iot:AttachSecurityProfile", "AttachThingPrincipal": "iot:AttachThingPrincipal", "CancelAuditMitigationActionsTask": "iot:CancelAuditMitigationActionsTask", "CancelAuditTask": "iot:CancelAuditTask", "CancelCertificateTransfer": "iot:CancelCertificateTransfer", "CancelDetectMitigationActionsTask": "iot:CancelDetectMitigationActionsTask", "CancelJob": "iot:CancelJob", "CancelJobExecution": "iot:CancelJobExecution", "ClearDefaultAuthorizer": "iot:ClearDefaultAuthorizer", "ConfirmTopicRuleDestination": "iot:ConfirmTopicRuleDestination", "CreateAuditSuppression": "iot:CreateAuditSuppression", "CreateAuthorizer": "iot:CreateAuthorizer", "CreateBillingGroup": "iot:CreateBillingGroup", "CreateCertificateFromCsr": "iot:CreateCertificateFromCsr", "CreateCustomMetric": "iot:CreateCustomMetric", "CreateDimension": "iot:CreateDimension", "CreateDomainConfiguration": "iot:CreateDomainConfiguration", "CreateDynamicThingGroup": "iot:CreateDynamicThingGroup", "CreateFleetMetric": "iot:CreateFleetMetric", "CreateJob": "iot:CreateJob", "CreateJobTemplate": "iot:CreateJobTemplate", "CreateKeysAndCertificate": "iot:CreateKeysAndCertificate", "CreateMitigationAction": "iot:CreateMitigationAction", "CreateOTAUpdate": "iot:CreateOTAUpdate", "CreatePolicy": "iot:CreatePolicy", "CreatePolicyVersion": "iot:CreatePolicyVersion", "CreateProvisioningClaim": "iot:CreateProvisioningClaim", "CreateProvisioningTemplate": "iot:CreateProvisioningTemplate", "CreateProvisioningTemplateVersion": "iot:CreateProvisioningTemplateVersion", "CreateRoleAlias": "iot:CreateRoleAlias", "CreateScheduledAudit": "iot:CreateScheduledAudit", "CreateSecurityProfile": "iot:CreateSecurityProfile", "CreateStream": "iot:CreateStream", "CreateThing": "iot:CreateThing", "CreateThingGroup": "iot:CreateThingGroup", "CreateThingType": "iot:CreateThingType", "CreateTopicRule": "iot:CreateTopicRule", "CreateTopicRuleDestination": "iot:CreateTopicRuleDestination", "DeleteAccountAuditConfiguration": "iot:DeleteAccountAuditConfiguration", "DeleteAuditSuppression": "iot:DeleteAuditSuppression", "DeleteAuthorizer": "iot:DeleteAuthorizer", "DeleteBillingGroup": "iot:DeleteBillingGroup", "DeleteCACertificate": "iot:DeleteCACertificate", "DeleteCertificate": "iot:DeleteCertificate", "DeleteCustomMetric": "iot:DeleteCustomMetric", "DeleteDimension": "iot:DeleteDimension", "DeleteDomainConfiguration": "iot:DeleteDomainConfiguration", "DeleteDynamicThingGroup": "iot:DeleteDynamicThingGroup", "DeleteFleetMetric": "iot:DeleteFleetMetric", "DeleteJob": "iot:DeleteJob", "DeleteJobExecution": "iot:DeleteJobExecution", "DeleteJobTemplate": "iot:DeleteJobTemplate", "DeleteMitigationAction": "iot:DeleteMitigationAction", "DeleteOTAUpdate": "iot:DeleteOTAUpdate", "DeletePolicy": "iot:DeletePolicy", "DeletePolicyVersion": "iot:DeletePolicyVersion", "DeleteProvisioningTemplate": "iot:DeleteProvisioningTemplate", "DeleteProvisioningTemplateVersion": "iot:DeleteProvisioningTemplateVersion", "DeleteRegistrationCode": "iot:DeleteRegistrationCode", "DeleteRoleAlias": "iot:DeleteRoleAlias", "DeleteScheduledAudit": "iot:DeleteScheduledAudit", "DeleteSecurityProfile": "iot:DeleteSecurityProfile", "DeleteStream": "iot:DeleteStream", "DeleteThing": "iot:DeleteThing", "DeleteThingGroup": "iot:DeleteThingGroup", "DeleteThingType": "iot:DeleteThingType", "DeleteTopicRule": "iot:DeleteTopicRule", "DeleteTopicRuleDestination": "iot:DeleteTopicRuleDestination", "DeleteV2LoggingLevel": "iot:DeleteV2LoggingLevel", "DeprecateThingType": "iot:DeprecateThingType", "DescribeAccountAuditConfiguration": "iot:DescribeAccountAuditConfiguration", "DescribeAuditFinding": "iot:DescribeAuditFinding", "DescribeAuditMitigationActionsTask": "iot:DescribeAuditMitigationActionsTask", "DescribeAuditSuppression": "iot:DescribeAuditSuppression", "DescribeAuditTask": "iot:DescribeAuditTask", "DescribeAuthorizer": "iot:DescribeAuthorizer", "DescribeBillingGroup": "iot:DescribeBillingGroup", "DescribeCACertificate": "iot:DescribeCACertificate", "DescribeCertificate": "iot:DescribeCertificate", "DescribeCustomMetric": "iot:DescribeCustomMetric", "DescribeDefaultAuthorizer": "iot:DescribeDefaultAuthorizer", "DescribeDetectMitigationActionsTask": "iot:DescribeDetectMitigationActionsTask", "DescribeDimension": "iot:DescribeDimension", "DescribeDomainConfiguration": "iot:DescribeDomainConfiguration", "DescribeEndpoint": "iot:DescribeEndpoint", "DescribeEventConfigurations": "iot:DescribeEventConfigurations", "DescribeFleetMetric": "iot:DescribeFleetMetric", "DescribeIndex": "iot:DescribeIndex", "DescribeJob": "iot:DescribeJob", "DescribeJobExecution": "iot:DescribeJobExecution", "DescribeJobTemplate": "iot:DescribeJobTemplate", "DescribeManagedJobTemplate": "iot:DescribeManagedJobTemplate", "DescribeMitigationAction": "iot:DescribeMitigationAction", "DescribeProvisioningTemplate": "iot:DescribeProvisioningTemplate", "DescribeProvisioningTemplateVersion": "iot:DescribeProvisioningTemplateVersion", "DescribeRoleAlias": "iot:DescribeRoleAlias", "DescribeScheduledAudit": "iot:DescribeScheduledAudit", "DescribeSecurityProfile": "iot:DescribeSecurityProfile", "DescribeStream": "iot:DescribeStream", "DescribeThing": "iot:DescribeThing", "DescribeThingGroup": "iot:DescribeThingGroup", "DescribeThingRegistrationTask": "iot:DescribeThingRegistrationTask", "DescribeThingType": "iot:DescribeThingType", "DetachPolicy": "iot:DetachPolicy", "DetachPrincipalPolicy": "iot:DetachPrincipalPolicy", "DetachSecurityProfile": "iot:DetachSecurityProfile", "DetachThingPrincipal": "iot:DetachThingPrincipal", "DisableTopicRule": "iot:DisableTopicRule", "EnableTopicRule": "iot:EnableTopicRule", "GetBehaviorModelTrainingSummaries": "iot:GetBehaviorModelTrainingSummaries", "GetBucketsAggregation": "iot:GetBucketsAggregation", "GetCardinality": "iot:GetCardinality", "GetEffectivePolicies": "iot:GetEffectivePolicies", "GetIndexingConfiguration": "iot:GetIndexingConfiguration", "GetJobDocument": "iot:GetJobDocument", "GetLoggingOptions": "iot:GetLoggingOptions", "GetOTAUpdate": "iot:GetOTAUpdate", "GetPercentiles": "iot:GetPercentiles", "GetPolicy": "iot:GetPolicy", "GetPolicyVersion": "iot:GetPolicyVersion", "GetRegistrationCode": "iot:GetRegistrationCode", "GetStatistics": "iot:GetStatistics", "GetTopicRule": "iot:GetTopicRule", "GetTopicRuleDestination": "iot:GetTopicRuleDestination", "GetV2LoggingOptions": "iot:GetV2LoggingOptions", "ListActiveViolations": "iot:ListActiveViolations", "ListAttachedPolicies": "iot:ListAttachedPolicies", "ListAuditFindings": "iot:ListAuditFindings", "ListAuditMitigationActionsExecutions": "iot:ListAuditMitigationActionsExecutions", "ListAuditMitigationActionsTasks": "iot:ListAuditMitigationActionsTasks", "ListAuditSuppressions": "iot:ListAuditSuppressions", "ListAuditTasks": "iot:ListAuditTasks", "ListAuthorizers": "iot:ListAuthorizers", "ListBillingGroups": "iot:ListBillingGroups", "ListCACertificates": "iot:ListCACertificates", "ListCertificates": "iot:ListCertificates", "ListCertificatesByCA": "iot:ListCertificatesByCA", "ListCustomMetrics": "iot:ListCustomMetrics", "ListDetectMitigationActionsExecutions": "iot:ListDetectMitigationActionsExecutions", "ListDetectMitigationActionsTasks": "iot:ListDetectMitigationActionsTasks", "ListDimensions": "iot:ListDimensions", "ListDomainConfigurations": "iot:ListDomainConfigurations", "ListFleetMetrics": "iot:ListFleetMetrics", "ListIndices": "iot:ListIndices", "ListJobExecutionsForJob": "iot:ListJobExecutionsForJob", "ListJobExecutionsForThing": "iot:ListJobExecutionsForThing", "ListJobTemplates": "iot:ListJobTemplates", "ListJobs": "iot:ListJobs", "ListManagedJobTemplates": "iot:ListManagedJobTemplates", "ListMitigationActions": "iot:ListMitigationActions", "ListOTAUpdates": "iot:ListOTAUpdates", "ListOutgoingCertificates": "iot:ListOutgoingCertificates", "ListPolicies": "iot:ListPolicies", "ListPolicyPrincipals": "iot:ListPolicyPrincipals", "ListPolicyVersions": "iot:ListPolicyVersions", "ListPrincipalPolicies": "iot:ListPrincipalPolicies", "ListPrincipalThings": "iot:ListPrincipalThings", "ListProvisioningTemplateVersions": "iot:ListProvisioningTemplateVersions", "ListProvisioningTemplates": "iot:ListProvisioningTemplates", "ListRoleAliases": "iot:ListRoleAliases", "ListScheduledAudits": "iot:ListScheduledAudits", "ListSecurityProfiles": "iot:ListSecurityProfiles", "ListSecurityProfilesForTarget": "iot:ListSecurityProfilesForTarget", "ListStreams": "iot:ListStreams", "ListTagsForResource": "iot:ListTagsForResource", "ListTargetsForPolicy": "iot:ListTargetsForPolicy", "ListTargetsForSecurityProfile": "iot:ListTargetsForSecurityProfile", "ListThingGroups": "iot:ListThingGroups", "ListThingGroupsForThing": "iot:ListThingGroupsForThing", "ListThingPrincipals": "iot:ListThingPrincipals", "ListThingRegistrationTaskReports": "iot:ListThingRegistrationTaskReports", "ListThingRegistrationTasks": "iot:ListThingRegistrationTasks", "ListThingTypes": "iot:ListThingTypes", "ListThings": "iot:ListThings", "ListThingsInBillingGroup": "iot:ListThingsInBillingGroup", "ListThingsInThingGroup": "iot:ListThingsInThingGroup", "ListTopicRuleDestinations": "iot:ListTopicRuleDestinations", "ListTopicRules": "iot:ListTopicRules", "ListV2LoggingLevels": "iot:ListV2LoggingLevels", "ListViolationEvents": "iot:ListViolationEvents", "RegisterCACertificate": "iot:RegisterCACertificate", "RegisterCertificate": "iot:RegisterCertificate", "RegisterCertificateWithoutCA": "iot:RegisterCertificateWithoutCA", "RegisterThing": "iot:RegisterThing", "RejectCertificateTransfer": "iot:RejectCertificateTransfer", "RemoveThingFromBillingGroup": "iot:RemoveThingFromBillingGroup", "RemoveThingFromThingGroup": "iot:RemoveThingFromThingGroup", "ReplaceTopicRule": "iot:ReplaceTopicRule", "SearchIndex": "iot:SearchIndex", "SetDefaultAuthorizer": "iot:SetDefaultAuthorizer", "SetDefaultPolicyVersion": "iot:SetDefaultPolicyVersion", "SetLoggingOptions": "iot:SetLoggingOptions", "SetV2LoggingLevel": "iot:SetV2LoggingLevel", "SetV2LoggingOptions": "iot:SetV2LoggingOptions", "StartAuditMitigationActionsTask": "iot:StartAuditMitigationActionsTask", "StartDetectMitigationActionsTask": "iot:StartDetectMitigationActionsTask", "StartOnDemandAuditTask": "iot:StartOnDemandAuditTask", "StartThingRegistrationTask": "iot:StartThingRegistrationTask", "StopThingRegistrationTask": "iot:StopThingRegistrationTask", "TagResource": "iot:TagResource", "TestAuthorization": "iot:TestAuthorization", "TestInvokeAuthorizer": "iot:TestInvokeAuthorizer", "TransferCertificate": "iot:TransferCertificate", "UntagResource": "iot:UntagResource", "UpdateAccountAuditConfiguration": "iot:UpdateAccountAuditConfiguration", "UpdateAuditSuppression": "iot:UpdateAuditSuppression", "UpdateAuthorizer": "iot:UpdateAuthorizer", "UpdateBillingGroup": "iot:UpdateBillingGroup", "UpdateCACertificate": "iot:UpdateCACertificate", "UpdateCertificate": "iot:UpdateCertificate", "UpdateCustomMetric": "iot:UpdateCustomMetric", "UpdateDimension": "iot:UpdateDimension", "UpdateDomainConfiguration": "iot:UpdateDomainConfiguration", "UpdateDynamicThingGroup": "iot:UpdateDynamicThingGroup", "UpdateEventConfigurations": "iot:UpdateEventConfigurations", "UpdateFleetMetric": "iot:UpdateFleetMetric", "UpdateIndexingConfiguration": "iot:UpdateIndexingConfiguration", "UpdateJob": "iot:UpdateJob", "UpdateMitigationAction": "iot:UpdateMitigationAction", "UpdateProvisioningTemplate": "iot:UpdateProvisioningTemplate", "UpdateRoleAlias": "iot:UpdateRoleAlias", "UpdateScheduledAudit": "iot:UpdateScheduledAudit", "UpdateSecurityProfile": "iot:UpdateSecurityProfile", "UpdateStream": "iot:UpdateStream", "UpdateThing": "iot:UpdateThing", "UpdateThingGroup": "iot:UpdateThingGroup", "UpdateThingGroupsForThing": "iot:UpdateThingGroupsForThing", "UpdateTopicRuleDestination": "iot:UpdateTopicRuleDestination", "ValidateSecurityProfileBehaviors": "iot:ValidateSecurityProfileBehaviors" }, "iot-data": { "DeleteThingShadow": "iot:DeleteThingShadow", "GetRetainedMessage": "iot:GetRetainedMessage", "GetThingShadow": "iot:GetThingShadow", "ListNamedShadowsForThing": "iot:ListNamedShadowsForThing", "ListRetainedMessages": "iot:ListRetainedMessages", "Publish": "iot:Publish", "UpdateThingShadow": "iot:UpdateThingShadow" }, "iot-jobs-data": { "DescribeJobExecution": "iot:DescribeJobExecution", "GetPendingJobExecutions": "iot:GetPendingJobExecutions", "StartNextPendingJobExecution": "iot:StartNextPendingJobExecution", "UpdateJobExecution": "iot:UpdateJobExecution" }, "iotanalytics": { "BatchPutMessage": "iotanalytics:BatchPutMessage", "CancelPipelineReprocessing": "iotanalytics:CancelPipelineReprocessing", "CreateChannel": "iotanalytics:CreateChannel", "CreateDataset": "iotanalytics:CreateDataset", "CreateDatasetContent": "iotanalytics:CreateDatasetContent", "CreateDatastore": "iotanalytics:CreateDatastore", "CreatePipeline": "iotanalytics:CreatePipeline", "DeleteChannel": "iotanalytics:DeleteChannel", "DeleteDataset": "iotanalytics:DeleteDataset", "DeleteDatasetContent": "iotanalytics:DeleteDatasetContent", "DeleteDatastore": "iotanalytics:DeleteDatastore", "DeletePipeline": "iotanalytics:DeletePipeline", "DescribeChannel": "iotanalytics:DescribeChannel", "DescribeDataset": "iotanalytics:DescribeDataset", "DescribeDatastore": "iotanalytics:DescribeDatastore", "DescribeLoggingOptions": "iotanalytics:DescribeLoggingOptions", "DescribePipeline": "iotanalytics:DescribePipeline", "GetDatasetContent": "iotanalytics:GetDatasetContent", "ListChannels": "iotanalytics:ListChannels", "ListDatasetContents": "iotanalytics:ListDatasetContents", "ListDatasets": "iotanalytics:ListDatasets", "ListDatastores": "iotanalytics:ListDatastores", "ListPipelines": "iotanalytics:ListPipelines", "ListTagsForResource": "iotanalytics:ListTagsForResource", "PutLoggingOptions": "iotanalytics:PutLoggingOptions", "RunPipelineActivity": "iotanalytics:RunPipelineActivity", "SampleChannelData": "iotanalytics:SampleChannelData", "StartPipelineReprocessing": "iotanalytics:StartPipelineReprocessing", "TagResource": "iotanalytics:TagResource", "UntagResource": "iotanalytics:UntagResource", "UpdateChannel": "iotanalytics:UpdateChannel", "UpdateDataset": "iotanalytics:UpdateDataset", "UpdateDatastore": "iotanalytics:UpdateDatastore", "UpdatePipeline": "iotanalytics:UpdatePipeline" }, "iotdeviceadvisor": { "CreateSuiteDefinition": "iotdeviceadvisor:CreateSuiteDefinition", "DeleteSuiteDefinition": "iotdeviceadvisor:DeleteSuiteDefinition", "GetSuiteDefinition": "iotdeviceadvisor:GetSuiteDefinition", "GetSuiteRun": "iotdeviceadvisor:GetSuiteRun", "GetSuiteRunReport": "iotdeviceadvisor:GetSuiteRunReport", "ListSuiteDefinitions": "iotdeviceadvisor:ListSuiteDefinitions", "ListSuiteRuns": "iotdeviceadvisor:ListSuiteRuns", "ListTagsForResource": "iotdeviceadvisor:ListTagsForResource", "StartSuiteRun": "iotdeviceadvisor:StartSuiteRun", "StopSuiteRun": "iotdeviceadvisor:StopSuiteRun", "TagResource": "iotdeviceadvisor:TagResource", "UntagResource": "iotdeviceadvisor:UntagResource", "UpdateSuiteDefinition": "iotdeviceadvisor:UpdateSuiteDefinition" }, "iotevents": { "CreateAlarmModel": "iotevents:CreateAlarmModel", "CreateDetectorModel": "iotevents:CreateDetectorModel", "CreateInput": "iotevents:CreateInput", "DeleteAlarmModel": "iotevents:DeleteAlarmModel", "DeleteDetectorModel": "iotevents:DeleteDetectorModel", "DeleteInput": "iotevents:DeleteInput", "DescribeAlarmModel": "iotevents:DescribeAlarmModel", "DescribeDetectorModel": "iotevents:DescribeDetectorModel", "DescribeDetectorModelAnalysis": "iotevents:DescribeDetectorModelAnalysis", "DescribeInput": "iotevents:DescribeInput", "DescribeLoggingOptions": "iotevents:DescribeLoggingOptions", "GetDetectorModelAnalysisResults": "iotevents:GetDetectorModelAnalysisResults", "ListAlarmModelVersions": "iotevents:ListAlarmModelVersions", "ListAlarmModels": "iotevents:ListAlarmModels", "ListDetectorModelVersions": "iotevents:ListDetectorModelVersions", "ListDetectorModels": "iotevents:ListDetectorModels", "ListInputRoutings": "iotevents:ListInputRoutings", "ListInputs": "iotevents:ListInputs", "ListTagsForResource": "iotevents:ListTagsForResource", "PutLoggingOptions": "iotevents:PutLoggingOptions", "StartDetectorModelAnalysis": "iotevents:StartDetectorModelAnalysis", "TagResource": "iotevents:TagResource", "UntagResource": "iotevents:UntagResource", "UpdateAlarmModel": "iotevents:UpdateAlarmModel", "UpdateDetectorModel": "iotevents:UpdateDetectorModel", "UpdateInput": "iotevents:UpdateInput" }, "iotfleethub": { "CreateApplication": "iotfleethub:CreateApplication", "DeleteApplication": "iotfleethub:DeleteApplication", "DescribeApplication": "iotfleethub:DescribeApplication", "ListApplications": "iotfleethub:ListApplications", "ListTagsForResource": "iotfleethub:ListTagsForResource", "TagResource": "iotfleethub:TagResource", "UntagResource": "iotfleethub:UntagResource", "UpdateApplication": "iotfleethub:UpdateApplication" }, "iotsitewise": { "AssociateAssets": "iotsitewise:AssociateAssets", "AssociateTimeSeriesToAssetProperty": "iotsitewise:AssociateTimeSeriesToAssetProperty", "BatchAssociateProjectAssets": "iotsitewise:BatchAssociateProjectAssets", "BatchDisassociateProjectAssets": "iotsitewise:BatchDisassociateProjectAssets", "BatchPutAssetPropertyValue": "iotsitewise:BatchPutAssetPropertyValue", "CreateAccessPolicy": "iotsitewise:CreateAccessPolicy", "CreateAsset": "iotsitewise:CreateAsset", "CreateAssetModel": "iotsitewise:CreateAssetModel", "CreateDashboard": "iotsitewise:CreateDashboard", "CreateGateway": "iotsitewise:CreateGateway", "CreatePortal": "iotsitewise:CreatePortal", "CreateProject": "iotsitewise:CreateProject", "DeleteAccessPolicy": "iotsitewise:DeleteAccessPolicy", "DeleteAsset": "iotsitewise:DeleteAsset", "DeleteAssetModel": "iotsitewise:DeleteAssetModel", "DeleteDashboard": "iotsitewise:DeleteDashboard", "DeleteGateway": "iotsitewise:DeleteGateway", "DeletePortal": "iotsitewise:DeletePortal", "DeleteProject": "iotsitewise:DeleteProject", "DeleteTimeSeries": "iotsitewise:DeleteTimeSeries", "DescribeAccessPolicy": "iotsitewise:DescribeAccessPolicy", "DescribeAsset": "iotsitewise:DescribeAsset", "DescribeAssetModel": "iotsitewise:DescribeAssetModel", "DescribeAssetProperty": "iotsitewise:DescribeAssetProperty", "DescribeDashboard": "iotsitewise:DescribeDashboard", "DescribeDefaultEncryptionConfiguration": "iotsitewise:DescribeDefaultEncryptionConfiguration", "DescribeGateway": "iotsitewise:DescribeGateway", "DescribeGatewayCapabilityConfiguration": "iotsitewise:DescribeGatewayCapabilityConfiguration", "DescribeLoggingOptions": "iotsitewise:DescribeLoggingOptions", "DescribePortal": "iotsitewise:DescribePortal", "DescribeProject": "iotsitewise:DescribeProject", "DescribeStorageConfiguration": "iotsitewise:DescribeStorageConfiguration", "DescribeTimeSeries": "iotsitewise:DescribeTimeSeries", "DisassociateAssets": "iotsitewise:DisassociateAssets", "DisassociateTimeSeriesFromAssetProperty": "iotsitewise:DisassociateTimeSeriesFromAssetProperty", "GetAssetPropertyAggregates": "iotsitewise:GetAssetPropertyAggregates", "GetAssetPropertyValue": "iotsitewise:GetAssetPropertyValue", "GetAssetPropertyValueHistory": "iotsitewise:GetAssetPropertyValueHistory", "GetInterpolatedAssetPropertyValues": "iotsitewise:GetInterpolatedAssetPropertyValues", "ListAccessPolicies": "iotsitewise:ListAccessPolicies", "ListAssetModels": "iotsitewise:ListAssetModels", "ListAssetRelationships": "iotsitewise:ListAssetRelationships", "ListAssets": "iotsitewise:ListAssets", "ListAssociatedAssets": "iotsitewise:ListAssociatedAssets", "ListDashboards": "iotsitewise:ListDashboards", "ListGateways": "iotsitewise:ListGateways", "ListPortals": "iotsitewise:ListPortals", "ListProjectAssets": "iotsitewise:ListProjectAssets", "ListProjects": "iotsitewise:ListProjects", "ListTagsForResource": "iotsitewise:ListTagsForResource", "ListTimeSeries": "iotsitewise:ListTimeSeries", "PutDefaultEncryptionConfiguration": "iotsitewise:PutDefaultEncryptionConfiguration", "PutLoggingOptions": "iotsitewise:PutLoggingOptions", "PutStorageConfiguration": "iotsitewise:PutStorageConfiguration", "TagResource": "iotsitewise:TagResource", "UntagResource": "iotsitewise:UntagResource", "UpdateAccessPolicy": "iotsitewise:UpdateAccessPolicy", "UpdateAsset": "iotsitewise:UpdateAsset", "UpdateAssetModel": "iotsitewise:UpdateAssetModel", "UpdateAssetProperty": "iotsitewise:UpdateAssetProperty", "UpdateDashboard": "iotsitewise:UpdateDashboard", "UpdateGateway": "iotsitewise:UpdateGateway", "UpdateGatewayCapabilityConfiguration": "iotsitewise:UpdateGatewayCapabilityConfiguration", "UpdatePortal": "iotsitewise:UpdatePortal", "UpdateProject": "iotsitewise:UpdateProject" }, "iotthingsgraph": { "AssociateEntityToThing": "iotthingsgraph:AssociateEntityToThing", "CreateFlowTemplate": "iotthingsgraph:CreateFlowTemplate", "CreateSystemInstance": "iotthingsgraph:CreateSystemInstance", "CreateSystemTemplate": "iotthingsgraph:CreateSystemTemplate", "DeleteFlowTemplate": "iotthingsgraph:DeleteFlowTemplate", "DeleteNamespace": "iotthingsgraph:DeleteNamespace", "DeleteSystemInstance": "iotthingsgraph:DeleteSystemInstance", "DeleteSystemTemplate": "iotthingsgraph:DeleteSystemTemplate", "DeploySystemInstance": "iotthingsgraph:DeploySystemInstance", "DeprecateFlowTemplate": "iotthingsgraph:DeprecateFlowTemplate", "DeprecateSystemTemplate": "iotthingsgraph:DeprecateSystemTemplate", "DescribeNamespace": "iotthingsgraph:DescribeNamespace", "DissociateEntityFromThing": "iotthingsgraph:DissociateEntityFromThing", "GetEntities": "iotthingsgraph:GetEntities", "GetFlowTemplate": "iotthingsgraph:GetFlowTemplate", "GetFlowTemplateRevisions": "iotthingsgraph:GetFlowTemplateRevisions", "GetNamespaceDeletionStatus": "iotthingsgraph:GetNamespaceDeletionStatus", "GetSystemInstance": "iotthingsgraph:GetSystemInstance", "GetSystemTemplate": "iotthingsgraph:GetSystemTemplate", "GetSystemTemplateRevisions": "iotthingsgraph:GetSystemTemplateRevisions", "GetUploadStatus": "iotthingsgraph:GetUploadStatus", "ListFlowExecutionMessages": "iotthingsgraph:ListFlowExecutionMessages", "ListTagsForResource": "iotthingsgraph:ListTagsForResource", "SearchEntities": "iotthingsgraph:SearchEntities", "SearchFlowExecutions": "iotthingsgraph:SearchFlowExecutions", "SearchFlowTemplates": "iotthingsgraph:SearchFlowTemplates", "SearchSystemInstances": "iotthingsgraph:SearchSystemInstances", "SearchSystemTemplates": "iotthingsgraph:SearchSystemTemplates", "SearchThings": "iotthingsgraph:SearchThings", "TagResource": "iotthingsgraph:TagResource", "UndeploySystemInstance": "iotthingsgraph:UndeploySystemInstance", "UntagResource": "iotthingsgraph:UntagResource", "UpdateFlowTemplate": "iotthingsgraph:UpdateFlowTemplate", "UpdateSystemTemplate": "iotthingsgraph:UpdateSystemTemplate", "UploadEntityDefinitions": "iotthingsgraph:UploadEntityDefinitions" }, "iottwinmaker": { "BatchPutPropertyValues": "iottwinmaker:BatchPutPropertyValues", "CreateComponentType": "iottwinmaker:CreateComponentType", "CreateEntity": "iottwinmaker:CreateEntity", "CreateScene": "iottwinmaker:CreateScene", "CreateWorkspace": "iottwinmaker:CreateWorkspace", "DeleteComponentType": "iottwinmaker:DeleteComponentType", "DeleteEntity": "iottwinmaker:DeleteEntity", "DeleteScene": "iottwinmaker:DeleteScene", "DeleteWorkspace": "iottwinmaker:DeleteWorkspace", "GetComponentType": "iottwinmaker:GetComponentType", "GetEntity": "iottwinmaker:GetEntity", "GetPropertyValue": "iottwinmaker:GetPropertyValue", "GetPropertyValueHistory": "iottwinmaker:GetPropertyValueHistory", "GetScene": "iottwinmaker:GetScene", "GetWorkspace": "iottwinmaker:GetWorkspace", "ListComponentTypes": "iottwinmaker:ListComponentTypes", "ListEntities": "iottwinmaker:ListEntities", "ListScenes": "iottwinmaker:ListScenes", "ListTagsForResource": "iottwinmaker:ListTagsForResource", "ListWorkspaces": "iottwinmaker:ListWorkspaces", "TagResource": "iottwinmaker:TagResource", "UntagResource": "iottwinmaker:UntagResource", "UpdateComponentType": "iottwinmaker:UpdateComponentType", "UpdateEntity": "iottwinmaker:UpdateEntity", "UpdateScene": "iottwinmaker:UpdateScene", "UpdateWorkspace": "iottwinmaker:UpdateWorkspace" }, "iotwireless": { "AssociateAwsAccountWithPartnerAccount": "iotwireless:AssociateAwsAccountWithPartnerAccount", "AssociateMulticastGroupWithFuotaTask": "iotwireless:AssociateMulticastGroupWithFuotaTask", "AssociateWirelessDeviceWithFuotaTask": "iotwireless:AssociateWirelessDeviceWithFuotaTask", "AssociateWirelessDeviceWithMulticastGroup": "iotwireless:AssociateWirelessDeviceWithMulticastGroup", "AssociateWirelessDeviceWithThing": "iotwireless:AssociateWirelessDeviceWithThing", "AssociateWirelessGatewayWithCertificate": "iotwireless:AssociateWirelessGatewayWithCertificate", "AssociateWirelessGatewayWithThing": "iotwireless:AssociateWirelessGatewayWithThing", "CancelMulticastGroupSession": "iotwireless:CancelMulticastGroupSession", "CreateDestination": "iotwireless:CreateDestination", "CreateDeviceProfile": "iotwireless:CreateDeviceProfile", "CreateFuotaTask": "iotwireless:CreateFuotaTask", "CreateMulticastGroup": "iotwireless:CreateMulticastGroup", "CreateServiceProfile": "iotwireless:CreateServiceProfile", "CreateWirelessDevice": "iotwireless:CreateWirelessDevice", "CreateWirelessGateway": "iotwireless:CreateWirelessGateway", "CreateWirelessGatewayTask": "iotwireless:CreateWirelessGatewayTask", "CreateWirelessGatewayTaskDefinition": "iotwireless:CreateWirelessGatewayTaskDefinition", "DeleteDestination": "iotwireless:DeleteDestination", "DeleteDeviceProfile": "iotwireless:DeleteDeviceProfile", "DeleteFuotaTask": "iotwireless:DeleteFuotaTask", "DeleteMulticastGroup": "iotwireless:DeleteMulticastGroup", "DeleteServiceProfile": "iotwireless:DeleteServiceProfile", "DeleteWirelessDevice": "iotwireless:DeleteWirelessDevice", "DeleteWirelessGateway": "iotwireless:DeleteWirelessGateway", "DeleteWirelessGatewayTask": "iotwireless:DeleteWirelessGatewayTask", "DeleteWirelessGatewayTaskDefinition": "iotwireless:DeleteWirelessGatewayTaskDefinition", "DisassociateAwsAccountFromPartnerAccount": "iotwireless:DisassociateAwsAccountFromPartnerAccount", "DisassociateMulticastGroupFromFuotaTask": "iotwireless:DisassociateMulticastGroupFromFuotaTask", "DisassociateWirelessDeviceFromFuotaTask": "iotwireless:DisassociateWirelessDeviceFromFuotaTask", "DisassociateWirelessDeviceFromMulticastGroup": "iotwireless:DisassociateWirelessDeviceFromMulticastGroup", "DisassociateWirelessDeviceFromThing": "iotwireless:DisassociateWirelessDeviceFromThing", "DisassociateWirelessGatewayFromCertificate": "iotwireless:DisassociateWirelessGatewayFromCertificate", "DisassociateWirelessGatewayFromThing": "iotwireless:DisassociateWirelessGatewayFromThing", "GetDestination": "iotwireless:GetDestination", "GetDeviceProfile": "iotwireless:GetDeviceProfile", "GetFuotaTask": "iotwireless:GetFuotaTask", "GetLogLevelsByResourceTypes": "iotwireless:GetLogLevelsByResourceTypes", "GetMulticastGroup": "iotwireless:GetMulticastGroup", "GetMulticastGroupSession": "iotwireless:GetMulticastGroupSession", "GetNetworkAnalyzerConfiguration": "iotwireless:GetNetworkAnalyzerConfiguration", "GetPartnerAccount": "iotwireless:GetPartnerAccount", "GetResourceEventConfiguration": "iotwireless:GetResourceEventConfiguration", "GetResourceLogLevel": "iotwireless:GetResourceLogLevel", "GetServiceEndpoint": "iotwireless:GetServiceEndpoint", "GetServiceProfile": "iotwireless:GetServiceProfile", "GetWirelessDevice": "iotwireless:GetWirelessDevice", "GetWirelessDeviceStatistics": "iotwireless:GetWirelessDeviceStatistics", "GetWirelessGateway": "iotwireless:GetWirelessGateway", "GetWirelessGatewayCertificate": "iotwireless:GetWirelessGatewayCertificate", "GetWirelessGatewayFirmwareInformation": "iotwireless:GetWirelessGatewayFirmwareInformation", "GetWirelessGatewayStatistics": "iotwireless:GetWirelessGatewayStatistics", "GetWirelessGatewayTask": "iotwireless:GetWirelessGatewayTask", "GetWirelessGatewayTaskDefinition": "iotwireless:GetWirelessGatewayTaskDefinition", "ListDestinations": "iotwireless:ListDestinations", "ListDeviceProfiles": "iotwireless:ListDeviceProfiles", "ListFuotaTasks": "iotwireless:ListFuotaTasks", "ListMulticastGroups": "iotwireless:ListMulticastGroups", "ListMulticastGroupsByFuotaTask": "iotwireless:ListMulticastGroupsByFuotaTask", "ListPartnerAccounts": "iotwireless:ListPartnerAccounts", "ListServiceProfiles": "iotwireless:ListServiceProfiles", "ListTagsForResource": "iotwireless:ListTagsForResource", "ListWirelessDevices": "iotwireless:ListWirelessDevices", "ListWirelessGatewayTaskDefinitions": "iotwireless:ListWirelessGatewayTaskDefinitions", "ListWirelessGateways": "iotwireless:ListWirelessGateways", "PutResourceLogLevel": "iotwireless:PutResourceLogLevel", "ResetAllResourceLogLevels": "iotwireless:ResetAllResourceLogLevels", "ResetResourceLogLevel": "iotwireless:ResetResourceLogLevel", "SendDataToMulticastGroup": "iotwireless:SendDataToMulticastGroup", "SendDataToWirelessDevice": "iotwireless:SendDataToWirelessDevice", "StartBulkAssociateWirelessDeviceWithMulticastGroup": "iotwireless:StartBulkAssociateWirelessDeviceWithMulticastGroup", "StartBulkDisassociateWirelessDeviceFromMulticastGroup": "iotwireless:StartBulkDisassociateWirelessDeviceFromMulticastGroup", "StartFuotaTask": "iotwireless:StartFuotaTask", "StartMulticastGroupSession": "iotwireless:StartMulticastGroupSession", "TagResource": "iotwireless:TagResource", "TestWirelessDevice": "iotwireless:TestWirelessDevice", "UntagResource": "iotwireless:UntagResource", "UpdateDestination": "iotwireless:UpdateDestination", "UpdateFuotaTask": "iotwireless:UpdateFuotaTask", "UpdateLogLevelsByResourceTypes": "iotwireless:UpdateLogLevelsByResourceTypes", "UpdateMulticastGroup": "iotwireless:UpdateMulticastGroup", "UpdateNetworkAnalyzerConfiguration": "iotwireless:UpdateNetworkAnalyzerConfiguration", "UpdatePartnerAccount": "iotwireless:UpdatePartnerAccount", "UpdateResourceEventConfiguration": "iotwireless:UpdateResourceEventConfiguration", "UpdateWirelessDevice": "iotwireless:UpdateWirelessDevice", "UpdateWirelessGateway": "iotwireless:UpdateWirelessGateway" }, "ivs": { "BatchGetChannel": "ivs:BatchGetChannel", "BatchGetStreamKey": "ivs:BatchGetStreamKey", "CreateChannel": "ivs:CreateChannel", "CreateRecordingConfiguration": "ivs:CreateRecordingConfiguration", "CreateStreamKey": "ivs:CreateStreamKey", "DeleteChannel": "ivs:DeleteChannel", "DeletePlaybackKeyPair": "ivs:DeletePlaybackKeyPair", "DeleteRecordingConfiguration": "ivs:DeleteRecordingConfiguration", "DeleteStreamKey": "ivs:DeleteStreamKey", "GetChannel": "ivs:GetChannel", "GetPlaybackKeyPair": "ivs:GetPlaybackKeyPair", "GetRecordingConfiguration": "ivs:GetRecordingConfiguration", "GetStream": "ivs:GetStream", "GetStreamKey": "ivs:GetStreamKey", "GetStreamSession": "ivs:GetStreamSession", "ImportPlaybackKeyPair": "ivs:ImportPlaybackKeyPair", "ListChannels": "ivs:ListChannels", "ListPlaybackKeyPairs": "ivs:ListPlaybackKeyPairs", "ListRecordingConfigurations": "ivs:ListRecordingConfigurations", "ListStreamKeys": "ivs:ListStreamKeys", "ListStreamSessions": "ivs:ListStreamSessions", "ListStreams": "ivs:ListStreams", "ListTagsForResource": "ivs:ListTagsForResource", "PutMetadata": "ivs:PutMetadata", "StopStream": "ivs:StopStream", "TagResource": "ivs:TagResource", "UntagResource": "ivs:UntagResource", "UpdateChannel": "ivs:UpdateChannel" }, "kafka": { "BatchAssociateScramSecret": "kafka:BatchAssociateScramSecret", "BatchDisassociateScramSecret": "kafka:BatchDisassociateScramSecret", "CreateCluster": "kafka:CreateCluster", "CreateConfiguration": "kafka:CreateConfiguration", "DeleteCluster": "kafka:DeleteCluster", "DeleteConfiguration": "kafka:DeleteConfiguration", "DescribeCluster": "kafka:DescribeCluster", "DescribeClusterOperation": "kafka:DescribeClusterOperation", "DescribeConfiguration": "kafka:DescribeConfiguration", "DescribeConfigurationRevision": "kafka:DescribeConfigurationRevision", "GetBootstrapBrokers": "kafka:GetBootstrapBrokers", "GetCompatibleKafkaVersions": "kafka:GetCompatibleKafkaVersions", "ListClusterOperations": "kafka:ListClusterOperations", "ListClusters": "kafka:ListClusters", "ListConfigurationRevisions": "kafka:ListConfigurationRevisions", "ListConfigurations": "kafka:ListConfigurations", "ListKafkaVersions": "kafka:ListKafkaVersions", "ListNodes": "kafka:ListNodes", "ListScramSecrets": "kafka:ListScramSecrets", "ListTagsForResource": "kafka:ListTagsForResource", "RebootBroker": "kafka:RebootBroker", "TagResource": "kafka:TagResource", "UntagResource": "kafka:UntagResource", "UpdateBrokerCount": "kafka:UpdateBrokerCount", "UpdateBrokerStorage": "kafka:UpdateBrokerStorage", "UpdateBrokerType": "kafka:UpdateBrokerType", "UpdateClusterConfiguration": "kafka:UpdateClusterConfiguration", "UpdateClusterKafkaVersion": "kafka:UpdateClusterKafkaVersion", "UpdateConfiguration": "kafka:UpdateConfiguration", "UpdateConnectivity": "kafka:UpdateConnectivity", "UpdateMonitoring": "kafka:UpdateMonitoring", "UpdateSecurity": "kafka:UpdateSecurity" }, "kafkaconnect": { "CreateConnector": "kafkaconnect:CreateConnector", "CreateCustomPlugin": "kafkaconnect:CreateCustomPlugin", "CreateWorkerConfiguration": "kafkaconnect:CreateWorkerConfiguration", "DeleteConnector": "kafkaconnect:DeleteConnector", "DescribeConnector": "kafkaconnect:DescribeConnector", "DescribeCustomPlugin": "kafkaconnect:DescribeCustomPlugin", "DescribeWorkerConfiguration": "kafkaconnect:DescribeWorkerConfiguration", "ListConnectors": "kafkaconnect:ListConnectors", "ListCustomPlugins": "kafkaconnect:ListCustomPlugins", "ListWorkerConfigurations": "kafkaconnect:ListWorkerConfigurations", "UpdateConnector": "kafkaconnect:UpdateConnector" }, "kendra": { "BatchDeleteDocument": "kendra:BatchDeleteDocument", "BatchGetDocumentStatus": "kendra:BatchGetDocumentStatus", "BatchPutDocument": "kendra:BatchPutDocument", "ClearQuerySuggestions": "kendra:ClearQuerySuggestions", "CreateDataSource": "kendra:CreateDataSource", "CreateFaq": "kendra:CreateFaq", "CreateIndex": "kendra:CreateIndex", "CreateQuerySuggestionsBlockList": "kendra:CreateQuerySuggestionsBlockList", "CreateThesaurus": "kendra:CreateThesaurus", "DeleteDataSource": "kendra:DeleteDataSource", "DeleteFaq": "kendra:DeleteFaq", "DeleteIndex": "kendra:DeleteIndex", "DeletePrincipalMapping": "kendra:DeletePrincipalMapping", "DeleteQuerySuggestionsBlockList": "kendra:DeleteQuerySuggestionsBlockList", "DeleteThesaurus": "kendra:DeleteThesaurus", "DescribeDataSource": "kendra:DescribeDataSource", "DescribeFaq": "kendra:DescribeFaq", "DescribeIndex": "kendra:DescribeIndex", "DescribePrincipalMapping": "kendra:DescribePrincipalMapping", "DescribeQuerySuggestionsBlockList": "kendra:DescribeQuerySuggestionsBlockList", "DescribeQuerySuggestionsConfig": "kendra:DescribeQuerySuggestionsConfig", "DescribeThesaurus": "kendra:DescribeThesaurus", "GetQuerySuggestions": "kendra:GetQuerySuggestions", "ListDataSourceSyncJobs": "kendra:ListDataSourceSyncJobs", "ListDataSources": "kendra:ListDataSources", "ListFaqs": "kendra:ListFaqs", "ListGroupsOlderThanOrderingId": "kendra:ListGroupsOlderThanOrderingId", "ListIndices": "kendra:ListIndices", "ListQuerySuggestionsBlockLists": "kendra:ListQuerySuggestionsBlockLists", "ListTagsForResource": "kendra:ListTagsForResource", "ListThesauri": "kendra:ListThesauri", "PutPrincipalMapping": "kendra:PutPrincipalMapping", "Query": "kendra:Query", "StartDataSourceSyncJob": "kendra:StartDataSourceSyncJob", "StopDataSourceSyncJob": "kendra:StopDataSourceSyncJob", "SubmitFeedback": "kendra:SubmitFeedback", "TagResource": "kendra:TagResource", "UntagResource": "kendra:UntagResource", "UpdateDataSource": "kendra:UpdateDataSource", "UpdateIndex": "kendra:UpdateIndex", "UpdateQuerySuggestionsBlockList": "kendra:UpdateQuerySuggestionsBlockList", "UpdateQuerySuggestionsConfig": "kendra:UpdateQuerySuggestionsConfig", "UpdateThesaurus": "kendra:UpdateThesaurus" }, "kinesis": { "AddTagsToStream": "kinesis:AddTagsToStream", "CreateStream": "kinesis:CreateStream", "DecreaseStreamRetentionPeriod": "kinesis:DecreaseStreamRetentionPeriod", "DeleteStream": "kinesis:DeleteStream", "DeregisterStreamConsumer": "kinesis:DeregisterStreamConsumer", "DescribeLimits": "kinesis:DescribeLimits", "DescribeStream": "kinesis:DescribeStream", "DescribeStreamConsumer": "kinesis:DescribeStreamConsumer", "DescribeStreamSummary": "kinesis:DescribeStreamSummary", "DisableEnhancedMonitoring": "kinesis:DisableEnhancedMonitoring", "EnableEnhancedMonitoring": "kinesis:EnableEnhancedMonitoring", "GetRecords": "kinesis:GetRecords", "GetShardIterator": "kinesis:GetShardIterator", "IncreaseStreamRetentionPeriod": "kinesis:IncreaseStreamRetentionPeriod", "ListShards": "kinesis:ListShards", "ListStreamConsumers": "kinesis:ListStreamConsumers", "ListStreams": "kinesis:ListStreams", "ListTagsForStream": "kinesis:ListTagsForStream", "MergeShards": "kinesis:MergeShards", "PutRecord": "kinesis:PutRecord", "PutRecords": "kinesis:PutRecords", "RegisterStreamConsumer": "kinesis:RegisterStreamConsumer", "RemoveTagsFromStream": "kinesis:RemoveTagsFromStream", "SplitShard": "kinesis:SplitShard", "StartStreamEncryption": "kinesis:StartStreamEncryption", "StopStreamEncryption": "kinesis:StopStreamEncryption", "SubscribeToShard": "kinesis:SubscribeToShard", "UpdateShardCount": "kinesis:UpdateShardCount" }, "kinesis-video-archived-media": { "GetClip": "kinesisvideo:GetClip", "GetDASHStreamingSessionURL": "kinesisvideo:GetDASHStreamingSessionURL", "GetHLSStreamingSessionURL": "kinesisvideo:GetHLSStreamingSessionURL", "GetMediaForFragmentList": "kinesisvideo:GetMediaForFragmentList", "ListFragments": "kinesisvideo:ListFragments" }, "kinesis-video-media": { "GetMedia": "kinesisvideo:GetMedia" }, "kinesisanalytics": { "AddApplicationCloudWatchLoggingOption": "kinesisanalytics:AddApplicationCloudWatchLoggingOption", "AddApplicationInput": "kinesisanalytics:AddApplicationInput", "AddApplicationInputProcessingConfiguration": "kinesisanalytics:AddApplicationInputProcessingConfiguration", "AddApplicationOutput": "kinesisanalytics:AddApplicationOutput", "AddApplicationReferenceDataSource": "kinesisanalytics:AddApplicationReferenceDataSource", "CreateApplication": "kinesisanalytics:CreateApplication", "DeleteApplication": "kinesisanalytics:DeleteApplication", "DeleteApplicationCloudWatchLoggingOption": "kinesisanalytics:DeleteApplicationCloudWatchLoggingOption", "DeleteApplicationInputProcessingConfiguration": "kinesisanalytics:DeleteApplicationInputProcessingConfiguration", "DeleteApplicationOutput": "kinesisanalytics:DeleteApplicationOutput", "DeleteApplicationReferenceDataSource": "kinesisanalytics:DeleteApplicationReferenceDataSource", "DescribeApplication": "kinesisanalytics:DescribeApplication", "DiscoverInputSchema": "kinesisanalytics:DiscoverInputSchema", "ListApplications": "kinesisanalytics:ListApplications", "ListTagsForResource": "kinesisanalytics:ListTagsForResource", "StartApplication": "kinesisanalytics:StartApplication", "StopApplication": "kinesisanalytics:StopApplication", "TagResource": "kinesisanalytics:TagResource", "UntagResource": "kinesisanalytics:UntagResource", "UpdateApplication": "kinesisanalytics:UpdateApplication" }, "kinesisvideo": { "CreateSignalingChannel": "kinesisvideo:CreateSignalingChannel", "CreateStream": "kinesisvideo:CreateStream", "DeleteSignalingChannel": "kinesisvideo:DeleteSignalingChannel", "DeleteStream": "kinesisvideo:DeleteStream", "DescribeSignalingChannel": "kinesisvideo:DescribeSignalingChannel", "DescribeStream": "kinesisvideo:DescribeStream", "GetDataEndpoint": "kinesisvideo:GetDataEndpoint", "GetSignalingChannelEndpoint": "kinesisvideo:GetSignalingChannelEndpoint", "ListSignalingChannels": "kinesisvideo:ListSignalingChannels", "ListStreams": "kinesisvideo:ListStreams", "ListTagsForResource": "kinesisvideo:ListTagsForResource", "ListTagsForStream": "kinesisvideo:ListTagsForStream", "TagResource": "kinesisvideo:TagResource", "TagStream": "kinesisvideo:TagStream", "UntagResource": "kinesisvideo:UntagResource", "UntagStream": "kinesisvideo:UntagStream", "UpdateDataRetention": "kinesisvideo:UpdateDataRetention", "UpdateSignalingChannel": "kinesisvideo:UpdateSignalingChannel", "UpdateStream": "kinesisvideo:UpdateStream" }, "kms": { "CancelKeyDeletion": "kms:CancelKeyDeletion", "ConnectCustomKeyStore": "kms:ConnectCustomKeyStore", "CreateAlias": "kms:CreateAlias", "CreateCustomKeyStore": "kms:CreateCustomKeyStore", "CreateGrant": "kms:CreateGrant", "CreateKey": "kms:CreateKey", "Decrypt": "kms:Decrypt", "DeleteAlias": "kms:DeleteAlias", "DeleteCustomKeyStore": "kms:DeleteCustomKeyStore", "DeleteImportedKeyMaterial": "kms:DeleteImportedKeyMaterial", "DescribeCustomKeyStores": "kms:DescribeCustomKeyStores", "DescribeKey": "kms:DescribeKey", "DisableKey": "kms:DisableKey", "DisableKeyRotation": "kms:DisableKeyRotation", "DisconnectCustomKeyStore": "kms:DisconnectCustomKeyStore", "EnableKey": "kms:EnableKey", "EnableKeyRotation": "kms:EnableKeyRotation", "Encrypt": "kms:Encrypt", "GenerateDataKey": "kms:GenerateDataKey", "GenerateDataKeyPair": "kms:GenerateDataKeyPair", "GenerateDataKeyPairWithoutPlaintext": "kms:GenerateDataKeyPairWithoutPlaintext", "GenerateDataKeyWithoutPlaintext": "kms:GenerateDataKeyWithoutPlaintext", "GenerateRandom": "kms:GenerateRandom", "GetKeyPolicy": "kms:GetKeyPolicy", "GetKeyRotationStatus": "kms:GetKeyRotationStatus", "GetParametersForImport": "kms:GetParametersForImport", "GetPublicKey": "kms:GetPublicKey", "ImportKeyMaterial": "kms:ImportKeyMaterial", "ListAliases": "kms:ListAliases", "ListGrants": "kms:ListGrants", "ListKeyPolicies": "kms:ListKeyPolicies", "ListKeys": "kms:ListKeys", "ListResourceTags": "kms:ListResourceTags", "ListRetirableGrants": "kms:ListRetirableGrants", "PutKeyPolicy": "kms:PutKeyPolicy", "ReplicateKey": "kms:ReplicateKey", "RetireGrant": "kms:RetireGrant", "RevokeGrant": "kms:RevokeGrant", "ScheduleKeyDeletion": "kms:ScheduleKeyDeletion", "Sign": "kms:Sign", "TagResource": "kms:TagResource", "UntagResource": "kms:UntagResource", "UpdateAlias": "kms:UpdateAlias", "UpdateCustomKeyStore": "kms:UpdateCustomKeyStore", "UpdateKeyDescription": "kms:UpdateKeyDescription", "UpdatePrimaryRegion": "kms:UpdatePrimaryRegion", "Verify": "kms:Verify" }, "lakeformation": { "AddLFTagsToResource": "lakeformation:AddLFTagsToResource", "BatchGrantPermissions": "lakeformation:BatchGrantPermissions", "BatchRevokePermissions": "lakeformation:BatchRevokePermissions", "CancelTransaction": "lakeformation:CancelTransaction", "CommitTransaction": "lakeformation:CommitTransaction", "CreateDataCellsFilter": "lakeformation:CreateDataCellsFilter", "CreateLFTag": "lakeformation:CreateLFTag", "DeleteDataCellsFilter": "lakeformation:DeleteDataCellsFilter", "DeleteLFTag": "lakeformation:DeleteLFTag", "DeleteObjectsOnCancel": "lakeformation:DeleteObjectsOnCancel", "DeregisterResource": "lakeformation:DeregisterResource", "DescribeResource": "lakeformation:DescribeResource", "DescribeTransaction": "lakeformation:DescribeTransaction", "ExtendTransaction": "lakeformation:ExtendTransaction", "GetDataLakeSettings": "lakeformation:GetDataLakeSettings", "GetEffectivePermissionsForPath": "lakeformation:GetEffectivePermissionsForPath", "GetLFTag": "lakeformation:GetLFTag", "GetQueryState": "lakeformation:GetQueryState", "GetQueryStatistics": "lakeformation:GetQueryStatistics", "GetResourceLFTags": "lakeformation:GetResourceLFTags", "GetTableObjects": "lakeformation:GetTableObjects", "GetWorkUnitResults": "lakeformation:GetWorkUnitResults", "GetWorkUnits": "lakeformation:GetWorkUnits", "GrantPermissions": "lakeformation:GrantPermissions", "ListDataCellsFilter": "lakeformation:ListDataCellsFilter", "ListLFTags": "lakeformation:ListLFTags", "ListPermissions": "lakeformation:ListPermissions", "ListResources": "lakeformation:ListResources", "ListTableStorageOptimizers": "lakeformation:ListTableStorageOptimizers", "ListTransactions": "lakeformation:ListTransactions", "PutDataLakeSettings": "lakeformation:PutDataLakeSettings", "RegisterResource": "lakeformation:RegisterResource", "RemoveLFTagsFromResource": "lakeformation:RemoveLFTagsFromResource", "RevokePermissions": "lakeformation:RevokePermissions", "SearchDatabasesByLFTags": "lakeformation:SearchDatabasesByLFTags", "SearchTablesByLFTags": "lakeformation:SearchTablesByLFTags", "StartQueryPlanning": "lakeformation:StartQueryPlanning", "StartTransaction": "lakeformation:StartTransaction", "UpdateLFTag": "lakeformation:UpdateLFTag", "UpdateResource": "lakeformation:UpdateResource", "UpdateTableObjects": "lakeformation:UpdateTableObjects", "UpdateTableStorageOptimizer": "lakeformation:UpdateTableStorageOptimizer" }, "lambda": { "AddLayerVersionPermission": "lambda:AddLayerVersionPermission", "AddPermission": "lambda:AddPermission", "CreateAlias": "lambda:CreateAlias", "CreateCodeSigningConfig": "lambda:CreateCodeSigningConfig", "CreateEventSourceMapping": "lambda:CreateEventSourceMapping", "CreateFunction": "lambda:CreateFunction", "DeleteAlias": "lambda:DeleteAlias", "DeleteCodeSigningConfig": "lambda:DeleteCodeSigningConfig", "DeleteEventSourceMapping": "lambda:DeleteEventSourceMapping", "DeleteFunction": "lambda:DeleteFunction", "DeleteFunctionCodeSigningConfig": "lambda:DeleteFunctionCodeSigningConfig", "DeleteFunctionConcurrency": "lambda:DeleteFunctionConcurrency", "DeleteFunctionEventInvokeConfig": "lambda:DeleteFunctionEventInvokeConfig", "DeleteLayerVersion": "lambda:DeleteLayerVersion", "DeleteProvisionedConcurrencyConfig": "lambda:DeleteProvisionedConcurrencyConfig", "GetAccountSettings": "lambda:GetAccountSettings", "GetAlias": "lambda:GetAlias", "GetCodeSigningConfig": "lambda:GetCodeSigningConfig", "GetEventSourceMapping": "lambda:GetEventSourceMapping", "GetFunction": "lambda:GetFunction", "GetFunctionCodeSigningConfig": "lambda:GetFunctionCodeSigningConfig", "GetFunctionConcurrency": "lambda:GetFunctionConcurrency", "GetFunctionConfiguration": "lambda:GetFunctionConfiguration", "GetFunctionEventInvokeConfig": "lambda:GetFunctionEventInvokeConfig", "GetLayerVersion": "lambda:GetLayerVersion", "GetLayerVersionPolicy": "lambda:GetLayerVersionPolicy", "GetPolicy": "lambda:GetPolicy", "GetProvisionedConcurrencyConfig": "lambda:GetProvisionedConcurrencyConfig", "InvokeAsync": "lambda:InvokeAsync", "ListAliases": "lambda:ListAliases", "ListCodeSigningConfigs": "lambda:ListCodeSigningConfigs", "ListEventSourceMappings": "lambda:ListEventSourceMappings", "ListFunctionEventInvokeConfigs": "lambda:ListFunctionEventInvokeConfigs", "ListFunctions": "lambda:ListFunctions", "ListFunctionsByCodeSigningConfig": "lambda:ListFunctionsByCodeSigningConfig", "ListLayerVersions": "lambda:ListLayerVersions", "ListLayers": "lambda:ListLayers", "ListProvisionedConcurrencyConfigs": "lambda:ListProvisionedConcurrencyConfigs", "ListTags": "lambda:ListTags", "ListVersionsByFunction": "lambda:ListVersionsByFunction", "PublishLayerVersion": "lambda:PublishLayerVersion", "PublishVersion": "lambda:PublishVersion", "PutFunctionCodeSigningConfig": "lambda:PutFunctionCodeSigningConfig", "PutFunctionConcurrency": "lambda:PutFunctionConcurrency", "PutFunctionEventInvokeConfig": "lambda:PutFunctionEventInvokeConfig", "PutProvisionedConcurrencyConfig": "lambda:PutProvisionedConcurrencyConfig", "RemoveLayerVersionPermission": "lambda:RemoveLayerVersionPermission", "RemovePermission": "lambda:RemovePermission", "TagResource": "lambda:TagResource", "UntagResource": "lambda:UntagResource", "UpdateAlias": "lambda:UpdateAlias", "UpdateCodeSigningConfig": "lambda:UpdateCodeSigningConfig", "UpdateEventSourceMapping": "lambda:UpdateEventSourceMapping", "UpdateFunctionCode": "lambda:UpdateFunctionCode", "UpdateFunctionConfiguration": "lambda:UpdateFunctionConfiguration", "UpdateFunctionEventInvokeConfig": "lambda:UpdateFunctionEventInvokeConfig" }, "lex-models": { "CreateBotVersion": "lex:CreateBotVersion", "CreateIntentVersion": "lex:CreateIntentVersion", "CreateSlotTypeVersion": "lex:CreateSlotTypeVersion", "DeleteBot": "lex:DeleteBot", "DeleteBotAlias": "lex:DeleteBotAlias", "DeleteBotChannelAssociation": "lex:DeleteBotChannelAssociation", "DeleteBotVersion": "lex:DeleteBotVersion", "DeleteIntent": "lex:DeleteIntent", "DeleteIntentVersion": "lex:DeleteIntentVersion", "DeleteSlotType": "lex:DeleteSlotType", "DeleteSlotTypeVersion": "lex:DeleteSlotTypeVersion", "DeleteUtterances": "lex:DeleteUtterances", "GetBot": "lex:GetBot", "GetBotAlias": "lex:GetBotAlias", "GetBotAliases": "lex:GetBotAliases", "GetBotChannelAssociation": "lex:GetBotChannelAssociation", "GetBotChannelAssociations": "lex:GetBotChannelAssociations", "GetBotVersions": "lex:GetBotVersions", "GetBots": "lex:GetBots", "GetBuiltinIntent": "lex:GetBuiltinIntent", "GetBuiltinIntents": "lex:GetBuiltinIntents", "GetBuiltinSlotTypes": "lex:GetBuiltinSlotTypes", "GetExport": "lex:GetExport", "GetImport": "lex:GetImport", "GetIntent": "lex:GetIntent", "GetIntentVersions": "lex:GetIntentVersions", "GetIntents": "lex:GetIntents", "GetMigration": "lex:GetMigration", "GetMigrations": "lex:GetMigrations", "GetSlotType": "lex:GetSlotType", "GetSlotTypeVersions": "lex:GetSlotTypeVersions", "GetSlotTypes": "lex:GetSlotTypes", "GetUtterancesView": "lex:GetUtterancesView", "ListTagsForResource": "lex:ListTagsForResource", "PutBot": "lex:PutBot", "PutBotAlias": "lex:PutBotAlias", "PutIntent": "lex:PutIntent", "PutSlotType": "lex:PutSlotType", "StartImport": "lex:StartImport", "StartMigration": "lex:StartMigration", "TagResource": "lex:TagResource", "UntagResource": "lex:UntagResource" }, "lex-runtime": { "DeleteSession": "lex:DeleteSession", "GetSession": "lex:GetSession", "PostContent": "lex:PostContent", "PostText": "lex:PostText", "PutSession": "lex:PutSession" }, "license-manager": { "AcceptGrant": "license-manager:AcceptGrant", "CheckInLicense": "license-manager:CheckInLicense", "CheckoutBorrowLicense": "license-manager:CheckoutBorrowLicense", "CheckoutLicense": "license-manager:CheckoutLicense", "CreateGrant": "license-manager:CreateGrant", "CreateGrantVersion": "license-manager:CreateGrantVersion", "CreateLicense": "license-manager:CreateLicense", "CreateLicenseConfiguration": "license-manager:CreateLicenseConfiguration", "CreateLicenseConversionTaskForResource": "license-manager:CreateLicenseConversionTaskForResource", "CreateLicenseManagerReportGenerator": "license-manager:CreateLicenseManagerReportGenerator", "CreateLicenseVersion": "license-manager:CreateLicenseVersion", "CreateToken": "license-manager:CreateToken", "DeleteGrant": "license-manager:DeleteGrant", "DeleteLicense": "license-manager:DeleteLicense", "DeleteLicenseConfiguration": "license-manager:DeleteLicenseConfiguration", "DeleteLicenseManagerReportGenerator": "license-manager:DeleteLicenseManagerReportGenerator", "DeleteToken": "license-manager:DeleteToken", "ExtendLicenseConsumption": "license-manager:ExtendLicenseConsumption", "GetAccessToken": "license-manager:GetAccessToken", "GetGrant": "license-manager:GetGrant", "GetLicense": "license-manager:GetLicense", "GetLicenseConfiguration": "license-manager:GetLicenseConfiguration", "GetLicenseConversionTask": "license-manager:GetLicenseConversionTask", "GetLicenseManagerReportGenerator": "license-manager:GetLicenseManagerReportGenerator", "GetLicenseUsage": "license-manager:GetLicenseUsage", "GetServiceSettings": "license-manager:GetServiceSettings", "ListAssociationsForLicenseConfiguration": "license-manager:ListAssociationsForLicenseConfiguration", "ListDistributedGrants": "license-manager:ListDistributedGrants", "ListFailuresForLicenseConfigurationOperations": "license-manager:ListFailuresForLicenseConfigurationOperations", "ListLicenseConfigurations": "license-manager:ListLicenseConfigurations", "ListLicenseConversionTasks": "license-manager:ListLicenseConversionTasks", "ListLicenseManagerReportGenerators": "license-manager:ListLicenseManagerReportGenerators", "ListLicenseSpecificationsForResource": "license-manager:ListLicenseSpecificationsForResource", "ListLicenseVersions": "license-manager:ListLicenseVersions", "ListLicenses": "license-manager:ListLicenses", "ListReceivedGrants": "license-manager:ListReceivedGrants", "ListReceivedLicenses": "license-manager:ListReceivedLicenses", "ListResourceInventory": "license-manager:ListResourceInventory", "ListTagsForResource": "license-manager:ListTagsForResource", "ListTokens": "license-manager:ListTokens", "ListUsageForLicenseConfiguration": "license-manager:ListUsageForLicenseConfiguration", "RejectGrant": "license-manager:RejectGrant", "TagResource": "license-manager:TagResource", "UntagResource": "license-manager:UntagResource", "UpdateLicenseConfiguration": "license-manager:UpdateLicenseConfiguration", "UpdateLicenseManagerReportGenerator": "license-manager:UpdateLicenseManagerReportGenerator", "UpdateLicenseSpecificationsForResource": "license-manager:UpdateLicenseSpecificationsForResource", "UpdateServiceSettings": "license-manager:UpdateServiceSettings" }, "lightsail": { "AllocateStaticIp": "lightsail:AllocateStaticIp", "AttachCertificateToDistribution": "lightsail:AttachCertificateToDistribution", "AttachDisk": "lightsail:AttachDisk", "AttachInstancesToLoadBalancer": "lightsail:AttachInstancesToLoadBalancer", "AttachLoadBalancerTlsCertificate": "lightsail:AttachLoadBalancerTlsCertificate", "AttachStaticIp": "lightsail:AttachStaticIp", "CloseInstancePublicPorts": "lightsail:CloseInstancePublicPorts", "CopySnapshot": "lightsail:CopySnapshot", "CreateBucket": "lightsail:CreateBucket", "CreateBucketAccessKey": "lightsail:CreateBucketAccessKey", "CreateCertificate": "lightsail:CreateCertificate", "CreateCloudFormationStack": "lightsail:CreateCloudFormationStack", "CreateContactMethod": "lightsail:CreateContactMethod", "CreateContainerService": "lightsail:CreateContainerService", "CreateContainerServiceDeployment": "lightsail:CreateContainerServiceDeployment", "CreateContainerServiceRegistryLogin": "lightsail:CreateContainerServiceRegistryLogin", "CreateDisk": "lightsail:CreateDisk", "CreateDiskFromSnapshot": "lightsail:CreateDiskFromSnapshot", "CreateDiskSnapshot": "lightsail:CreateDiskSnapshot", "CreateDistribution": "lightsail:CreateDistribution", "CreateDomain": "lightsail:CreateDomain", "CreateDomainEntry": "lightsail:CreateDomainEntry", "CreateInstanceSnapshot": "lightsail:CreateInstanceSnapshot", "CreateInstances": "lightsail:CreateInstances", "CreateInstancesFromSnapshot": "lightsail:CreateInstancesFromSnapshot", "CreateKeyPair": "lightsail:CreateKeyPair", "CreateLoadBalancer": "lightsail:CreateLoadBalancer", "CreateLoadBalancerTlsCertificate": "lightsail:CreateLoadBalancerTlsCertificate", "CreateRelationalDatabase": "lightsail:CreateRelationalDatabase", "CreateRelationalDatabaseFromSnapshot": "lightsail:CreateRelationalDatabaseFromSnapshot", "CreateRelationalDatabaseSnapshot": "lightsail:CreateRelationalDatabaseSnapshot", "DeleteAlarm": "lightsail:DeleteAlarm", "DeleteAutoSnapshot": "lightsail:DeleteAutoSnapshot", "DeleteBucket": "lightsail:DeleteBucket", "DeleteBucketAccessKey": "lightsail:DeleteBucketAccessKey", "DeleteCertificate": "lightsail:DeleteCertificate", "DeleteContactMethod": "lightsail:DeleteContactMethod", "DeleteContainerImage": "lightsail:DeleteContainerImage", "DeleteContainerService": "lightsail:DeleteContainerService", "DeleteDisk": "lightsail:DeleteDisk", "DeleteDiskSnapshot": "lightsail:DeleteDiskSnapshot", "DeleteDistribution": "lightsail:DeleteDistribution", "DeleteDomain": "lightsail:DeleteDomain", "DeleteDomainEntry": "lightsail:DeleteDomainEntry", "DeleteInstance": "lightsail:DeleteInstance", "DeleteInstanceSnapshot": "lightsail:DeleteInstanceSnapshot", "DeleteKeyPair": "lightsail:DeleteKeyPair", "DeleteKnownHostKeys": "lightsail:DeleteKnownHostKeys", "DeleteLoadBalancer": "lightsail:DeleteLoadBalancer", "DeleteLoadBalancerTlsCertificate": "lightsail:DeleteLoadBalancerTlsCertificate", "DeleteRelationalDatabase": "lightsail:DeleteRelationalDatabase", "DeleteRelationalDatabaseSnapshot": "lightsail:DeleteRelationalDatabaseSnapshot", "DetachCertificateFromDistribution": "lightsail:DetachCertificateFromDistribution", "DetachDisk": "lightsail:DetachDisk", "DetachInstancesFromLoadBalancer": "lightsail:DetachInstancesFromLoadBalancer", "DetachStaticIp": "lightsail:DetachStaticIp", "DisableAddOn": "lightsail:DisableAddOn", "DownloadDefaultKeyPair": "lightsail:DownloadDefaultKeyPair", "EnableAddOn": "lightsail:EnableAddOn", "ExportSnapshot": "lightsail:ExportSnapshot", "GetActiveNames": "lightsail:GetActiveNames", "GetAlarms": "lightsail:GetAlarms", "GetAutoSnapshots": "lightsail:GetAutoSnapshots", "GetBlueprints": "lightsail:GetBlueprints", "GetBucketAccessKeys": "lightsail:GetBucketAccessKeys", "GetBucketBundles": "lightsail:GetBucketBundles", "GetBucketMetricData": "lightsail:GetBucketMetricData", "GetBuckets": "lightsail:GetBuckets", "GetBundles": "lightsail:GetBundles", "GetCertificates": "lightsail:GetCertificates", "GetCloudFormationStackRecords": "lightsail:GetCloudFormationStackRecords", "GetContactMethods": "lightsail:GetContactMethods", "GetContainerAPIMetadata": "lightsail:GetContainerAPIMetadata", "GetContainerImages": "lightsail:GetContainerImages", "GetContainerLog": "lightsail:GetContainerLog", "GetContainerServiceDeployments": "lightsail:GetContainerServiceDeployments", "GetContainerServiceMetricData": "lightsail:GetContainerServiceMetricData", "GetContainerServicePowers": "lightsail:GetContainerServicePowers", "GetContainerServices": "lightsail:GetContainerServices", "GetDisk": "lightsail:GetDisk", "GetDiskSnapshot": "lightsail:GetDiskSnapshot", "GetDiskSnapshots": "lightsail:GetDiskSnapshots", "GetDisks": "lightsail:GetDisks", "GetDistributionBundles": "lightsail:GetDistributionBundles", "GetDistributionLatestCacheReset": "lightsail:GetDistributionLatestCacheReset", "GetDistributionMetricData": "lightsail:GetDistributionMetricData", "GetDistributions": "lightsail:GetDistributions", "GetDomain": "lightsail:GetDomain", "GetDomains": "lightsail:GetDomains", "GetExportSnapshotRecords": "lightsail:GetExportSnapshotRecords", "GetInstance": "lightsail:GetInstance", "GetInstanceAccessDetails": "lightsail:GetInstanceAccessDetails", "GetInstanceMetricData": "lightsail:GetInstanceMetricData", "GetInstancePortStates": "lightsail:GetInstancePortStates", "GetInstanceSnapshot": "lightsail:GetInstanceSnapshot", "GetInstanceSnapshots": "lightsail:GetInstanceSnapshots", "GetInstanceState": "lightsail:GetInstanceState", "GetInstances": "lightsail:GetInstances", "GetKeyPair": "lightsail:GetKeyPair", "GetKeyPairs": "lightsail:GetKeyPairs", "GetLoadBalancer": "lightsail:GetLoadBalancer", "GetLoadBalancerMetricData": "lightsail:GetLoadBalancerMetricData", "GetLoadBalancerTlsCertificates": "lightsail:GetLoadBalancerTlsCertificates", "GetLoadBalancers": "lightsail:GetLoadBalancers", "GetOperation": "lightsail:GetOperation", "GetOperations": "lightsail:GetOperations", "GetOperationsForResource": "lightsail:GetOperationsForResource", "GetRegions": "lightsail:GetRegions", "GetRelationalDatabase": "lightsail:GetRelationalDatabase", "GetRelationalDatabaseBlueprints": "lightsail:GetRelationalDatabaseBlueprints", "GetRelationalDatabaseBundles": "lightsail:GetRelationalDatabaseBundles", "GetRelationalDatabaseEvents": "lightsail:GetRelationalDatabaseEvents", "GetRelationalDatabaseLogEvents": "lightsail:GetRelationalDatabaseLogEvents", "GetRelationalDatabaseLogStreams": "lightsail:GetRelationalDatabaseLogStreams", "GetRelationalDatabaseMasterUserPassword": "lightsail:GetRelationalDatabaseMasterUserPassword", "GetRelationalDatabaseMetricData": "lightsail:GetRelationalDatabaseMetricData", "GetRelationalDatabaseParameters": "lightsail:GetRelationalDatabaseParameters", "GetRelationalDatabaseSnapshot": "lightsail:GetRelationalDatabaseSnapshot", "GetRelationalDatabaseSnapshots": "lightsail:GetRelationalDatabaseSnapshots", "GetRelationalDatabases": "lightsail:GetRelationalDatabases", "GetStaticIp": "lightsail:GetStaticIp", "GetStaticIps": "lightsail:GetStaticIps", "ImportKeyPair": "lightsail:ImportKeyPair", "IsVpcPeered": "lightsail:IsVpcPeered", "OpenInstancePublicPorts": "lightsail:OpenInstancePublicPorts", "PeerVpc": "lightsail:PeerVpc", "PutAlarm": "lightsail:PutAlarm", "PutInstancePublicPorts": "lightsail:PutInstancePublicPorts", "RebootInstance": "lightsail:RebootInstance", "RebootRelationalDatabase": "lightsail:RebootRelationalDatabase", "RegisterContainerImage": "lightsail:RegisterContainerImage", "ReleaseStaticIp": "lightsail:ReleaseStaticIp", "ResetDistributionCache": "lightsail:ResetDistributionCache", "SendContactMethodVerification": "lightsail:SendContactMethodVerification", "SetIpAddressType": "lightsail:SetIpAddressType", "SetResourceAccessForBucket": "lightsail:SetResourceAccessForBucket", "StartInstance": "lightsail:StartInstance", "StartRelationalDatabase": "lightsail:StartRelationalDatabase", "StopInstance": "lightsail:StopInstance", "StopRelationalDatabase": "lightsail:StopRelationalDatabase", "TagResource": "lightsail:TagResource", "TestAlarm": "lightsail:TestAlarm", "UnpeerVpc": "lightsail:UnpeerVpc", "UntagResource": "lightsail:UntagResource", "UpdateBucket": "lightsail:UpdateBucket", "UpdateBucketBundle": "lightsail:UpdateBucketBundle", "UpdateContainerService": "lightsail:UpdateContainerService", "UpdateDistribution": "lightsail:UpdateDistribution", "UpdateDistributionBundle": "lightsail:UpdateDistributionBundle", "UpdateDomainEntry": "lightsail:UpdateDomainEntry", "UpdateLoadBalancerAttribute": "lightsail:UpdateLoadBalancerAttribute", "UpdateRelationalDatabase": "lightsail:UpdateRelationalDatabase", "UpdateRelationalDatabaseParameters": "lightsail:UpdateRelationalDatabaseParameters" }, "logs": { "AssociateKmsKey": "logs:AssociateKmsKey", "CancelExportTask": "logs:CancelExportTask", "CreateExportTask": "logs:CreateExportTask", "CreateLogGroup": "logs:CreateLogGroup", "CreateLogStream": "logs:CreateLogStream", "DeleteDestination": "logs:DeleteDestination", "DeleteLogGroup": "logs:DeleteLogGroup", "DeleteLogStream": "logs:DeleteLogStream", "DeleteMetricFilter": "logs:DeleteMetricFilter", "DeleteQueryDefinition": "logs:DeleteQueryDefinition", "DeleteResourcePolicy": "logs:DeleteResourcePolicy", "DeleteRetentionPolicy": "logs:DeleteRetentionPolicy", "DeleteSubscriptionFilter": "logs:DeleteSubscriptionFilter", "DescribeDestinations": "logs:DescribeDestinations", "DescribeExportTasks": "logs:DescribeExportTasks", "DescribeLogGroups": "logs:DescribeLogGroups", "DescribeLogStreams": "logs:DescribeLogStreams", "DescribeMetricFilters": "logs:DescribeMetricFilters", "DescribeQueries": "logs:DescribeQueries", "DescribeQueryDefinitions": "logs:DescribeQueryDefinitions", "DescribeResourcePolicies": "logs:DescribeResourcePolicies", "DescribeSubscriptionFilters": "logs:DescribeSubscriptionFilters", "DisassociateKmsKey": "logs:DisassociateKmsKey", "FilterLogEvents": "logs:FilterLogEvents", "GetLogEvents": "logs:GetLogEvents", "GetLogGroupFields": "logs:GetLogGroupFields", "GetLogRecord": "logs:GetLogRecord", "GetQueryResults": "logs:GetQueryResults", "ListTagsLogGroup": "logs:ListTagsLogGroup", "PutDestination": "logs:PutDestination", "PutDestinationPolicy": "logs:PutDestinationPolicy", "PutLogEvents": "logs:PutLogEvents", "PutMetricFilter": "logs:PutMetricFilter", "PutQueryDefinition": "logs:PutQueryDefinition", "PutResourcePolicy": "logs:PutResourcePolicy", "PutRetentionPolicy": "logs:PutRetentionPolicy", "PutSubscriptionFilter": "logs:PutSubscriptionFilter", "StartQuery": "logs:StartQuery", "StopQuery": "logs:StopQuery", "TagLogGroup": "logs:TagLogGroup", "TestMetricFilter": "logs:TestMetricFilter", "UntagLogGroup": "logs:UntagLogGroup" }, "lookoutequipment": { "CreateDataset": "lookoutequipment:CreateDataset", "CreateInferenceScheduler": "lookoutequipment:CreateInferenceScheduler", "CreateModel": "lookoutequipment:CreateModel", "DeleteDataset": "lookoutequipment:DeleteDataset", "DeleteInferenceScheduler": "lookoutequipment:DeleteInferenceScheduler", "DeleteModel": "lookoutequipment:DeleteModel", "DescribeDataIngestionJob": "lookoutequipment:DescribeDataIngestionJob", "DescribeDataset": "lookoutequipment:DescribeDataset", "DescribeInferenceScheduler": "lookoutequipment:DescribeInferenceScheduler", "DescribeModel": "lookoutequipment:DescribeModel", "ListDataIngestionJobs": "lookoutequipment:ListDataIngestionJobs", "ListDatasets": "lookoutequipment:ListDatasets", "ListInferenceExecutions": "lookoutequipment:ListInferenceExecutions", "ListInferenceSchedulers": "lookoutequipment:ListInferenceSchedulers", "ListModels": "lookoutequipment:ListModels", "ListTagsForResource": "lookoutequipment:ListTagsForResource", "StartDataIngestionJob": "lookoutequipment:StartDataIngestionJob", "StartInferenceScheduler": "lookoutequipment:StartInferenceScheduler", "StopInferenceScheduler": "lookoutequipment:StopInferenceScheduler", "TagResource": "lookoutequipment:TagResource", "UntagResource": "lookoutequipment:UntagResource", "UpdateInferenceScheduler": "lookoutequipment:UpdateInferenceScheduler" }, "lookoutmetrics": { "ActivateAnomalyDetector": "lookoutmetrics:ActivateAnomalyDetector", "BackTestAnomalyDetector": "lookoutmetrics:BackTestAnomalyDetector", "CreateAlert": "lookoutmetrics:CreateAlert", "CreateAnomalyDetector": "lookoutmetrics:CreateAnomalyDetector", "CreateMetricSet": "lookoutmetrics:CreateMetricSet", "DeleteAlert": "lookoutmetrics:DeleteAlert", "DeleteAnomalyDetector": "lookoutmetrics:DeleteAnomalyDetector", "DescribeAlert": "lookoutmetrics:DescribeAlert", "DescribeAnomalyDetectionExecutions": "lookoutmetrics:DescribeAnomalyDetectionExecutions", "DescribeAnomalyDetector": "lookoutmetrics:DescribeAnomalyDetector", "DescribeMetricSet": "lookoutmetrics:DescribeMetricSet", "GetAnomalyGroup": "lookoutmetrics:GetAnomalyGroup", "GetFeedback": "lookoutmetrics:GetFeedback", "GetSampleData": "lookoutmetrics:GetSampleData", "ListAlerts": "lookoutmetrics:ListAlerts", "ListAnomalyDetectors": "lookoutmetrics:ListAnomalyDetectors", "ListAnomalyGroupRelatedMetrics": "lookoutmetrics:ListAnomalyGroupRelatedMetrics", "ListAnomalyGroupSummaries": "lookoutmetrics:ListAnomalyGroupSummaries", "ListAnomalyGroupTimeSeries": "lookoutmetrics:ListAnomalyGroupTimeSeries", "ListMetricSets": "lookoutmetrics:ListMetricSets", "ListTagsForResource": "lookoutmetrics:ListTagsForResource", "PutFeedback": "lookoutmetrics:PutFeedback", "TagResource": "lookoutmetrics:TagResource", "UntagResource": "lookoutmetrics:UntagResource", "UpdateAnomalyDetector": "lookoutmetrics:UpdateAnomalyDetector", "UpdateMetricSet": "lookoutmetrics:UpdateMetricSet" }, "lookoutvision": { "CreateDataset": "lookoutvision:CreateDataset", "CreateModel": "lookoutvision:CreateModel", "CreateProject": "lookoutvision:CreateProject", "DeleteDataset": "lookoutvision:DeleteDataset", "DeleteModel": "lookoutvision:DeleteModel", "DeleteProject": "lookoutvision:DeleteProject", "DescribeDataset": "lookoutvision:DescribeDataset", "DescribeModel": "lookoutvision:DescribeModel", "DescribeModelPackagingJob": "lookoutvision:DescribeModelPackagingJob", "DescribeProject": "lookoutvision:DescribeProject", "DetectAnomalies": "lookoutvision:DetectAnomalies", "ListDatasetEntries": "lookoutvision:ListDatasetEntries", "ListModelPackagingJobs": "lookoutvision:ListModelPackagingJobs", "ListModels": "lookoutvision:ListModels", "ListProjects": "lookoutvision:ListProjects", "ListTagsForResource": "lookoutvision:ListTagsForResource", "StartModel": "lookoutvision:StartModel", "StartModelPackagingJob": "lookoutvision:StartModelPackagingJob", "StopModel": "lookoutvision:StopModel", "TagResource": "lookoutvision:TagResource", "UntagResource": "lookoutvision:UntagResource", "UpdateDatasetEntries": "lookoutvision:UpdateDatasetEntries" }, "machinelearning": { "AddTags": "machinelearning:AddTags", "CreateBatchPrediction": "machinelearning:CreateBatchPrediction", "CreateDataSourceFromRDS": "machinelearning:CreateDataSourceFromRDS", "CreateDataSourceFromRedshift": "machinelearning:CreateDataSourceFromRedshift", "CreateDataSourceFromS3": "machinelearning:CreateDataSourceFromS3", "CreateEvaluation": "machinelearning:CreateEvaluation", "CreateMLModel": "machinelearning:CreateMLModel", "CreateRealtimeEndpoint": "machinelearning:CreateRealtimeEndpoint", "DeleteBatchPrediction": "machinelearning:DeleteBatchPrediction", "DeleteDataSource": "machinelearning:DeleteDataSource", "DeleteEvaluation": "machinelearning:DeleteEvaluation", "DeleteMLModel": "machinelearning:DeleteMLModel", "DeleteRealtimeEndpoint": "machinelearning:DeleteRealtimeEndpoint", "DeleteTags": "machinelearning:DeleteTags", "DescribeBatchPredictions": "machinelearning:DescribeBatchPredictions", "DescribeDataSources": "machinelearning:DescribeDataSources", "DescribeEvaluations": "machinelearning:DescribeEvaluations", "DescribeMLModels": "machinelearning:DescribeMLModels", "DescribeTags": "machinelearning:DescribeTags", "GetBatchPrediction": "machinelearning:GetBatchPrediction", "GetDataSource": "machinelearning:GetDataSource", "GetEvaluation": "machinelearning:GetEvaluation", "GetMLModel": "machinelearning:GetMLModel", "Predict": "machinelearning:Predict", "UpdateBatchPrediction": "machinelearning:UpdateBatchPrediction", "UpdateDataSource": "machinelearning:UpdateDataSource", "UpdateEvaluation": "machinelearning:UpdateEvaluation", "UpdateMLModel": "machinelearning:UpdateMLModel" }, "macie": { "AssociateMemberAccount": "macie:AssociateMemberAccount", "AssociateS3Resources": "macie:AssociateS3Resources", "DisassociateMemberAccount": "macie:DisassociateMemberAccount", "DisassociateS3Resources": "macie:DisassociateS3Resources", "ListMemberAccounts": "macie:ListMemberAccounts", "ListS3Resources": "macie:ListS3Resources", "UpdateS3Resources": "macie:UpdateS3Resources" }, "macie2": { "AcceptInvitation": "macie2:AcceptInvitation", "BatchGetCustomDataIdentifiers": "macie2:BatchGetCustomDataIdentifiers", "CreateClassificationJob": "macie2:CreateClassificationJob", "CreateCustomDataIdentifier": "macie2:CreateCustomDataIdentifier", "CreateFindingsFilter": "macie2:CreateFindingsFilter", "CreateInvitations": "macie2:CreateInvitations", "CreateMember": "macie2:CreateMember", "CreateSampleFindings": "macie2:CreateSampleFindings", "DeclineInvitations": "macie2:DeclineInvitations", "DeleteCustomDataIdentifier": "macie2:DeleteCustomDataIdentifier", "DeleteFindingsFilter": "macie2:DeleteFindingsFilter", "DeleteInvitations": "macie2:DeleteInvitations", "DeleteMember": "macie2:DeleteMember", "DescribeBuckets": "macie2:DescribeBuckets", "DescribeClassificationJob": "macie2:DescribeClassificationJob", "DescribeOrganizationConfiguration": "macie2:DescribeOrganizationConfiguration", "DisableMacie": "macie2:DisableMacie", "DisableOrganizationAdminAccount": "macie2:DisableOrganizationAdminAccount", "DisassociateFromAdministratorAccount": "macie2:DisassociateFromAdministratorAccount", "DisassociateFromMasterAccount": "macie2:DisassociateFromMasterAccount", "DisassociateMember": "macie2:DisassociateMember", "EnableMacie": "macie2:EnableMacie", "EnableOrganizationAdminAccount": "macie2:EnableOrganizationAdminAccount", "GetAdministratorAccount": "macie2:GetAdministratorAccount", "GetBucketStatistics": "macie2:GetBucketStatistics", "GetClassificationExportConfiguration": "macie2:GetClassificationExportConfiguration", "GetCustomDataIdentifier": "macie2:GetCustomDataIdentifier", "GetFindingStatistics": "macie2:GetFindingStatistics", "GetFindings": "macie2:GetFindings", "GetFindingsFilter": "macie2:GetFindingsFilter", "GetFindingsPublicationConfiguration": "macie2:GetFindingsPublicationConfiguration", "GetInvitationsCount": "macie2:GetInvitationsCount", "GetMacieSession": "macie2:GetMacieSession", "GetMasterAccount": "macie2:GetMasterAccount", "GetMember": "macie2:GetMember", "GetUsageStatistics": "macie2:GetUsageStatistics", "GetUsageTotals": "macie2:GetUsageTotals", "ListClassificationJobs": "macie2:ListClassificationJobs", "ListCustomDataIdentifiers": "macie2:ListCustomDataIdentifiers", "ListFindings": "macie2:ListFindings", "ListFindingsFilters": "macie2:ListFindingsFilters", "ListInvitations": "macie2:ListInvitations", "ListManagedDataIdentifiers": "macie2:ListManagedDataIdentifiers", "ListMembers": "macie2:ListMembers", "ListOrganizationAdminAccounts": "macie2:ListOrganizationAdminAccounts", "ListTagsForResource": "macie2:ListTagsForResource", "PutClassificationExportConfiguration": "macie2:PutClassificationExportConfiguration", "PutFindingsPublicationConfiguration": "macie2:PutFindingsPublicationConfiguration", "SearchResources": "macie2:SearchResources", "TagResource": "macie2:TagResource", "TestCustomDataIdentifier": "macie2:TestCustomDataIdentifier", "UntagResource": "macie2:UntagResource", "UpdateClassificationJob": "macie2:UpdateClassificationJob", "UpdateFindingsFilter": "macie2:UpdateFindingsFilter", "UpdateMacieSession": "macie2:UpdateMacieSession", "UpdateMemberSession": "macie2:UpdateMemberSession", "UpdateOrganizationConfiguration": "macie2:UpdateOrganizationConfiguration" }, "managedblockchain": { "CreateMember": "managedblockchain:CreateMember", "CreateNetwork": "managedblockchain:CreateNetwork", "CreateNode": "managedblockchain:CreateNode", "CreateProposal": "managedblockchain:CreateProposal", "DeleteMember": "managedblockchain:DeleteMember", "DeleteNode": "managedblockchain:DeleteNode", "GetMember": "managedblockchain:GetMember", "GetNetwork": "managedblockchain:GetNetwork", "GetNode": "managedblockchain:GetNode", "GetProposal": "managedblockchain:GetProposal", "ListInvitations": "managedblockchain:ListInvitations", "ListMembers": "managedblockchain:ListMembers", "ListNetworks": "managedblockchain:ListNetworks", "ListNodes": "managedblockchain:ListNodes", "ListProposalVotes": "managedblockchain:ListProposalVotes", "ListProposals": "managedblockchain:ListProposals", "ListTagsForResource": "managedblockchain:ListTagsForResource", "RejectInvitation": "managedblockchain:RejectInvitation", "TagResource": "managedblockchain:TagResource", "UntagResource": "managedblockchain:UntagResource", "UpdateMember": "managedblockchain:UpdateMember", "UpdateNode": "managedblockchain:UpdateNode", "VoteOnProposal": "managedblockchain:VoteOnProposal" }, "mediaconnect": { "AddFlowMediaStreams": "mediaconnect:AddFlowMediaStreams", "AddFlowOutputs": "mediaconnect:AddFlowOutputs", "AddFlowSources": "mediaconnect:AddFlowSources", "AddFlowVpcInterfaces": "mediaconnect:AddFlowVpcInterfaces", "CreateFlow": "mediaconnect:CreateFlow", "DeleteFlow": "mediaconnect:DeleteFlow", "DescribeFlow": "mediaconnect:DescribeFlow", "DescribeOffering": "mediaconnect:DescribeOffering", "DescribeReservation": "mediaconnect:DescribeReservation", "GrantFlowEntitlements": "mediaconnect:GrantFlowEntitlements", "ListEntitlements": "mediaconnect:ListEntitlements", "ListFlows": "mediaconnect:ListFlows", "ListOfferings": "mediaconnect:ListOfferings", "ListReservations": "mediaconnect:ListReservations", "ListTagsForResource": "mediaconnect:ListTagsForResource", "PurchaseOffering": "mediaconnect:PurchaseOffering", "RemoveFlowMediaStream": "mediaconnect:RemoveFlowMediaStream", "RemoveFlowOutput": "mediaconnect:RemoveFlowOutput", "RemoveFlowSource": "mediaconnect:RemoveFlowSource", "RemoveFlowVpcInterface": "mediaconnect:RemoveFlowVpcInterface", "RevokeFlowEntitlement": "mediaconnect:RevokeFlowEntitlement", "StartFlow": "mediaconnect:StartFlow", "StopFlow": "mediaconnect:StopFlow", "TagResource": "mediaconnect:TagResource", "UntagResource": "mediaconnect:UntagResource", "UpdateFlow": "mediaconnect:UpdateFlow", "UpdateFlowEntitlement": "mediaconnect:UpdateFlowEntitlement", "UpdateFlowMediaStream": "mediaconnect:UpdateFlowMediaStream", "UpdateFlowOutput": "mediaconnect:UpdateFlowOutput", "UpdateFlowSource": "mediaconnect:UpdateFlowSource" }, "mediaconvert": { "AssociateCertificate": "mediaconvert:AssociateCertificate", "CancelJob": "mediaconvert:CancelJob", "CreateJob": "mediaconvert:CreateJob", "CreateJobTemplate": "mediaconvert:CreateJobTemplate", "CreatePreset": "mediaconvert:CreatePreset", "CreateQueue": "mediaconvert:CreateQueue", "DeleteJobTemplate": "mediaconvert:DeleteJobTemplate", "DeletePolicy": "mediaconvert:DeletePolicy", "DeletePreset": "mediaconvert:DeletePreset", "DeleteQueue": "mediaconvert:DeleteQueue", "DescribeEndpoints": "mediaconvert:DescribeEndpoints", "DisassociateCertificate": "mediaconvert:DisassociateCertificate", "GetJob": "mediaconvert:GetJob", "GetJobTemplate": "mediaconvert:GetJobTemplate", "GetPolicy": "mediaconvert:GetPolicy", "GetPreset": "mediaconvert:GetPreset", "GetQueue": "mediaconvert:GetQueue", "ListJobTemplates": "mediaconvert:ListJobTemplates", "ListJobs": "mediaconvert:ListJobs", "ListPresets": "mediaconvert:ListPresets", "ListQueues": "mediaconvert:ListQueues", "ListTagsForResource": "mediaconvert:ListTagsForResource", "PutPolicy": "mediaconvert:PutPolicy", "TagResource": "mediaconvert:TagResource", "UntagResource": "mediaconvert:UntagResource", "UpdateJobTemplate": "mediaconvert:UpdateJobTemplate", "UpdatePreset": "mediaconvert:UpdatePreset", "UpdateQueue": "mediaconvert:UpdateQueue" }, "mediapackage-vod": { "ConfigureLogs": "mediapackage-vod:ConfigureLogs", "CreateAsset": "mediapackage-vod:CreateAsset", "CreatePackagingConfiguration": "mediapackage-vod:CreatePackagingConfiguration", "CreatePackagingGroup": "mediapackage-vod:CreatePackagingGroup", "DeleteAsset": "mediapackage-vod:DeleteAsset", "DeletePackagingConfiguration": "mediapackage-vod:DeletePackagingConfiguration", "DeletePackagingGroup": "mediapackage-vod:DeletePackagingGroup", "DescribeAsset": "mediapackage-vod:DescribeAsset", "DescribePackagingConfiguration": "mediapackage-vod:DescribePackagingConfiguration", "DescribePackagingGroup": "mediapackage-vod:DescribePackagingGroup", "ListAssets": "mediapackage-vod:ListAssets", "ListPackagingConfigurations": "mediapackage-vod:ListPackagingConfigurations", "ListPackagingGroups": "mediapackage-vod:ListPackagingGroups", "ListTagsForResource": "mediapackage-vod:ListTagsForResource", "TagResource": "mediapackage-vod:TagResource", "UntagResource": "mediapackage-vod:UntagResource", "UpdatePackagingGroup": "mediapackage-vod:UpdatePackagingGroup" }, "mediatailor": { "CreateChannel": "mediatailor:CreateChannel", "CreateProgram": "mediatailor:CreateProgram", "CreateSourceLocation": "mediatailor:CreateSourceLocation", "CreateVodSource": "mediatailor:CreateVodSource", "DeleteChannel": "mediatailor:DeleteChannel", "DeleteChannelPolicy": "mediatailor:DeleteChannelPolicy", "DeletePlaybackConfiguration": "mediatailor:DeletePlaybackConfiguration", "DeleteProgram": "mediatailor:DeleteProgram", "DeleteSourceLocation": "mediatailor:DeleteSourceLocation", "DeleteVodSource": "mediatailor:DeleteVodSource", "DescribeChannel": "mediatailor:DescribeChannel", "DescribeProgram": "mediatailor:DescribeProgram", "DescribeSourceLocation": "mediatailor:DescribeSourceLocation", "DescribeVodSource": "mediatailor:DescribeVodSource", "GetChannelPolicy": "mediatailor:GetChannelPolicy", "GetChannelSchedule": "mediatailor:GetChannelSchedule", "GetPlaybackConfiguration": "mediatailor:GetPlaybackConfiguration", "ListAlerts": "mediatailor:ListAlerts", "ListChannels": "mediatailor:ListChannels", "ListPlaybackConfigurations": "mediatailor:ListPlaybackConfigurations", "ListSourceLocations": "mediatailor:ListSourceLocations", "ListTagsForResource": "mediatailor:ListTagsForResource", "ListVodSources": "mediatailor:ListVodSources", "PutChannelPolicy": "mediatailor:PutChannelPolicy", "PutPlaybackConfiguration": "mediatailor:PutPlaybackConfiguration", "StartChannel": "mediatailor:StartChannel", "StopChannel": "mediatailor:StopChannel", "TagResource": "mediatailor:TagResource", "UntagResource": "mediatailor:UntagResource", "UpdateChannel": "mediatailor:UpdateChannel", "UpdateSourceLocation": "mediatailor:UpdateSourceLocation", "UpdateVodSource": "mediatailor:UpdateVodSource" }, "memorydb": { "CopySnapshot": "memorydb:CopySnapshot", "CreateCluster": "memorydb:CreateCluster", "CreateParameterGroup": "memorydb:CreateParameterGroup", "CreateSnapshot": "memorydb:CreateSnapshot", "CreateSubnetGroup": "memorydb:CreateSubnetGroup", "CreateUser": "memorydb:CreateUser", "DeleteCluster": "memorydb:DeleteCluster", "DeleteParameterGroup": "memorydb:DeleteParameterGroup", "DeleteSnapshot": "memorydb:DeleteSnapshot", "DeleteSubnetGroup": "memorydb:DeleteSubnetGroup", "DeleteUser": "memorydb:DeleteUser", "DescribeClusters": "memorydb:DescribeClusters", "DescribeEngineVersions": "memorydb:DescribeEngineVersions", "DescribeEvents": "memorydb:DescribeEvents", "DescribeParameterGroups": "memorydb:DescribeParameterGroups", "DescribeParameters": "memorydb:DescribeParameters", "DescribeServiceUpdates": "memorydb:DescribeServiceUpdates", "DescribeSnapshots": "memorydb:DescribeSnapshots", "DescribeSubnetGroups": "memorydb:DescribeSubnetGroups", "DescribeUsers": "memorydb:DescribeUsers", "FailoverShard": "memorydb:FailoverShard", "ListTags": "memorydb:ListTags", "ResetParameterGroup": "memorydb:ResetParameterGroup", "TagResource": "memorydb:TagResource", "UntagResource": "memorydb:UntagResource", "UpdateCluster": "memorydb:UpdateCluster", "UpdateParameterGroup": "memorydb:UpdateParameterGroup", "UpdateSubnetGroup": "memorydb:UpdateSubnetGroup", "UpdateUser": "memorydb:UpdateUser" }, "mgn": { "ChangeServerLifeCycleState": "mgn:ChangeServerLifeCycleState", "CreateReplicationConfigurationTemplate": "mgn:CreateReplicationConfigurationTemplate", "DeleteJob": "mgn:DeleteJob", "DeleteReplicationConfigurationTemplate": "mgn:DeleteReplicationConfigurationTemplate", "DeleteSourceServer": "mgn:DeleteSourceServer", "DeleteVcenterClient": "mgn:DeleteVcenterClient", "DescribeJobLogItems": "mgn:DescribeJobLogItems", "DescribeJobs": "mgn:DescribeJobs", "DescribeReplicationConfigurationTemplates": "mgn:DescribeReplicationConfigurationTemplates", "DescribeSourceServers": "mgn:DescribeSourceServers", "DescribeVcenterClients": "mgn:DescribeVcenterClients", "DisconnectFromService": "mgn:DisconnectFromService", "FinalizeCutover": "mgn:FinalizeCutover", "GetLaunchConfiguration": "mgn:GetLaunchConfiguration", "GetReplicationConfiguration": "mgn:GetReplicationConfiguration", "InitializeService": "mgn:InitializeService", "ListTagsForResource": "mgn:ListTagsForResource", "MarkAsArchived": "mgn:MarkAsArchived", "RetryDataReplication": "mgn:RetryDataReplication", "StartCutover": "mgn:StartCutover", "StartReplication": "mgn:StartReplication", "StartTest": "mgn:StartTest", "TagResource": "mgn:TagResource", "TerminateTargetInstances": "mgn:TerminateTargetInstances", "UntagResource": "mgn:UntagResource", "UpdateLaunchConfiguration": "mgn:UpdateLaunchConfiguration", "UpdateReplicationConfiguration": "mgn:UpdateReplicationConfiguration", "UpdateReplicationConfigurationTemplate": "mgn:UpdateReplicationConfigurationTemplate", "UpdateSourceServerReplicationType": "mgn:UpdateSourceServerReplicationType" }, "mobile": { "CreateProject": "mobilehub:CreateProject", "DeleteProject": "mobilehub:DeleteProject", "DescribeBundle": "mobilehub:DescribeBundle", "ExportBundle": "mobilehub:ExportBundle", "ExportProject": "mobilehub:ExportProject", "ListBundles": "mobilehub:ListBundles", "ListProjects": "mobilehub:ListProjects", "UpdateProject": "mobilehub:UpdateProject" }, "mq": { "CreateBroker": "mq:CreateBroker", "CreateConfiguration": "mq:CreateConfiguration", "CreateTags": "mq:CreateTags", "CreateUser": "mq:CreateUser", "DeleteBroker": "mq:DeleteBroker", "DeleteTags": "mq:DeleteTags", "DeleteUser": "mq:DeleteUser", "DescribeBroker": "mq:DescribeBroker", "DescribeBrokerEngineTypes": "mq:DescribeBrokerEngineTypes", "DescribeBrokerInstanceOptions": "mq:DescribeBrokerInstanceOptions", "DescribeConfiguration": "mq:DescribeConfiguration", "DescribeConfigurationRevision": "mq:DescribeConfigurationRevision", "DescribeUser": "mq:DescribeUser", "ListBrokers": "mq:ListBrokers", "ListConfigurationRevisions": "mq:ListConfigurationRevisions", "ListConfigurations": "mq:ListConfigurations", "ListTags": "mq:ListTags", "ListUsers": "mq:ListUsers", "RebootBroker": "mq:RebootBroker", "UpdateBroker": "mq:UpdateBroker", "UpdateConfiguration": "mq:UpdateConfiguration", "UpdateUser": "mq:UpdateUser" }, "mturk": { "AcceptQualificationRequest": "mechanicalturk:AcceptQualificationRequest", "ApproveAssignment": "mechanicalturk:ApproveAssignment", "AssociateQualificationWithWorker": "mechanicalturk:AssociateQualificationWithWorker", "CreateAdditionalAssignmentsForHIT": "mechanicalturk:CreateAdditionalAssignmentsForHIT", "CreateHIT": "mechanicalturk:CreateHIT", "CreateHITType": "mechanicalturk:CreateHITType", "CreateHITWithHITType": "mechanicalturk:CreateHITWithHITType", "CreateQualificationType": "mechanicalturk:CreateQualificationType", "CreateWorkerBlock": "mechanicalturk:CreateWorkerBlock", "DeleteHIT": "mechanicalturk:DeleteHIT", "DeleteQualificationType": "mechanicalturk:DeleteQualificationType", "DeleteWorkerBlock": "mechanicalturk:DeleteWorkerBlock", "DisassociateQualificationFromWorker": "mechanicalturk:DisassociateQualificationFromWorker", "GetAccountBalance": "mechanicalturk:GetAccountBalance", "GetAssignment": "mechanicalturk:GetAssignment", "GetFileUploadURL": "mechanicalturk:GetFileUploadURL", "GetHIT": "mechanicalturk:GetHIT", "GetQualificationScore": "mechanicalturk:GetQualificationScore", "GetQualificationType": "mechanicalturk:GetQualificationType", "ListAssignmentsForHIT": "mechanicalturk:ListAssignmentsForHIT", "ListBonusPayments": "mechanicalturk:ListBonusPayments", "ListHITs": "mechanicalturk:ListHITs", "ListHITsForQualificationType": "mechanicalturk:ListHITsForQualificationType", "ListQualificationRequests": "mechanicalturk:ListQualificationRequests", "ListQualificationTypes": "mechanicalturk:ListQualificationTypes", "ListReviewPolicyResultsForHIT": "mechanicalturk:ListReviewPolicyResultsForHIT", "ListReviewableHITs": "mechanicalturk:ListReviewableHITs", "ListWorkerBlocks": "mechanicalturk:ListWorkerBlocks", "ListWorkersWithQualificationType": "mechanicalturk:ListWorkersWithQualificationType", "NotifyWorkers": "mechanicalturk:NotifyWorkers", "RejectAssignment": "mechanicalturk:RejectAssignment", "RejectQualificationRequest": "mechanicalturk:RejectQualificationRequest", "SendBonus": "mechanicalturk:SendBonus", "SendTestEventNotification": "mechanicalturk:SendTestEventNotification", "UpdateExpirationForHIT": "mechanicalturk:UpdateExpirationForHIT", "UpdateHITReviewStatus": "mechanicalturk:UpdateHITReviewStatus", "UpdateHITTypeOfHIT": "mechanicalturk:UpdateHITTypeOfHIT", "UpdateNotificationSettings": "mechanicalturk:UpdateNotificationSettings", "UpdateQualificationType": "mechanicalturk:UpdateQualificationType" }, "network-firewall": { "AssociateFirewallPolicy": "network-firewall:AssociateFirewallPolicy", "AssociateSubnets": "network-firewall:AssociateSubnets", "CreateFirewall": "network-firewall:CreateFirewall", "CreateFirewallPolicy": "network-firewall:CreateFirewallPolicy", "CreateRuleGroup": "network-firewall:CreateRuleGroup", "DeleteFirewall": "network-firewall:DeleteFirewall", "DeleteFirewallPolicy": "network-firewall:DeleteFirewallPolicy", "DeleteResourcePolicy": "network-firewall:DeleteResourcePolicy", "DeleteRuleGroup": "network-firewall:DeleteRuleGroup", "DescribeFirewall": "network-firewall:DescribeFirewall", "DescribeFirewallPolicy": "network-firewall:DescribeFirewallPolicy", "DescribeLoggingConfiguration": "network-firewall:DescribeLoggingConfiguration", "DescribeResourcePolicy": "network-firewall:DescribeResourcePolicy", "DescribeRuleGroup": "network-firewall:DescribeRuleGroup", "DisassociateSubnets": "network-firewall:DisassociateSubnets", "ListFirewallPolicies": "network-firewall:ListFirewallPolicies", "ListFirewalls": "network-firewall:ListFirewalls", "ListRuleGroups": "network-firewall:ListRuleGroups", "ListTagsForResource": "network-firewall:ListTagsForResource", "PutResourcePolicy": "network-firewall:PutResourcePolicy", "TagResource": "network-firewall:TagResource", "UntagResource": "network-firewall:UntagResource", "UpdateFirewallDeleteProtection": "network-firewall:UpdateFirewallDeleteProtection", "UpdateFirewallDescription": "network-firewall:UpdateFirewallDescription", "UpdateFirewallPolicy": "network-firewall:UpdateFirewallPolicy", "UpdateFirewallPolicyChangeProtection": "network-firewall:UpdateFirewallPolicyChangeProtection", "UpdateLoggingConfiguration": "network-firewall:UpdateLoggingConfiguration", "UpdateRuleGroup": "network-firewall:UpdateRuleGroup", "UpdateSubnetChangeProtection": "network-firewall:UpdateSubnetChangeProtection" }, "networkmanager": { "AcceptAttachment": "networkmanager:AcceptAttachment", "AssociateConnectPeer": "networkmanager:AssociateConnectPeer", "AssociateCustomerGateway": "networkmanager:AssociateCustomerGateway", "AssociateLink": "networkmanager:AssociateLink", "AssociateTransitGatewayConnectPeer": "networkmanager:AssociateTransitGatewayConnectPeer", "CreateConnectAttachment": "networkmanager:CreateConnectAttachment", "CreateConnectPeer": "networkmanager:CreateConnectPeer", "CreateConnection": "networkmanager:CreateConnection", "CreateCoreNetwork": "networkmanager:CreateCoreNetwork", "CreateDevice": "networkmanager:CreateDevice", "CreateGlobalNetwork": "networkmanager:CreateGlobalNetwork", "CreateLink": "networkmanager:CreateLink", "CreateSite": "networkmanager:CreateSite", "CreateSiteToSiteVpnAttachment": "networkmanager:CreateSiteToSiteVpnAttachment", "CreateVpcAttachment": "networkmanager:CreateVpcAttachment", "DeleteAttachment": "networkmanager:DeleteAttachment", "DeleteConnectPeer": "networkmanager:DeleteConnectPeer", "DeleteConnection": "networkmanager:DeleteConnection", "DeleteCoreNetwork": "networkmanager:DeleteCoreNetwork", "DeleteCoreNetworkPolicyVersion": "networkmanager:DeleteCoreNetworkPolicyVersion", "DeleteDevice": "networkmanager:DeleteDevice", "DeleteGlobalNetwork": "networkmanager:DeleteGlobalNetwork", "DeleteLink": "networkmanager:DeleteLink", "DeleteResourcePolicy": "networkmanager:DeleteResourcePolicy", "DeleteSite": "networkmanager:DeleteSite", "DeregisterTransitGateway": "networkmanager:DeregisterTransitGateway", "DescribeGlobalNetworks": "networkmanager:DescribeGlobalNetworks", "DisassociateConnectPeer": "networkmanager:DisassociateConnectPeer", "DisassociateCustomerGateway": "networkmanager:DisassociateCustomerGateway", "DisassociateLink": "networkmanager:DisassociateLink", "DisassociateTransitGatewayConnectPeer": "networkmanager:DisassociateTransitGatewayConnectPeer", "ExecuteCoreNetworkChangeSet": "networkmanager:ExecuteCoreNetworkChangeSet", "GetConnectAttachment": "networkmanager:GetConnectAttachment", "GetConnectPeer": "networkmanager:GetConnectPeer", "GetConnectPeerAssociations": "networkmanager:GetConnectPeerAssociations", "GetConnections": "networkmanager:GetConnections", "GetCoreNetwork": "networkmanager:GetCoreNetwork", "GetCoreNetworkChangeSet": "networkmanager:GetCoreNetworkChangeSet", "GetCoreNetworkPolicy": "networkmanager:GetCoreNetworkPolicy", "GetCustomerGatewayAssociations": "networkmanager:GetCustomerGatewayAssociations", "GetDevices": "networkmanager:GetDevices", "GetLinkAssociations": "networkmanager:GetLinkAssociations", "GetLinks": "networkmanager:GetLinks", "GetNetworkResourceCounts": "networkmanager:GetNetworkResourceCounts", "GetNetworkResourceRelationships": "networkmanager:GetNetworkResourceRelationships", "GetNetworkResources": "networkmanager:GetNetworkResources", "GetNetworkRoutes": "networkmanager:GetNetworkRoutes", "GetNetworkTelemetry": "networkmanager:GetNetworkTelemetry", "GetResourcePolicy": "networkmanager:GetResourcePolicy", "GetRouteAnalysis": "networkmanager:GetRouteAnalysis", "GetSiteToSiteVpnAttachment": "networkmanager:GetSiteToSiteVpnAttachment", "GetSites": "networkmanager:GetSites", "GetTransitGatewayConnectPeerAssociations": "networkmanager:GetTransitGatewayConnectPeerAssociations", "GetTransitGatewayRegistrations": "networkmanager:GetTransitGatewayRegistrations", "GetVpcAttachment": "networkmanager:GetVpcAttachment", "ListAttachments": "networkmanager:ListAttachments", "ListConnectPeers": "networkmanager:ListConnectPeers", "ListCoreNetworkPolicyVersions": "networkmanager:ListCoreNetworkPolicyVersions", "ListCoreNetworks": "networkmanager:ListCoreNetworks", "ListTagsForResource": "networkmanager:ListTagsForResource", "PutCoreNetworkPolicy": "networkmanager:PutCoreNetworkPolicy", "PutResourcePolicy": "networkmanager:PutResourcePolicy", "RegisterTransitGateway": "networkmanager:RegisterTransitGateway", "RejectAttachment": "networkmanager:RejectAttachment", "RestoreCoreNetworkPolicyVersion": "networkmanager:RestoreCoreNetworkPolicyVersion", "StartRouteAnalysis": "networkmanager:StartRouteAnalysis", "TagResource": "networkmanager:TagResource", "UntagResource": "networkmanager:UntagResource", "UpdateConnection": "networkmanager:UpdateConnection", "UpdateCoreNetwork": "networkmanager:UpdateCoreNetwork", "UpdateDevice": "networkmanager:UpdateDevice", "UpdateGlobalNetwork": "networkmanager:UpdateGlobalNetwork", "UpdateLink": "networkmanager:UpdateLink", "UpdateNetworkResourceMetadata": "networkmanager:UpdateNetworkResourceMetadata", "UpdateSite": "networkmanager:UpdateSite", "UpdateVpcAttachment": "networkmanager:UpdateVpcAttachment" }, "nimble": { "AcceptEulas": "nimble:AcceptEulas", "CreateLaunchProfile": "nimble:CreateLaunchProfile", "CreateStreamingImage": "nimble:CreateStreamingImage", "CreateStreamingSession": "nimble:CreateStreamingSession", "CreateStreamingSessionStream": "nimble:CreateStreamingSessionStream", "CreateStudio": "nimble:CreateStudio", "CreateStudioComponent": "nimble:CreateStudioComponent", "DeleteLaunchProfile": "nimble:DeleteLaunchProfile", "DeleteLaunchProfileMember": "nimble:DeleteLaunchProfileMember", "DeleteStreamingImage": "nimble:DeleteStreamingImage", "DeleteStreamingSession": "nimble:DeleteStreamingSession", "DeleteStudio": "nimble:DeleteStudio", "DeleteStudioComponent": "nimble:DeleteStudioComponent", "DeleteStudioMember": "nimble:DeleteStudioMember", "GetEula": "nimble:GetEula", "GetLaunchProfile": "nimble:GetLaunchProfile", "GetLaunchProfileDetails": "nimble:GetLaunchProfileDetails", "GetLaunchProfileInitialization": "nimble:GetLaunchProfileInitialization", "GetLaunchProfileMember": "nimble:GetLaunchProfileMember", "GetStreamingImage": "nimble:GetStreamingImage", "GetStreamingSession": "nimble:GetStreamingSession", "GetStreamingSessionStream": "nimble:GetStreamingSessionStream", "GetStudio": "nimble:GetStudio", "GetStudioComponent": "nimble:GetStudioComponent", "GetStudioMember": "nimble:GetStudioMember", "ListEulaAcceptances": "nimble:ListEulaAcceptances", "ListEulas": "nimble:ListEulas", "ListLaunchProfileMembers": "nimble:ListLaunchProfileMembers", "ListLaunchProfiles": "nimble:ListLaunchProfiles", "ListStreamingImages": "nimble:ListStreamingImages", "ListStreamingSessions": "nimble:ListStreamingSessions", "ListStudioComponents": "nimble:ListStudioComponents", "ListStudioMembers": "nimble:ListStudioMembers", "ListStudios": "nimble:ListStudios", "ListTagsForResource": "nimble:ListTagsForResource", "PutLaunchProfileMembers": "nimble:PutLaunchProfileMembers", "PutStudioMembers": "nimble:PutStudioMembers", "StartStreamingSession": "nimble:StartStreamingSession", "StartStudioSSOConfigurationRepair": "nimble:StartStudioSSOConfigurationRepair", "StopStreamingSession": "nimble:StopStreamingSession", "TagResource": "nimble:TagResource", "UntagResource": "nimble:UntagResource", "UpdateLaunchProfile": "nimble:UpdateLaunchProfile", "UpdateLaunchProfileMember": "nimble:UpdateLaunchProfileMember", "UpdateStreamingImage": "nimble:UpdateStreamingImage", "UpdateStudio": "nimble:UpdateStudio", "UpdateStudioComponent": "nimble:UpdateStudioComponent" }, "opsworks": { "AssignInstance": "opsworks:AssignInstance", "AssignVolume": "opsworks:AssignVolume", "AssociateElasticIp": "opsworks:AssociateElasticIp", "AttachElasticLoadBalancer": "opsworks:AttachElasticLoadBalancer", "CloneStack": "opsworks:CloneStack", "CreateApp": "opsworks:CreateApp", "CreateDeployment": "opsworks:CreateDeployment", "CreateInstance": "opsworks:CreateInstance", "CreateLayer": "opsworks:CreateLayer", "CreateStack": "opsworks:CreateStack", "CreateUserProfile": "opsworks:CreateUserProfile", "DeleteApp": "opsworks:DeleteApp", "DeleteInstance": "opsworks:DeleteInstance", "DeleteLayer": "opsworks:DeleteLayer", "DeleteStack": "opsworks:DeleteStack", "DeleteUserProfile": "opsworks:DeleteUserProfile", "DeregisterEcsCluster": "opsworks:DeregisterEcsCluster", "DeregisterElasticIp": "opsworks:DeregisterElasticIp", "DeregisterInstance": "opsworks:DeregisterInstance", "DeregisterRdsDbInstance": "opsworks:DeregisterRdsDbInstance", "DeregisterVolume": "opsworks:DeregisterVolume", "DescribeAgentVersions": "opsworks:DescribeAgentVersions", "DescribeApps": "opsworks:DescribeApps", "DescribeCommands": "opsworks:DescribeCommands", "DescribeDeployments": "opsworks:DescribeDeployments", "DescribeEcsClusters": "opsworks:DescribeEcsClusters", "DescribeElasticIps": "opsworks:DescribeElasticIps", "DescribeElasticLoadBalancers": "opsworks:DescribeElasticLoadBalancers", "DescribeInstances": "opsworks:DescribeInstances", "DescribeLayers": "opsworks:DescribeLayers", "DescribeLoadBasedAutoScaling": "opsworks:DescribeLoadBasedAutoScaling", "DescribeMyUserProfile": "opsworks:DescribeMyUserProfile", "DescribeOperatingSystems": "opsworks:DescribeOperatingSystems", "DescribePermissions": "opsworks:DescribePermissions", "DescribeRaidArrays": "opsworks:DescribeRaidArrays", "DescribeRdsDbInstances": "opsworks:DescribeRdsDbInstances", "DescribeServiceErrors": "opsworks:DescribeServiceErrors", "DescribeStackProvisioningParameters": "opsworks:DescribeStackProvisioningParameters", "DescribeStackSummary": "opsworks:DescribeStackSummary", "DescribeStacks": "opsworks:DescribeStacks", "DescribeTimeBasedAutoScaling": "opsworks:DescribeTimeBasedAutoScaling", "DescribeUserProfiles": "opsworks:DescribeUserProfiles", "DescribeVolumes": "opsworks:DescribeVolumes", "DetachElasticLoadBalancer": "opsworks:DetachElasticLoadBalancer", "DisassociateElasticIp": "opsworks:DisassociateElasticIp", "GetHostnameSuggestion": "opsworks:GetHostnameSuggestion", "GrantAccess": "opsworks:GrantAccess", "ListTags": "opsworks:ListTags", "RebootInstance": "opsworks:RebootInstance", "RegisterEcsCluster": "opsworks:RegisterEcsCluster", "RegisterElasticIp": "opsworks:RegisterElasticIp", "RegisterInstance": "opsworks:RegisterInstance", "RegisterRdsDbInstance": "opsworks:RegisterRdsDbInstance", "RegisterVolume": "opsworks:RegisterVolume", "SetLoadBasedAutoScaling": "opsworks:SetLoadBasedAutoScaling", "SetPermission": "opsworks:SetPermission", "SetTimeBasedAutoScaling": "opsworks:SetTimeBasedAutoScaling", "StartInstance": "opsworks:StartInstance", "StartStack": "opsworks:StartStack", "StopInstance": "opsworks:StopInstance", "StopStack": "opsworks:StopStack", "TagResource": "opsworks:TagResource", "UnassignInstance": "opsworks:UnassignInstance", "UnassignVolume": "opsworks:UnassignVolume", "UntagResource": "opsworks:UntagResource", "UpdateApp": "opsworks:UpdateApp", "UpdateElasticIp": "opsworks:UpdateElasticIp", "UpdateInstance": "opsworks:UpdateInstance", "UpdateLayer": "opsworks:UpdateLayer", "UpdateMyUserProfile": "opsworks:UpdateMyUserProfile", "UpdateRdsDbInstance": "opsworks:UpdateRdsDbInstance", "UpdateStack": "opsworks:UpdateStack", "UpdateUserProfile": "opsworks:UpdateUserProfile", "UpdateVolume": "opsworks:UpdateVolume" }, "opsworkscm": { "AssociateNode": "opsworks-cm:AssociateNode", "CreateBackup": "opsworks-cm:CreateBackup", "CreateServer": "opsworks-cm:CreateServer", "DeleteBackup": "opsworks-cm:DeleteBackup", "DeleteServer": "opsworks-cm:DeleteServer", "DescribeAccountAttributes": "opsworks-cm:DescribeAccountAttributes", "DescribeBackups": "opsworks-cm:DescribeBackups", "DescribeEvents": "opsworks-cm:DescribeEvents", "DescribeNodeAssociationStatus": "opsworks-cm:DescribeNodeAssociationStatus", "DescribeServers": "opsworks-cm:DescribeServers", "DisassociateNode": "opsworks-cm:DisassociateNode", "ExportServerEngineAttribute": "opsworks-cm:ExportServerEngineAttribute", "ListTagsForResource": "opsworks-cm:ListTagsForResource", "RestoreServer": "opsworks-cm:RestoreServer", "StartMaintenance": "opsworks-cm:StartMaintenance", "TagResource": "opsworks-cm:TagResource", "UntagResource": "opsworks-cm:UntagResource", "UpdateServer": "opsworks-cm:UpdateServer", "UpdateServerEngineAttributes": "opsworks-cm:UpdateServerEngineAttributes" }, "organizations": { "AcceptHandshake": "organizations:AcceptHandshake", "AttachPolicy": "organizations:AttachPolicy", "CancelHandshake": "organizations:CancelHandshake", "CreateAccount": "organizations:CreateAccount", "CreateGovCloudAccount": "organizations:CreateGovCloudAccount", "CreateOrganization": "organizations:CreateOrganization", "CreateOrganizationalUnit": "organizations:CreateOrganizationalUnit", "CreatePolicy": "organizations:CreatePolicy", "DeclineHandshake": "organizations:DeclineHandshake", "DeleteOrganization": "organizations:DeleteOrganization", "DeleteOrganizationalUnit": "organizations:DeleteOrganizationalUnit", "DeletePolicy": "organizations:DeletePolicy", "DeregisterDelegatedAdministrator": "organizations:DeregisterDelegatedAdministrator", "DescribeAccount": "organizations:DescribeAccount", "DescribeCreateAccountStatus": "organizations:DescribeCreateAccountStatus", "DescribeEffectivePolicy": "organizations:DescribeEffectivePolicy", "DescribeHandshake": "organizations:DescribeHandshake", "DescribeOrganization": "organizations:DescribeOrganization", "DescribeOrganizationalUnit": "organizations:DescribeOrganizationalUnit", "DescribePolicy": "organizations:DescribePolicy", "DetachPolicy": "organizations:DetachPolicy", "DisableAWSServiceAccess": "organizations:DisableAWSServiceAccess", "DisablePolicyType": "organizations:DisablePolicyType", "EnableAWSServiceAccess": "organizations:EnableAWSServiceAccess", "EnableAllFeatures": "organizations:EnableAllFeatures", "EnablePolicyType": "organizations:EnablePolicyType", "InviteAccountToOrganization": "organizations:InviteAccountToOrganization", "LeaveOrganization": "organizations:LeaveOrganization", "ListAWSServiceAccessForOrganization": "organizations:ListAWSServiceAccessForOrganization", "ListAccounts": "organizations:ListAccounts", "ListAccountsForParent": "organizations:ListAccountsForParent", "ListChildren": "organizations:ListChildren", "ListCreateAccountStatus": "organizations:ListCreateAccountStatus", "ListDelegatedAdministrators": "organizations:ListDelegatedAdministrators", "ListDelegatedServicesForAccount": "organizations:ListDelegatedServicesForAccount", "ListHandshakesForAccount": "organizations:ListHandshakesForAccount", "ListHandshakesForOrganization": "organizations:ListHandshakesForOrganization", "ListOrganizationalUnitsForParent": "organizations:ListOrganizationalUnitsForParent", "ListParents": "organizations:ListParents", "ListPolicies": "organizations:ListPolicies", "ListPoliciesForTarget": "organizations:ListPoliciesForTarget", "ListRoots": "organizations:ListRoots", "ListTagsForResource": "organizations:ListTagsForResource", "ListTargetsForPolicy": "organizations:ListTargetsForPolicy", "MoveAccount": "organizations:MoveAccount", "RegisterDelegatedAdministrator": "organizations:RegisterDelegatedAdministrator", "RemoveAccountFromOrganization": "organizations:RemoveAccountFromOrganization", "TagResource": "organizations:TagResource", "UntagResource": "organizations:UntagResource", "UpdateOrganizationalUnit": "organizations:UpdateOrganizationalUnit", "UpdatePolicy": "organizations:UpdatePolicy" }, "outposts": { "CancelOrder": "outposts:CancelOrder", "CreateOrder": "outposts:CreateOrder", "CreateOutpost": "outposts:CreateOutpost", "CreateSite": "outposts:CreateSite", "DeleteOutpost": "outposts:DeleteOutpost", "DeleteSite": "outposts:DeleteSite", "GetCatalogItem": "outposts:GetCatalogItem", "GetOutpost": "outposts:GetOutpost", "GetOutpostInstanceTypes": "outposts:GetOutpostInstanceTypes", "GetSite": "outposts:GetSite", "GetSiteAddress": "outposts:GetSiteAddress", "ListCatalogItems": "outposts:ListCatalogItems", "ListOrders": "outposts:ListOrders", "ListOutposts": "outposts:ListOutposts", "ListSites": "outposts:ListSites", "ListTagsForResource": "outposts:ListTagsForResource", "TagResource": "outposts:TagResource", "UntagResource": "outposts:UntagResource", "UpdateOutpost": "outposts:UpdateOutpost", "UpdateSite": "outposts:UpdateSite", "UpdateSiteAddress": "outposts:UpdateSiteAddress", "UpdateSiteRackPhysicalProperties": "outposts:UpdateSiteRackPhysicalProperties" }, "panorama": { "CreateApplicationInstance": "panorama:CreateApplicationInstance", "CreateJobForDevices": "panorama:CreateJobForDevices", "CreateNodeFromTemplateJob": "panorama:CreateNodeFromTemplateJob", "CreatePackage": "panorama:CreatePackage", "CreatePackageImportJob": "panorama:CreatePackageImportJob", "DeleteDevice": "panorama:DeleteDevice", "DeletePackage": "panorama:DeletePackage", "DeregisterPackageVersion": "panorama:DeregisterPackageVersion", "DescribeApplicationInstance": "panorama:DescribeApplicationInstance", "DescribeApplicationInstanceDetails": "panorama:DescribeApplicationInstanceDetails", "DescribeDevice": "panorama:DescribeDevice", "DescribeDeviceJob": "panorama:DescribeDeviceJob", "DescribeNode": "panorama:DescribeNode", "DescribeNodeFromTemplateJob": "panorama:DescribeNodeFromTemplateJob", "DescribePackage": "panorama:DescribePackage", "DescribePackageImportJob": "panorama:DescribePackageImportJob", "DescribePackageVersion": "panorama:DescribePackageVersion", "ListApplicationInstanceDependencies": "panorama:ListApplicationInstanceDependencies", "ListApplicationInstanceNodeInstances": "panorama:ListApplicationInstanceNodeInstances", "ListApplicationInstances": "panorama:ListApplicationInstances", "ListDevices": "panorama:ListDevices", "ListDevicesJobs": "panorama:ListDevicesJobs", "ListNodeFromTemplateJobs": "panorama:ListNodeFromTemplateJobs", "ListNodes": "panorama:ListNodes", "ListPackageImportJobs": "panorama:ListPackageImportJobs", "ListPackages": "panorama:ListPackages", "ListTagsForResource": "panorama:ListTagsForResource", "ProvisionDevice": "panorama:ProvisionDevice", "RegisterPackageVersion": "panorama:RegisterPackageVersion", "RemoveApplicationInstance": "panorama:RemoveApplicationInstance", "TagResource": "panorama:TagResource", "UntagResource": "panorama:UntagResource", "UpdateDeviceMetadata": "panorama:UpdateDeviceMetadata" }, "personalize": { "CreateBatchInferenceJob": "personalize:CreateBatchInferenceJob", "CreateBatchSegmentJob": "personalize:CreateBatchSegmentJob", "CreateCampaign": "personalize:CreateCampaign", "CreateDataset": "personalize:CreateDataset", "CreateDatasetExportJob": "personalize:CreateDatasetExportJob", "CreateDatasetGroup": "personalize:CreateDatasetGroup", "CreateDatasetImportJob": "personalize:CreateDatasetImportJob", "CreateEventTracker": "personalize:CreateEventTracker", "CreateFilter": "personalize:CreateFilter", "CreateRecommender": "personalize:CreateRecommender", "CreateSchema": "personalize:CreateSchema", "CreateSolution": "personalize:CreateSolution", "CreateSolutionVersion": "personalize:CreateSolutionVersion", "DeleteCampaign": "personalize:DeleteCampaign", "DeleteDataset": "personalize:DeleteDataset", "DeleteDatasetGroup": "personalize:DeleteDatasetGroup", "DeleteEventTracker": "personalize:DeleteEventTracker", "DeleteFilter": "personalize:DeleteFilter", "DeleteRecommender": "personalize:DeleteRecommender", "DeleteSchema": "personalize:DeleteSchema", "DeleteSolution": "personalize:DeleteSolution", "DescribeAlgorithm": "personalize:DescribeAlgorithm", "DescribeBatchInferenceJob": "personalize:DescribeBatchInferenceJob", "DescribeBatchSegmentJob": "personalize:DescribeBatchSegmentJob", "DescribeCampaign": "personalize:DescribeCampaign", "DescribeDataset": "personalize:DescribeDataset", "DescribeDatasetExportJob": "personalize:DescribeDatasetExportJob", "DescribeDatasetGroup": "personalize:DescribeDatasetGroup", "DescribeDatasetImportJob": "personalize:DescribeDatasetImportJob", "DescribeEventTracker": "personalize:DescribeEventTracker", "DescribeFeatureTransformation": "personalize:DescribeFeatureTransformation", "DescribeFilter": "personalize:DescribeFilter", "DescribeRecipe": "personalize:DescribeRecipe", "DescribeRecommender": "personalize:DescribeRecommender", "DescribeSchema": "personalize:DescribeSchema", "DescribeSolution": "personalize:DescribeSolution", "DescribeSolutionVersion": "personalize:DescribeSolutionVersion", "GetSolutionMetrics": "personalize:GetSolutionMetrics", "ListBatchInferenceJobs": "personalize:ListBatchInferenceJobs", "ListBatchSegmentJobs": "personalize:ListBatchSegmentJobs", "ListCampaigns": "personalize:ListCampaigns", "ListDatasetExportJobs": "personalize:ListDatasetExportJobs", "ListDatasetGroups": "personalize:ListDatasetGroups", "ListDatasetImportJobs": "personalize:ListDatasetImportJobs", "ListDatasets": "personalize:ListDatasets", "ListEventTrackers": "personalize:ListEventTrackers", "ListFilters": "personalize:ListFilters", "ListRecipes": "personalize:ListRecipes", "ListRecommenders": "personalize:ListRecommenders", "ListSchemas": "personalize:ListSchemas", "ListSolutionVersions": "personalize:ListSolutionVersions", "ListSolutions": "personalize:ListSolutions", "StopSolutionVersionCreation": "personalize:StopSolutionVersionCreation", "UpdateCampaign": "personalize:UpdateCampaign", "UpdateRecommender": "personalize:UpdateRecommender" }, "pi": { "DescribeDimensionKeys": "pi:DescribeDimensionKeys", "GetDimensionKeyDetails": "pi:GetDimensionKeyDetails", "GetResourceMetrics": "pi:GetResourceMetrics" }, "pinpoint": { "CreateApp": "mobiletargeting:CreateApp", "CreateCampaign": "mobiletargeting:CreateCampaign", "CreateEmailTemplate": "mobiletargeting:CreateEmailTemplate", "CreateExportJob": "mobiletargeting:CreateExportJob", "CreateImportJob": "mobiletargeting:CreateImportJob", "CreateJourney": "mobiletargeting:CreateJourney", "CreatePushTemplate": "mobiletargeting:CreatePushTemplate", "CreateRecommenderConfiguration": "mobiletargeting:CreateRecommenderConfiguration", "CreateSegment": "mobiletargeting:CreateSegment", "CreateSmsTemplate": "mobiletargeting:CreateSmsTemplate", "CreateVoiceTemplate": "mobiletargeting:CreateVoiceTemplate", "DeleteAdmChannel": "mobiletargeting:DeleteAdmChannel", "DeleteApnsChannel": "mobiletargeting:DeleteApnsChannel", "DeleteApnsSandboxChannel": "mobiletargeting:DeleteApnsSandboxChannel", "DeleteApnsVoipChannel": "mobiletargeting:DeleteApnsVoipChannel", "DeleteApnsVoipSandboxChannel": "mobiletargeting:DeleteApnsVoipSandboxChannel", "DeleteApp": "mobiletargeting:DeleteApp", "DeleteBaiduChannel": "mobiletargeting:DeleteBaiduChannel", "DeleteCampaign": "mobiletargeting:DeleteCampaign", "DeleteEmailChannel": "mobiletargeting:DeleteEmailChannel", "DeleteEmailTemplate": "mobiletargeting:DeleteEmailTemplate", "DeleteEndpoint": "mobiletargeting:DeleteEndpoint", "DeleteEventStream": "mobiletargeting:DeleteEventStream", "DeleteGcmChannel": "mobiletargeting:DeleteGcmChannel", "DeleteJourney": "mobiletargeting:DeleteJourney", "DeletePushTemplate": "mobiletargeting:DeletePushTemplate", "DeleteRecommenderConfiguration": "mobiletargeting:DeleteRecommenderConfiguration", "DeleteSegment": "mobiletargeting:DeleteSegment", "DeleteSmsChannel": "mobiletargeting:DeleteSmsChannel", "DeleteSmsTemplate": "mobiletargeting:DeleteSmsTemplate", "DeleteUserEndpoints": "mobiletargeting:DeleteUserEndpoints", "DeleteVoiceChannel": "mobiletargeting:DeleteVoiceChannel", "DeleteVoiceTemplate": "mobiletargeting:DeleteVoiceTemplate", "GetAdmChannel": "mobiletargeting:GetAdmChannel", "GetApnsChannel": "mobiletargeting:GetApnsChannel", "GetApnsSandboxChannel": "mobiletargeting:GetApnsSandboxChannel", "GetApnsVoipChannel": "mobiletargeting:GetApnsVoipChannel", "GetApnsVoipSandboxChannel": "mobiletargeting:GetApnsVoipSandboxChannel", "GetApp": "mobiletargeting:GetApp", "GetApplicationDateRangeKpi": "mobiletargeting:GetApplicationDateRangeKpi", "GetApplicationSettings": "mobiletargeting:GetApplicationSettings", "GetApps": "mobiletargeting:GetApps", "GetBaiduChannel": "mobiletargeting:GetBaiduChannel", "GetCampaign": "mobiletargeting:GetCampaign", "GetCampaignActivities": "mobiletargeting:GetCampaignActivities", "GetCampaignDateRangeKpi": "mobiletargeting:GetCampaignDateRangeKpi", "GetCampaignVersion": "mobiletargeting:GetCampaignVersion", "GetCampaignVersions": "mobiletargeting:GetCampaignVersions", "GetCampaigns": "mobiletargeting:GetCampaigns", "GetChannels": "mobiletargeting:GetChannels", "GetEmailChannel": "mobiletargeting:GetEmailChannel", "GetEmailTemplate": "mobiletargeting:GetEmailTemplate", "GetEndpoint": "mobiletargeting:GetEndpoint", "GetEventStream": "mobiletargeting:GetEventStream", "GetExportJob": "mobiletargeting:GetExportJob", "GetExportJobs": "mobiletargeting:GetExportJobs", "GetGcmChannel": "mobiletargeting:GetGcmChannel", "GetImportJob": "mobiletargeting:GetImportJob", "GetImportJobs": "mobiletargeting:GetImportJobs", "GetInAppMessages": "mobiletargeting:GetInAppMessages", "GetJourney": "mobiletargeting:GetJourney", "GetJourneyDateRangeKpi": "mobiletargeting:GetJourneyDateRangeKpi", "GetJourneyExecutionActivityMetrics": "mobiletargeting:GetJourneyExecutionActivityMetrics", "GetJourneyExecutionMetrics": "mobiletargeting:GetJourneyExecutionMetrics", "GetPushTemplate": "mobiletargeting:GetPushTemplate", "GetRecommenderConfiguration": "mobiletargeting:GetRecommenderConfiguration", "GetRecommenderConfigurations": "mobiletargeting:GetRecommenderConfigurations", "GetSegment": "mobiletargeting:GetSegment", "GetSegmentExportJobs": "mobiletargeting:GetSegmentExportJobs", "GetSegmentImportJobs": "mobiletargeting:GetSegmentImportJobs", "GetSegmentVersion": "mobiletargeting:GetSegmentVersion", "GetSegmentVersions": "mobiletargeting:GetSegmentVersions", "GetSegments": "mobiletargeting:GetSegments", "GetSmsChannel": "mobiletargeting:GetSmsChannel", "GetSmsTemplate": "mobiletargeting:GetSmsTemplate", "GetUserEndpoints": "mobiletargeting:GetUserEndpoints", "GetVoiceChannel": "mobiletargeting:GetVoiceChannel", "GetVoiceTemplate": "mobiletargeting:GetVoiceTemplate", "ListJourneys": "mobiletargeting:ListJourneys", "ListTagsForResource": "mobiletargeting:ListTagsForResource", "ListTemplateVersions": "mobiletargeting:ListTemplateVersions", "ListTemplates": "mobiletargeting:ListTemplates", "PhoneNumberValidate": "mobiletargeting:PhoneNumberValidate", "PutEventStream": "mobiletargeting:PutEventStream", "PutEvents": "mobiletargeting:PutEvents", "RemoveAttributes": "mobiletargeting:RemoveAttributes", "SendMessages": "mobiletargeting:SendMessages", "SendUsersMessages": "mobiletargeting:SendUsersMessages", "TagResource": "mobiletargeting:TagResource", "UntagResource": "mobiletargeting:UntagResource", "UpdateAdmChannel": "mobiletargeting:UpdateAdmChannel", "UpdateApnsChannel": "mobiletargeting:UpdateApnsChannel", "UpdateApnsSandboxChannel": "mobiletargeting:UpdateApnsSandboxChannel", "UpdateApnsVoipChannel": "mobiletargeting:UpdateApnsVoipChannel", "UpdateApnsVoipSandboxChannel": "mobiletargeting:UpdateApnsVoipSandboxChannel", "UpdateApplicationSettings": "mobiletargeting:UpdateApplicationSettings", "UpdateBaiduChannel": "mobiletargeting:UpdateBaiduChannel", "UpdateCampaign": "mobiletargeting:UpdateCampaign", "UpdateEmailChannel": "mobiletargeting:UpdateEmailChannel", "UpdateEmailTemplate": "mobiletargeting:UpdateEmailTemplate", "UpdateEndpoint": "mobiletargeting:UpdateEndpoint", "UpdateEndpointsBatch": "mobiletargeting:UpdateEndpointsBatch", "UpdateGcmChannel": "mobiletargeting:UpdateGcmChannel", "UpdateJourney": "mobiletargeting:UpdateJourney", "UpdateJourneyState": "mobiletargeting:UpdateJourneyState", "UpdatePushTemplate": "mobiletargeting:UpdatePushTemplate", "UpdateRecommenderConfiguration": "mobiletargeting:UpdateRecommenderConfiguration", "UpdateSegment": "mobiletargeting:UpdateSegment", "UpdateSmsChannel": "mobiletargeting:UpdateSmsChannel", "UpdateSmsTemplate": "mobiletargeting:UpdateSmsTemplate", "UpdateTemplateActiveVersion": "mobiletargeting:UpdateTemplateActiveVersion", "UpdateVoiceChannel": "mobiletargeting:UpdateVoiceChannel", "UpdateVoiceTemplate": "mobiletargeting:UpdateVoiceTemplate" }, "polly": { "DeleteLexicon": "polly:DeleteLexicon", "DescribeVoices": "polly:DescribeVoices", "GetLexicon": "polly:GetLexicon", "GetSpeechSynthesisTask": "polly:GetSpeechSynthesisTask", "ListLexicons": "polly:ListLexicons", "ListSpeechSynthesisTasks": "polly:ListSpeechSynthesisTasks", "PutLexicon": "polly:PutLexicon", "StartSpeechSynthesisTask": "polly:StartSpeechSynthesisTask", "SynthesizeSpeech": "polly:SynthesizeSpeech" }, "pricing": { "DescribeServices": "pricing:DescribeServices", "GetAttributeValues": "pricing:GetAttributeValues", "GetProducts": "pricing:GetProducts" }, "proton": { "AcceptEnvironmentAccountConnection": "proton:AcceptEnvironmentAccountConnection", "CancelEnvironmentDeployment": "proton:CancelEnvironmentDeployment", "CancelServiceInstanceDeployment": "proton:CancelServiceInstanceDeployment", "CancelServicePipelineDeployment": "proton:CancelServicePipelineDeployment", "CreateEnvironment": "proton:CreateEnvironment", "CreateEnvironmentAccountConnection": "proton:CreateEnvironmentAccountConnection", "CreateEnvironmentTemplate": "proton:CreateEnvironmentTemplate", "CreateEnvironmentTemplateVersion": "proton:CreateEnvironmentTemplateVersion", "CreateRepository": "proton:CreateRepository", "CreateService": "proton:CreateService", "CreateServiceTemplate": "proton:CreateServiceTemplate", "CreateServiceTemplateVersion": "proton:CreateServiceTemplateVersion", "CreateTemplateSyncConfig": "proton:CreateTemplateSyncConfig", "DeleteEnvironment": "proton:DeleteEnvironment", "DeleteEnvironmentAccountConnection": "proton:DeleteEnvironmentAccountConnection", "DeleteEnvironmentTemplate": "proton:DeleteEnvironmentTemplate", "DeleteEnvironmentTemplateVersion": "proton:DeleteEnvironmentTemplateVersion", "DeleteRepository": "proton:DeleteRepository", "DeleteService": "proton:DeleteService", "DeleteServiceTemplate": "proton:DeleteServiceTemplate", "DeleteServiceTemplateVersion": "proton:DeleteServiceTemplateVersion", "DeleteTemplateSyncConfig": "proton:DeleteTemplateSyncConfig", "GetAccountSettings": "proton:GetAccountSettings", "GetEnvironment": "proton:GetEnvironment", "GetEnvironmentAccountConnection": "proton:GetEnvironmentAccountConnection", "GetEnvironmentTemplate": "proton:GetEnvironmentTemplate", "GetEnvironmentTemplateVersion": "proton:GetEnvironmentTemplateVersion", "GetRepository": "proton:GetRepository", "GetRepositorySyncStatus": "proton:GetRepositorySyncStatus", "GetService": "proton:GetService", "GetServiceInstance": "proton:GetServiceInstance", "GetServiceTemplate": "proton:GetServiceTemplate", "GetServiceTemplateVersion": "proton:GetServiceTemplateVersion", "GetTemplateSyncConfig": "proton:GetTemplateSyncConfig", "GetTemplateSyncStatus": "proton:GetTemplateSyncStatus", "ListEnvironmentAccountConnections": "proton:ListEnvironmentAccountConnections", "ListEnvironmentTemplateVersions": "proton:ListEnvironmentTemplateVersions", "ListEnvironmentTemplates": "proton:ListEnvironmentTemplates", "ListEnvironments": "proton:ListEnvironments", "ListRepositories": "proton:ListRepositories", "ListRepositorySyncDefinitions": "proton:ListRepositorySyncDefinitions", "ListServiceInstances": "proton:ListServiceInstances", "ListServiceTemplateVersions": "proton:ListServiceTemplateVersions", "ListServiceTemplates": "proton:ListServiceTemplates", "ListServices": "proton:ListServices", "ListTagsForResource": "proton:ListTagsForResource", "RejectEnvironmentAccountConnection": "proton:RejectEnvironmentAccountConnection", "TagResource": "proton:TagResource", "UntagResource": "proton:UntagResource", "UpdateAccountSettings": "proton:UpdateAccountSettings", "UpdateEnvironment": "proton:UpdateEnvironment", "UpdateEnvironmentAccountConnection": "proton:UpdateEnvironmentAccountConnection", "UpdateEnvironmentTemplate": "proton:UpdateEnvironmentTemplate", "UpdateEnvironmentTemplateVersion": "proton:UpdateEnvironmentTemplateVersion", "UpdateService": "proton:UpdateService", "UpdateServiceInstance": "proton:UpdateServiceInstance", "UpdateServicePipeline": "proton:UpdateServicePipeline", "UpdateServiceTemplate": "proton:UpdateServiceTemplate", "UpdateServiceTemplateVersion": "proton:UpdateServiceTemplateVersion", "UpdateTemplateSyncConfig": "proton:UpdateTemplateSyncConfig" }, "qldb": { "CancelJournalKinesisStream": "qldb:CancelJournalKinesisStream", "CreateLedger": "qldb:CreateLedger", "DeleteLedger": "qldb:DeleteLedger", "DescribeJournalKinesisStream": "qldb:DescribeJournalKinesisStream", "DescribeJournalS3Export": "qldb:DescribeJournalS3Export", "DescribeLedger": "qldb:DescribeLedger", "ExportJournalToS3": "qldb:ExportJournalToS3", "GetBlock": "qldb:GetBlock", "GetDigest": "qldb:GetDigest", "GetRevision": "qldb:GetRevision", "ListJournalKinesisStreamsForLedger": "qldb:ListJournalKinesisStreamsForLedger", "ListJournalS3Exports": "qldb:ListJournalS3Exports", "ListJournalS3ExportsForLedger": "qldb:ListJournalS3ExportsForLedger", "ListLedgers": "qldb:ListLedgers", "ListTagsForResource": "qldb:ListTagsForResource", "StreamJournalToKinesis": "qldb:StreamJournalToKinesis", "TagResource": "qldb:TagResource", "UntagResource": "qldb:UntagResource", "UpdateLedger": "qldb:UpdateLedger", "UpdateLedgerPermissionsMode": "qldb:UpdateLedgerPermissionsMode" }, "quicksight": { "CancelIngestion": "quicksight:CancelIngestion", "CreateAccountCustomization": "quicksight:CreateAccountCustomization", "CreateAnalysis": "quicksight:CreateAnalysis", "CreateDashboard": "quicksight:CreateDashboard", "CreateDataSet": "quicksight:CreateDataSet", "CreateDataSource": "quicksight:CreateDataSource", "CreateFolder": "quicksight:CreateFolder", "CreateFolderMembership": "quicksight:CreateFolderMembership", "CreateGroup": "quicksight:CreateGroup", "CreateGroupMembership": "quicksight:CreateGroupMembership", "CreateIAMPolicyAssignment": "quicksight:CreateIAMPolicyAssignment", "CreateIngestion": "quicksight:CreateIngestion", "CreateNamespace": "quicksight:CreateNamespace", "CreateTemplate": "quicksight:CreateTemplate", "CreateTemplateAlias": "quicksight:CreateTemplateAlias", "CreateTheme": "quicksight:CreateTheme", "CreateThemeAlias": "quicksight:CreateThemeAlias", "DeleteAccountCustomization": "quicksight:DeleteAccountCustomization", "DeleteAnalysis": "quicksight:DeleteAnalysis", "DeleteDashboard": "quicksight:DeleteDashboard", "DeleteDataSet": "quicksight:DeleteDataSet", "DeleteDataSource": "quicksight:DeleteDataSource", "DeleteFolder": "quicksight:DeleteFolder", "DeleteFolderMembership": "quicksight:DeleteFolderMembership", "DeleteGroup": "quicksight:DeleteGroup", "DeleteGroupMembership": "quicksight:DeleteGroupMembership", "DeleteIAMPolicyAssignment": "quicksight:DeleteIAMPolicyAssignment", "DeleteNamespace": "quicksight:DeleteNamespace", "DeleteTemplate": "quicksight:DeleteTemplate", "DeleteTemplateAlias": "quicksight:DeleteTemplateAlias", "DeleteTheme": "quicksight:DeleteTheme", "DeleteThemeAlias": "quicksight:DeleteThemeAlias", "DeleteUser": "quicksight:DeleteUser", "DeleteUserByPrincipalId": "quicksight:DeleteUserByPrincipalId", "DescribeAccountCustomization": "quicksight:DescribeAccountCustomization", "DescribeAccountSettings": "quicksight:DescribeAccountSettings", "DescribeAnalysis": "quicksight:DescribeAnalysis", "DescribeAnalysisPermissions": "quicksight:DescribeAnalysisPermissions", "DescribeDashboard": "quicksight:DescribeDashboard", "DescribeDashboardPermissions": "quicksight:DescribeDashboardPermissions", "DescribeDataSet": "quicksight:DescribeDataSet", "DescribeDataSetPermissions": "quicksight:DescribeDataSetPermissions", "DescribeDataSource": "quicksight:DescribeDataSource", "DescribeDataSourcePermissions": "quicksight:DescribeDataSourcePermissions", "DescribeFolder": "quicksight:DescribeFolder", "DescribeFolderPermissions": "quicksight:DescribeFolderPermissions", "DescribeFolderResolvedPermissions": "quicksight:DescribeFolderResolvedPermissions", "DescribeGroup": "quicksight:DescribeGroup", "DescribeIAMPolicyAssignment": "quicksight:DescribeIAMPolicyAssignment", "DescribeIngestion": "quicksight:DescribeIngestion", "DescribeIpRestriction": "quicksight:DescribeIpRestriction", "DescribeNamespace": "quicksight:DescribeNamespace", "DescribeTemplate": "quicksight:DescribeTemplate", "DescribeTemplateAlias": "quicksight:DescribeTemplateAlias", "DescribeTemplatePermissions": "quicksight:DescribeTemplatePermissions", "DescribeTheme": "quicksight:DescribeTheme", "DescribeThemeAlias": "quicksight:DescribeThemeAlias", "DescribeThemePermissions": "quicksight:DescribeThemePermissions", "DescribeUser": "quicksight:DescribeUser", "GenerateEmbedUrlForAnonymousUser": "quicksight:GenerateEmbedUrlForAnonymousUser", "GenerateEmbedUrlForRegisteredUser": "quicksight:GenerateEmbedUrlForRegisteredUser", "GetDashboardEmbedUrl": "quicksight:GetDashboardEmbedUrl", "GetSessionEmbedUrl": "quicksight:GetSessionEmbedUrl", "ListAnalyses": "quicksight:ListAnalyses", "ListDashboardVersions": "quicksight:ListDashboardVersions", "ListDashboards": "quicksight:ListDashboards", "ListDataSets": "quicksight:ListDataSets", "ListDataSources": "quicksight:ListDataSources", "ListFolderMembers": "quicksight:ListFolderMembers", "ListFolders": "quicksight:ListFolders", "ListGroupMemberships": "quicksight:ListGroupMemberships", "ListGroups": "quicksight:ListGroups", "ListIAMPolicyAssignments": "quicksight:ListIAMPolicyAssignments", "ListIAMPolicyAssignmentsForUser": "quicksight:ListIAMPolicyAssignmentsForUser", "ListIngestions": "quicksight:ListIngestions", "ListNamespaces": "quicksight:ListNamespaces", "ListTagsForResource": "quicksight:ListTagsForResource", "ListTemplateAliases": "quicksight:ListTemplateAliases", "ListTemplateVersions": "quicksight:ListTemplateVersions", "ListTemplates": "quicksight:ListTemplates", "ListThemeAliases": "quicksight:ListThemeAliases", "ListThemeVersions": "quicksight:ListThemeVersions", "ListThemes": "quicksight:ListThemes", "ListUserGroups": "quicksight:ListUserGroups", "ListUsers": "quicksight:ListUsers", "RegisterUser": "quicksight:RegisterUser", "RestoreAnalysis": "quicksight:RestoreAnalysis", "SearchAnalyses": "quicksight:SearchAnalyses", "SearchDashboards": "quicksight:SearchDashboards", "SearchFolders": "quicksight:SearchFolders", "TagResource": "quicksight:TagResource", "UntagResource": "quicksight:UntagResource", "UpdateAccountCustomization": "quicksight:UpdateAccountCustomization", "UpdateAccountSettings": "quicksight:UpdateAccountSettings", "UpdateAnalysis": "quicksight:UpdateAnalysis", "UpdateAnalysisPermissions": "quicksight:UpdateAnalysisPermissions", "UpdateDashboard": "quicksight:UpdateDashboard", "UpdateDashboardPermissions": "quicksight:UpdateDashboardPermissions", "UpdateDashboardPublishedVersion": "quicksight:UpdateDashboardPublishedVersion", "UpdateDataSet": "quicksight:UpdateDataSet", "UpdateDataSetPermissions": "quicksight:UpdateDataSetPermissions", "UpdateDataSource": "quicksight:UpdateDataSource", "UpdateDataSourcePermissions": "quicksight:UpdateDataSourcePermissions", "UpdateFolder": "quicksight:UpdateFolder", "UpdateFolderPermissions": "quicksight:UpdateFolderPermissions", "UpdateGroup": "quicksight:UpdateGroup", "UpdateIAMPolicyAssignment": "quicksight:UpdateIAMPolicyAssignment", "UpdateIpRestriction": "quicksight:UpdateIpRestriction", "UpdateTemplate": "quicksight:UpdateTemplate", "UpdateTemplateAlias": "quicksight:UpdateTemplateAlias", "UpdateTemplatePermissions": "quicksight:UpdateTemplatePermissions", "UpdateTheme": "quicksight:UpdateTheme", "UpdateThemeAlias": "quicksight:UpdateThemeAlias", "UpdateThemePermissions": "quicksight:UpdateThemePermissions", "UpdateUser": "quicksight:UpdateUser" }, "ram": { "AcceptResourceShareInvitation": "ram:AcceptResourceShareInvitation", "AssociateResourceShare": "ram:AssociateResourceShare", "AssociateResourceSharePermission": "ram:AssociateResourceSharePermission", "CreateResourceShare": "ram:CreateResourceShare", "DeleteResourceShare": "ram:DeleteResourceShare", "DisassociateResourceShare": "ram:DisassociateResourceShare", "DisassociateResourceSharePermission": "ram:DisassociateResourceSharePermission", "EnableSharingWithAwsOrganization": "ram:EnableSharingWithAwsOrganization", "GetPermission": "ram:GetPermission", "GetResourcePolicies": "ram:GetResourcePolicies", "GetResourceShareAssociations": "ram:GetResourceShareAssociations", "GetResourceShareInvitations": "ram:GetResourceShareInvitations", "GetResourceShares": "ram:GetResourceShares", "ListPendingInvitationResources": "ram:ListPendingInvitationResources", "ListPermissions": "ram:ListPermissions", "ListPrincipals": "ram:ListPrincipals", "ListResourceSharePermissions": "ram:ListResourceSharePermissions", "ListResourceTypes": "ram:ListResourceTypes", "ListResources": "ram:ListResources", "PromoteResourceShareCreatedFromPolicy": "ram:PromoteResourceShareCreatedFromPolicy", "RejectResourceShareInvitation": "ram:RejectResourceShareInvitation", "TagResource": "ram:TagResource", "UntagResource": "ram:UntagResource", "UpdateResourceShare": "ram:UpdateResourceShare" }, "rbin": { "CreateRule": "rbin:CreateRule", "DeleteRule": "rbin:DeleteRule", "GetRule": "rbin:GetRule", "ListRules": "rbin:ListRules", "ListTagsForResource": "rbin:ListTagsForResource", "TagResource": "rbin:TagResource", "UntagResource": "rbin:UntagResource", "UpdateRule": "rbin:UpdateRule" }, "rds": { "AddRoleToDBCluster": "rds:AddRoleToDBCluster", "AddRoleToDBInstance": "rds:AddRoleToDBInstance", "AddSourceIdentifierToSubscription": "rds:AddSourceIdentifierToSubscription", "AddTagsToResource": "rds:AddTagsToResource", "ApplyPendingMaintenanceAction": "rds:ApplyPendingMaintenanceAction", "AuthorizeDBSecurityGroupIngress": "rds:AuthorizeDBSecurityGroupIngress", "BacktrackDBCluster": "rds:BacktrackDBCluster", "CancelExportTask": "rds:CancelExportTask", "CopyDBClusterParameterGroup": "rds:CopyDBClusterParameterGroup", "CopyDBClusterSnapshot": "rds:CopyDBClusterSnapshot", "CopyDBParameterGroup": "rds:CopyDBParameterGroup", "CopyDBSnapshot": "rds:CopyDBSnapshot", "CopyOptionGroup": "rds:CopyOptionGroup", "CreateCustomAvailabilityZone": "rds:CreateCustomAvailabilityZone", "CreateCustomDBEngineVersion": "rds:CreateCustomDBEngineVersion", "CreateDBCluster": "rds:CreateDBCluster", "CreateDBClusterEndpoint": "rds:CreateDBClusterEndpoint", "CreateDBClusterParameterGroup": "rds:CreateDBClusterParameterGroup", "CreateDBClusterSnapshot": "rds:CreateDBClusterSnapshot", "CreateDBInstance": "rds:CreateDBInstance", "CreateDBInstanceReadReplica": "rds:CreateDBInstanceReadReplica", "CreateDBParameterGroup": "rds:CreateDBParameterGroup", "CreateDBProxy": "rds:CreateDBProxy", "CreateDBProxyEndpoint": "rds:CreateDBProxyEndpoint", "CreateDBSecurityGroup": "rds:CreateDBSecurityGroup", "CreateDBSnapshot": "rds:CreateDBSnapshot", "CreateDBSubnetGroup": "rds:CreateDBSubnetGroup", "CreateEventSubscription": "rds:CreateEventSubscription", "CreateGlobalCluster": "rds:CreateGlobalCluster", "CreateOptionGroup": "rds:CreateOptionGroup", "DeleteCustomAvailabilityZone": "rds:DeleteCustomAvailabilityZone", "DeleteCustomDBEngineVersion": "rds:DeleteCustomDBEngineVersion", "DeleteDBCluster": "rds:DeleteDBCluster", "DeleteDBClusterEndpoint": "rds:DeleteDBClusterEndpoint", "DeleteDBClusterParameterGroup": "rds:DeleteDBClusterParameterGroup", "DeleteDBClusterSnapshot": "rds:DeleteDBClusterSnapshot", "DeleteDBInstance": "rds:DeleteDBInstance", "DeleteDBInstanceAutomatedBackup": "rds:DeleteDBInstanceAutomatedBackup", "DeleteDBParameterGroup": "rds:DeleteDBParameterGroup", "DeleteDBProxy": "rds:DeleteDBProxy", "DeleteDBProxyEndpoint": "rds:DeleteDBProxyEndpoint", "DeleteDBSecurityGroup": "rds:DeleteDBSecurityGroup", "DeleteDBSnapshot": "rds:DeleteDBSnapshot", "DeleteDBSubnetGroup": "rds:DeleteDBSubnetGroup", "DeleteEventSubscription": "rds:DeleteEventSubscription", "DeleteGlobalCluster": "rds:DeleteGlobalCluster", "DeleteInstallationMedia": "rds:DeleteInstallationMedia", "DeleteOptionGroup": "rds:DeleteOptionGroup", "DeregisterDBProxyTargets": "rds:DeregisterDBProxyTargets", "DescribeAccountAttributes": "rds:DescribeAccountAttributes", "DescribeCertificates": "rds:DescribeCertificates", "DescribeCustomAvailabilityZones": "rds:DescribeCustomAvailabilityZones", "DescribeDBClusterBacktracks": "rds:DescribeDBClusterBacktracks", "DescribeDBClusterEndpoints": "rds:DescribeDBClusterEndpoints", "DescribeDBClusterParameterGroups": "rds:DescribeDBClusterParameterGroups", "DescribeDBClusterParameters": "rds:DescribeDBClusterParameters", "DescribeDBClusterSnapshotAttributes": "rds:DescribeDBClusterSnapshotAttributes", "DescribeDBClusterSnapshots": "rds:DescribeDBClusterSnapshots", "DescribeDBClusters": "rds:DescribeDBClusters", "DescribeDBEngineVersions": "rds:DescribeDBEngineVersions", "DescribeDBInstanceAutomatedBackups": "rds:DescribeDBInstanceAutomatedBackups", "DescribeDBInstances": "rds:DescribeDBInstances", "DescribeDBLogFiles": "rds:DescribeDBLogFiles", "DescribeDBParameterGroups": "rds:DescribeDBParameterGroups", "DescribeDBParameters": "rds:DescribeDBParameters", "DescribeDBProxies": "rds:DescribeDBProxies", "DescribeDBProxyEndpoints": "rds:DescribeDBProxyEndpoints", "DescribeDBProxyTargetGroups": "rds:DescribeDBProxyTargetGroups", "DescribeDBProxyTargets": "rds:DescribeDBProxyTargets", "DescribeDBSecurityGroups": "rds:DescribeDBSecurityGroups", "DescribeDBSnapshotAttributes": "rds:DescribeDBSnapshotAttributes", "DescribeDBSnapshots": "rds:DescribeDBSnapshots", "DescribeDBSubnetGroups": "rds:DescribeDBSubnetGroups", "DescribeEngineDefaultClusterParameters": "rds:DescribeEngineDefaultClusterParameters", "DescribeEngineDefaultParameters": "rds:DescribeEngineDefaultParameters", "DescribeEventCategories": "rds:DescribeEventCategories", "DescribeEventSubscriptions": "rds:DescribeEventSubscriptions", "DescribeEvents": "rds:DescribeEvents", "DescribeExportTasks": "rds:DescribeExportTasks", "DescribeGlobalClusters": "rds:DescribeGlobalClusters", "DescribeInstallationMedia": "rds:DescribeInstallationMedia", "DescribeOptionGroupOptions": "rds:DescribeOptionGroupOptions", "DescribeOptionGroups": "rds:DescribeOptionGroups", "DescribeOrderableDBInstanceOptions": "rds:DescribeOrderableDBInstanceOptions", "DescribePendingMaintenanceActions": "rds:DescribePendingMaintenanceActions", "DescribeReservedDBInstances": "rds:DescribeReservedDBInstances", "DescribeReservedDBInstancesOfferings": "rds:DescribeReservedDBInstancesOfferings", "DescribeSourceRegions": "rds:DescribeSourceRegions", "DescribeValidDBInstanceModifications": "rds:DescribeValidDBInstanceModifications", "DownloadDBLogFilePortion": "rds:DownloadDBLogFilePortion", "FailoverDBCluster": "rds:FailoverDBCluster", "FailoverGlobalCluster": "rds:FailoverGlobalCluster", "ImportInstallationMedia": "rds:ImportInstallationMedia", "ListTagsForResource": "rds:ListTagsForResource", "ModifyCertificates": "rds:ModifyCertificates", "ModifyCurrentDBClusterCapacity": "rds:ModifyCurrentDBClusterCapacity", "ModifyCustomDBEngineVersion": "rds:ModifyCustomDBEngineVersion", "ModifyDBCluster": "rds:ModifyDBCluster", "ModifyDBClusterEndpoint": "rds:ModifyDBClusterEndpoint", "ModifyDBClusterParameterGroup": "rds:ModifyDBClusterParameterGroup", "ModifyDBClusterSnapshotAttribute": "rds:ModifyDBClusterSnapshotAttribute", "ModifyDBInstance": "rds:ModifyDBInstance", "ModifyDBParameterGroup": "rds:ModifyDBParameterGroup", "ModifyDBProxy": "rds:ModifyDBProxy", "ModifyDBProxyEndpoint": "rds:ModifyDBProxyEndpoint", "ModifyDBProxyTargetGroup": "rds:ModifyDBProxyTargetGroup", "ModifyDBSnapshot": "rds:ModifyDBSnapshot", "ModifyDBSnapshotAttribute": "rds:ModifyDBSnapshotAttribute", "ModifyDBSubnetGroup": "rds:ModifyDBSubnetGroup", "ModifyEventSubscription": "rds:ModifyEventSubscription", "ModifyGlobalCluster": "rds:ModifyGlobalCluster", "ModifyOptionGroup": "rds:ModifyOptionGroup", "PromoteReadReplica": "rds:PromoteReadReplica", "PromoteReadReplicaDBCluster": "rds:PromoteReadReplicaDBCluster", "PurchaseReservedDBInstancesOffering": "rds:PurchaseReservedDBInstancesOffering", "RebootDBCluster": "rds:RebootDBCluster", "RebootDBInstance": "rds:RebootDBInstance", "RegisterDBProxyTargets": "rds:RegisterDBProxyTargets", "RemoveFromGlobalCluster": "rds:RemoveFromGlobalCluster", "RemoveRoleFromDBCluster": "rds:RemoveRoleFromDBCluster", "RemoveRoleFromDBInstance": "rds:RemoveRoleFromDBInstance", "RemoveSourceIdentifierFromSubscription": "rds:RemoveSourceIdentifierFromSubscription", "RemoveTagsFromResource": "rds:RemoveTagsFromResource", "ResetDBClusterParameterGroup": "rds:ResetDBClusterParameterGroup", "ResetDBParameterGroup": "rds:ResetDBParameterGroup", "RestoreDBClusterFromS3": "rds:RestoreDBClusterFromS3", "RestoreDBClusterFromSnapshot": "rds:RestoreDBClusterFromSnapshot", "RestoreDBClusterToPointInTime": "rds:RestoreDBClusterToPointInTime", "RestoreDBInstanceFromDBSnapshot": "rds:RestoreDBInstanceFromDBSnapshot", "RestoreDBInstanceFromS3": "rds:RestoreDBInstanceFromS3", "RestoreDBInstanceToPointInTime": "rds:RestoreDBInstanceToPointInTime", "RevokeDBSecurityGroupIngress": "rds:RevokeDBSecurityGroupIngress", "StartActivityStream": "rds:StartActivityStream", "StartDBCluster": "rds:StartDBCluster", "StartDBInstance": "rds:StartDBInstance", "StartDBInstanceAutomatedBackupsReplication": "rds:StartDBInstanceAutomatedBackupsReplication", "StartExportTask": "rds:StartExportTask", "StopActivityStream": "rds:StopActivityStream", "StopDBCluster": "rds:StopDBCluster", "StopDBInstance": "rds:StopDBInstance", "StopDBInstanceAutomatedBackupsReplication": "rds:StopDBInstanceAutomatedBackupsReplication" }, "rds-data": { "BatchExecuteStatement": "rds-data:BatchExecuteStatement", "BeginTransaction": "rds-data:BeginTransaction", "CommitTransaction": "rds-data:CommitTransaction", "ExecuteSql": "rds-data:ExecuteSql", "ExecuteStatement": "rds-data:ExecuteStatement", "RollbackTransaction": "rds-data:RollbackTransaction" }, "redshift": { "AcceptReservedNodeExchange": "redshift:AcceptReservedNodeExchange", "AssociateDataShareConsumer": "redshift:AssociateDataShareConsumer", "AuthorizeClusterSecurityGroupIngress": "redshift:AuthorizeClusterSecurityGroupIngress", "AuthorizeDataShare": "redshift:AuthorizeDataShare", "AuthorizeSnapshotAccess": "redshift:AuthorizeSnapshotAccess", "BatchDeleteClusterSnapshots": "redshift:BatchDeleteClusterSnapshots", "BatchModifyClusterSnapshots": "redshift:BatchModifyClusterSnapshots", "CancelResize": "redshift:CancelResize", "CopyClusterSnapshot": "redshift:CopyClusterSnapshot", "CreateAuthenticationProfile": "redshift:CreateAuthenticationProfile", "CreateCluster": "redshift:CreateCluster", "CreateClusterParameterGroup": "redshift:CreateClusterParameterGroup", "CreateClusterSecurityGroup": "redshift:CreateClusterSecurityGroup", "CreateClusterSnapshot": "redshift:CreateClusterSnapshot", "CreateClusterSubnetGroup": "redshift:CreateClusterSubnetGroup", "CreateEventSubscription": "redshift:CreateEventSubscription", "CreateHsmClientCertificate": "redshift:CreateHsmClientCertificate", "CreateHsmConfiguration": "redshift:CreateHsmConfiguration", "CreateScheduledAction": "redshift:CreateScheduledAction", "CreateSnapshotCopyGrant": "redshift:CreateSnapshotCopyGrant", "CreateSnapshotSchedule": "redshift:CreateSnapshotSchedule", "CreateTags": "redshift:CreateTags", "CreateUsageLimit": "redshift:CreateUsageLimit", "DeauthorizeDataShare": "redshift:DeauthorizeDataShare", "DeleteAuthenticationProfile": "redshift:DeleteAuthenticationProfile", "DeleteCluster": "redshift:DeleteCluster", "DeleteClusterParameterGroup": "redshift:DeleteClusterParameterGroup", "DeleteClusterSecurityGroup": "redshift:DeleteClusterSecurityGroup", "DeleteClusterSnapshot": "redshift:DeleteClusterSnapshot", "DeleteClusterSubnetGroup": "redshift:DeleteClusterSubnetGroup", "DeleteEventSubscription": "redshift:DeleteEventSubscription", "DeleteHsmClientCertificate": "redshift:DeleteHsmClientCertificate", "DeleteHsmConfiguration": "redshift:DeleteHsmConfiguration", "DeleteScheduledAction": "redshift:DeleteScheduledAction", "DeleteSnapshotCopyGrant": "redshift:DeleteSnapshotCopyGrant", "DeleteSnapshotSchedule": "redshift:DeleteSnapshotSchedule", "DeleteTags": "redshift:DeleteTags", "DeleteUsageLimit": "redshift:DeleteUsageLimit", "DescribeAccountAttributes": "redshift:DescribeAccountAttributes", "DescribeAuthenticationProfiles": "redshift:DescribeAuthenticationProfiles", "DescribeClusterDbRevisions": "redshift:DescribeClusterDbRevisions", "DescribeClusterParameterGroups": "redshift:DescribeClusterParameterGroups", "DescribeClusterParameters": "redshift:DescribeClusterParameters", "DescribeClusterSecurityGroups": "redshift:DescribeClusterSecurityGroups", "DescribeClusterSnapshots": "redshift:DescribeClusterSnapshots", "DescribeClusterSubnetGroups": "redshift:DescribeClusterSubnetGroups", "DescribeClusterTracks": "redshift:DescribeClusterTracks", "DescribeClusterVersions": "redshift:DescribeClusterVersions", "DescribeClusters": "redshift:DescribeClusters", "DescribeDataShares": "redshift:DescribeDataShares", "DescribeDataSharesForConsumer": "redshift:DescribeDataSharesForConsumer", "DescribeDataSharesForProducer": "redshift:DescribeDataSharesForProducer", "DescribeDefaultClusterParameters": "redshift:DescribeDefaultClusterParameters", "DescribeEventCategories": "redshift:DescribeEventCategories", "DescribeEventSubscriptions": "redshift:DescribeEventSubscriptions", "DescribeEvents": "redshift:DescribeEvents", "DescribeHsmClientCertificates": "redshift:DescribeHsmClientCertificates", "DescribeHsmConfigurations": "redshift:DescribeHsmConfigurations", "DescribeLoggingStatus": "redshift:DescribeLoggingStatus", "DescribeNodeConfigurationOptions": "redshift:DescribeNodeConfigurationOptions", "DescribeOrderableClusterOptions": "redshift:DescribeOrderableClusterOptions", "DescribeReservedNodeOfferings": "redshift:DescribeReservedNodeOfferings", "DescribeReservedNodes": "redshift:DescribeReservedNodes", "DescribeResize": "redshift:DescribeResize", "DescribeScheduledActions": "redshift:DescribeScheduledActions", "DescribeSnapshotCopyGrants": "redshift:DescribeSnapshotCopyGrants", "DescribeSnapshotSchedules": "redshift:DescribeSnapshotSchedules", "DescribeStorage": "redshift:DescribeStorage", "DescribeTableRestoreStatus": "redshift:DescribeTableRestoreStatus", "DescribeTags": "redshift:DescribeTags", "DescribeUsageLimits": "redshift:DescribeUsageLimits", "DisableLogging": "redshift:DisableLogging", "DisableSnapshotCopy": "redshift:DisableSnapshotCopy", "DisassociateDataShareConsumer": "redshift:DisassociateDataShareConsumer", "EnableLogging": "redshift:EnableLogging", "EnableSnapshotCopy": "redshift:EnableSnapshotCopy", "GetClusterCredentials": "redshift:GetClusterCredentials", "GetReservedNodeExchangeOfferings": "redshift:GetReservedNodeExchangeOfferings", "ModifyAquaConfiguration": "redshift:ModifyAquaConfiguration", "ModifyAuthenticationProfile": "redshift:ModifyAuthenticationProfile", "ModifyCluster": "redshift:ModifyCluster", "ModifyClusterDbRevision": "redshift:ModifyClusterDbRevision", "ModifyClusterIamRoles": "redshift:ModifyClusterIamRoles", "ModifyClusterMaintenance": "redshift:ModifyClusterMaintenance", "ModifyClusterParameterGroup": "redshift:ModifyClusterParameterGroup", "ModifyClusterSnapshot": "redshift:ModifyClusterSnapshot", "ModifyClusterSnapshotSchedule": "redshift:ModifyClusterSnapshotSchedule", "ModifyClusterSubnetGroup": "redshift:ModifyClusterSubnetGroup", "ModifyEventSubscription": "redshift:ModifyEventSubscription", "ModifyScheduledAction": "redshift:ModifyScheduledAction", "ModifySnapshotCopyRetentionPeriod": "redshift:ModifySnapshotCopyRetentionPeriod", "ModifySnapshotSchedule": "redshift:ModifySnapshotSchedule", "ModifyUsageLimit": "redshift:ModifyUsageLimit", "PauseCluster": "redshift:PauseCluster", "PurchaseReservedNodeOffering": "redshift:PurchaseReservedNodeOffering", "RebootCluster": "redshift:RebootCluster", "RejectDataShare": "redshift:RejectDataShare", "ResetClusterParameterGroup": "redshift:ResetClusterParameterGroup", "ResizeCluster": "redshift:ResizeCluster", "RestoreFromClusterSnapshot": "redshift:RestoreFromClusterSnapshot", "RestoreTableFromClusterSnapshot": "redshift:RestoreTableFromClusterSnapshot", "ResumeCluster": "redshift:ResumeCluster", "RevokeClusterSecurityGroupIngress": "redshift:RevokeClusterSecurityGroupIngress", "RevokeSnapshotAccess": "redshift:RevokeSnapshotAccess", "RotateEncryptionKey": "redshift:RotateEncryptionKey" }, "redshift-data": { "BatchExecuteStatement": "redshift-data:BatchExecuteStatement", "CancelStatement": "redshift-data:CancelStatement", "DescribeStatement": "redshift-data:DescribeStatement", "DescribeTable": "redshift-data:DescribeTable", "ExecuteStatement": "redshift-data:ExecuteStatement", "GetStatementResult": "redshift-data:GetStatementResult", "ListDatabases": "redshift-data:ListDatabases", "ListSchemas": "redshift-data:ListSchemas", "ListStatements": "redshift-data:ListStatements", "ListTables": "redshift-data:ListTables" }, "rekognition": { "CompareFaces": "rekognition:CompareFaces", "CreateCollection": "rekognition:CreateCollection", "CreateDataset": "rekognition:CreateDataset", "CreateProject": "rekognition:CreateProject", "CreateProjectVersion": "rekognition:CreateProjectVersion", "CreateStreamProcessor": "rekognition:CreateStreamProcessor", "DeleteCollection": "rekognition:DeleteCollection", "DeleteDataset": "rekognition:DeleteDataset", "DeleteFaces": "rekognition:DeleteFaces", "DeleteProject": "rekognition:DeleteProject", "DeleteProjectVersion": "rekognition:DeleteProjectVersion", "DeleteStreamProcessor": "rekognition:DeleteStreamProcessor", "DescribeCollection": "rekognition:DescribeCollection", "DescribeDataset": "rekognition:DescribeDataset", "DescribeProjectVersions": "rekognition:DescribeProjectVersions", "DescribeProjects": "rekognition:DescribeProjects", "DescribeStreamProcessor": "rekognition:DescribeStreamProcessor", "DetectCustomLabels": "rekognition:DetectCustomLabels", "DetectFaces": "rekognition:DetectFaces", "DetectLabels": "rekognition:DetectLabels", "DetectModerationLabels": "rekognition:DetectModerationLabels", "DetectProtectiveEquipment": "rekognition:DetectProtectiveEquipment", "DetectText": "rekognition:DetectText", "DistributeDatasetEntries": "rekognition:DistributeDatasetEntries", "GetCelebrityInfo": "rekognition:GetCelebrityInfo", "GetCelebrityRecognition": "rekognition:GetCelebrityRecognition", "GetContentModeration": "rekognition:GetContentModeration", "GetFaceDetection": "rekognition:GetFaceDetection", "GetFaceSearch": "rekognition:GetFaceSearch", "GetLabelDetection": "rekognition:GetLabelDetection", "GetPersonTracking": "rekognition:GetPersonTracking", "GetSegmentDetection": "rekognition:GetSegmentDetection", "GetTextDetection": "rekognition:GetTextDetection", "IndexFaces": "rekognition:IndexFaces", "ListCollections": "rekognition:ListCollections", "ListDatasetEntries": "rekognition:ListDatasetEntries", "ListDatasetLabels": "rekognition:ListDatasetLabels", "ListFaces": "rekognition:ListFaces", "ListStreamProcessors": "rekognition:ListStreamProcessors", "ListTagsForResource": "rekognition:ListTagsForResource", "RecognizeCelebrities": "rekognition:RecognizeCelebrities", "SearchFaces": "rekognition:SearchFaces", "SearchFacesByImage": "rekognition:SearchFacesByImage", "StartCelebrityRecognition": "rekognition:StartCelebrityRecognition", "StartContentModeration": "rekognition:StartContentModeration", "StartFaceDetection": "rekognition:StartFaceDetection", "StartFaceSearch": "rekognition:StartFaceSearch", "StartLabelDetection": "rekognition:StartLabelDetection", "StartPersonTracking": "rekognition:StartPersonTracking", "StartProjectVersion": "rekognition:StartProjectVersion", "StartSegmentDetection": "rekognition:StartSegmentDetection", "StartStreamProcessor": "rekognition:StartStreamProcessor", "StartTextDetection": "rekognition:StartTextDetection", "StopProjectVersion": "rekognition:StopProjectVersion", "StopStreamProcessor": "rekognition:StopStreamProcessor", "TagResource": "rekognition:TagResource", "UntagResource": "rekognition:UntagResource", "UpdateDatasetEntries": "rekognition:UpdateDatasetEntries" }, "resiliencehub": { "AddDraftAppVersionResourceMappings": "resiliencehub:AddDraftAppVersionResourceMappings", "CreateApp": "resiliencehub:CreateApp", "CreateRecommendationTemplate": "resiliencehub:CreateRecommendationTemplate", "CreateResiliencyPolicy": "resiliencehub:CreateResiliencyPolicy", "DeleteApp": "resiliencehub:DeleteApp", "DeleteAppAssessment": "resiliencehub:DeleteAppAssessment", "DeleteRecommendationTemplate": "resiliencehub:DeleteRecommendationTemplate", "DeleteResiliencyPolicy": "resiliencehub:DeleteResiliencyPolicy", "DescribeApp": "resiliencehub:DescribeApp", "DescribeAppAssessment": "resiliencehub:DescribeAppAssessment", "DescribeAppVersionResourcesResolutionStatus": "resiliencehub:DescribeAppVersionResourcesResolutionStatus", "DescribeAppVersionTemplate": "resiliencehub:DescribeAppVersionTemplate", "DescribeDraftAppVersionResourcesImportStatus": "resiliencehub:DescribeDraftAppVersionResourcesImportStatus", "DescribeResiliencyPolicy": "resiliencehub:DescribeResiliencyPolicy", "ImportResourcesToDraftAppVersion": "resiliencehub:ImportResourcesToDraftAppVersion", "ListAlarmRecommendations": "resiliencehub:ListAlarmRecommendations", "ListAppAssessments": "resiliencehub:ListAppAssessments", "ListAppComponentCompliances": "resiliencehub:ListAppComponentCompliances", "ListAppComponentRecommendations": "resiliencehub:ListAppComponentRecommendations", "ListAppVersionResourceMappings": "resiliencehub:ListAppVersionResourceMappings", "ListAppVersionResources": "resiliencehub:ListAppVersionResources", "ListAppVersions": "resiliencehub:ListAppVersions", "ListApps": "resiliencehub:ListApps", "ListRecommendationTemplates": "resiliencehub:ListRecommendationTemplates", "ListResiliencyPolicies": "resiliencehub:ListResiliencyPolicies", "ListSopRecommendations": "resiliencehub:ListSopRecommendations", "ListSuggestedResiliencyPolicies": "resiliencehub:ListSuggestedResiliencyPolicies", "ListTagsForResource": "resiliencehub:ListTagsForResource", "ListTestRecommendations": "resiliencehub:ListTestRecommendations", "ListUnsupportedAppVersionResources": "resiliencehub:ListUnsupportedAppVersionResources", "PublishAppVersion": "resiliencehub:PublishAppVersion", "PutDraftAppVersionTemplate": "resiliencehub:PutDraftAppVersionTemplate", "RemoveDraftAppVersionResourceMappings": "resiliencehub:RemoveDraftAppVersionResourceMappings", "ResolveAppVersionResources": "resiliencehub:ResolveAppVersionResources", "StartAppAssessment": "resiliencehub:StartAppAssessment", "TagResource": "resiliencehub:TagResource", "UntagResource": "resiliencehub:UntagResource", "UpdateApp": "resiliencehub:UpdateApp", "UpdateResiliencyPolicy": "resiliencehub:UpdateResiliencyPolicy" }, "resource-groups": { "CreateGroup": "resource-groups:CreateGroup", "DeleteGroup": "resource-groups:DeleteGroup", "GetGroup": "resource-groups:GetGroup", "GetGroupConfiguration": "resource-groups:GetGroupConfiguration", "GetGroupQuery": "resource-groups:GetGroupQuery", "GetTags": "resource-groups:GetTags", "GroupResources": "resource-groups:GroupResources", "ListGroupResources": "resource-groups:ListGroupResources", "ListGroups": "resource-groups:ListGroups", "PutGroupConfiguration": "resource-groups:PutGroupConfiguration", "SearchResources": "resource-groups:SearchResources", "Tag": "resource-groups:Tag", "UngroupResources": "resource-groups:UngroupResources", "Untag": "resource-groups:Untag", "UpdateGroup": "resource-groups:UpdateGroup", "UpdateGroupQuery": "resource-groups:UpdateGroupQuery" }, "resourcegroupstaggingapi": { "DescribeReportCreation": "tag:DescribeReportCreation", "GetComplianceSummary": "tag:GetComplianceSummary", "GetResources": "tag:GetResources", "GetTagKeys": "tag:GetTagKeys", "GetTagValues": "tag:GetTagValues", "StartReportCreation": "tag:StartReportCreation", "TagResources": "tag:TagResources", "UntagResources": "tag:UntagResources" }, "robomaker": { "BatchDeleteWorlds": "robomaker:BatchDeleteWorlds", "BatchDescribeSimulationJob": "robomaker:BatchDescribeSimulationJob", "CancelDeploymentJob": "robomaker:CancelDeploymentJob", "CancelSimulationJob": "robomaker:CancelSimulationJob", "CancelSimulationJobBatch": "robomaker:CancelSimulationJobBatch", "CancelWorldExportJob": "robomaker:CancelWorldExportJob", "CancelWorldGenerationJob": "robomaker:CancelWorldGenerationJob", "CreateDeploymentJob": "robomaker:CreateDeploymentJob", "CreateFleet": "robomaker:CreateFleet", "CreateRobot": "robomaker:CreateRobot", "CreateRobotApplication": "robomaker:CreateRobotApplication", "CreateRobotApplicationVersion": "robomaker:CreateRobotApplicationVersion", "CreateSimulationApplication": "robomaker:CreateSimulationApplication", "CreateSimulationApplicationVersion": "robomaker:CreateSimulationApplicationVersion", "CreateSimulationJob": "robomaker:CreateSimulationJob", "CreateWorldExportJob": "robomaker:CreateWorldExportJob", "CreateWorldGenerationJob": "robomaker:CreateWorldGenerationJob", "CreateWorldTemplate": "robomaker:CreateWorldTemplate", "DeleteFleet": "robomaker:DeleteFleet", "DeleteRobot": "robomaker:DeleteRobot", "DeleteRobotApplication": "robomaker:DeleteRobotApplication", "DeleteSimulationApplication": "robomaker:DeleteSimulationApplication", "DeleteWorldTemplate": "robomaker:DeleteWorldTemplate", "DeregisterRobot": "robomaker:DeregisterRobot", "DescribeDeploymentJob": "robomaker:DescribeDeploymentJob", "DescribeFleet": "robomaker:DescribeFleet", "DescribeRobot": "robomaker:DescribeRobot", "DescribeRobotApplication": "robomaker:DescribeRobotApplication", "DescribeSimulationApplication": "robomaker:DescribeSimulationApplication", "DescribeSimulationJob": "robomaker:DescribeSimulationJob", "DescribeSimulationJobBatch": "robomaker:DescribeSimulationJobBatch", "DescribeWorld": "robomaker:DescribeWorld", "DescribeWorldExportJob": "robomaker:DescribeWorldExportJob", "DescribeWorldGenerationJob": "robomaker:DescribeWorldGenerationJob", "DescribeWorldTemplate": "robomaker:DescribeWorldTemplate", "GetWorldTemplateBody": "robomaker:GetWorldTemplateBody", "ListDeploymentJobs": "robomaker:ListDeploymentJobs", "ListFleets": "robomaker:ListFleets", "ListRobotApplications": "robomaker:ListRobotApplications", "ListRobots": "robomaker:ListRobots", "ListSimulationApplications": "robomaker:ListSimulationApplications", "ListSimulationJobBatches": "robomaker:ListSimulationJobBatches", "ListSimulationJobs": "robomaker:ListSimulationJobs", "ListTagsForResource": "robomaker:ListTagsForResource", "ListWorldExportJobs": "robomaker:ListWorldExportJobs", "ListWorldGenerationJobs": "robomaker:ListWorldGenerationJobs", "ListWorldTemplates": "robomaker:ListWorldTemplates", "ListWorlds": "robomaker:ListWorlds", "RegisterRobot": "robomaker:RegisterRobot", "RestartSimulationJob": "robomaker:RestartSimulationJob", "StartSimulationJobBatch": "robomaker:StartSimulationJobBatch", "SyncDeploymentJob": "robomaker:SyncDeploymentJob", "TagResource": "robomaker:TagResource", "UntagResource": "robomaker:UntagResource", "UpdateRobotApplication": "robomaker:UpdateRobotApplication", "UpdateSimulationApplication": "robomaker:UpdateSimulationApplication", "UpdateWorldTemplate": "robomaker:UpdateWorldTemplate" }, "route53": { "ActivateKeySigningKey": "route53:ActivateKeySigningKey", "AssociateVPCWithHostedZone": "route53:AssociateVPCWithHostedZone", "ChangeResourceRecordSets": "route53:ChangeResourceRecordSets", "ChangeTagsForResource": "route53:ChangeTagsForResource", "CreateHealthCheck": "route53:CreateHealthCheck", "CreateHostedZone": "route53:CreateHostedZone", "CreateKeySigningKey": "route53:CreateKeySigningKey", "CreateQueryLoggingConfig": "route53:CreateQueryLoggingConfig", "CreateReusableDelegationSet": "route53:CreateReusableDelegationSet", "CreateTrafficPolicy": "route53:CreateTrafficPolicy", "CreateTrafficPolicyInstance": "route53:CreateTrafficPolicyInstance", "CreateTrafficPolicyVersion": "route53:CreateTrafficPolicyVersion", "CreateVPCAssociationAuthorization": "route53:CreateVPCAssociationAuthorization", "DeactivateKeySigningKey": "route53:DeactivateKeySigningKey", "DeleteHealthCheck": "route53:DeleteHealthCheck", "DeleteHostedZone": "route53:DeleteHostedZone", "DeleteKeySigningKey": "route53:DeleteKeySigningKey", "DeleteQueryLoggingConfig": "route53:DeleteQueryLoggingConfig", "DeleteReusableDelegationSet": "route53:DeleteReusableDelegationSet", "DeleteTrafficPolicy": "route53:DeleteTrafficPolicy", "DeleteTrafficPolicyInstance": "route53:DeleteTrafficPolicyInstance", "DeleteVPCAssociationAuthorization": "route53:DeleteVPCAssociationAuthorization", "DisableHostedZoneDNSSEC": "route53:DisableHostedZoneDNSSEC", "DisassociateVPCFromHostedZone": "route53:DisassociateVPCFromHostedZone", "EnableHostedZoneDNSSEC": "route53:EnableHostedZoneDNSSEC", "GetAccountLimit": "route53:GetAccountLimit", "GetChange": "route53:GetChange", "GetCheckerIpRanges": "route53:GetCheckerIpRanges", "GetDNSSEC": "route53:GetDNSSEC", "GetGeoLocation": "route53:GetGeoLocation", "GetHealthCheck": "route53:GetHealthCheck", "GetHealthCheckCount": "route53:GetHealthCheckCount", "GetHealthCheckLastFailureReason": "route53:GetHealthCheckLastFailureReason", "GetHealthCheckStatus": "route53:GetHealthCheckStatus", "GetHostedZone": "route53:GetHostedZone", "GetHostedZoneCount": "route53:GetHostedZoneCount", "GetHostedZoneLimit": "route53:GetHostedZoneLimit", "GetQueryLoggingConfig": "route53:GetQueryLoggingConfig", "GetReusableDelegationSet": "route53:GetReusableDelegationSet", "GetReusableDelegationSetLimit": "route53:GetReusableDelegationSetLimit", "GetTrafficPolicy": "route53:GetTrafficPolicy", "GetTrafficPolicyInstance": "route53:GetTrafficPolicyInstance", "GetTrafficPolicyInstanceCount": "route53:GetTrafficPolicyInstanceCount", "ListGeoLocations": "route53:ListGeoLocations", "ListHealthChecks": "route53:ListHealthChecks", "ListHostedZones": "route53:ListHostedZones", "ListHostedZonesByName": "route53:ListHostedZonesByName", "ListHostedZonesByVPC": "route53:ListHostedZonesByVPC", "ListQueryLoggingConfigs": "route53:ListQueryLoggingConfigs", "ListResourceRecordSets": "route53:ListResourceRecordSets", "ListReusableDelegationSets": "route53:ListReusableDelegationSets", "ListTagsForResource": "route53:ListTagsForResource", "ListTagsForResources": "route53:ListTagsForResources", "ListTrafficPolicies": "route53:ListTrafficPolicies", "ListTrafficPolicyInstances": "route53:ListTrafficPolicyInstances", "ListTrafficPolicyInstancesByHostedZone": "route53:ListTrafficPolicyInstancesByHostedZone", "ListTrafficPolicyInstancesByPolicy": "route53:ListTrafficPolicyInstancesByPolicy", "ListTrafficPolicyVersions": "route53:ListTrafficPolicyVersions", "ListVPCAssociationAuthorizations": "route53:ListVPCAssociationAuthorizations", "TestDNSAnswer": "route53:TestDNSAnswer", "UpdateHealthCheck": "route53:UpdateHealthCheck", "UpdateHostedZoneComment": "route53:UpdateHostedZoneComment", "UpdateTrafficPolicyComment": "route53:UpdateTrafficPolicyComment", "UpdateTrafficPolicyInstance": "route53:UpdateTrafficPolicyInstance" }, "route53-recovery-cluster": { "GetRoutingControlState": "route53-recovery-cluster:GetRoutingControlState", "UpdateRoutingControlState": "route53-recovery-cluster:UpdateRoutingControlState", "UpdateRoutingControlStates": "route53-recovery-cluster:UpdateRoutingControlStates" }, "route53-recovery-control-config": { "CreateCluster": "route53-recovery-control-config:CreateCluster", "CreateControlPanel": "route53-recovery-control-config:CreateControlPanel", "CreateRoutingControl": "route53-recovery-control-config:CreateRoutingControl", "CreateSafetyRule": "route53-recovery-control-config:CreateSafetyRule", "DeleteCluster": "route53-recovery-control-config:DeleteCluster", "DeleteControlPanel": "route53-recovery-control-config:DeleteControlPanel", "DeleteRoutingControl": "route53-recovery-control-config:DeleteRoutingControl", "DeleteSafetyRule": "route53-recovery-control-config:DeleteSafetyRule", "DescribeCluster": "route53-recovery-control-config:DescribeCluster", "DescribeControlPanel": "route53-recovery-control-config:DescribeControlPanel", "DescribeRoutingControl": "route53-recovery-control-config:DescribeRoutingControl", "DescribeSafetyRule": "route53-recovery-control-config:DescribeSafetyRule", "ListAssociatedRoute53HealthChecks": "route53-recovery-control-config:ListAssociatedRoute53HealthChecks", "ListClusters": "route53-recovery-control-config:ListClusters", "ListControlPanels": "route53-recovery-control-config:ListControlPanels", "ListRoutingControls": "route53-recovery-control-config:ListRoutingControls", "ListSafetyRules": "route53-recovery-control-config:ListSafetyRules", "ListTagsForResource": "route53-recovery-control-config:ListTagsForResource", "TagResource": "route53-recovery-control-config:TagResource", "UntagResource": "route53-recovery-control-config:UntagResource", "UpdateControlPanel": "route53-recovery-control-config:UpdateControlPanel", "UpdateRoutingControl": "route53-recovery-control-config:UpdateRoutingControl", "UpdateSafetyRule": "route53-recovery-control-config:UpdateSafetyRule" }, "route53-recovery-readiness": { "CreateCell": "route53-recovery-readiness:CreateCell", "CreateCrossAccountAuthorization": "route53-recovery-readiness:CreateCrossAccountAuthorization", "CreateReadinessCheck": "route53-recovery-readiness:CreateReadinessCheck", "CreateRecoveryGroup": "route53-recovery-readiness:CreateRecoveryGroup", "CreateResourceSet": "route53-recovery-readiness:CreateResourceSet", "DeleteCell": "route53-recovery-readiness:DeleteCell", "DeleteCrossAccountAuthorization": "route53-recovery-readiness:DeleteCrossAccountAuthorization", "DeleteReadinessCheck": "route53-recovery-readiness:DeleteReadinessCheck", "DeleteRecoveryGroup": "route53-recovery-readiness:DeleteRecoveryGroup", "DeleteResourceSet": "route53-recovery-readiness:DeleteResourceSet", "GetArchitectureRecommendations": "route53-recovery-readiness:GetArchitectureRecommendations", "GetCell": "route53-recovery-readiness:GetCell", "GetCellReadinessSummary": "route53-recovery-readiness:GetCellReadinessSummary", "GetReadinessCheck": "route53-recovery-readiness:GetReadinessCheck", "GetReadinessCheckResourceStatus": "route53-recovery-readiness:GetReadinessCheckResourceStatus", "GetReadinessCheckStatus": "route53-recovery-readiness:GetReadinessCheckStatus", "GetRecoveryGroup": "route53-recovery-readiness:GetRecoveryGroup", "GetRecoveryGroupReadinessSummary": "route53-recovery-readiness:GetRecoveryGroupReadinessSummary", "GetResourceSet": "route53-recovery-readiness:GetResourceSet", "ListCells": "route53-recovery-readiness:ListCells", "ListCrossAccountAuthorizations": "route53-recovery-readiness:ListCrossAccountAuthorizations", "ListReadinessChecks": "route53-recovery-readiness:ListReadinessChecks", "ListRecoveryGroups": "route53-recovery-readiness:ListRecoveryGroups", "ListResourceSets": "route53-recovery-readiness:ListResourceSets", "ListRules": "route53-recovery-readiness:ListRules", "ListTagsForResources": "route53-recovery-readiness:ListTagsForResources", "TagResource": "route53-recovery-readiness:TagResource", "UntagResource": "route53-recovery-readiness:UntagResource", "UpdateCell": "route53-recovery-readiness:UpdateCell", "UpdateReadinessCheck": "route53-recovery-readiness:UpdateReadinessCheck", "UpdateRecoveryGroup": "route53-recovery-readiness:UpdateRecoveryGroup", "UpdateResourceSet": "route53-recovery-readiness:UpdateResourceSet" }, "route53domains": { "AcceptDomainTransferFromAnotherAwsAccount": "route53domains:AcceptDomainTransferFromAnotherAwsAccount", "CancelDomainTransferToAnotherAwsAccount": "route53domains:CancelDomainTransferToAnotherAwsAccount", "CheckDomainAvailability": "route53domains:CheckDomainAvailability", "CheckDomainTransferability": "route53domains:CheckDomainTransferability", "DeleteDomain": "route53domains:DeleteDomain", "DeleteTagsForDomain": "route53domains:DeleteTagsForDomain", "DisableDomainAutoRenew": "route53domains:DisableDomainAutoRenew", "DisableDomainTransferLock": "route53domains:DisableDomainTransferLock", "EnableDomainAutoRenew": "route53domains:EnableDomainAutoRenew", "EnableDomainTransferLock": "route53domains:EnableDomainTransferLock", "GetContactReachabilityStatus": "route53domains:GetContactReachabilityStatus", "GetDomainDetail": "route53domains:GetDomainDetail", "GetDomainSuggestions": "route53domains:GetDomainSuggestions", "GetOperationDetail": "route53domains:GetOperationDetail", "ListDomains": "route53domains:ListDomains", "ListOperations": "route53domains:ListOperations", "ListPrices": "route53domains:ListPrices", "ListTagsForDomain": "route53domains:ListTagsForDomain", "RegisterDomain": "route53domains:RegisterDomain", "RejectDomainTransferFromAnotherAwsAccount": "route53domains:RejectDomainTransferFromAnotherAwsAccount", "RenewDomain": "route53domains:RenewDomain", "ResendContactReachabilityEmail": "route53domains:ResendContactReachabilityEmail", "RetrieveDomainAuthCode": "route53domains:RetrieveDomainAuthCode", "TransferDomain": "route53domains:TransferDomain", "TransferDomainToAnotherAwsAccount": "route53domains:TransferDomainToAnotherAwsAccount", "UpdateDomainContact": "route53domains:UpdateDomainContact", "UpdateDomainContactPrivacy": "route53domains:UpdateDomainContactPrivacy", "UpdateDomainNameservers": "route53domains:UpdateDomainNameservers", "UpdateTagsForDomain": "route53domains:UpdateTagsForDomain", "ViewBilling": "route53domains:ViewBilling" }, "route53resolver": { "AssociateFirewallRuleGroup": "route53resolver:AssociateFirewallRuleGroup", "AssociateResolverEndpointIpAddress": "route53resolver:AssociateResolverEndpointIpAddress", "AssociateResolverQueryLogConfig": "route53resolver:AssociateResolverQueryLogConfig", "AssociateResolverRule": "route53resolver:AssociateResolverRule", "CreateFirewallDomainList": "route53resolver:CreateFirewallDomainList", "CreateFirewallRule": "route53resolver:CreateFirewallRule", "CreateFirewallRuleGroup": "route53resolver:CreateFirewallRuleGroup", "CreateResolverEndpoint": "route53resolver:CreateResolverEndpoint", "CreateResolverQueryLogConfig": "route53resolver:CreateResolverQueryLogConfig", "CreateResolverRule": "route53resolver:CreateResolverRule", "DeleteFirewallDomainList": "route53resolver:DeleteFirewallDomainList", "DeleteFirewallRule": "route53resolver:DeleteFirewallRule", "DeleteFirewallRuleGroup": "route53resolver:DeleteFirewallRuleGroup", "DeleteResolverEndpoint": "route53resolver:DeleteResolverEndpoint", "DeleteResolverQueryLogConfig": "route53resolver:DeleteResolverQueryLogConfig", "DeleteResolverRule": "route53resolver:DeleteResolverRule", "DisassociateFirewallRuleGroup": "route53resolver:DisassociateFirewallRuleGroup", "DisassociateResolverEndpointIpAddress": "route53resolver:DisassociateResolverEndpointIpAddress", "DisassociateResolverQueryLogConfig": "route53resolver:DisassociateResolverQueryLogConfig", "DisassociateResolverRule": "route53resolver:DisassociateResolverRule", "GetFirewallConfig": "route53resolver:GetFirewallConfig", "GetFirewallDomainList": "route53resolver:GetFirewallDomainList", "GetFirewallRuleGroup": "route53resolver:GetFirewallRuleGroup", "GetFirewallRuleGroupAssociation": "route53resolver:GetFirewallRuleGroupAssociation", "GetFirewallRuleGroupPolicy": "route53resolver:GetFirewallRuleGroupPolicy", "GetResolverConfig": "route53resolver:GetResolverConfig", "GetResolverDnssecConfig": "route53resolver:GetResolverDnssecConfig", "GetResolverEndpoint": "route53resolver:GetResolverEndpoint", "GetResolverQueryLogConfig": "route53resolver:GetResolverQueryLogConfig", "GetResolverQueryLogConfigAssociation": "route53resolver:GetResolverQueryLogConfigAssociation", "GetResolverQueryLogConfigPolicy": "route53resolver:GetResolverQueryLogConfigPolicy", "GetResolverRule": "route53resolver:GetResolverRule", "GetResolverRuleAssociation": "route53resolver:GetResolverRuleAssociation", "GetResolverRulePolicy": "route53resolver:GetResolverRulePolicy", "ImportFirewallDomains": "route53resolver:ImportFirewallDomains", "ListFirewallConfigs": "route53resolver:ListFirewallConfigs", "ListFirewallDomainLists": "route53resolver:ListFirewallDomainLists", "ListFirewallDomains": "route53resolver:ListFirewallDomains", "ListFirewallRuleGroupAssociations": "route53resolver:ListFirewallRuleGroupAssociations", "ListFirewallRuleGroups": "route53resolver:ListFirewallRuleGroups", "ListFirewallRules": "route53resolver:ListFirewallRules", "ListResolverConfigs": "route53resolver:ListResolverConfigs", "ListResolverDnssecConfigs": "route53resolver:ListResolverDnssecConfigs", "ListResolverEndpointIpAddresses": "route53resolver:ListResolverEndpointIpAddresses", "ListResolverEndpoints": "route53resolver:ListResolverEndpoints", "ListResolverQueryLogConfigAssociations": "route53resolver:ListResolverQueryLogConfigAssociations", "ListResolverQueryLogConfigs": "route53resolver:ListResolverQueryLogConfigs", "ListResolverRuleAssociations": "route53resolver:ListResolverRuleAssociations", "ListResolverRules": "route53resolver:ListResolverRules", "ListTagsForResource": "route53resolver:ListTagsForResource", "PutFirewallRuleGroupPolicy": "route53resolver:PutFirewallRuleGroupPolicy", "PutResolverQueryLogConfigPolicy": "route53resolver:PutResolverQueryLogConfigPolicy", "PutResolverRulePolicy": "route53resolver:PutResolverRulePolicy", "TagResource": "route53resolver:TagResource", "UntagResource": "route53resolver:UntagResource", "UpdateFirewallConfig": "route53resolver:UpdateFirewallConfig", "UpdateFirewallDomains": "route53resolver:UpdateFirewallDomains", "UpdateFirewallRule": "route53resolver:UpdateFirewallRule", "UpdateFirewallRuleGroupAssociation": "route53resolver:UpdateFirewallRuleGroupAssociation", "UpdateResolverConfig": "route53resolver:UpdateResolverConfig", "UpdateResolverDnssecConfig": "route53resolver:UpdateResolverDnssecConfig", "UpdateResolverEndpoint": "route53resolver:UpdateResolverEndpoint", "UpdateResolverRule": "route53resolver:UpdateResolverRule" }, "rum": { "CreateAppMonitor": "rum:CreateAppMonitor", "DeleteAppMonitor": "rum:DeleteAppMonitor", "GetAppMonitor": "rum:GetAppMonitor", "GetAppMonitorData": "rum:GetAppMonitorData", "ListAppMonitors": "rum:ListAppMonitors", "ListTagsForResource": "rum:ListTagsForResource", "PutRumEvents": "rum:PutRumEvents", "TagResource": "rum:TagResource", "UntagResource": "rum:UntagResource", "UpdateAppMonitor": "rum:UpdateAppMonitor" }, "s3": { "AbortMultipartUpload": "s3:AbortMultipartUpload", "CompleteMultipartUpload": "s3:PutObject", "CreateBucket": "s3:CreateBucket", "CreateMultipartUpload": "s3:PutObject", "DeleteBucket": "s3:DeleteBucket", "DeleteBucketPolicy": "s3:DeleteBucketPolicy", "DeleteBucketWebsite": "s3:DeleteBucketWebsite", "DeleteObject": "s3:DeleteObject", "DeleteObjectTagging": "s3:DeleteObjectTagging", "GetBucketAcl": "s3:GetBucketAcl", "GetBucketLocation": "s3:GetBucketLocation", "GetBucketLogging": "s3:GetBucketLogging", "GetBucketNotification": "s3:GetBucketNotification", "GetBucketOwnershipControls": "s3:GetBucketOwnershipControls", "GetBucketPolicy": "s3:GetBucketPolicy", "GetBucketPolicyStatus": "s3:GetBucketPolicyStatus", "GetBucketRequestPayment": "s3:GetBucketRequestPayment", "GetBucketTagging": "s3:GetBucketTagging", "GetBucketVersioning": "s3:GetBucketVersioning", "GetBucketWebsite": "s3:GetBucketWebsite", "GetObject": "s3:GetObject", "GetObjectAcl": "s3:GetObjectAcl", "GetObjectLegalHold": "s3:GetObjectLegalHold", "GetObjectRetention": "s3:GetObjectRetention", "GetObjectTagging": "s3:GetObjectTagging", "GetObjectTorrent": "s3:GetObjectTorrent", "HeadObject": "s3:GetObject", "ListBuckets": "s3:ListAllMyBuckets", "ListObjects": "s3:ListBucket", "ListObjectsV2": "s3:ListBucket", "PutBucketAcl": "s3:PutBucketAcl", "PutBucketLogging": "s3:PutBucketLogging", "PutBucketNotification": "s3:PutBucketNotification", "PutBucketOwnershipControls": "s3:PutBucketOwnershipControls", "PutBucketPolicy": "s3:PutBucketPolicy", "PutBucketRequestPayment": "s3:PutBucketRequestPayment", "PutBucketTagging": "s3:PutBucketTagging", "PutBucketVersioning": "s3:PutBucketVersioning", "PutBucketWebsite": "s3:PutBucketWebsite", "PutObject": "s3:PutObject", "PutObjectAcl": "s3:PutObjectAcl", "PutObjectLegalHold": "s3:PutObjectLegalHold", "PutObjectRetention": "s3:PutObjectRetention", "PutObjectTagging": "s3:PutObjectTagging", "RestoreObject": "s3:RestoreObject", "UploadPart": "s3:PutObject" }, "sagemaker": { "AddAssociation": "sagemaker:AddAssociation", "AddTags": "sagemaker:AddTags", "AssociateTrialComponent": "sagemaker:AssociateTrialComponent", "BatchDescribeModelPackage": "sagemaker:BatchDescribeModelPackage", "CreateAction": "sagemaker:CreateAction", "CreateAlgorithm": "sagemaker:CreateAlgorithm", "CreateApp": "sagemaker:CreateApp", "CreateAppImageConfig": "sagemaker:CreateAppImageConfig", "CreateArtifact": "sagemaker:CreateArtifact", "CreateAutoMLJob": "sagemaker:CreateAutoMLJob", "CreateCodeRepository": "sagemaker:CreateCodeRepository", "CreateCompilationJob": "sagemaker:CreateCompilationJob", "CreateContext": "sagemaker:CreateContext", "CreateDataQualityJobDefinition": "sagemaker:CreateDataQualityJobDefinition", "CreateDeviceFleet": "sagemaker:CreateDeviceFleet", "CreateDomain": "sagemaker:CreateDomain", "CreateEdgePackagingJob": "sagemaker:CreateEdgePackagingJob", "CreateEndpoint": "sagemaker:CreateEndpoint", "CreateEndpointConfig": "sagemaker:CreateEndpointConfig", "CreateExperiment": "sagemaker:CreateExperiment", "CreateFeatureGroup": "sagemaker:CreateFeatureGroup", "CreateFlowDefinition": "sagemaker:CreateFlowDefinition", "CreateHumanTaskUi": "sagemaker:CreateHumanTaskUi", "CreateHyperParameterTuningJob": "sagemaker:CreateHyperParameterTuningJob", "CreateImage": "sagemaker:CreateImage", "CreateImageVersion": "sagemaker:CreateImageVersion", "CreateInferenceRecommendationsJob": "sagemaker:CreateInferenceRecommendationsJob", "CreateLabelingJob": "sagemaker:CreateLabelingJob", "CreateModel": "sagemaker:CreateModel", "CreateModelBiasJobDefinition": "sagemaker:CreateModelBiasJobDefinition", "CreateModelExplainabilityJobDefinition": "sagemaker:CreateModelExplainabilityJobDefinition", "CreateModelPackage": "sagemaker:CreateModelPackage", "CreateModelPackageGroup": "sagemaker:CreateModelPackageGroup", "CreateModelQualityJobDefinition": "sagemaker:CreateModelQualityJobDefinition", "CreateMonitoringSchedule": "sagemaker:CreateMonitoringSchedule", "CreateNotebookInstance": "sagemaker:CreateNotebookInstance", "CreateNotebookInstanceLifecycleConfig": "sagemaker:CreateNotebookInstanceLifecycleConfig", "CreatePipeline": "sagemaker:CreatePipeline", "CreatePresignedDomainUrl": "sagemaker:CreatePresignedDomainUrl", "CreatePresignedNotebookInstanceUrl": "sagemaker:CreatePresignedNotebookInstanceUrl", "CreateProcessingJob": "sagemaker:CreateProcessingJob", "CreateProject": "sagemaker:CreateProject", "CreateTrainingJob": "sagemaker:CreateTrainingJob", "CreateTransformJob": "sagemaker:CreateTransformJob", "CreateTrial": "sagemaker:CreateTrial", "CreateTrialComponent": "sagemaker:CreateTrialComponent", "CreateUserProfile": "sagemaker:CreateUserProfile", "CreateWorkforce": "sagemaker:CreateWorkforce", "CreateWorkteam": "sagemaker:CreateWorkteam", "DeleteAction": "sagemaker:DeleteAction", "DeleteAlgorithm": "sagemaker:DeleteAlgorithm", "DeleteApp": "sagemaker:DeleteApp", "DeleteAppImageConfig": "sagemaker:DeleteAppImageConfig", "DeleteArtifact": "sagemaker:DeleteArtifact", "DeleteAssociation": "sagemaker:DeleteAssociation", "DeleteCodeRepository": "sagemaker:DeleteCodeRepository", "DeleteContext": "sagemaker:DeleteContext", "DeleteDataQualityJobDefinition": "sagemaker:DeleteDataQualityJobDefinition", "DeleteDeviceFleet": "sagemaker:DeleteDeviceFleet", "DeleteDomain": "sagemaker:DeleteDomain", "DeleteEndpoint": "sagemaker:DeleteEndpoint", "DeleteEndpointConfig": "sagemaker:DeleteEndpointConfig", "DeleteExperiment": "sagemaker:DeleteExperiment", "DeleteFeatureGroup": "sagemaker:DeleteFeatureGroup", "DeleteFlowDefinition": "sagemaker:DeleteFlowDefinition", "DeleteHumanTaskUi": "sagemaker:DeleteHumanTaskUi", "DeleteImage": "sagemaker:DeleteImage", "DeleteImageVersion": "sagemaker:DeleteImageVersion", "DeleteModel": "sagemaker:DeleteModel", "DeleteModelBiasJobDefinition": "sagemaker:DeleteModelBiasJobDefinition", "DeleteModelExplainabilityJobDefinition": "sagemaker:DeleteModelExplainabilityJobDefinition", "DeleteModelPackage": "sagemaker:DeleteModelPackage", "DeleteModelPackageGroup": "sagemaker:DeleteModelPackageGroup", "DeleteModelPackageGroupPolicy": "sagemaker:DeleteModelPackageGroupPolicy", "DeleteModelQualityJobDefinition": "sagemaker:DeleteModelQualityJobDefinition", "DeleteMonitoringSchedule": "sagemaker:DeleteMonitoringSchedule", "DeleteNotebookInstance": "sagemaker:DeleteNotebookInstance", "DeleteNotebookInstanceLifecycleConfig": "sagemaker:DeleteNotebookInstanceLifecycleConfig", "DeletePipeline": "sagemaker:DeletePipeline", "DeleteProject": "sagemaker:DeleteProject", "DeleteTags": "sagemaker:DeleteTags", "DeleteTrial": "sagemaker:DeleteTrial", "DeleteTrialComponent": "sagemaker:DeleteTrialComponent", "DeleteUserProfile": "sagemaker:DeleteUserProfile", "DeleteWorkforce": "sagemaker:DeleteWorkforce", "DeleteWorkteam": "sagemaker:DeleteWorkteam", "DeregisterDevices": "sagemaker:DeregisterDevices", "DescribeAction": "sagemaker:DescribeAction", "DescribeAlgorithm": "sagemaker:DescribeAlgorithm", "DescribeApp": "sagemaker:DescribeApp", "DescribeAppImageConfig": "sagemaker:DescribeAppImageConfig", "DescribeArtifact": "sagemaker:DescribeArtifact", "DescribeAutoMLJob": "sagemaker:DescribeAutoMLJob", "DescribeCodeRepository": "sagemaker:DescribeCodeRepository", "DescribeCompilationJob": "sagemaker:DescribeCompilationJob", "DescribeContext": "sagemaker:DescribeContext", "DescribeDataQualityJobDefinition": "sagemaker:DescribeDataQualityJobDefinition", "DescribeDevice": "sagemaker:DescribeDevice", "DescribeDeviceFleet": "sagemaker:DescribeDeviceFleet", "DescribeDomain": "sagemaker:DescribeDomain", "DescribeEdgePackagingJob": "sagemaker:DescribeEdgePackagingJob", "DescribeEndpoint": "sagemaker:DescribeEndpoint", "DescribeEndpointConfig": "sagemaker:DescribeEndpointConfig", "DescribeExperiment": "sagemaker:DescribeExperiment", "DescribeFeatureGroup": "sagemaker:DescribeFeatureGroup", "DescribeFlowDefinition": "sagemaker:DescribeFlowDefinition", "DescribeHumanTaskUi": "sagemaker:DescribeHumanTaskUi", "DescribeHyperParameterTuningJob": "sagemaker:DescribeHyperParameterTuningJob", "DescribeImage": "sagemaker:DescribeImage", "DescribeImageVersion": "sagemaker:DescribeImageVersion", "DescribeInferenceRecommendationsJob": "sagemaker:DescribeInferenceRecommendationsJob", "DescribeLabelingJob": "sagemaker:DescribeLabelingJob", "DescribeLineageGroup": "sagemaker:DescribeLineageGroup", "DescribeModel": "sagemaker:DescribeModel", "DescribeModelBiasJobDefinition": "sagemaker:DescribeModelBiasJobDefinition", "DescribeModelExplainabilityJobDefinition": "sagemaker:DescribeModelExplainabilityJobDefinition", "DescribeModelPackage": "sagemaker:DescribeModelPackage", "DescribeModelPackageGroup": "sagemaker:DescribeModelPackageGroup", "DescribeModelQualityJobDefinition": "sagemaker:DescribeModelQualityJobDefinition", "DescribeMonitoringSchedule": "sagemaker:DescribeMonitoringSchedule", "DescribeNotebookInstance": "sagemaker:DescribeNotebookInstance", "DescribeNotebookInstanceLifecycleConfig": "sagemaker:DescribeNotebookInstanceLifecycleConfig", "DescribePipeline": "sagemaker:DescribePipeline", "DescribePipelineDefinitionForExecution": "sagemaker:DescribePipelineDefinitionForExecution", "DescribePipelineExecution": "sagemaker:DescribePipelineExecution", "DescribeProcessingJob": "sagemaker:DescribeProcessingJob", "DescribeProject": "sagemaker:DescribeProject", "DescribeSubscribedWorkteam": "sagemaker:DescribeSubscribedWorkteam", "DescribeTrainingJob": "sagemaker:DescribeTrainingJob", "DescribeTransformJob": "sagemaker:DescribeTransformJob", "DescribeTrial": "sagemaker:DescribeTrial", "DescribeTrialComponent": "sagemaker:DescribeTrialComponent", "DescribeUserProfile": "sagemaker:DescribeUserProfile", "DescribeWorkforce": "sagemaker:DescribeWorkforce", "DescribeWorkteam": "sagemaker:DescribeWorkteam", "DisableSagemakerServicecatalogPortfolio": "sagemaker:DisableSagemakerServicecatalogPortfolio", "DisassociateTrialComponent": "sagemaker:DisassociateTrialComponent", "EnableSagemakerServicecatalogPortfolio": "sagemaker:EnableSagemakerServicecatalogPortfolio", "GetDeviceFleetReport": "sagemaker:GetDeviceFleetReport", "GetLineageGroupPolicy": "sagemaker:GetLineageGroupPolicy", "GetModelPackageGroupPolicy": "sagemaker:GetModelPackageGroupPolicy", "GetSagemakerServicecatalogPortfolioStatus": "sagemaker:GetSagemakerServicecatalogPortfolioStatus", "GetSearchSuggestions": "sagemaker:GetSearchSuggestions", "ListActions": "sagemaker:ListActions", "ListAlgorithms": "sagemaker:ListAlgorithms", "ListAppImageConfigs": "sagemaker:ListAppImageConfigs", "ListApps": "sagemaker:ListApps", "ListArtifacts": "sagemaker:ListArtifacts", "ListAssociations": "sagemaker:ListAssociations", "ListAutoMLJobs": "sagemaker:ListAutoMLJobs", "ListCandidatesForAutoMLJob": "sagemaker:ListCandidatesForAutoMLJob", "ListCodeRepositories": "sagemaker:ListCodeRepositories", "ListCompilationJobs": "sagemaker:ListCompilationJobs", "ListContexts": "sagemaker:ListContexts", "ListDataQualityJobDefinitions": "sagemaker:ListDataQualityJobDefinitions", "ListDeviceFleets": "sagemaker:ListDeviceFleets", "ListDevices": "sagemaker:ListDevices", "ListDomains": "sagemaker:ListDomains", "ListEdgePackagingJobs": "sagemaker:ListEdgePackagingJobs", "ListEndpointConfigs": "sagemaker:ListEndpointConfigs", "ListEndpoints": "sagemaker:ListEndpoints", "ListExperiments": "sagemaker:ListExperiments", "ListFeatureGroups": "sagemaker:ListFeatureGroups", "ListFlowDefinitions": "sagemaker:ListFlowDefinitions", "ListHumanTaskUis": "sagemaker:ListHumanTaskUis", "ListHyperParameterTuningJobs": "sagemaker:ListHyperParameterTuningJobs", "ListImageVersions": "sagemaker:ListImageVersions", "ListImages": "sagemaker:ListImages", "ListInferenceRecommendationsJobs": "sagemaker:ListInferenceRecommendationsJobs", "ListLabelingJobs": "sagemaker:ListLabelingJobs", "ListLabelingJobsForWorkteam": "sagemaker:ListLabelingJobsForWorkteam", "ListLineageGroups": "sagemaker:ListLineageGroups", "ListModelBiasJobDefinitions": "sagemaker:ListModelBiasJobDefinitions", "ListModelExplainabilityJobDefinitions": "sagemaker:ListModelExplainabilityJobDefinitions", "ListModelMetadata": "sagemaker:ListModelMetadata", "ListModelPackageGroups": "sagemaker:ListModelPackageGroups", "ListModelPackages": "sagemaker:ListModelPackages", "ListModelQualityJobDefinitions": "sagemaker:ListModelQualityJobDefinitions", "ListModels": "sagemaker:ListModels", "ListMonitoringExecutions": "sagemaker:ListMonitoringExecutions", "ListMonitoringSchedules": "sagemaker:ListMonitoringSchedules", "ListNotebookInstanceLifecycleConfigs": "sagemaker:ListNotebookInstanceLifecycleConfigs", "ListNotebookInstances": "sagemaker:ListNotebookInstances", "ListPipelineExecutionSteps": "sagemaker:ListPipelineExecutionSteps", "ListPipelineExecutions": "sagemaker:ListPipelineExecutions", "ListPipelineParametersForExecution": "sagemaker:ListPipelineParametersForExecution", "ListPipelines": "sagemaker:ListPipelines", "ListProcessingJobs": "sagemaker:ListProcessingJobs", "ListProjects": "sagemaker:ListProjects", "ListSubscribedWorkteams": "sagemaker:ListSubscribedWorkteams", "ListTags": "sagemaker:ListTags", "ListTrainingJobs": "sagemaker:ListTrainingJobs", "ListTrainingJobsForHyperParameterTuningJob": "sagemaker:ListTrainingJobsForHyperParameterTuningJob", "ListTransformJobs": "sagemaker:ListTransformJobs", "ListTrialComponents": "sagemaker:ListTrialComponents", "ListTrials": "sagemaker:ListTrials", "ListUserProfiles": "sagemaker:ListUserProfiles", "ListWorkforces": "sagemaker:ListWorkforces", "ListWorkteams": "sagemaker:ListWorkteams", "PutModelPackageGroupPolicy": "sagemaker:PutModelPackageGroupPolicy", "QueryLineage": "sagemaker:QueryLineage", "RegisterDevices": "sagemaker:RegisterDevices", "RenderUiTemplate": "sagemaker:RenderUiTemplate", "Search": "sagemaker:Search", "SendPipelineExecutionStepFailure": "sagemaker:SendPipelineExecutionStepFailure", "SendPipelineExecutionStepSuccess": "sagemaker:SendPipelineExecutionStepSuccess", "StartMonitoringSchedule": "sagemaker:StartMonitoringSchedule", "StartNotebookInstance": "sagemaker:StartNotebookInstance", "StartPipelineExecution": "sagemaker:StartPipelineExecution", "StopAutoMLJob": "sagemaker:StopAutoMLJob", "StopCompilationJob": "sagemaker:StopCompilationJob", "StopEdgePackagingJob": "sagemaker:StopEdgePackagingJob", "StopHyperParameterTuningJob": "sagemaker:StopHyperParameterTuningJob", "StopInferenceRecommendationsJob": "sagemaker:StopInferenceRecommendationsJob", "StopLabelingJob": "sagemaker:StopLabelingJob", "StopMonitoringSchedule": "sagemaker:StopMonitoringSchedule", "StopNotebookInstance": "sagemaker:StopNotebookInstance", "StopPipelineExecution": "sagemaker:StopPipelineExecution", "StopProcessingJob": "sagemaker:StopProcessingJob", "StopTrainingJob": "sagemaker:StopTrainingJob", "StopTransformJob": "sagemaker:StopTransformJob", "UpdateAction": "sagemaker:UpdateAction", "UpdateAppImageConfig": "sagemaker:UpdateAppImageConfig", "UpdateArtifact": "sagemaker:UpdateArtifact", "UpdateCodeRepository": "sagemaker:UpdateCodeRepository", "UpdateContext": "sagemaker:UpdateContext", "UpdateDeviceFleet": "sagemaker:UpdateDeviceFleet", "UpdateDevices": "sagemaker:UpdateDevices", "UpdateDomain": "sagemaker:UpdateDomain", "UpdateEndpoint": "sagemaker:UpdateEndpoint", "UpdateEndpointWeightsAndCapacities": "sagemaker:UpdateEndpointWeightsAndCapacities", "UpdateExperiment": "sagemaker:UpdateExperiment", "UpdateImage": "sagemaker:UpdateImage", "UpdateModelPackage": "sagemaker:UpdateModelPackage", "UpdateMonitoringSchedule": "sagemaker:UpdateMonitoringSchedule", "UpdateNotebookInstance": "sagemaker:UpdateNotebookInstance", "UpdateNotebookInstanceLifecycleConfig": "sagemaker:UpdateNotebookInstanceLifecycleConfig", "UpdatePipeline": "sagemaker:UpdatePipeline", "UpdatePipelineExecution": "sagemaker:UpdatePipelineExecution", "UpdateProject": "sagemaker:UpdateProject", "UpdateTrainingJob": "sagemaker:UpdateTrainingJob", "UpdateTrial": "sagemaker:UpdateTrial", "UpdateTrialComponent": "sagemaker:UpdateTrialComponent", "UpdateUserProfile": "sagemaker:UpdateUserProfile", "UpdateWorkforce": "sagemaker:UpdateWorkforce", "UpdateWorkteam": "sagemaker:UpdateWorkteam" }, "sagemaker-runtime": { "InvokeEndpoint": "sagemaker:InvokeEndpoint", "InvokeEndpointAsync": "sagemaker:InvokeEndpointAsync" }, "savingsplans": { "CreateSavingsPlan": "savingsplans:CreateSavingsPlan", "DeleteQueuedSavingsPlan": "savingsplans:DeleteQueuedSavingsPlan", "DescribeSavingsPlanRates": "savingsplans:DescribeSavingsPlanRates", "DescribeSavingsPlans": "savingsplans:DescribeSavingsPlans", "DescribeSavingsPlansOfferingRates": "savingsplans:DescribeSavingsPlansOfferingRates", "DescribeSavingsPlansOfferings": "savingsplans:DescribeSavingsPlansOfferings", "ListTagsForResource": "savingsplans:ListTagsForResource", "TagResource": "savingsplans:TagResource", "UntagResource": "savingsplans:UntagResource" }, "schemas": { "CreateDiscoverer": "schemas:CreateDiscoverer", "CreateRegistry": "schemas:CreateRegistry", "CreateSchema": "schemas:CreateSchema", "DeleteDiscoverer": "schemas:DeleteDiscoverer", "DeleteRegistry": "schemas:DeleteRegistry", "DeleteResourcePolicy": "schemas:DeleteResourcePolicy", "DeleteSchema": "schemas:DeleteSchema", "DeleteSchemaVersion": "schemas:DeleteSchemaVersion", "DescribeCodeBinding": "schemas:DescribeCodeBinding", "DescribeDiscoverer": "schemas:DescribeDiscoverer", "DescribeRegistry": "schemas:DescribeRegistry", "DescribeSchema": "schemas:DescribeSchema", "ExportSchema": "schemas:ExportSchema", "GetCodeBindingSource": "schemas:GetCodeBindingSource", "GetDiscoveredSchema": "schemas:GetDiscoveredSchema", "GetResourcePolicy": "schemas:GetResourcePolicy", "ListDiscoverers": "schemas:ListDiscoverers", "ListRegistries": "schemas:ListRegistries", "ListSchemaVersions": "schemas:ListSchemaVersions", "ListSchemas": "schemas:ListSchemas", "ListTagsForResource": "schemas:ListTagsForResource", "PutCodeBinding": "schemas:PutCodeBinding", "PutResourcePolicy": "schemas:PutResourcePolicy", "SearchSchemas": "schemas:SearchSchemas", "StartDiscoverer": "schemas:StartDiscoverer", "StopDiscoverer": "schemas:StopDiscoverer", "TagResource": "schemas:TagResource", "UntagResource": "schemas:UntagResource", "UpdateDiscoverer": "schemas:UpdateDiscoverer", "UpdateRegistry": "schemas:UpdateRegistry", "UpdateSchema": "schemas:UpdateSchema" }, "sdb": { "BatchDeleteAttributes": "sdb:BatchDeleteAttributes", "BatchPutAttributes": "sdb:BatchPutAttributes", "CreateDomain": "sdb:CreateDomain", "DeleteAttributes": "sdb:DeleteAttributes", "DeleteDomain": "sdb:DeleteDomain", "DomainMetadata": "sdb:DomainMetadata", "GetAttributes": "sdb:GetAttributes", "ListDomains": "sdb:ListDomains", "PutAttributes": "sdb:PutAttributes", "Select": "sdb:Select" }, "secretsmanager": { "CancelRotateSecret": "secretsmanager:CancelRotateSecret", "CreateSecret": "secretsmanager:CreateSecret", "DeleteResourcePolicy": "secretsmanager:DeleteResourcePolicy", "DeleteSecret": "secretsmanager:DeleteSecret", "DescribeSecret": "secretsmanager:DescribeSecret", "GetRandomPassword": "secretsmanager:GetRandomPassword", "GetResourcePolicy": "secretsmanager:GetResourcePolicy", "GetSecretValue": "secretsmanager:GetSecretValue", "ListSecretVersionIds": "secretsmanager:ListSecretVersionIds", "ListSecrets": "secretsmanager:ListSecrets", "PutResourcePolicy": "secretsmanager:PutResourcePolicy", "PutSecretValue": "secretsmanager:PutSecretValue", "RemoveRegionsFromReplication": "secretsmanager:RemoveRegionsFromReplication", "ReplicateSecretToRegions": "secretsmanager:ReplicateSecretToRegions", "RestoreSecret": "secretsmanager:RestoreSecret", "RotateSecret": "secretsmanager:RotateSecret", "StopReplicationToReplica": "secretsmanager:StopReplicationToReplica", "TagResource": "secretsmanager:TagResource", "UntagResource": "secretsmanager:UntagResource", "UpdateSecret": "secretsmanager:UpdateSecret", "UpdateSecretVersionStage": "secretsmanager:UpdateSecretVersionStage", "ValidateResourcePolicy": "secretsmanager:ValidateResourcePolicy" }, "securityhub": { "AcceptAdministratorInvitation": "securityhub:AcceptAdministratorInvitation", "AcceptInvitation": "securityhub:AcceptInvitation", "BatchDisableStandards": "securityhub:BatchDisableStandards", "BatchEnableStandards": "securityhub:BatchEnableStandards", "BatchImportFindings": "securityhub:BatchImportFindings", "BatchUpdateFindings": "securityhub:BatchUpdateFindings", "CreateActionTarget": "securityhub:CreateActionTarget", "CreateFindingAggregator": "securityhub:CreateFindingAggregator", "CreateInsight": "securityhub:CreateInsight", "CreateMembers": "securityhub:CreateMembers", "DeclineInvitations": "securityhub:DeclineInvitations", "DeleteActionTarget": "securityhub:DeleteActionTarget", "DeleteFindingAggregator": "securityhub:DeleteFindingAggregator", "DeleteInsight": "securityhub:DeleteInsight", "DeleteInvitations": "securityhub:DeleteInvitations", "DeleteMembers": "securityhub:DeleteMembers", "DescribeActionTargets": "securityhub:DescribeActionTargets", "DescribeHub": "securityhub:DescribeHub", "DescribeOrganizationConfiguration": "securityhub:DescribeOrganizationConfiguration", "DescribeProducts": "securityhub:DescribeProducts", "DescribeStandards": "securityhub:DescribeStandards", "DescribeStandardsControls": "securityhub:DescribeStandardsControls", "DisableImportFindingsForProduct": "securityhub:DisableImportFindingsForProduct", "DisableOrganizationAdminAccount": "securityhub:DisableOrganizationAdminAccount", "DisableSecurityHub": "securityhub:DisableSecurityHub", "DisassociateFromAdministratorAccount": "securityhub:DisassociateFromAdministratorAccount", "DisassociateFromMasterAccount": "securityhub:DisassociateFromMasterAccount", "DisassociateMembers": "securityhub:DisassociateMembers", "EnableImportFindingsForProduct": "securityhub:EnableImportFindingsForProduct", "EnableOrganizationAdminAccount": "securityhub:EnableOrganizationAdminAccount", "EnableSecurityHub": "securityhub:EnableSecurityHub", "GetAdministratorAccount": "securityhub:GetAdministratorAccount", "GetEnabledStandards": "securityhub:GetEnabledStandards", "GetFindingAggregator": "securityhub:GetFindingAggregator", "GetFindings": "securityhub:GetFindings", "GetInsightResults": "securityhub:GetInsightResults", "GetInsights": "securityhub:GetInsights", "GetInvitationsCount": "securityhub:GetInvitationsCount", "GetMasterAccount": "securityhub:GetMasterAccount", "GetMembers": "securityhub:GetMembers", "InviteMembers": "securityhub:InviteMembers", "ListEnabledProductsForImport": "securityhub:ListEnabledProductsForImport", "ListFindingAggregators": "securityhub:ListFindingAggregators", "ListInvitations": "securityhub:ListInvitations", "ListMembers": "securityhub:ListMembers", "ListOrganizationAdminAccounts": "securityhub:ListOrganizationAdminAccounts", "ListTagsForResource": "securityhub:ListTagsForResource", "TagResource": "securityhub:TagResource", "UntagResource": "securityhub:UntagResource", "UpdateActionTarget": "securityhub:UpdateActionTarget", "UpdateFindingAggregator": "securityhub:UpdateFindingAggregator", "UpdateFindings": "securityhub:UpdateFindings", "UpdateInsight": "securityhub:UpdateInsight", "UpdateOrganizationConfiguration": "securityhub:UpdateOrganizationConfiguration", "UpdateSecurityHubConfiguration": "securityhub:UpdateSecurityHubConfiguration", "UpdateStandardsControl": "securityhub:UpdateStandardsControl" }, "servicecatalog": { "AcceptPortfolioShare": "servicecatalog:AcceptPortfolioShare", "AssociateBudgetWithResource": "servicecatalog:AssociateBudgetWithResource", "AssociatePrincipalWithPortfolio": "servicecatalog:AssociatePrincipalWithPortfolio", "AssociateProductWithPortfolio": "servicecatalog:AssociateProductWithPortfolio", "AssociateServiceActionWithProvisioningArtifact": "servicecatalog:AssociateServiceActionWithProvisioningArtifact", "AssociateTagOptionWithResource": "servicecatalog:AssociateTagOptionWithResource", "BatchAssociateServiceActionWithProvisioningArtifact": "servicecatalog:BatchAssociateServiceActionWithProvisioningArtifact", "BatchDisassociateServiceActionFromProvisioningArtifact": "servicecatalog:BatchDisassociateServiceActionFromProvisioningArtifact", "CopyProduct": "servicecatalog:CopyProduct", "CreateConstraint": "servicecatalog:CreateConstraint", "CreatePortfolio": "servicecatalog:CreatePortfolio", "CreatePortfolioShare": "servicecatalog:CreatePortfolioShare", "CreateProduct": "servicecatalog:CreateProduct", "CreateProvisionedProductPlan": "servicecatalog:CreateProvisionedProductPlan", "CreateProvisioningArtifact": "servicecatalog:CreateProvisioningArtifact", "CreateServiceAction": "servicecatalog:CreateServiceAction", "CreateTagOption": "servicecatalog:CreateTagOption", "DeleteConstraint": "servicecatalog:DeleteConstraint", "DeletePortfolio": "servicecatalog:DeletePortfolio", "DeletePortfolioShare": "servicecatalog:DeletePortfolioShare", "DeleteProduct": "servicecatalog:DeleteProduct", "DeleteProvisionedProductPlan": "servicecatalog:DeleteProvisionedProductPlan", "DeleteProvisioningArtifact": "servicecatalog:DeleteProvisioningArtifact", "DeleteServiceAction": "servicecatalog:DeleteServiceAction", "DeleteTagOption": "servicecatalog:DeleteTagOption", "DescribeConstraint": "servicecatalog:DescribeConstraint", "DescribeCopyProductStatus": "servicecatalog:DescribeCopyProductStatus", "DescribePortfolio": "servicecatalog:DescribePortfolio", "DescribePortfolioShareStatus": "servicecatalog:DescribePortfolioShareStatus", "DescribePortfolioShares": "servicecatalog:DescribePortfolioShares", "DescribeProduct": "servicecatalog:DescribeProduct", "DescribeProductAsAdmin": "servicecatalog:DescribeProductAsAdmin", "DescribeProductView": "servicecatalog:DescribeProductView", "DescribeProvisionedProduct": "servicecatalog:DescribeProvisionedProduct", "DescribeProvisionedProductPlan": "servicecatalog:DescribeProvisionedProductPlan", "DescribeProvisioningArtifact": "servicecatalog:DescribeProvisioningArtifact", "DescribeProvisioningParameters": "servicecatalog:DescribeProvisioningParameters", "DescribeRecord": "servicecatalog:DescribeRecord", "DescribeServiceAction": "servicecatalog:DescribeServiceAction", "DescribeServiceActionExecutionParameters": "servicecatalog:DescribeServiceActionExecutionParameters", "DescribeTagOption": "servicecatalog:DescribeTagOption", "DisableAWSOrganizationsAccess": "servicecatalog:DisableAWSOrganizationsAccess", "DisassociateBudgetFromResource": "servicecatalog:DisassociateBudgetFromResource", "DisassociatePrincipalFromPortfolio": "servicecatalog:DisassociatePrincipalFromPortfolio", "DisassociateProductFromPortfolio": "servicecatalog:DisassociateProductFromPortfolio", "DisassociateServiceActionFromProvisioningArtifact": "servicecatalog:DisassociateServiceActionFromProvisioningArtifact", "DisassociateTagOptionFromResource": "servicecatalog:DisassociateTagOptionFromResource", "EnableAWSOrganizationsAccess": "servicecatalog:EnableAWSOrganizationsAccess", "ExecuteProvisionedProductPlan": "servicecatalog:ExecuteProvisionedProductPlan", "ExecuteProvisionedProductServiceAction": "servicecatalog:ExecuteProvisionedProductServiceAction", "GetAWSOrganizationsAccessStatus": "servicecatalog:GetAWSOrganizationsAccessStatus", "GetProvisionedProductOutputs": "servicecatalog:GetProvisionedProductOutputs", "ImportAsProvisionedProduct": "servicecatalog:ImportAsProvisionedProduct", "ListAcceptedPortfolioShares": "servicecatalog:ListAcceptedPortfolioShares", "ListBudgetsForResource": "servicecatalog:ListBudgetsForResource", "ListConstraintsForPortfolio": "servicecatalog:ListConstraintsForPortfolio", "ListLaunchPaths": "servicecatalog:ListLaunchPaths", "ListOrganizationPortfolioAccess": "servicecatalog:ListOrganizationPortfolioAccess", "ListPortfolioAccess": "servicecatalog:ListPortfolioAccess", "ListPortfolios": "servicecatalog:ListPortfolios", "ListPortfoliosForProduct": "servicecatalog:ListPortfoliosForProduct", "ListPrincipalsForPortfolio": "servicecatalog:ListPrincipalsForPortfolio", "ListProvisionedProductPlans": "servicecatalog:ListProvisionedProductPlans", "ListProvisioningArtifacts": "servicecatalog:ListProvisioningArtifacts", "ListProvisioningArtifactsForServiceAction": "servicecatalog:ListProvisioningArtifactsForServiceAction", "ListRecordHistory": "servicecatalog:ListRecordHistory", "ListResourcesForTagOption": "servicecatalog:ListResourcesForTagOption", "ListServiceActions": "servicecatalog:ListServiceActions", "ListServiceActionsForProvisioningArtifact": "servicecatalog:ListServiceActionsForProvisioningArtifact", "ListStackInstancesForProvisionedProduct": "servicecatalog:ListStackInstancesForProvisionedProduct", "ListTagOptions": "servicecatalog:ListTagOptions", "ProvisionProduct": "servicecatalog:ProvisionProduct", "RejectPortfolioShare": "servicecatalog:RejectPortfolioShare", "ScanProvisionedProducts": "servicecatalog:ScanProvisionedProducts", "SearchProducts": "servicecatalog:SearchProducts", "SearchProductsAsAdmin": "servicecatalog:SearchProductsAsAdmin", "SearchProvisionedProducts": "servicecatalog:SearchProvisionedProducts", "TerminateProvisionedProduct": "servicecatalog:TerminateProvisionedProduct", "UpdateConstraint": "servicecatalog:UpdateConstraint", "UpdatePortfolio": "servicecatalog:UpdatePortfolio", "UpdatePortfolioShare": "servicecatalog:UpdatePortfolioShare", "UpdateProduct": "servicecatalog:UpdateProduct", "UpdateProvisionedProduct": "servicecatalog:UpdateProvisionedProduct", "UpdateProvisionedProductProperties": "servicecatalog:UpdateProvisionedProductProperties", "UpdateProvisioningArtifact": "servicecatalog:UpdateProvisioningArtifact", "UpdateServiceAction": "servicecatalog:UpdateServiceAction", "UpdateTagOption": "servicecatalog:UpdateTagOption" }, "ses": { "CloneReceiptRuleSet": "ses:CloneReceiptRuleSet", "CreateConfigurationSet": "ses:CreateConfigurationSet", "CreateConfigurationSetEventDestination": "ses:CreateConfigurationSetEventDestination", "CreateConfigurationSetTrackingOptions": "ses:CreateConfigurationSetTrackingOptions", "CreateCustomVerificationEmailTemplate": "ses:CreateCustomVerificationEmailTemplate", "CreateReceiptFilter": "ses:CreateReceiptFilter", "CreateReceiptRule": "ses:CreateReceiptRule", "CreateReceiptRuleSet": "ses:CreateReceiptRuleSet", "CreateTemplate": "ses:CreateTemplate", "DeleteConfigurationSet": "ses:DeleteConfigurationSet", "DeleteConfigurationSetEventDestination": "ses:DeleteConfigurationSetEventDestination", "DeleteConfigurationSetTrackingOptions": "ses:DeleteConfigurationSetTrackingOptions", "DeleteCustomVerificationEmailTemplate": "ses:DeleteCustomVerificationEmailTemplate", "DeleteIdentity": "ses:DeleteIdentity", "DeleteIdentityPolicy": "ses:DeleteIdentityPolicy", "DeleteReceiptFilter": "ses:DeleteReceiptFilter", "DeleteReceiptRule": "ses:DeleteReceiptRule", "DeleteReceiptRuleSet": "ses:DeleteReceiptRuleSet", "DeleteTemplate": "ses:DeleteTemplate", "DeleteVerifiedEmailAddress": "ses:DeleteVerifiedEmailAddress", "DescribeActiveReceiptRuleSet": "ses:DescribeActiveReceiptRuleSet", "DescribeConfigurationSet": "ses:DescribeConfigurationSet", "DescribeReceiptRule": "ses:DescribeReceiptRule", "DescribeReceiptRuleSet": "ses:DescribeReceiptRuleSet", "GetAccountSendingEnabled": "ses:GetAccountSendingEnabled", "GetCustomVerificationEmailTemplate": "ses:GetCustomVerificationEmailTemplate", "GetIdentityDkimAttributes": "ses:GetIdentityDkimAttributes", "GetIdentityMailFromDomainAttributes": "ses:GetIdentityMailFromDomainAttributes", "GetIdentityNotificationAttributes": "ses:GetIdentityNotificationAttributes", "GetIdentityPolicies": "ses:GetIdentityPolicies", "GetIdentityVerificationAttributes": "ses:GetIdentityVerificationAttributes", "GetSendQuota": "ses:GetSendQuota", "GetSendStatistics": "ses:GetSendStatistics", "GetTemplate": "ses:GetTemplate", "ListConfigurationSets": "ses:ListConfigurationSets", "ListCustomVerificationEmailTemplates": "ses:ListCustomVerificationEmailTemplates", "ListIdentities": "ses:ListIdentities", "ListIdentityPolicies": "ses:ListIdentityPolicies", "ListReceiptFilters": "ses:ListReceiptFilters", "ListReceiptRuleSets": "ses:ListReceiptRuleSets", "ListTemplates": "ses:ListTemplates", "ListVerifiedEmailAddresses": "ses:ListVerifiedEmailAddresses", "PutConfigurationSetDeliveryOptions": "ses:PutConfigurationSetDeliveryOptions", "PutIdentityPolicy": "ses:PutIdentityPolicy", "ReorderReceiptRuleSet": "ses:ReorderReceiptRuleSet", "SendBounce": "ses:SendBounce", "SendBulkTemplatedEmail": "ses:SendBulkTemplatedEmail", "SendCustomVerificationEmail": "ses:SendCustomVerificationEmail", "SendEmail": "ses:SendEmail", "SendRawEmail": "ses:SendRawEmail", "SendTemplatedEmail": "ses:SendTemplatedEmail", "SetActiveReceiptRuleSet": "ses:SetActiveReceiptRuleSet", "SetIdentityDkimEnabled": "ses:SetIdentityDkimEnabled", "SetIdentityFeedbackForwardingEnabled": "ses:SetIdentityFeedbackForwardingEnabled", "SetIdentityHeadersInNotificationsEnabled": "ses:SetIdentityHeadersInNotificationsEnabled", "SetIdentityMailFromDomain": "ses:SetIdentityMailFromDomain", "SetIdentityNotificationTopic": "ses:SetIdentityNotificationTopic", "SetReceiptRulePosition": "ses:SetReceiptRulePosition", "TestRenderTemplate": "ses:TestRenderTemplate", "UpdateAccountSendingEnabled": "ses:UpdateAccountSendingEnabled", "UpdateConfigurationSetEventDestination": "ses:UpdateConfigurationSetEventDestination", "UpdateConfigurationSetReputationMetricsEnabled": "ses:UpdateConfigurationSetReputationMetricsEnabled", "UpdateConfigurationSetSendingEnabled": "ses:UpdateConfigurationSetSendingEnabled", "UpdateConfigurationSetTrackingOptions": "ses:UpdateConfigurationSetTrackingOptions", "UpdateCustomVerificationEmailTemplate": "ses:UpdateCustomVerificationEmailTemplate", "UpdateReceiptRule": "ses:UpdateReceiptRule", "UpdateTemplate": "ses:UpdateTemplate", "VerifyDomainDkim": "ses:VerifyDomainDkim", "VerifyDomainIdentity": "ses:VerifyDomainIdentity", "VerifyEmailAddress": "ses:VerifyEmailAddress", "VerifyEmailIdentity": "ses:VerifyEmailIdentity" }, "shield": { "AssociateDRTLogBucket": "shield:AssociateDRTLogBucket", "AssociateDRTRole": "shield:AssociateDRTRole", "AssociateHealthCheck": "shield:AssociateHealthCheck", "AssociateProactiveEngagementDetails": "shield:AssociateProactiveEngagementDetails", "CreateProtection": "shield:CreateProtection", "CreateProtectionGroup": "shield:CreateProtectionGroup", "CreateSubscription": "shield:CreateSubscription", "DeleteProtection": "shield:DeleteProtection", "DeleteProtectionGroup": "shield:DeleteProtectionGroup", "DeleteSubscription": "shield:DeleteSubscription", "DescribeAttack": "shield:DescribeAttack", "DescribeAttackStatistics": "shield:DescribeAttackStatistics", "DescribeDRTAccess": "shield:DescribeDRTAccess", "DescribeEmergencyContactSettings": "shield:DescribeEmergencyContactSettings", "DescribeProtection": "shield:DescribeProtection", "DescribeProtectionGroup": "shield:DescribeProtectionGroup", "DescribeSubscription": "shield:DescribeSubscription", "DisableApplicationLayerAutomaticResponse": "shield:DisableApplicationLayerAutomaticResponse", "DisableProactiveEngagement": "shield:DisableProactiveEngagement", "DisassociateDRTLogBucket": "shield:DisassociateDRTLogBucket", "DisassociateDRTRole": "shield:DisassociateDRTRole", "DisassociateHealthCheck": "shield:DisassociateHealthCheck", "EnableApplicationLayerAutomaticResponse": "shield:EnableApplicationLayerAutomaticResponse", "EnableProactiveEngagement": "shield:EnableProactiveEngagement", "GetSubscriptionState": "shield:GetSubscriptionState", "ListAttacks": "shield:ListAttacks", "ListProtectionGroups": "shield:ListProtectionGroups", "ListProtections": "shield:ListProtections", "ListResourcesInProtectionGroup": "shield:ListResourcesInProtectionGroup", "ListTagsForResource": "shield:ListTagsForResource", "TagResource": "shield:TagResource", "UntagResource": "shield:UntagResource", "UpdateApplicationLayerAutomaticResponse": "shield:UpdateApplicationLayerAutomaticResponse", "UpdateEmergencyContactSettings": "shield:UpdateEmergencyContactSettings", "UpdateProtectionGroup": "shield:UpdateProtectionGroup", "UpdateSubscription": "shield:UpdateSubscription" }, "signer": { "AddProfilePermission": "signer:AddProfilePermission", "CancelSigningProfile": "signer:CancelSigningProfile", "DescribeSigningJob": "signer:DescribeSigningJob", "GetSigningPlatform": "signer:GetSigningPlatform", "GetSigningProfile": "signer:GetSigningProfile", "ListProfilePermissions": "signer:ListProfilePermissions", "ListSigningJobs": "signer:ListSigningJobs", "ListSigningPlatforms": "signer:ListSigningPlatforms", "ListSigningProfiles": "signer:ListSigningProfiles", "ListTagsForResource": "signer:ListTagsForResource", "PutSigningProfile": "signer:PutSigningProfile", "RemoveProfilePermission": "signer:RemoveProfilePermission", "RevokeSignature": "signer:RevokeSignature", "RevokeSigningProfile": "signer:RevokeSigningProfile", "StartSigningJob": "signer:StartSigningJob", "TagResource": "signer:TagResource", "UntagResource": "signer:UntagResource" }, "sms-voice": { "CreateConfigurationSet": "sms-voice:CreateConfigurationSet", "CreateConfigurationSetEventDestination": "sms-voice:CreateConfigurationSetEventDestination", "DeleteConfigurationSet": "sms-voice:DeleteConfigurationSet", "DeleteConfigurationSetEventDestination": "sms-voice:DeleteConfigurationSetEventDestination", "GetConfigurationSetEventDestinations": "sms-voice:GetConfigurationSetEventDestinations", "ListConfigurationSets": "sms-voice:ListConfigurationSets", "SendVoiceMessage": "sms-voice:SendVoiceMessage", "UpdateConfigurationSetEventDestination": "sms-voice:UpdateConfigurationSetEventDestination" }, "snow-device-management": { "CancelTask": "snow-device-management:CancelTask", "CreateTask": "snow-device-management:CreateTask", "DescribeDevice": "snow-device-management:DescribeDevice", "DescribeDeviceEc2Instances": "snow-device-management:DescribeDeviceEc2Instances", "DescribeExecution": "snow-device-management:DescribeExecution", "DescribeTask": "snow-device-management:DescribeTask", "ListDeviceResources": "snow-device-management:ListDeviceResources", "ListDevices": "snow-device-management:ListDevices", "ListExecutions": "snow-device-management:ListExecutions", "ListTagsForResource": "snow-device-management:ListTagsForResource", "ListTasks": "snow-device-management:ListTasks", "TagResource": "snow-device-management:TagResource", "UntagResource": "snow-device-management:UntagResource" }, "snowball": { "CancelCluster": "snowball:CancelCluster", "CancelJob": "snowball:CancelJob", "CreateAddress": "snowball:CreateAddress", "CreateCluster": "snowball:CreateCluster", "CreateJob": "snowball:CreateJob", "CreateLongTermPricing": "snowball:CreateLongTermPricing", "CreateReturnShippingLabel": "snowball:CreateReturnShippingLabel", "DescribeAddress": "snowball:DescribeAddress", "DescribeAddresses": "snowball:DescribeAddresses", "DescribeCluster": "snowball:DescribeCluster", "DescribeJob": "snowball:DescribeJob", "DescribeReturnShippingLabel": "snowball:DescribeReturnShippingLabel", "GetJobManifest": "snowball:GetJobManifest", "GetJobUnlockCode": "snowball:GetJobUnlockCode", "GetSnowballUsage": "snowball:GetSnowballUsage", "GetSoftwareUpdates": "snowball:GetSoftwareUpdates", "ListClusterJobs": "snowball:ListClusterJobs", "ListClusters": "snowball:ListClusters", "ListCompatibleImages": "snowball:ListCompatibleImages", "ListJobs": "snowball:ListJobs", "ListLongTermPricing": "snowball:ListLongTermPricing", "UpdateCluster": "snowball:UpdateCluster", "UpdateJob": "snowball:UpdateJob", "UpdateJobShipmentState": "snowball:UpdateJobShipmentState", "UpdateLongTermPricing": "snowball:UpdateLongTermPricing" }, "sns": { "AddPermission": "sns:AddPermission", "CheckIfPhoneNumberIsOptedOut": "sns:CheckIfPhoneNumberIsOptedOut", "ConfirmSubscription": "sns:ConfirmSubscription", "CreatePlatformApplication": "sns:CreatePlatformApplication", "CreatePlatformEndpoint": "sns:CreatePlatformEndpoint", "CreateSMSSandboxPhoneNumber": "sns:CreateSMSSandboxPhoneNumber", "CreateTopic": "sns:CreateTopic", "DeleteEndpoint": "sns:DeleteEndpoint", "DeletePlatformApplication": "sns:DeletePlatformApplication", "DeleteSMSSandboxPhoneNumber": "sns:DeleteSMSSandboxPhoneNumber", "DeleteTopic": "sns:DeleteTopic", "GetEndpointAttributes": "sns:GetEndpointAttributes", "GetPlatformApplicationAttributes": "sns:GetPlatformApplicationAttributes", "GetSMSAttributes": "sns:GetSMSAttributes", "GetSMSSandboxAccountStatus": "sns:GetSMSSandboxAccountStatus", "GetSubscriptionAttributes": "sns:GetSubscriptionAttributes", "GetTopicAttributes": "sns:GetTopicAttributes", "ListEndpointsByPlatformApplication": "sns:ListEndpointsByPlatformApplication", "ListOriginationNumbers": "sns:ListOriginationNumbers", "ListPhoneNumbersOptedOut": "sns:ListPhoneNumbersOptedOut", "ListPlatformApplications": "sns:ListPlatformApplications", "ListSMSSandboxPhoneNumbers": "sns:ListSMSSandboxPhoneNumbers", "ListSubscriptions": "sns:ListSubscriptions", "ListSubscriptionsByTopic": "sns:ListSubscriptionsByTopic", "ListTagsForResource": "sns:ListTagsForResource", "ListTopics": "sns:ListTopics", "OptInPhoneNumber": "sns:OptInPhoneNumber", "Publish": "sns:Publish", "RemovePermission": "sns:RemovePermission", "SetEndpointAttributes": "sns:SetEndpointAttributes", "SetPlatformApplicationAttributes": "sns:SetPlatformApplicationAttributes", "SetSMSAttributes": "sns:SetSMSAttributes", "SetSubscriptionAttributes": "sns:SetSubscriptionAttributes", "SetTopicAttributes": "sns:SetTopicAttributes", "Subscribe": "sns:Subscribe", "TagResource": "sns:TagResource", "Unsubscribe": "sns:Unsubscribe", "UntagResource": "sns:UntagResource", "VerifySMSSandboxPhoneNumber": "sns:VerifySMSSandboxPhoneNumber" }, "sqs": { "AddPermission": "sqs:AddPermission", "ChangeMessageVisibility": "sqs:ChangeMessageVisibility", "ChangeMessageVisibilityBatch": "sqs:ChangeMessageVisibility", "CreateQueue": "sqs:CreateQueue", "DeleteMessage": "sqs:DeleteMessage", "DeleteMessageBatch": "sqs:DeleteMessage", "DeleteQueue": "sqs:DeleteQueue", "GetQueueAttributes": "sqs:GetQueueAttributes", "GetQueueUrl": "sqs:GetQueueUrl", "ListDeadLetterSourceQueues": "sqs:ListDeadLetterSourceQueues", "ListQueueTags": "sqs:ListQueueTags", "ListQueues": "sqs:ListQueues", "PurgeQueue": "sqs:PurgeQueue", "ReceiveMessage": "sqs:ReceiveMessage", "RemovePermission": "sqs:RemovePermission", "SendMessage": "sqs:SendMessage", "SendMessageBatch": "sqs:SendMessage", "SetQueueAttributes": "sqs:SetQueueAttributes", "TagQueue": "sqs:TagQueue", "UntagQueue": "sqs:UntagQueue" }, "ssm": { "AddTagsToResource": "ssm:AddTagsToResource", "AssociateOpsItemRelatedItem": "ssm:AssociateOpsItemRelatedItem", "CancelCommand": "ssm:CancelCommand", "CancelMaintenanceWindowExecution": "ssm:CancelMaintenanceWindowExecution", "CreateActivation": "ssm:CreateActivation", "CreateAssociation": "ssm:CreateAssociation", "CreateAssociationBatch": "ssm:CreateAssociationBatch", "CreateDocument": "ssm:CreateDocument", "CreateMaintenanceWindow": "ssm:CreateMaintenanceWindow", "CreateOpsItem": "ssm:CreateOpsItem", "CreateOpsMetadata": "ssm:CreateOpsMetadata", "CreatePatchBaseline": "ssm:CreatePatchBaseline", "CreateResourceDataSync": "ssm:CreateResourceDataSync", "DeleteActivation": "ssm:DeleteActivation", "DeleteAssociation": "ssm:DeleteAssociation", "DeleteDocument": "ssm:DeleteDocument", "DeleteInventory": "ssm:DeleteInventory", "DeleteMaintenanceWindow": "ssm:DeleteMaintenanceWindow", "DeleteOpsMetadata": "ssm:DeleteOpsMetadata", "DeleteParameter": "ssm:DeleteParameter", "DeleteParameters": "ssm:DeleteParameters", "DeletePatchBaseline": "ssm:DeletePatchBaseline", "DeleteResourceDataSync": "ssm:DeleteResourceDataSync", "DeregisterManagedInstance": "ssm:DeregisterManagedInstance", "DeregisterPatchBaselineForPatchGroup": "ssm:DeregisterPatchBaselineForPatchGroup", "DeregisterTargetFromMaintenanceWindow": "ssm:DeregisterTargetFromMaintenanceWindow", "DeregisterTaskFromMaintenanceWindow": "ssm:DeregisterTaskFromMaintenanceWindow", "DescribeActivations": "ssm:DescribeActivations", "DescribeAssociation": "ssm:DescribeAssociation", "DescribeAssociationExecutionTargets": "ssm:DescribeAssociationExecutionTargets", "DescribeAssociationExecutions": "ssm:DescribeAssociationExecutions", "DescribeAutomationExecutions": "ssm:DescribeAutomationExecutions", "DescribeAutomationStepExecutions": "ssm:DescribeAutomationStepExecutions", "DescribeAvailablePatches": "ssm:DescribeAvailablePatches", "DescribeDocument": "ssm:DescribeDocument", "DescribeDocumentPermission": "ssm:DescribeDocumentPermission", "DescribeEffectiveInstanceAssociations": "ssm:DescribeEffectiveInstanceAssociations", "DescribeEffectivePatchesForPatchBaseline": "ssm:DescribeEffectivePatchesForPatchBaseline", "DescribeInstanceAssociationsStatus": "ssm:DescribeInstanceAssociationsStatus", "DescribeInstanceInformation": "ssm:DescribeInstanceInformation", "DescribeInstancePatchStates": "ssm:DescribeInstancePatchStates", "DescribeInstancePatchStatesForPatchGroup": "ssm:DescribeInstancePatchStatesForPatchGroup", "DescribeInstancePatches": "ssm:DescribeInstancePatches", "DescribeInventoryDeletions": "ssm:DescribeInventoryDeletions", "DescribeMaintenanceWindowExecutionTaskInvocations": "ssm:DescribeMaintenanceWindowExecutionTaskInvocations", "DescribeMaintenanceWindowExecutionTasks": "ssm:DescribeMaintenanceWindowExecutionTasks", "DescribeMaintenanceWindowExecutions": "ssm:DescribeMaintenanceWindowExecutions", "DescribeMaintenanceWindowSchedule": "ssm:DescribeMaintenanceWindowSchedule", "DescribeMaintenanceWindowTargets": "ssm:DescribeMaintenanceWindowTargets", "DescribeMaintenanceWindowTasks": "ssm:DescribeMaintenanceWindowTasks", "DescribeMaintenanceWindows": "ssm:DescribeMaintenanceWindows", "DescribeMaintenanceWindowsForTarget": "ssm:DescribeMaintenanceWindowsForTarget", "DescribeOpsItems": "ssm:DescribeOpsItems", "DescribeParameters": "ssm:DescribeParameters", "DescribePatchBaselines": "ssm:DescribePatchBaselines", "DescribePatchGroupState": "ssm:DescribePatchGroupState", "DescribePatchGroups": "ssm:DescribePatchGroups", "DescribePatchProperties": "ssm:DescribePatchProperties", "DescribeSessions": "ssm:DescribeSessions", "DisassociateOpsItemRelatedItem": "ssm:DisassociateOpsItemRelatedItem", "GetAutomationExecution": "ssm:GetAutomationExecution", "GetCalendarState": "ssm:GetCalendarState", "GetCommandInvocation": "ssm:GetCommandInvocation", "GetConnectionStatus": "ssm:GetConnectionStatus", "GetDefaultPatchBaseline": "ssm:GetDefaultPatchBaseline", "GetDeployablePatchSnapshotForInstance": "ssm:GetDeployablePatchSnapshotForInstance", "GetDocument": "ssm:GetDocument", "GetInventory": "ssm:GetInventory", "GetInventorySchema": "ssm:GetInventorySchema", "GetMaintenanceWindow": "ssm:GetMaintenanceWindow", "GetMaintenanceWindowExecution": "ssm:GetMaintenanceWindowExecution", "GetMaintenanceWindowExecutionTask": "ssm:GetMaintenanceWindowExecutionTask", "GetMaintenanceWindowExecutionTaskInvocation": "ssm:GetMaintenanceWindowExecutionTaskInvocation", "GetMaintenanceWindowTask": "ssm:GetMaintenanceWindowTask", "GetOpsItem": "ssm:GetOpsItem", "GetOpsMetadata": "ssm:GetOpsMetadata", "GetOpsSummary": "ssm:GetOpsSummary", "GetParameter": "ssm:GetParameter", "GetParameterHistory": "ssm:GetParameterHistory", "GetParameters": "ssm:GetParameters", "GetParametersByPath": "ssm:GetParametersByPath", "GetPatchBaseline": "ssm:GetPatchBaseline", "GetPatchBaselineForPatchGroup": "ssm:GetPatchBaselineForPatchGroup", "GetServiceSetting": "ssm:GetServiceSetting", "LabelParameterVersion": "ssm:LabelParameterVersion", "ListAssociationVersions": "ssm:ListAssociationVersions", "ListAssociations": "ssm:ListAssociations", "ListCommandInvocations": "ssm:ListCommandInvocations", "ListCommands": "ssm:ListCommands", "ListComplianceItems": "ssm:ListComplianceItems", "ListComplianceSummaries": "ssm:ListComplianceSummaries", "ListDocumentMetadataHistory": "ssm:ListDocumentMetadataHistory", "ListDocumentVersions": "ssm:ListDocumentVersions", "ListDocuments": "ssm:ListDocuments", "ListInventoryEntries": "ssm:ListInventoryEntries", "ListOpsItemEvents": "ssm:ListOpsItemEvents", "ListOpsItemRelatedItems": "ssm:ListOpsItemRelatedItems", "ListOpsMetadata": "ssm:ListOpsMetadata", "ListResourceComplianceSummaries": "ssm:ListResourceComplianceSummaries", "ListResourceDataSync": "ssm:ListResourceDataSync", "ListTagsForResource": "ssm:ListTagsForResource", "ModifyDocumentPermission": "ssm:ModifyDocumentPermission", "PutComplianceItems": "ssm:PutComplianceItems", "PutInventory": "ssm:PutInventory", "PutParameter": "ssm:PutParameter", "RegisterDefaultPatchBaseline": "ssm:RegisterDefaultPatchBaseline", "RegisterPatchBaselineForPatchGroup": "ssm:RegisterPatchBaselineForPatchGroup", "RegisterTargetWithMaintenanceWindow": "ssm:RegisterTargetWithMaintenanceWindow", "RegisterTaskWithMaintenanceWindow": "ssm:RegisterTaskWithMaintenanceWindow", "RemoveTagsFromResource": "ssm:RemoveTagsFromResource", "ResetServiceSetting": "ssm:ResetServiceSetting", "ResumeSession": "ssm:ResumeSession", "SendAutomationSignal": "ssm:SendAutomationSignal", "SendCommand": "ssm:SendCommand", "StartAssociationsOnce": "ssm:StartAssociationsOnce", "StartAutomationExecution": "ssm:StartAutomationExecution", "StartChangeRequestExecution": "ssm:StartChangeRequestExecution", "StartSession": "ssm:StartSession", "StopAutomationExecution": "ssm:StopAutomationExecution", "TerminateSession": "ssm:TerminateSession", "UpdateAssociation": "ssm:UpdateAssociation", "UpdateAssociationStatus": "ssm:UpdateAssociationStatus", "UpdateDocument": "ssm:UpdateDocument", "UpdateDocumentDefaultVersion": "ssm:UpdateDocumentDefaultVersion", "UpdateDocumentMetadata": "ssm:UpdateDocumentMetadata", "UpdateMaintenanceWindow": "ssm:UpdateMaintenanceWindow", "UpdateMaintenanceWindowTarget": "ssm:UpdateMaintenanceWindowTarget", "UpdateMaintenanceWindowTask": "ssm:UpdateMaintenanceWindowTask", "UpdateManagedInstanceRole": "ssm:UpdateManagedInstanceRole", "UpdateOpsItem": "ssm:UpdateOpsItem", "UpdateOpsMetadata": "ssm:UpdateOpsMetadata", "UpdatePatchBaseline": "ssm:UpdatePatchBaseline", "UpdateResourceDataSync": "ssm:UpdateResourceDataSync", "UpdateServiceSetting": "ssm:UpdateServiceSetting" }, "ssm-contacts": { "AcceptPage": "ssm-contacts:AcceptPage", "ActivateContactChannel": "ssm-contacts:ActivateContactChannel", "CreateContact": "ssm-contacts:CreateContact", "CreateContactChannel": "ssm-contacts:CreateContactChannel", "DeactivateContactChannel": "ssm-contacts:DeactivateContactChannel", "DeleteContact": "ssm-contacts:DeleteContact", "DeleteContactChannel": "ssm-contacts:DeleteContactChannel", "DescribeEngagement": "ssm-contacts:DescribeEngagement", "DescribePage": "ssm-contacts:DescribePage", "GetContact": "ssm-contacts:GetContact", "GetContactChannel": "ssm-contacts:GetContactChannel", "GetContactPolicy": "ssm-contacts:GetContactPolicy", "ListContactChannels": "ssm-contacts:ListContactChannels", "ListContacts": "ssm-contacts:ListContacts", "ListEngagements": "ssm-contacts:ListEngagements", "ListPageReceipts": "ssm-contacts:ListPageReceipts", "ListPagesByContact": "ssm-contacts:ListPagesByContact", "ListPagesByEngagement": "ssm-contacts:ListPagesByEngagement", "ListTagsForResource": "ssm-contacts:ListTagsForResource", "PutContactPolicy": "ssm-contacts:PutContactPolicy", "SendActivationCode": "ssm-contacts:SendActivationCode", "StartEngagement": "ssm-contacts:StartEngagement", "StopEngagement": "ssm-contacts:StopEngagement", "TagResource": "ssm-contacts:TagResource", "UntagResource": "ssm-contacts:UntagResource", "UpdateContact": "ssm-contacts:UpdateContact", "UpdateContactChannel": "ssm-contacts:UpdateContactChannel" }, "ssm-incidents": { "CreateReplicationSet": "ssm-incidents:CreateReplicationSet", "CreateResponsePlan": "ssm-incidents:CreateResponsePlan", "CreateTimelineEvent": "ssm-incidents:CreateTimelineEvent", "DeleteIncidentRecord": "ssm-incidents:DeleteIncidentRecord", "DeleteReplicationSet": "ssm-incidents:DeleteReplicationSet", "DeleteResourcePolicy": "ssm-incidents:DeleteResourcePolicy", "DeleteResponsePlan": "ssm-incidents:DeleteResponsePlan", "DeleteTimelineEvent": "ssm-incidents:DeleteTimelineEvent", "GetIncidentRecord": "ssm-incidents:GetIncidentRecord", "GetReplicationSet": "ssm-incidents:GetReplicationSet", "GetResourcePolicies": "ssm-incidents:GetResourcePolicies", "GetResponsePlan": "ssm-incidents:GetResponsePlan", "GetTimelineEvent": "ssm-incidents:GetTimelineEvent", "ListIncidentRecords": "ssm-incidents:ListIncidentRecords", "ListRelatedItems": "ssm-incidents:ListRelatedItems", "ListReplicationSets": "ssm-incidents:ListReplicationSets", "ListResponsePlans": "ssm-incidents:ListResponsePlans", "ListTagsForResource": "ssm-incidents:ListTagsForResource", "ListTimelineEvents": "ssm-incidents:ListTimelineEvents", "PutResourcePolicy": "ssm-incidents:PutResourcePolicy", "StartIncident": "ssm-incidents:StartIncident", "TagResource": "ssm-incidents:TagResource", "UntagResource": "ssm-incidents:UntagResource", "UpdateDeletionProtection": "ssm-incidents:UpdateDeletionProtection", "UpdateIncidentRecord": "ssm-incidents:UpdateIncidentRecord", "UpdateRelatedItems": "ssm-incidents:UpdateRelatedItems", "UpdateReplicationSet": "ssm-incidents:UpdateReplicationSet", "UpdateResponsePlan": "ssm-incidents:UpdateResponsePlan", "UpdateTimelineEvent": "ssm-incidents:UpdateTimelineEvent" }, "sso": {}, "stepfunctions": { "CreateActivity": "states:CreateActivity", "CreateStateMachine": "states:CreateStateMachine", "DeleteActivity": "states:DeleteActivity", "DeleteStateMachine": "states:DeleteStateMachine", "DescribeActivity": "states:DescribeActivity", "DescribeExecution": "states:DescribeExecution", "DescribeStateMachine": "states:DescribeStateMachine", "DescribeStateMachineForExecution": "states:DescribeStateMachineForExecution", "GetActivityTask": "states:GetActivityTask", "GetExecutionHistory": "states:GetExecutionHistory", "ListActivities": "states:ListActivities", "ListExecutions": "states:ListExecutions", "ListStateMachines": "states:ListStateMachines", "ListTagsForResource": "states:ListTagsForResource", "SendTaskFailure": "states:SendTaskFailure", "SendTaskHeartbeat": "states:SendTaskHeartbeat", "SendTaskSuccess": "states:SendTaskSuccess", "StartExecution": "states:StartExecution", "StartSyncExecution": "states:StartSyncExecution", "StopExecution": "states:StopExecution", "TagResource": "states:TagResource", "UntagResource": "states:UntagResource", "UpdateStateMachine": "states:UpdateStateMachine" }, "storagegateway": { "ActivateGateway": "storagegateway:ActivateGateway", "AddCache": "storagegateway:AddCache", "AddTagsToResource": "storagegateway:AddTagsToResource", "AddUploadBuffer": "storagegateway:AddUploadBuffer", "AddWorkingStorage": "storagegateway:AddWorkingStorage", "AssignTapePool": "storagegateway:AssignTapePool", "AssociateFileSystem": "storagegateway:AssociateFileSystem", "AttachVolume": "storagegateway:AttachVolume", "CancelArchival": "storagegateway:CancelArchival", "CancelRetrieval": "storagegateway:CancelRetrieval", "CreateCachediSCSIVolume": "storagegateway:CreateCachediSCSIVolume", "CreateNFSFileShare": "storagegateway:CreateNFSFileShare", "CreateSMBFileShare": "storagegateway:CreateSMBFileShare", "CreateSnapshot": "storagegateway:CreateSnapshot", "CreateSnapshotFromVolumeRecoveryPoint": "storagegateway:CreateSnapshotFromVolumeRecoveryPoint", "CreateStorediSCSIVolume": "storagegateway:CreateStorediSCSIVolume", "CreateTapePool": "storagegateway:CreateTapePool", "CreateTapeWithBarcode": "storagegateway:CreateTapeWithBarcode", "CreateTapes": "storagegateway:CreateTapes", "DeleteAutomaticTapeCreationPolicy": "storagegateway:DeleteAutomaticTapeCreationPolicy", "DeleteBandwidthRateLimit": "storagegateway:DeleteBandwidthRateLimit", "DeleteChapCredentials": "storagegateway:DeleteChapCredentials", "DeleteFileShare": "storagegateway:DeleteFileShare", "DeleteGateway": "storagegateway:DeleteGateway", "DeleteSnapshotSchedule": "storagegateway:DeleteSnapshotSchedule", "DeleteTape": "storagegateway:DeleteTape", "DeleteTapeArchive": "storagegateway:DeleteTapeArchive", "DeleteTapePool": "storagegateway:DeleteTapePool", "DeleteVolume": "storagegateway:DeleteVolume", "DescribeAvailabilityMonitorTest": "storagegateway:DescribeAvailabilityMonitorTest", "DescribeBandwidthRateLimit": "storagegateway:DescribeBandwidthRateLimit", "DescribeBandwidthRateLimitSchedule": "storagegateway:DescribeBandwidthRateLimitSchedule", "DescribeCache": "storagegateway:DescribeCache", "DescribeCachediSCSIVolumes": "storagegateway:DescribeCachediSCSIVolumes", "DescribeChapCredentials": "storagegateway:DescribeChapCredentials", "DescribeFileSystemAssociations": "storagegateway:DescribeFileSystemAssociations", "DescribeGatewayInformation": "storagegateway:DescribeGatewayInformation", "DescribeMaintenanceStartTime": "storagegateway:DescribeMaintenanceStartTime", "DescribeNFSFileShares": "storagegateway:DescribeNFSFileShares", "DescribeSMBFileShares": "storagegateway:DescribeSMBFileShares", "DescribeSMBSettings": "storagegateway:DescribeSMBSettings", "DescribeSnapshotSchedule": "storagegateway:DescribeSnapshotSchedule", "DescribeStorediSCSIVolumes": "storagegateway:DescribeStorediSCSIVolumes", "DescribeTapeArchives": "storagegateway:DescribeTapeArchives", "DescribeTapeRecoveryPoints": "storagegateway:DescribeTapeRecoveryPoints", "DescribeTapes": "storagegateway:DescribeTapes", "DescribeUploadBuffer": "storagegateway:DescribeUploadBuffer", "DescribeVTLDevices": "storagegateway:DescribeVTLDevices", "DescribeWorkingStorage": "storagegateway:DescribeWorkingStorage", "DetachVolume": "storagegateway:DetachVolume", "DisableGateway": "storagegateway:DisableGateway", "DisassociateFileSystem": "storagegateway:DisassociateFileSystem", "JoinDomain": "storagegateway:JoinDomain", "ListAutomaticTapeCreationPolicies": "storagegateway:ListAutomaticTapeCreationPolicies", "ListFileShares": "storagegateway:ListFileShares", "ListFileSystemAssociations": "storagegateway:ListFileSystemAssociations", "ListGateways": "storagegateway:ListGateways", "ListLocalDisks": "storagegateway:ListLocalDisks", "ListTagsForResource": "storagegateway:ListTagsForResource", "ListTapePools": "storagegateway:ListTapePools", "ListTapes": "storagegateway:ListTapes", "ListVolumeInitiators": "storagegateway:ListVolumeInitiators", "ListVolumeRecoveryPoints": "storagegateway:ListVolumeRecoveryPoints", "ListVolumes": "storagegateway:ListVolumes", "NotifyWhenUploaded": "storagegateway:NotifyWhenUploaded", "RefreshCache": "storagegateway:RefreshCache", "RemoveTagsFromResource": "storagegateway:RemoveTagsFromResource", "ResetCache": "storagegateway:ResetCache", "RetrieveTapeArchive": "storagegateway:RetrieveTapeArchive", "RetrieveTapeRecoveryPoint": "storagegateway:RetrieveTapeRecoveryPoint", "SetLocalConsolePassword": "storagegateway:SetLocalConsolePassword", "SetSMBGuestPassword": "storagegateway:SetSMBGuestPassword", "ShutdownGateway": "storagegateway:ShutdownGateway", "StartAvailabilityMonitorTest": "storagegateway:StartAvailabilityMonitorTest", "StartGateway": "storagegateway:StartGateway", "UpdateAutomaticTapeCreationPolicy": "storagegateway:UpdateAutomaticTapeCreationPolicy", "UpdateBandwidthRateLimit": "storagegateway:UpdateBandwidthRateLimit", "UpdateBandwidthRateLimitSchedule": "storagegateway:UpdateBandwidthRateLimitSchedule", "UpdateChapCredentials": "storagegateway:UpdateChapCredentials", "UpdateFileSystemAssociation": "storagegateway:UpdateFileSystemAssociation", "UpdateGatewayInformation": "storagegateway:UpdateGatewayInformation", "UpdateGatewaySoftwareNow": "storagegateway:UpdateGatewaySoftwareNow", "UpdateMaintenanceStartTime": "storagegateway:UpdateMaintenanceStartTime", "UpdateNFSFileShare": "storagegateway:UpdateNFSFileShare", "UpdateSMBFileShare": "storagegateway:UpdateSMBFileShare", "UpdateSMBFileShareVisibility": "storagegateway:UpdateSMBFileShareVisibility", "UpdateSMBSecurityStrategy": "storagegateway:UpdateSMBSecurityStrategy", "UpdateSnapshotSchedule": "storagegateway:UpdateSnapshotSchedule", "UpdateVTLDeviceType": "storagegateway:UpdateVTLDeviceType" }, "sts": { "AssumeRole": "sts:AssumeRole", "AssumeRoleWithSAML": "sts:AssumeRoleWithSAML", "AssumeRoleWithWebIdentity": "sts:AssumeRoleWithWebIdentity", "DecodeAuthorizationMessage": "sts:DecodeAuthorizationMessage", "GetAccessKeyInfo": "sts:GetAccessKeyInfo", "GetCallerIdentity": "sts:GetCallerIdentity", "GetFederationToken": "sts:GetFederationToken", "GetSessionToken": "sts:GetSessionToken" }, "swf": { "CountClosedWorkflowExecutions": "swf:CountClosedWorkflowExecutions", "CountOpenWorkflowExecutions": "swf:CountOpenWorkflowExecutions", "CountPendingActivityTasks": "swf:CountPendingActivityTasks", "CountPendingDecisionTasks": "swf:CountPendingDecisionTasks", "DeprecateActivityType": "swf:DeprecateActivityType", "DeprecateDomain": "swf:DeprecateDomain", "DeprecateWorkflowType": "swf:DeprecateWorkflowType", "DescribeActivityType": "swf:DescribeActivityType", "DescribeDomain": "swf:DescribeDomain", "DescribeWorkflowExecution": "swf:DescribeWorkflowExecution", "DescribeWorkflowType": "swf:DescribeWorkflowType", "GetWorkflowExecutionHistory": "swf:GetWorkflowExecutionHistory", "ListActivityTypes": "swf:ListActivityTypes", "ListClosedWorkflowExecutions": "swf:ListClosedWorkflowExecutions", "ListDomains": "swf:ListDomains", "ListOpenWorkflowExecutions": "swf:ListOpenWorkflowExecutions", "ListTagsForResource": "swf:ListTagsForResource", "ListWorkflowTypes": "swf:ListWorkflowTypes", "PollForActivityTask": "swf:PollForActivityTask", "PollForDecisionTask": "swf:PollForDecisionTask", "RecordActivityTaskHeartbeat": "swf:RecordActivityTaskHeartbeat", "RegisterActivityType": "swf:RegisterActivityType", "RegisterDomain": "swf:RegisterDomain", "RegisterWorkflowType": "swf:RegisterWorkflowType", "RequestCancelWorkflowExecution": "swf:RequestCancelWorkflowExecution", "RespondActivityTaskCanceled": "swf:RespondActivityTaskCanceled", "RespondActivityTaskCompleted": "swf:RespondActivityTaskCompleted", "RespondActivityTaskFailed": "swf:RespondActivityTaskFailed", "RespondDecisionTaskCompleted": "swf:RespondDecisionTaskCompleted", "SignalWorkflowExecution": "swf:SignalWorkflowExecution", "StartWorkflowExecution": "swf:StartWorkflowExecution", "TagResource": "swf:TagResource", "TerminateWorkflowExecution": "swf:TerminateWorkflowExecution", "UndeprecateActivityType": "swf:UndeprecateActivityType", "UndeprecateDomain": "swf:UndeprecateDomain", "UndeprecateWorkflowType": "swf:UndeprecateWorkflowType", "UntagResource": "swf:UntagResource" }, "synthetics": { "CreateCanary": "synthetics:CreateCanary", "DeleteCanary": "synthetics:DeleteCanary", "DescribeCanaries": "synthetics:DescribeCanaries", "DescribeCanariesLastRun": "synthetics:DescribeCanariesLastRun", "DescribeRuntimeVersions": "synthetics:DescribeRuntimeVersions", "GetCanary": "synthetics:GetCanary", "GetCanaryRuns": "synthetics:GetCanaryRuns", "ListTagsForResource": "synthetics:ListTagsForResource", "StartCanary": "synthetics:StartCanary", "StopCanary": "synthetics:StopCanary", "TagResource": "synthetics:TagResource", "UntagResource": "synthetics:UntagResource", "UpdateCanary": "synthetics:UpdateCanary" }, "textract": { "AnalyzeDocument": "textract:AnalyzeDocument", "AnalyzeExpense": "textract:AnalyzeExpense", "AnalyzeID": "textract:AnalyzeID", "DetectDocumentText": "textract:DetectDocumentText", "GetDocumentAnalysis": "textract:GetDocumentAnalysis", "GetDocumentTextDetection": "textract:GetDocumentTextDetection", "GetExpenseAnalysis": "textract:GetExpenseAnalysis", "StartDocumentAnalysis": "textract:StartDocumentAnalysis", "StartDocumentTextDetection": "textract:StartDocumentTextDetection", "StartExpenseAnalysis": "textract:StartExpenseAnalysis" }, "transcribe": { "CreateCallAnalyticsCategory": "transcribe:CreateCallAnalyticsCategory", "CreateLanguageModel": "transcribe:CreateLanguageModel", "CreateMedicalVocabulary": "transcribe:CreateMedicalVocabulary", "CreateVocabulary": "transcribe:CreateVocabulary", "CreateVocabularyFilter": "transcribe:CreateVocabularyFilter", "DeleteCallAnalyticsCategory": "transcribe:DeleteCallAnalyticsCategory", "DeleteCallAnalyticsJob": "transcribe:DeleteCallAnalyticsJob", "DeleteLanguageModel": "transcribe:DeleteLanguageModel", "DeleteMedicalTranscriptionJob": "transcribe:DeleteMedicalTranscriptionJob", "DeleteMedicalVocabulary": "transcribe:DeleteMedicalVocabulary", "DeleteTranscriptionJob": "transcribe:DeleteTranscriptionJob", "DeleteVocabulary": "transcribe:DeleteVocabulary", "DeleteVocabularyFilter": "transcribe:DeleteVocabularyFilter", "DescribeLanguageModel": "transcribe:DescribeLanguageModel", "GetCallAnalyticsCategory": "transcribe:GetCallAnalyticsCategory", "GetCallAnalyticsJob": "transcribe:GetCallAnalyticsJob", "GetMedicalTranscriptionJob": "transcribe:GetMedicalTranscriptionJob", "GetMedicalVocabulary": "transcribe:GetMedicalVocabulary", "GetTranscriptionJob": "transcribe:GetTranscriptionJob", "GetVocabulary": "transcribe:GetVocabulary", "GetVocabularyFilter": "transcribe:GetVocabularyFilter", "ListCallAnalyticsCategories": "transcribe:ListCallAnalyticsCategories", "ListCallAnalyticsJobs": "transcribe:ListCallAnalyticsJobs", "ListLanguageModels": "transcribe:ListLanguageModels", "ListMedicalTranscriptionJobs": "transcribe:ListMedicalTranscriptionJobs", "ListMedicalVocabularies": "transcribe:ListMedicalVocabularies", "ListTranscriptionJobs": "transcribe:ListTranscriptionJobs", "ListVocabularies": "transcribe:ListVocabularies", "ListVocabularyFilters": "transcribe:ListVocabularyFilters", "StartCallAnalyticsJob": "transcribe:StartCallAnalyticsJob", "StartMedicalTranscriptionJob": "transcribe:StartMedicalTranscriptionJob", "StartTranscriptionJob": "transcribe:StartTranscriptionJob", "UpdateCallAnalyticsCategory": "transcribe:UpdateCallAnalyticsCategory", "UpdateMedicalVocabulary": "transcribe:UpdateMedicalVocabulary", "UpdateVocabulary": "transcribe:UpdateVocabulary", "UpdateVocabularyFilter": "transcribe:UpdateVocabularyFilter" }, "transfer": { "CreateAccess": "transfer:CreateAccess", "CreateServer": "transfer:CreateServer", "CreateUser": "transfer:CreateUser", "CreateWorkflow": "transfer:CreateWorkflow", "DeleteAccess": "transfer:DeleteAccess", "DeleteServer": "transfer:DeleteServer", "DeleteSshPublicKey": "transfer:DeleteSshPublicKey", "DeleteUser": "transfer:DeleteUser", "DeleteWorkflow": "transfer:DeleteWorkflow", "DescribeAccess": "transfer:DescribeAccess", "DescribeExecution": "transfer:DescribeExecution", "DescribeSecurityPolicy": "transfer:DescribeSecurityPolicy", "DescribeServer": "transfer:DescribeServer", "DescribeUser": "transfer:DescribeUser", "DescribeWorkflow": "transfer:DescribeWorkflow", "ImportSshPublicKey": "transfer:ImportSshPublicKey", "ListAccesses": "transfer:ListAccesses", "ListExecutions": "transfer:ListExecutions", "ListSecurityPolicies": "transfer:ListSecurityPolicies", "ListServers": "transfer:ListServers", "ListTagsForResource": "transfer:ListTagsForResource", "ListUsers": "transfer:ListUsers", "ListWorkflows": "transfer:ListWorkflows", "SendWorkflowStepState": "transfer:SendWorkflowStepState", "StartServer": "transfer:StartServer", "StopServer": "transfer:StopServer", "TagResource": "transfer:TagResource", "TestIdentityProvider": "transfer:TestIdentityProvider", "UntagResource": "transfer:UntagResource", "UpdateAccess": "transfer:UpdateAccess", "UpdateServer": "transfer:UpdateServer", "UpdateUser": "transfer:UpdateUser" }, "translate": { "CreateParallelData": "translate:CreateParallelData", "DeleteParallelData": "translate:DeleteParallelData", "DeleteTerminology": "translate:DeleteTerminology", "DescribeTextTranslationJob": "translate:DescribeTextTranslationJob", "GetParallelData": "translate:GetParallelData", "GetTerminology": "translate:GetTerminology", "ImportTerminology": "translate:ImportTerminology", "ListParallelData": "translate:ListParallelData", "ListTerminologies": "translate:ListTerminologies", "ListTextTranslationJobs": "translate:ListTextTranslationJobs", "StartTextTranslationJob": "translate:StartTextTranslationJob", "StopTextTranslationJob": "translate:StopTextTranslationJob", "TranslateText": "translate:TranslateText", "UpdateParallelData": "translate:UpdateParallelData" }, "waf": { "CreateByteMatchSet": "waf:CreateByteMatchSet", "CreateGeoMatchSet": "waf:CreateGeoMatchSet", "CreateIPSet": "waf:CreateIPSet", "CreateRateBasedRule": "waf:CreateRateBasedRule", "CreateRegexMatchSet": "waf:CreateRegexMatchSet", "CreateRegexPatternSet": "waf:CreateRegexPatternSet", "CreateRule": "waf:CreateRule", "CreateRuleGroup": "waf:CreateRuleGroup", "CreateSizeConstraintSet": "waf:CreateSizeConstraintSet", "CreateSqlInjectionMatchSet": "waf:CreateSqlInjectionMatchSet", "CreateWebACL": "waf:CreateWebACL", "CreateWebACLMigrationStack": "waf:CreateWebACLMigrationStack", "CreateXssMatchSet": "waf:CreateXssMatchSet", "DeleteByteMatchSet": "waf:DeleteByteMatchSet", "DeleteGeoMatchSet": "waf:DeleteGeoMatchSet", "DeleteIPSet": "waf:DeleteIPSet", "DeleteLoggingConfiguration": "waf:DeleteLoggingConfiguration", "DeletePermissionPolicy": "waf:DeletePermissionPolicy", "DeleteRateBasedRule": "waf:DeleteRateBasedRule", "DeleteRegexMatchSet": "waf:DeleteRegexMatchSet", "DeleteRegexPatternSet": "waf:DeleteRegexPatternSet", "DeleteRule": "waf:DeleteRule", "DeleteRuleGroup": "waf:DeleteRuleGroup", "DeleteSizeConstraintSet": "waf:DeleteSizeConstraintSet", "DeleteSqlInjectionMatchSet": "waf:DeleteSqlInjectionMatchSet", "DeleteWebACL": "waf:DeleteWebACL", "DeleteXssMatchSet": "waf:DeleteXssMatchSet", "GetByteMatchSet": "waf:GetByteMatchSet", "GetChangeToken": "waf:GetChangeToken", "GetChangeTokenStatus": "waf:GetChangeTokenStatus", "GetGeoMatchSet": "waf:GetGeoMatchSet", "GetIPSet": "waf:GetIPSet", "GetLoggingConfiguration": "waf:GetLoggingConfiguration", "GetPermissionPolicy": "waf:GetPermissionPolicy", "GetRateBasedRule": "waf:GetRateBasedRule", "GetRateBasedRuleManagedKeys": "waf:GetRateBasedRuleManagedKeys", "GetRegexMatchSet": "waf:GetRegexMatchSet", "GetRegexPatternSet": "waf:GetRegexPatternSet", "GetRule": "waf:GetRule", "GetRuleGroup": "waf:GetRuleGroup", "GetSampledRequests": "waf:GetSampledRequests", "GetSizeConstraintSet": "waf:GetSizeConstraintSet", "GetSqlInjectionMatchSet": "waf:GetSqlInjectionMatchSet", "GetWebACL": "waf:GetWebACL", "GetXssMatchSet": "waf:GetXssMatchSet", "ListActivatedRulesInRuleGroup": "waf:ListActivatedRulesInRuleGroup", "ListByteMatchSets": "waf:ListByteMatchSets", "ListGeoMatchSets": "waf:ListGeoMatchSets", "ListIPSets": "waf:ListIPSets", "ListLoggingConfigurations": "waf:ListLoggingConfigurations", "ListRateBasedRules": "waf:ListRateBasedRules", "ListRegexMatchSets": "waf:ListRegexMatchSets", "ListRegexPatternSets": "waf:ListRegexPatternSets", "ListRuleGroups": "waf:ListRuleGroups", "ListRules": "waf:ListRules", "ListSizeConstraintSets": "waf:ListSizeConstraintSets", "ListSqlInjectionMatchSets": "waf:ListSqlInjectionMatchSets", "ListSubscribedRuleGroups": "waf:ListSubscribedRuleGroups", "ListTagsForResource": "waf:ListTagsForResource", "ListWebACLs": "waf:ListWebACLs", "ListXssMatchSets": "waf:ListXssMatchSets", "PutLoggingConfiguration": "waf:PutLoggingConfiguration", "PutPermissionPolicy": "waf:PutPermissionPolicy", "TagResource": "waf:TagResource", "UntagResource": "waf:UntagResource", "UpdateByteMatchSet": "waf:UpdateByteMatchSet", "UpdateGeoMatchSet": "waf:UpdateGeoMatchSet", "UpdateIPSet": "waf:UpdateIPSet", "UpdateRateBasedRule": "waf:UpdateRateBasedRule", "UpdateRegexMatchSet": "waf:UpdateRegexMatchSet", "UpdateRegexPatternSet": "waf:UpdateRegexPatternSet", "UpdateRule": "waf:UpdateRule", "UpdateRuleGroup": "waf:UpdateRuleGroup", "UpdateSizeConstraintSet": "waf:UpdateSizeConstraintSet", "UpdateSqlInjectionMatchSet": "waf:UpdateSqlInjectionMatchSet", "UpdateWebACL": "waf:UpdateWebACL", "UpdateXssMatchSet": "waf:UpdateXssMatchSet" }, "waf-regional": { "AssociateWebACL": "waf-regional:AssociateWebACL", "CreateByteMatchSet": "waf-regional:CreateByteMatchSet", "CreateGeoMatchSet": "waf-regional:CreateGeoMatchSet", "CreateIPSet": "waf-regional:CreateIPSet", "CreateRateBasedRule": "waf-regional:CreateRateBasedRule", "CreateRegexMatchSet": "waf-regional:CreateRegexMatchSet", "CreateRegexPatternSet": "waf-regional:CreateRegexPatternSet", "CreateRule": "waf-regional:CreateRule", "CreateRuleGroup": "waf-regional:CreateRuleGroup", "CreateSizeConstraintSet": "waf-regional:CreateSizeConstraintSet", "CreateSqlInjectionMatchSet": "waf-regional:CreateSqlInjectionMatchSet", "CreateWebACL": "waf-regional:CreateWebACL", "CreateWebACLMigrationStack": "waf-regional:CreateWebACLMigrationStack", "CreateXssMatchSet": "waf-regional:CreateXssMatchSet", "DeleteByteMatchSet": "waf-regional:DeleteByteMatchSet", "DeleteGeoMatchSet": "waf-regional:DeleteGeoMatchSet", "DeleteIPSet": "waf-regional:DeleteIPSet", "DeleteLoggingConfiguration": "waf-regional:DeleteLoggingConfiguration", "DeletePermissionPolicy": "waf-regional:DeletePermissionPolicy", "DeleteRateBasedRule": "waf-regional:DeleteRateBasedRule", "DeleteRegexMatchSet": "waf-regional:DeleteRegexMatchSet", "DeleteRegexPatternSet": "waf-regional:DeleteRegexPatternSet", "DeleteRule": "waf-regional:DeleteRule", "DeleteRuleGroup": "waf-regional:DeleteRuleGroup", "DeleteSizeConstraintSet": "waf-regional:DeleteSizeConstraintSet", "DeleteSqlInjectionMatchSet": "waf-regional:DeleteSqlInjectionMatchSet", "DeleteWebACL": "waf-regional:DeleteWebACL", "DeleteXssMatchSet": "waf-regional:DeleteXssMatchSet", "DisassociateWebACL": "waf-regional:DisassociateWebACL", "GetByteMatchSet": "waf-regional:GetByteMatchSet", "GetChangeToken": "waf-regional:GetChangeToken", "GetChangeTokenStatus": "waf-regional:GetChangeTokenStatus", "GetGeoMatchSet": "waf-regional:GetGeoMatchSet", "GetIPSet": "waf-regional:GetIPSet", "GetLoggingConfiguration": "waf-regional:GetLoggingConfiguration", "GetPermissionPolicy": "waf-regional:GetPermissionPolicy", "GetRateBasedRule": "waf-regional:GetRateBasedRule", "GetRateBasedRuleManagedKeys": "waf-regional:GetRateBasedRuleManagedKeys", "GetRegexMatchSet": "waf-regional:GetRegexMatchSet", "GetRegexPatternSet": "waf-regional:GetRegexPatternSet", "GetRule": "waf-regional:GetRule", "GetRuleGroup": "waf-regional:GetRuleGroup", "GetSampledRequests": "waf-regional:GetSampledRequests", "GetSizeConstraintSet": "waf-regional:GetSizeConstraintSet", "GetSqlInjectionMatchSet": "waf-regional:GetSqlInjectionMatchSet", "GetWebACL": "waf-regional:GetWebACL", "GetWebACLForResource": "waf-regional:GetWebACLForResource", "GetXssMatchSet": "waf-regional:GetXssMatchSet", "ListActivatedRulesInRuleGroup": "waf-regional:ListActivatedRulesInRuleGroup", "ListByteMatchSets": "waf-regional:ListByteMatchSets", "ListGeoMatchSets": "waf-regional:ListGeoMatchSets", "ListIPSets": "waf-regional:ListIPSets", "ListLoggingConfigurations": "waf-regional:ListLoggingConfigurations", "ListRateBasedRules": "waf-regional:ListRateBasedRules", "ListRegexMatchSets": "waf-regional:ListRegexMatchSets", "ListRegexPatternSets": "waf-regional:ListRegexPatternSets", "ListResourcesForWebACL": "waf-regional:ListResourcesForWebACL", "ListRuleGroups": "waf-regional:ListRuleGroups", "ListRules": "waf-regional:ListRules", "ListSizeConstraintSets": "waf-regional:ListSizeConstraintSets", "ListSqlInjectionMatchSets": "waf-regional:ListSqlInjectionMatchSets", "ListSubscribedRuleGroups": "waf-regional:ListSubscribedRuleGroups", "ListTagsForResource": "waf-regional:ListTagsForResource", "ListWebACLs": "waf-regional:ListWebACLs", "ListXssMatchSets": "waf-regional:ListXssMatchSets", "PutLoggingConfiguration": "waf-regional:PutLoggingConfiguration", "PutPermissionPolicy": "waf-regional:PutPermissionPolicy", "TagResource": "waf-regional:TagResource", "UntagResource": "waf-regional:UntagResource", "UpdateByteMatchSet": "waf-regional:UpdateByteMatchSet", "UpdateGeoMatchSet": "waf-regional:UpdateGeoMatchSet", "UpdateIPSet": "waf-regional:UpdateIPSet", "UpdateRateBasedRule": "waf-regional:UpdateRateBasedRule", "UpdateRegexMatchSet": "waf-regional:UpdateRegexMatchSet", "UpdateRegexPatternSet": "waf-regional:UpdateRegexPatternSet", "UpdateRule": "waf-regional:UpdateRule", "UpdateRuleGroup": "waf-regional:UpdateRuleGroup", "UpdateSizeConstraintSet": "waf-regional:UpdateSizeConstraintSet", "UpdateSqlInjectionMatchSet": "waf-regional:UpdateSqlInjectionMatchSet", "UpdateWebACL": "waf-regional:UpdateWebACL", "UpdateXssMatchSet": "waf-regional:UpdateXssMatchSet" }, "wafv2": { "AssociateWebACL": "wafv2:AssociateWebACL", "CheckCapacity": "wafv2:CheckCapacity", "CreateIPSet": "wafv2:CreateIPSet", "CreateRegexPatternSet": "wafv2:CreateRegexPatternSet", "CreateRuleGroup": "wafv2:CreateRuleGroup", "CreateWebACL": "wafv2:CreateWebACL", "DeleteFirewallManagerRuleGroups": "wafv2:DeleteFirewallManagerRuleGroups", "DeleteIPSet": "wafv2:DeleteIPSet", "DeleteLoggingConfiguration": "wafv2:DeleteLoggingConfiguration", "DeletePermissionPolicy": "wafv2:DeletePermissionPolicy", "DeleteRegexPatternSet": "wafv2:DeleteRegexPatternSet", "DeleteRuleGroup": "wafv2:DeleteRuleGroup", "DeleteWebACL": "wafv2:DeleteWebACL", "DescribeManagedRuleGroup": "wafv2:DescribeManagedRuleGroup", "DisassociateWebACL": "wafv2:DisassociateWebACL", "GetIPSet": "wafv2:GetIPSet", "GetLoggingConfiguration": "wafv2:GetLoggingConfiguration", "GetManagedRuleSet": "wafv2:GetManagedRuleSet", "GetPermissionPolicy": "wafv2:GetPermissionPolicy", "GetRateBasedStatementManagedKeys": "wafv2:GetRateBasedStatementManagedKeys", "GetRegexPatternSet": "wafv2:GetRegexPatternSet", "GetRuleGroup": "wafv2:GetRuleGroup", "GetSampledRequests": "wafv2:GetSampledRequests", "GetWebACL": "wafv2:GetWebACL", "GetWebACLForResource": "wafv2:GetWebACLForResource", "ListAvailableManagedRuleGroups": "wafv2:ListAvailableManagedRuleGroups", "ListIPSets": "wafv2:ListIPSets", "ListLoggingConfigurations": "wafv2:ListLoggingConfigurations", "ListManagedRuleSets": "wafv2:ListManagedRuleSets", "ListRegexPatternSets": "wafv2:ListRegexPatternSets", "ListResourcesForWebACL": "wafv2:ListResourcesForWebACL", "ListRuleGroups": "wafv2:ListRuleGroups", "ListTagsForResource": "wafv2:ListTagsForResource", "ListWebACLs": "wafv2:ListWebACLs", "PutLoggingConfiguration": "wafv2:PutLoggingConfiguration", "PutManagedRuleSetVersions": "wafv2:PutManagedRuleSetVersions", "PutPermissionPolicy": "wafv2:PutPermissionPolicy", "TagResource": "wafv2:TagResource", "UntagResource": "wafv2:UntagResource", "UpdateIPSet": "wafv2:UpdateIPSet", "UpdateManagedRuleSetVersionExpiryDate": "wafv2:UpdateManagedRuleSetVersionExpiryDate", "UpdateRegexPatternSet": "wafv2:UpdateRegexPatternSet", "UpdateRuleGroup": "wafv2:UpdateRuleGroup", "UpdateWebACL": "wafv2:UpdateWebACL" }, "wellarchitected": { "AssociateLenses": "wellarchitected:AssociateLenses", "CreateLensShare": "wellarchitected:CreateLensShare", "CreateLensVersion": "wellarchitected:CreateLensVersion", "CreateMilestone": "wellarchitected:CreateMilestone", "CreateWorkload": "wellarchitected:CreateWorkload", "CreateWorkloadShare": "wellarchitected:CreateWorkloadShare", "DeleteLens": "wellarchitected:DeleteLens", "DeleteLensShare": "wellarchitected:DeleteLensShare", "DeleteWorkload": "wellarchitected:DeleteWorkload", "DeleteWorkloadShare": "wellarchitected:DeleteWorkloadShare", "DisassociateLenses": "wellarchitected:DisassociateLenses", "ExportLens": "wellarchitected:ExportLens", "GetAnswer": "wellarchitected:GetAnswer", "GetLens": "wellarchitected:GetLens", "GetLensReview": "wellarchitected:GetLensReview", "GetLensReviewReport": "wellarchitected:GetLensReviewReport", "GetLensVersionDifference": "wellarchitected:GetLensVersionDifference", "GetMilestone": "wellarchitected:GetMilestone", "GetWorkload": "wellarchitected:GetWorkload", "ImportLens": "wellarchitected:ImportLens", "ListAnswers": "wellarchitected:ListAnswers", "ListLensReviewImprovements": "wellarchitected:ListLensReviewImprovements", "ListLensReviews": "wellarchitected:ListLensReviews", "ListLensShares": "wellarchitected:ListLensShares", "ListLenses": "wellarchitected:ListLenses", "ListMilestones": "wellarchitected:ListMilestones", "ListNotifications": "wellarchitected:ListNotifications", "ListShareInvitations": "wellarchitected:ListShareInvitations", "ListTagsForResource": "wellarchitected:ListTagsForResource", "ListWorkloadShares": "wellarchitected:ListWorkloadShares", "ListWorkloads": "wellarchitected:ListWorkloads", "TagResource": "wellarchitected:TagResource", "UntagResource": "wellarchitected:UntagResource", "UpdateAnswer": "wellarchitected:UpdateAnswer", "UpdateLensReview": "wellarchitected:UpdateLensReview", "UpdateShareInvitation": "wellarchitected:UpdateShareInvitation", "UpdateWorkload": "wellarchitected:UpdateWorkload", "UpdateWorkloadShare": "wellarchitected:UpdateWorkloadShare", "UpgradeLensReview": "wellarchitected:UpgradeLensReview" }, "wisdom": { "CreateAssistant": "wisdom:CreateAssistant", "CreateAssistantAssociation": "wisdom:CreateAssistantAssociation", "CreateContent": "wisdom:CreateContent", "CreateKnowledgeBase": "wisdom:CreateKnowledgeBase", "CreateSession": "wisdom:CreateSession", "DeleteAssistant": "wisdom:DeleteAssistant", "DeleteAssistantAssociation": "wisdom:DeleteAssistantAssociation", "DeleteContent": "wisdom:DeleteContent", "DeleteKnowledgeBase": "wisdom:DeleteKnowledgeBase", "GetAssistant": "wisdom:GetAssistant", "GetAssistantAssociation": "wisdom:GetAssistantAssociation", "GetContent": "wisdom:GetContent", "GetContentSummary": "wisdom:GetContentSummary", "GetKnowledgeBase": "wisdom:GetKnowledgeBase", "GetRecommendations": "wisdom:GetRecommendations", "GetSession": "wisdom:GetSession", "ListAssistantAssociations": "wisdom:ListAssistantAssociations", "ListAssistants": "wisdom:ListAssistants", "ListContents": "wisdom:ListContents", "ListKnowledgeBases": "wisdom:ListKnowledgeBases", "ListTagsForResource": "wisdom:ListTagsForResource", "NotifyRecommendationsReceived": "wisdom:NotifyRecommendationsReceived", "QueryAssistant": "wisdom:QueryAssistant", "RemoveKnowledgeBaseTemplateUri": "wisdom:RemoveKnowledgeBaseTemplateUri", "SearchContent": "wisdom:SearchContent", "SearchSessions": "wisdom:SearchSessions", "StartContentUpload": "wisdom:StartContentUpload", "TagResource": "wisdom:TagResource", "UntagResource": "wisdom:UntagResource", "UpdateContent": "wisdom:UpdateContent", "UpdateKnowledgeBaseTemplateUri": "wisdom:UpdateKnowledgeBaseTemplateUri" }, "workdocs": { "AbortDocumentVersionUpload": "workdocs:AbortDocumentVersionUpload", "ActivateUser": "workdocs:ActivateUser", "AddResourcePermissions": "workdocs:AddResourcePermissions", "CreateComment": "workdocs:CreateComment", "CreateCustomMetadata": "workdocs:CreateCustomMetadata", "CreateFolder": "workdocs:CreateFolder", "CreateLabels": "workdocs:CreateLabels", "CreateNotificationSubscription": "workdocs:CreateNotificationSubscription", "CreateUser": "workdocs:CreateUser", "DeactivateUser": "workdocs:DeactivateUser", "DeleteComment": "workdocs:DeleteComment", "DeleteCustomMetadata": "workdocs:DeleteCustomMetadata", "DeleteDocument": "workdocs:DeleteDocument", "DeleteFolder": "workdocs:DeleteFolder", "DeleteFolderContents": "workdocs:DeleteFolderContents", "DeleteLabels": "workdocs:DeleteLabels", "DeleteNotificationSubscription": "workdocs:DeleteNotificationSubscription", "DeleteUser": "workdocs:DeleteUser", "DescribeActivities": "workdocs:DescribeActivities", "DescribeComments": "workdocs:DescribeComments", "DescribeDocumentVersions": "workdocs:DescribeDocumentVersions", "DescribeFolderContents": "workdocs:DescribeFolderContents", "DescribeGroups": "workdocs:DescribeGroups", "DescribeNotificationSubscriptions": "workdocs:DescribeNotificationSubscriptions", "DescribeResourcePermissions": "workdocs:DescribeResourcePermissions", "DescribeRootFolders": "workdocs:DescribeRootFolders", "DescribeUsers": "workdocs:DescribeUsers", "GetCurrentUser": "workdocs:GetCurrentUser", "GetDocument": "workdocs:GetDocument", "GetDocumentPath": "workdocs:GetDocumentPath", "GetDocumentVersion": "workdocs:GetDocumentVersion", "GetFolder": "workdocs:GetFolder", "GetFolderPath": "workdocs:GetFolderPath", "GetResources": "workdocs:GetResources", "InitiateDocumentVersionUpload": "workdocs:InitiateDocumentVersionUpload", "RemoveAllResourcePermissions": "workdocs:RemoveAllResourcePermissions", "RemoveResourcePermission": "workdocs:RemoveResourcePermission", "UpdateDocument": "workdocs:UpdateDocument", "UpdateDocumentVersion": "workdocs:UpdateDocumentVersion", "UpdateFolder": "workdocs:UpdateFolder", "UpdateUser": "workdocs:UpdateUser" }, "worklink": { "AssociateDomain": "worklink:AssociateDomain", "AssociateWebsiteAuthorizationProvider": "worklink:AssociateWebsiteAuthorizationProvider", "AssociateWebsiteCertificateAuthority": "worklink:AssociateWebsiteCertificateAuthority", "CreateFleet": "worklink:CreateFleet", "DeleteFleet": "worklink:DeleteFleet", "DescribeAuditStreamConfiguration": "worklink:DescribeAuditStreamConfiguration", "DescribeCompanyNetworkConfiguration": "worklink:DescribeCompanyNetworkConfiguration", "DescribeDevice": "worklink:DescribeDevice", "DescribeDevicePolicyConfiguration": "worklink:DescribeDevicePolicyConfiguration", "DescribeDomain": "worklink:DescribeDomain", "DescribeFleetMetadata": "worklink:DescribeFleetMetadata", "DescribeIdentityProviderConfiguration": "worklink:DescribeIdentityProviderConfiguration", "DescribeWebsiteCertificateAuthority": "worklink:DescribeWebsiteCertificateAuthority", "DisassociateDomain": "worklink:DisassociateDomain", "DisassociateWebsiteAuthorizationProvider": "worklink:DisassociateWebsiteAuthorizationProvider", "DisassociateWebsiteCertificateAuthority": "worklink:DisassociateWebsiteCertificateAuthority", "ListDevices": "worklink:ListDevices", "ListDomains": "worklink:ListDomains", "ListFleets": "worklink:ListFleets", "ListTagsForResource": "worklink:ListTagsForResource", "ListWebsiteAuthorizationProviders": "worklink:ListWebsiteAuthorizationProviders", "ListWebsiteCertificateAuthorities": "worklink:ListWebsiteCertificateAuthorities", "RestoreDomainAccess": "worklink:RestoreDomainAccess", "RevokeDomainAccess": "worklink:RevokeDomainAccess", "SignOutUser": "worklink:SignOutUser", "TagResource": "worklink:TagResource", "UntagResource": "worklink:UntagResource", "UpdateAuditStreamConfiguration": "worklink:UpdateAuditStreamConfiguration", "UpdateCompanyNetworkConfiguration": "worklink:UpdateCompanyNetworkConfiguration", "UpdateDevicePolicyConfiguration": "worklink:UpdateDevicePolicyConfiguration", "UpdateDomainMetadata": "worklink:UpdateDomainMetadata", "UpdateFleetMetadata": "worklink:UpdateFleetMetadata", "UpdateIdentityProviderConfiguration": "worklink:UpdateIdentityProviderConfiguration" }, "workmail": { "AssociateDelegateToResource": "workmail:AssociateDelegateToResource", "AssociateMemberToGroup": "workmail:AssociateMemberToGroup", "CancelMailboxExportJob": "workmail:CancelMailboxExportJob", "CreateAlias": "workmail:CreateAlias", "CreateGroup": "workmail:CreateGroup", "CreateMobileDeviceAccessRule": "workmail:CreateMobileDeviceAccessRule", "CreateOrganization": "workmail:CreateOrganization", "CreateResource": "workmail:CreateResource", "CreateUser": "workmail:CreateUser", "DeleteAccessControlRule": "workmail:DeleteAccessControlRule", "DeleteAlias": "workmail:DeleteAlias", "DeleteGroup": "workmail:DeleteGroup", "DeleteMailboxPermissions": "workmail:DeleteMailboxPermissions", "DeleteMobileDeviceAccessOverride": "workmail:DeleteMobileDeviceAccessOverride", "DeleteMobileDeviceAccessRule": "workmail:DeleteMobileDeviceAccessRule", "DeleteOrganization": "workmail:DeleteOrganization", "DeleteResource": "workmail:DeleteResource", "DeleteRetentionPolicy": "workmail:DeleteRetentionPolicy", "DeleteUser": "workmail:DeleteUser", "DeregisterFromWorkMail": "workmail:DeregisterFromWorkMail", "DeregisterMailDomain": "workmail:DeregisterMailDomain", "DescribeGroup": "workmail:DescribeGroup", "DescribeInboundDmarcSettings": "workmail:DescribeInboundDmarcSettings", "DescribeMailboxExportJob": "workmail:DescribeMailboxExportJob", "DescribeOrganization": "workmail:DescribeOrganization", "DescribeResource": "workmail:DescribeResource", "DescribeUser": "workmail:DescribeUser", "DisassociateDelegateFromResource": "workmail:DisassociateDelegateFromResource", "DisassociateMemberFromGroup": "workmail:DisassociateMemberFromGroup", "GetAccessControlEffect": "workmail:GetAccessControlEffect", "GetDefaultRetentionPolicy": "workmail:GetDefaultRetentionPolicy", "GetMailDomain": "workmail:GetMailDomain", "GetMailboxDetails": "workmail:GetMailboxDetails", "GetMobileDeviceAccessEffect": "workmail:GetMobileDeviceAccessEffect", "GetMobileDeviceAccessOverride": "workmail:GetMobileDeviceAccessOverride", "ListAccessControlRules": "workmail:ListAccessControlRules", "ListAliases": "workmail:ListAliases", "ListGroupMembers": "workmail:ListGroupMembers", "ListGroups": "workmail:ListGroups", "ListMailDomains": "workmail:ListMailDomains", "ListMailboxExportJobs": "workmail:ListMailboxExportJobs", "ListMailboxPermissions": "workmail:ListMailboxPermissions", "ListMobileDeviceAccessOverrides": "workmail:ListMobileDeviceAccessOverrides", "ListMobileDeviceAccessRules": "workmail:ListMobileDeviceAccessRules", "ListOrganizations": "workmail:ListOrganizations", "ListResourceDelegates": "workmail:ListResourceDelegates", "ListResources": "workmail:ListResources", "ListTagsForResource": "workmail:ListTagsForResource", "ListUsers": "workmail:ListUsers", "PutAccessControlRule": "workmail:PutAccessControlRule", "PutInboundDmarcSettings": "workmail:PutInboundDmarcSettings", "PutMailboxPermissions": "workmail:PutMailboxPermissions", "PutMobileDeviceAccessOverride": "workmail:PutMobileDeviceAccessOverride", "PutRetentionPolicy": "workmail:PutRetentionPolicy", "RegisterMailDomain": "workmail:RegisterMailDomain", "RegisterToWorkMail": "workmail:RegisterToWorkMail", "ResetPassword": "workmail:ResetPassword", "StartMailboxExportJob": "workmail:StartMailboxExportJob", "TagResource": "workmail:TagResource", "UntagResource": "workmail:UntagResource", "UpdateDefaultMailDomain": "workmail:UpdateDefaultMailDomain", "UpdateMailboxQuota": "workmail:UpdateMailboxQuota", "UpdateMobileDeviceAccessRule": "workmail:UpdateMobileDeviceAccessRule", "UpdatePrimaryEmailAddress": "workmail:UpdatePrimaryEmailAddress", "UpdateResource": "workmail:UpdateResource" }, "workmailmessageflow": { "GetRawMessageContent": "workmailmessageflow:GetRawMessageContent", "PutRawMessageContent": "workmailmessageflow:PutRawMessageContent" }, "workspaces": { "AssociateConnectionAlias": "workspaces:AssociateConnectionAlias", "AssociateIpGroups": "workspaces:AssociateIpGroups", "AuthorizeIpRules": "workspaces:AuthorizeIpRules", "CopyWorkspaceImage": "workspaces:CopyWorkspaceImage", "CreateConnectionAlias": "workspaces:CreateConnectionAlias", "CreateIpGroup": "workspaces:CreateIpGroup", "CreateTags": "workspaces:CreateTags", "CreateUpdatedWorkspaceImage": "workspaces:CreateUpdatedWorkspaceImage", "CreateWorkspaceBundle": "workspaces:CreateWorkspaceBundle", "CreateWorkspaces": "workspaces:CreateWorkspaces", "DeleteConnectionAlias": "workspaces:DeleteConnectionAlias", "DeleteIpGroup": "workspaces:DeleteIpGroup", "DeleteTags": "workspaces:DeleteTags", "DeleteWorkspaceBundle": "workspaces:DeleteWorkspaceBundle", "DeleteWorkspaceImage": "workspaces:DeleteWorkspaceImage", "DeregisterWorkspaceDirectory": "workspaces:DeregisterWorkspaceDirectory", "DescribeAccount": "workspaces:DescribeAccount", "DescribeAccountModifications": "workspaces:DescribeAccountModifications", "DescribeClientProperties": "workspaces:DescribeClientProperties", "DescribeConnectionAliasPermissions": "workspaces:DescribeConnectionAliasPermissions", "DescribeConnectionAliases": "workspaces:DescribeConnectionAliases", "DescribeIpGroups": "workspaces:DescribeIpGroups", "DescribeTags": "workspaces:DescribeTags", "DescribeWorkspaceBundles": "workspaces:DescribeWorkspaceBundles", "DescribeWorkspaceDirectories": "workspaces:DescribeWorkspaceDirectories", "DescribeWorkspaceImagePermissions": "workspaces:DescribeWorkspaceImagePermissions", "DescribeWorkspaceImages": "workspaces:DescribeWorkspaceImages", "DescribeWorkspaceSnapshots": "workspaces:DescribeWorkspaceSnapshots", "DescribeWorkspaces": "workspaces:DescribeWorkspaces", "DescribeWorkspacesConnectionStatus": "workspaces:DescribeWorkspacesConnectionStatus", "DisassociateConnectionAlias": "workspaces:DisassociateConnectionAlias", "DisassociateIpGroups": "workspaces:DisassociateIpGroups", "ImportWorkspaceImage": "workspaces:ImportWorkspaceImage", "ListAvailableManagementCidrRanges": "workspaces:ListAvailableManagementCidrRanges", "MigrateWorkspace": "workspaces:MigrateWorkspace", "ModifyAccount": "workspaces:ModifyAccount", "ModifyClientProperties": "workspaces:ModifyClientProperties", "ModifySelfservicePermissions": "workspaces:ModifySelfservicePermissions", "ModifyWorkspaceAccessProperties": "workspaces:ModifyWorkspaceAccessProperties", "ModifyWorkspaceCreationProperties": "workspaces:ModifyWorkspaceCreationProperties", "ModifyWorkspaceProperties": "workspaces:ModifyWorkspaceProperties", "ModifyWorkspaceState": "workspaces:ModifyWorkspaceState", "RebootWorkspaces": "workspaces:RebootWorkspaces", "RebuildWorkspaces": "workspaces:RebuildWorkspaces", "RegisterWorkspaceDirectory": "workspaces:RegisterWorkspaceDirectory", "RestoreWorkspace": "workspaces:RestoreWorkspace", "RevokeIpRules": "workspaces:RevokeIpRules", "StartWorkspaces": "workspaces:StartWorkspaces", "StopWorkspaces": "workspaces:StopWorkspaces", "TerminateWorkspaces": "workspaces:TerminateWorkspaces", "UpdateConnectionAliasPermission": "workspaces:UpdateConnectionAliasPermission", "UpdateRulesOfIpGroup": "workspaces:UpdateRulesOfIpGroup", "UpdateWorkspaceBundle": "workspaces:UpdateWorkspaceBundle", "UpdateWorkspaceImagePermission": "workspaces:UpdateWorkspaceImagePermission" }, "workspaces-web": { "AssociateBrowserSettings": "workspaces-web:AssociateBrowserSettings", "AssociateNetworkSettings": "workspaces-web:AssociateNetworkSettings", "AssociateTrustStore": "workspaces-web:AssociateTrustStore", "AssociateUserSettings": "workspaces-web:AssociateUserSettings", "CreateBrowserSettings": "workspaces-web:CreateBrowserSettings", "CreateIdentityProvider": "workspaces-web:CreateIdentityProvider", "CreateNetworkSettings": "workspaces-web:CreateNetworkSettings", "CreatePortal": "workspaces-web:CreatePortal", "CreateTrustStore": "workspaces-web:CreateTrustStore", "CreateUserSettings": "workspaces-web:CreateUserSettings", "DeleteBrowserSettings": "workspaces-web:DeleteBrowserSettings", "DeleteIdentityProvider": "workspaces-web:DeleteIdentityProvider", "DeleteNetworkSettings": "workspaces-web:DeleteNetworkSettings", "DeletePortal": "workspaces-web:DeletePortal", "DeleteTrustStore": "workspaces-web:DeleteTrustStore", "DeleteUserSettings": "workspaces-web:DeleteUserSettings", "DisassociateBrowserSettings": "workspaces-web:DisassociateBrowserSettings", "DisassociateNetworkSettings": "workspaces-web:DisassociateNetworkSettings", "DisassociateTrustStore": "workspaces-web:DisassociateTrustStore", "DisassociateUserSettings": "workspaces-web:DisassociateUserSettings", "GetBrowserSettings": "workspaces-web:GetBrowserSettings", "GetIdentityProvider": "workspaces-web:GetIdentityProvider", "GetNetworkSettings": "workspaces-web:GetNetworkSettings", "GetPortal": "workspaces-web:GetPortal", "GetPortalServiceProviderMetadata": "workspaces-web:GetPortalServiceProviderMetadata", "GetTrustStore": "workspaces-web:GetTrustStore", "GetTrustStoreCertificate": "workspaces-web:GetTrustStoreCertificate", "GetUserSettings": "workspaces-web:GetUserSettings", "ListBrowserSettings": "workspaces-web:ListBrowserSettings", "ListIdentityProviders": "workspaces-web:ListIdentityProviders", "ListNetworkSettings": "workspaces-web:ListNetworkSettings", "ListPortals": "workspaces-web:ListPortals", "ListTagsForResource": "workspaces-web:ListTagsForResource", "ListTrustStoreCertificates": "workspaces-web:ListTrustStoreCertificates", "ListTrustStores": "workspaces-web:ListTrustStores", "ListUserSettings": "workspaces-web:ListUserSettings", "TagResource": "workspaces-web:TagResource", "UntagResource": "workspaces-web:UntagResource", "UpdateBrowserSettings": "workspaces-web:UpdateBrowserSettings", "UpdateIdentityProvider": "workspaces-web:UpdateIdentityProvider", "UpdateNetworkSettings": "workspaces-web:UpdateNetworkSettings", "UpdatePortal": "workspaces-web:UpdatePortal", "UpdateTrustStore": "workspaces-web:UpdateTrustStore", "UpdateUserSettings": "workspaces-web:UpdateUserSettings" }, "xray": { "BatchGetTraces": "xray:BatchGetTraces", "CreateGroup": "xray:CreateGroup", "CreateSamplingRule": "xray:CreateSamplingRule", "DeleteGroup": "xray:DeleteGroup", "DeleteSamplingRule": "xray:DeleteSamplingRule", "GetEncryptionConfig": "xray:GetEncryptionConfig", "GetGroup": "xray:GetGroup", "GetGroups": "xray:GetGroups", "GetInsight": "xray:GetInsight", "GetInsightEvents": "xray:GetInsightEvents", "GetInsightImpactGraph": "xray:GetInsightImpactGraph", "GetInsightSummaries": "xray:GetInsightSummaries", "GetSamplingRules": "xray:GetSamplingRules", "GetSamplingStatisticSummaries": "xray:GetSamplingStatisticSummaries", "GetSamplingTargets": "xray:GetSamplingTargets", "GetServiceGraph": "xray:GetServiceGraph", "GetTimeSeriesServiceStatistics": "xray:GetTimeSeriesServiceStatistics", "GetTraceGraph": "xray:GetTraceGraph", "GetTraceSummaries": "xray:GetTraceSummaries", "ListTagsForResource": "xray:ListTagsForResource", "PutEncryptionConfig": "xray:PutEncryptionConfig", "PutTelemetryRecords": "xray:PutTelemetryRecords", "PutTraceSegments": "xray:PutTraceSegments", "TagResource": "xray:TagResource", "UntagResource": "xray:UntagResource", "UpdateGroup": "xray:UpdateGroup", "UpdateSamplingRule": "xray:UpdateSamplingRule" } } ================================================ FILE: chalice/policy.py ================================================ """Policy generator based on allowed API calls. This module will take a set of API calls for services and make a best effort attempt to generate an IAM policy for you. """ from __future__ import print_function import os import json import uuid from typing import Optional, Any, List, Dict, Set # noqa import botocore.session from chalice.constants import ( CLOUDWATCH_LOGS, VPC_ATTACH_POLICY, XRAY_POLICY) from chalice.utils import OSUtils # noqa from chalice.config import Config # noqa APIPolicyT = Dict[str, Dict[str, str]] CustomPolicyT = Dict[str, Dict[str, List[str]]] def policy_from_source_code(source_code: str) -> Dict[str, Any]: from chalice.analyzer import get_client_calls_for_app client_calls = get_client_calls_for_app(source_code) builder = PolicyBuilder() policy = builder.build_policy_from_api_calls(client_calls) return policy def load_api_policy_actions() -> APIPolicyT: return _load_json_file('policies.json') def load_custom_policy_actions() -> CustomPolicyT: return _load_json_file('policies-extra.json') def _load_json_file(relative_filename: str) -> Dict[str, Any]: policy_json = os.path.join( os.path.dirname(os.path.abspath(__file__)), relative_filename) with open(policy_json) as f: return json.loads(f.read()) def diff_policies(old: Dict[str, Any], new: Dict[str, Any]) -> Dict[str, Set[str]]: diff = {} old_actions = _create_simple_format(old) new_actions = _create_simple_format(new) removed = old_actions - new_actions added = new_actions - old_actions if removed: diff['removed'] = removed if added: diff['added'] = added return diff def _create_simple_format(policy: Dict[str, Any]) -> Set[str]: # This won't be sufficient is the analyzer is ever able # to work out which resources you're accessing. actions: Set[str] = set() for statement in policy['Statement']: actions.update(statement['Action']) return actions class AppPolicyGenerator(object): def __init__(self, osutils: OSUtils) -> None: self._osutils = osutils def generate_policy(self, config: Config) -> Dict[str, Any]: """Auto generate policy for an application.""" # Admittedly, this is pretty bare bones logic for the time # being. All it really does it work out, given a Config instance, # which files need to analyzed and then delegates to the # appropriately analyzer functions to do the real work. # This may change in the future. app_py = os.path.join(config.project_dir, 'app.py') assert self._osutils.file_exists(app_py) app_source = self._osutils.get_file_contents(app_py, binary=False) app_policy = policy_from_source_code(app_source) app_policy['Statement'].append(CLOUDWATCH_LOGS) if config.subnet_ids and config.security_group_ids: app_policy['Statement'].append(VPC_ATTACH_POLICY) if config.xray_enabled: app_policy['Statement'].append(XRAY_POLICY) return app_policy class PolicyBuilder(object): VERSION = '2012-10-17' def __init__(self, session: Optional[Any] = None, api_policy_actions: Optional[APIPolicyT] = None, custom_policy_actions: Optional[CustomPolicyT] = None ) -> None: if session is None: session = botocore.session.get_session() # The difference between api_policy_actions and custom_policy_actions # is that api_policy_actions correspond to the 1-1 method to API calls # exposed in boto3/botocore clients whereas custom_policy_actions # correspond to method names that represent high level abstractions # that are typically hand written (e.g s3.download_file()). These # are kept as separate files because we manage these files # separately. if api_policy_actions is None: api_policy_actions = load_api_policy_actions() if custom_policy_actions is None: custom_policy_actions = load_custom_policy_actions() self._session = session self._api_policy_actions = api_policy_actions self._custom_policy_actions = custom_policy_actions def build_policy_from_api_calls(self, client_calls: Dict[str, Set[str]] ) -> Dict[str, Any]: statements = self._build_statements_from_client_calls(client_calls) policy = { 'Version': self.VERSION, 'Statement': statements } return policy def _build_statements_from_client_calls(self, client_calls: Dict[str, Set[str]] ) -> List[Dict[str, Any]]: statements = [] # client_calls = service_name -> set([method_calls]) for service in sorted(client_calls): api_actions = self._get_actions_from_api_calls( service, client_calls) custom_actions = self._get_actions_from_high_level_calls( service, client_calls) actions = api_actions + custom_actions if actions: statements.append({ 'Effect': 'Allow', 'Action': actions, # Probably impossible, but it would be nice # to even keep track of what resources are used # so we can create ARNs and further restrict the policies. 'Resource': ['*'], 'Sid': str(uuid.uuid4()).replace('-', ''), }) return statements def _get_actions_from_api_calls(self, service: str, client_calls: Dict[str, Set[str]] ) -> List[str]: if service not in self._api_policy_actions: print("Unsupported service for auto policy generation: %s" % service) return [] service_actions = self._api_policy_actions[service] method_calls = client_calls[service] # Next thing we need to do is convert the method_name to # MethodName. To do this reliably we're going to use # botocore clients. client = self._session.create_client(service, region_name='us-east-1') mapping = client.meta.method_to_api_mapping actions = [service_actions[mapping[method_name]] for method_name in method_calls if mapping.get(method_name) in service_actions] actions.sort() return actions def _get_actions_from_high_level_calls(self, service: str, client_calls: Dict[str, Set[str]] ) -> List[str]: # This gets any actions associated with high level abstractions # e.g s3.download_file(), s3.upload_file(), etc. if service not in self._custom_policy_actions: # We don't warn the user in this case but it's unlikely there # are high level abstractions for a service, this only applies # to s3, dynamodb, and a few other services. return [] service_actions = self._custom_policy_actions[service] method_calls = client_calls[service] actions: Set[str] = set() for method_name in method_calls: if method_name in service_actions: actions.update(service_actions[method_name]) return list(sorted(actions)) ================================================ FILE: chalice/py.typed ================================================ # Marker file for PEP 561. This package provides inline type annotations. ================================================ FILE: chalice/templates/0000-rest-api/.chalice/config.json ================================================ { "version": "2.0", "app_name": "{{app_name}}", "stages": { "dev": { "api_gateway_stage": "api" } } } ================================================ FILE: chalice/templates/0000-rest-api/.gitignore ================================================ .chalice/deployments/ .chalice/venv/ ================================================ FILE: chalice/templates/0000-rest-api/app.py ================================================ from chalice import Chalice app = Chalice(app_name='{{app_name}}') @app.route('/') def index(): return {'hello': 'world'} # The view function above will return {"hello": "world"} # whenever you make an HTTP GET request to '/'. # # Here are a few more examples: # # @app.route('/hello/{name}') # def hello_name(name): # # '/hello/james' -> {"hello": "james"} # return {'hello': name} # # @app.route('/users', methods=['POST']) # def create_user(): # # This is the JSON body the user sent in their POST request. # user_as_json = app.current_request.json_body # # We'll echo the json body back to the user in a 'user' key. # return {'user': user_as_json} # # See the README documentation for more examples. # ================================================ FILE: chalice/templates/0000-rest-api/chalicelib/__init__.py ================================================ ================================================ FILE: chalice/templates/0000-rest-api/metadata.json ================================================ {"description": "REST API"} ================================================ FILE: chalice/templates/0000-rest-api/requirements-dev.txt ================================================ chalice=={{chalice_version}} pytest ================================================ FILE: chalice/templates/0000-rest-api/requirements.txt ================================================ ================================================ FILE: chalice/templates/0000-rest-api/tests/__init__.py ================================================ ================================================ FILE: chalice/templates/0000-rest-api/tests/test_app.py ================================================ from chalice.test import Client from app import app def test_index(): with Client(app) as client: response = client.http.get('/') assert response.json_body == {'hello': 'world'} ================================================ FILE: chalice/templates/0002-s3-event-handler/.chalice/config.json ================================================ { "version": "2.0", "app_name": "{{app_name}}", "stages": { "dev": { "api_gateway_stage": "api" } } } ================================================ FILE: chalice/templates/0002-s3-event-handler/.gitignore ================================================ .chalice/deployments/ .chalice/venv/ ================================================ FILE: chalice/templates/0002-s3-event-handler/app.py ================================================ import os from chalice import Chalice app = Chalice(app_name='{{app_name}}') app.debug = True # Set the value of APP_BUCKET_NAME in the .chalice/config.json file. S3_BUCKET = os.environ.get('APP_BUCKET_NAME', '') @app.on_s3_event(bucket=S3_BUCKET, events=['s3:ObjectCreated:*']) def s3_handler(event): app.log.debug("Received event for bucket: %s, key: %s", event.bucket, event.key) ================================================ FILE: chalice/templates/0002-s3-event-handler/chalicelib/__init__.py ================================================ ================================================ FILE: chalice/templates/0002-s3-event-handler/metadata.json ================================================ {"description": "S3 Event Handler"} ================================================ FILE: chalice/templates/0002-s3-event-handler/requirements-dev.txt ================================================ chalice pytest ================================================ FILE: chalice/templates/0002-s3-event-handler/requirements.txt ================================================ ================================================ FILE: chalice/templates/0002-s3-event-handler/tests/__init__.py ================================================ ================================================ FILE: chalice/templates/0002-s3-event-handler/tests/test_app.py ================================================ from chalice.test import Client from app import app def test_s3_handler(): with Client(app) as client: event = client.events.generate_s3_event( bucket='mybucket', key='mykey') client.lambda_.invoke('s3_handler', event) ================================================ FILE: chalice/templates/0007-lambda-only/.chalice/config.json ================================================ { "version": "2.0", "app_name": "{{app_name}}", "stages": { "dev": { "api_gateway_stage": "api" } } } ================================================ FILE: chalice/templates/0007-lambda-only/.gitignore ================================================ .chalice/deployments/ .chalice/venv/ ================================================ FILE: chalice/templates/0007-lambda-only/app.py ================================================ from chalice import Chalice app = Chalice(app_name='{{app_name}}') @app.lambda_function() def first_function(event, context): return {'hello': 'world'} @app.lambda_function() def second_function(event, context): return {'hello': 'world2'} ================================================ FILE: chalice/templates/0007-lambda-only/chalicelib/__init__.py ================================================ ================================================ FILE: chalice/templates/0007-lambda-only/metadata.json ================================================ {"description": "Lambda Functions only"} ================================================ FILE: chalice/templates/0007-lambda-only/requirements-dev.txt ================================================ chalice pytest ================================================ FILE: chalice/templates/0007-lambda-only/requirements.txt ================================================ ================================================ FILE: chalice/templates/0007-lambda-only/tests/__init__.py ================================================ ================================================ FILE: chalice/templates/0007-lambda-only/tests/test_app.py ================================================ from chalice.test import Client from app import app def test_index(): with Client(app) as client: response = client.lambda_.invoke('first_function', {}) assert response.payload == {'hello': 'world'} ================================================ FILE: chalice/templates/0009-legacy/.chalice/config.json ================================================ { "version": "2.0", "app_name": "{{app_name}}", "stages": { "dev": { "api_gateway_stage": "api" } } } ================================================ FILE: chalice/templates/0009-legacy/.gitignore ================================================ .chalice/deployments/ .chalice/venv/ ================================================ FILE: chalice/templates/0009-legacy/app.py ================================================ from chalice import Chalice app = Chalice(app_name='{{app_name}}') @app.route('/') def index(): return {'hello': 'world'} # The view function above will return {"hello": "world"} # whenever you make an HTTP GET request to '/'. # # Here are a few more examples: # # @app.route('/hello/{name}') # def hello_name(name): # # '/hello/james' -> {"hello": "james"} # return {'hello': name} # # @app.route('/users', methods=['POST']) # def create_user(): # # This is the JSON body the user sent in their POST request. # user_as_json = app.current_request.json_body # # We'll echo the json body back to the user in a 'user' key. # return {'user': user_as_json} # # See the README documentation for more examples. # ================================================ FILE: chalice/templates/0009-legacy/metadata.json ================================================ {"description": "Legacy REST API Template"} ================================================ FILE: chalice/templates/0009-legacy/requirements.txt ================================================ ================================================ FILE: chalice/templates/6001-cdk-ddb/README.rst ================================================ REST API backed by Amazon DynamoDB ================================== This template provides a REST API that's backed by an Amazon DynamoDB table. This application is deployed using the AWS CDK. For more information, see the `Deploying with the AWS CDK `__ tutorial. Quickstart ---------- First, you'll need to install the AWS CDK if you haven't already. The CDK requires Node.js and npm to run. See the `Getting started with the AWS CDK `__ for more details. :: $ npm install -g aws-cdk Next you'll need to install the requirements for the project. :: $ pip install -r requirements.txt There's also separate requirements files in the ``infrastructure`` and ``runtime`` directories if you'd prefer to have separate virtual environments for your CDK and Chalice app. To deploy the application, ``cd`` to the ``infrastructure`` directory. If this is you're first time using the CDK you'll need to bootstrap your environment. :: $ cdk bootstrap Then you can deploy your application using the CDK. :: $ cdk deploy Project layout -------------- This project template combines a CDK application and a Chalice application. These correspond to the ``infrastructure`` and ``runtime`` directory respectively. To run any CDK CLI commands, ensure you're in the ``infrastructure`` directory, and to run any Chalice CLI commands ensure you're in the ``runtime`` directory. ================================================ FILE: chalice/templates/6001-cdk-ddb/infrastructure/app.py ================================================ #!/usr/bin/env python3 try: from aws_cdk import core as cdk except ImportError: import aws_cdk as cdk from stacks.chaliceapp import ChaliceApp app = cdk.App() ChaliceApp(app, '{{app_name}}') app.synth() ================================================ FILE: chalice/templates/6001-cdk-ddb/infrastructure/cdk.json ================================================ { "app": "python3 app.py", "context": { "aws-cdk:enableDiffNoFail": "true", "@aws-cdk/core:stackRelativeExports": "true" } } ================================================ FILE: chalice/templates/6001-cdk-ddb/infrastructure/requirements.txt ================================================ aws-cdk-lib>2.0,<3.0 ================================================ FILE: chalice/templates/6001-cdk-ddb/infrastructure/stacks/__init__.py ================================================ ================================================ FILE: chalice/templates/6001-cdk-ddb/infrastructure/stacks/chaliceapp.py ================================================ import os from aws_cdk import aws_dynamodb as dynamodb try: from aws_cdk import core as cdk except ImportError: import aws_cdk as cdk from chalice.cdk import Chalice RUNTIME_SOURCE_DIR = os.path.join( os.path.dirname(os.path.dirname(__file__)), os.pardir, 'runtime') class ChaliceApp(cdk.Stack): def __init__(self, scope, id, **kwargs): super().__init__(scope, id, **kwargs) self.dynamodb_table = self._create_ddb_table() self.chalice = Chalice( self, 'ChaliceApp', source_dir=RUNTIME_SOURCE_DIR, stage_config={ 'environment_variables': { 'APP_TABLE_NAME': self.dynamodb_table.table_name } } ) self.dynamodb_table.grant_read_write_data( self.chalice.get_role('DefaultRole') ) def _create_ddb_table(self): dynamodb_table = dynamodb.Table( self, 'AppTable', partition_key=dynamodb.Attribute( name='PK', type=dynamodb.AttributeType.STRING), sort_key=dynamodb.Attribute( name='SK', type=dynamodb.AttributeType.STRING ), removal_policy=cdk.RemovalPolicy.DESTROY) cdk.CfnOutput(self, 'AppTableName', value=dynamodb_table.table_name) return dynamodb_table ================================================ FILE: chalice/templates/6001-cdk-ddb/metadata.json ================================================ {"description": "[CDK] Rest API with a DynamoDB table"} ================================================ FILE: chalice/templates/6001-cdk-ddb/requirements.txt ================================================ -r infrastructure/requirements.txt -r runtime/requirements.txt ================================================ FILE: chalice/templates/6001-cdk-ddb/runtime/.chalice/config.json ================================================ { "version": "2.0", "app_name": "{{app_name}}", "stages": { "dev": { "api_gateway_stage": "api", "lambda_functions": { "api_handler": { "environment_variables": { "APP_TABLE_NAME": "" } } } } } } ================================================ FILE: chalice/templates/6001-cdk-ddb/runtime/.gitignore ================================================ .chalice/deployments/ .chalice/venv/ ================================================ FILE: chalice/templates/6001-cdk-ddb/runtime/app.py ================================================ import os import boto3 from chalice import Chalice app = Chalice(app_name='{{app_name}}') dynamodb = boto3.resource('dynamodb') dynamodb_table = dynamodb.Table(os.environ.get('APP_TABLE_NAME', '')) @app.route('/users', methods=['POST']) def create_user(): request = app.current_request.json_body item = { 'PK': 'User#%s' % request['username'], 'SK': 'Profile#%s' % request['username'], } item.update(request) dynamodb_table.put_item(Item=item) return {} @app.route('/users/{username}', methods=['GET']) def get_user(username): key = { 'PK': 'User#%s' % username, 'SK': 'Profile#%s' % username, } item = dynamodb_table.get_item(Key=key)['Item'] del item['PK'] del item['SK'] return item ================================================ FILE: chalice/templates/6001-cdk-ddb/runtime/requirements.txt ================================================ boto3<2.0.0 ================================================ FILE: chalice/test.py ================================================ from __future__ import annotations import os import json import base64 import contextlib from types import TracebackType from typing import Optional, Type, Generator, Dict, Any, List # noqa from chalice import Chalice # noqa from chalice.config import Config from chalice.local import LocalGateway, LambdaContext, LocalGatewayException from chalice.cli.factory import CLIFactory class FunctionNotFoundError(Exception): pass class Client(object): def __init__(self, app: Chalice, stage_name: str = 'dev', project_dir: str = '.') -> None: self._app = app self._project_dir = project_dir self._stage_name = stage_name self._http_client: Optional[TestHTTPClient] = None self._events_client: Optional[TestEventsClient] = None self._lambda_client: Optional[TestLambdaClient] = None self._chalice_config_obj: Optional[Config] = None # We have to be careful about not passing in the CLIFactory # because this is a public interface we're exposing and we don't # want the CLIFactory to be part of that. self._cli_factory = CLIFactory(project_dir) @property def _chalice_config(self) -> Config: if self._chalice_config_obj is None: try: self._chalice_config_obj = self._cli_factory.create_config_obj( chalice_stage_name=self._stage_name) except RuntimeError: # Being able to load a valid config is not required to use # the test client. If you don't have one that you just won't # get any config related data added to your environment. self._chalice_config_obj = Config.create() return self._chalice_config_obj @property def http(self) -> TestHTTPClient: if self._http_client is None: self._http_client = TestHTTPClient(self._app, self._chalice_config) return self._http_client @property def lambda_(self) -> TestLambdaClient: if self._lambda_client is None: self._lambda_client = TestLambdaClient( self._app, self._chalice_config) return self._lambda_client @property def events(self) -> TestEventsClient: if self._events_client is None: self._events_client = TestEventsClient(self._app) return self._events_client def __enter__(self) -> Client: return self def __exit__(self, exception_type: Optional[Type[BaseException]], exception_value: Optional[BaseException], traceback: Optional[TracebackType], ) -> None: pass class BaseClient(object): @contextlib.contextmanager def _patched_env_vars(self, environment_variables): # type: ignore # re: the "type: ignore". Mypy is incredibly pedantic that # os.environ is of type "_Environ[str]" and it can't be assigned # an expression of Dict[str, str]. While that's technically "correct", # that's way too much effort to get the typing right on this, so # we're just ignoring the type checking for this method. original = os.environ patched = os.environ.copy() patched.update(environment_variables) os.environ = patched try: yield finally: os.environ = original class TestHTTPClient(BaseClient): def __init__(self, app: Chalice, config: Config) -> None: self._app = app self._config = config self._local_gateway = LocalGateway(app, self._config) def request(self, method: str, path: str, headers: Optional[Dict[str, str]] = None, body: bytes = b'') -> HTTPResponse: if headers is None: headers = {} scoped = self._config.scope(self._config.chalice_stage, 'api_handler') with self._patched_env_vars(scoped.environment_variables): try: response = self._local_gateway.handle_request( method=method.upper(), path=path, headers=headers, body=body ) except LocalGatewayException as e: return self._error_response(e) return HTTPResponse.create_from_dict(response) def _error_response(self, e: LocalGatewayException) -> HTTPResponse: return HTTPResponse( headers=e.headers, body=e.body if e.body else b'', status_code=e.CODE ) def get(self, path: str, **kwargs: Any) -> HTTPResponse: return self.request('GET', path, **kwargs) def post(self, path: str, **kwargs: Any) -> HTTPResponse: return self.request('POST', path, **kwargs) def put(self, path: str, **kwargs: Any) -> HTTPResponse: return self.request('PUT', path, **kwargs) def patch(self, path: str, **kwargs: Any) -> HTTPResponse: return self.request('PATCH', path, **kwargs) def options(self, path: str, **kwargs: Any) -> HTTPResponse: return self.request('OPTIONS', path, **kwargs) def delete(self, path: str, **kwargs: Any) -> HTTPResponse: return self.request('DELETE', path, **kwargs) def head(self, path: str, **kwargs: Any) -> HTTPResponse: return self.request('HEAD', path, **kwargs) class HTTPResponse(object): def __init__(self, body: bytes, headers: Dict[str, str], status_code: int) -> None: self.body = body self.headers = headers self.status_code = status_code @property def json_body(self) -> Any: try: return json.loads(self.body) except ValueError: return None @classmethod def create_from_dict(cls, response_dict: Dict[str, Any]) -> HTTPResponse: # Takes the response dict we have to send back to lambda # and exposes it as a python object. if response_dict.get('isBase64Encoded', False): body = base64.b64decode(response_dict['body']) else: body = response_dict['body'].encode('utf-8') combined_headers = response_dict['headers'] combined_headers.update(response_dict['multiValueHeaders']) return cls( body=body, status_code=response_dict['statusCode'], headers=combined_headers, ) class TestEventsClient(BaseClient): def __init__(self, app: Chalice) -> None: self._app = app def generate_sns_event(self, message: str, subject: str = '', message_attributes: Optional[Dict[str, Any]] = None ) -> Dict[str, Any]: if message_attributes is None: message_attributes = { 'AttributeKey': { 'Type': 'String', 'Value': 'AttributeValue' } } sns_event = {'Records': [{ 'EventSource': 'aws:sns', 'EventSubscriptionArn': 'arn:subscription-arn', 'EventVersion': '1.0', 'Sns': { 'Message': message, 'MessageAttributes': message_attributes, 'MessageId': 'abcdefgh-51e4-5ae2-9964-b296c8d65d1a', 'Signature': 'signature', 'SignatureVersion': '1', 'SigningCertUrl': ( 'https://sns.us-west-2.amazonaws.com/cert.pem'), 'Subject': subject, 'Timestamp': '2018-06-26T19:41:38.695Z', 'TopicArn': 'arn:aws:sns:us-west-2:12345:TopicName', 'Type': 'Notification', 'UnsubscribeUrl': 'https://unsubscribe-url/' } }]} return sns_event def generate_s3_event(self, bucket: str, key: str, event_name: str = 'ObjectCreated:Put' ) -> Dict[str, Any]: s3_event = { 'Records': [ {'awsRegion': 'us-west-2', 'eventName': event_name, 'eventSource': 'aws:s3', 'eventTime': '2018-05-22T04:41:23.823Z', 'eventVersion': '2.0', 'requestParameters': {'sourceIPAddress': '1.1.1.1'}, 'responseElements': { 'x-amz-id-2': 'request-id-2', 'x-amz-request-id': 'request-id-1' }, 's3': { 'bucket': { 'arn': 'arn:aws:s3:::%s' % bucket, 'name': bucket, 'ownerIdentity': { 'principalId': 'ABCD' } }, 'configurationId': 'config-id', 'object': { 'eTag': 'd41d8cd98f00b204e9800998ecf8427e', 'key': key, 'sequencer': '005B039F73C627CE8B', 'size': 0 }, 's3SchemaVersion': '1.0' }, 'userIdentity': {'principalId': 'AWS:XYZ'} } ] } return s3_event def generate_sqs_event(self, message_bodies: List[str], queue_name: str = 'queue-name') -> Dict[str, Any]: records = [{ 'attributes': { 'ApproximateFirstReceiveTimestamp': '1530576251596', 'ApproximateReceiveCount': '1', 'SenderId': 'sender-id', 'SentTimestamp': '1530576251595' }, 'awsRegion': 'us-west-2', 'body': body, 'eventSource': 'aws:sqs', 'eventSourceARN': 'arn:aws:sqs:us-west-2:12345:%s' % queue_name, 'md5OfBody': '754ac2f7a12df38320e0c5eafd060145', 'messageAttributes': {}, 'messageId': 'message-id', 'receiptHandle': 'receipt-handle' } for body in message_bodies] sqs_event = {'Records': records} return sqs_event def generate_cw_event(self, source: str, detail_type: str, detail: Dict[str, Any], resources: List[str], region: str = 'us-west-2' ) -> Dict[str, Any]: event = { "version": 0, "id": "7bf73129-1428-4cd3-a780-95db273d1602", "detail-type": detail_type, "source": source, "account": "123456789012", "time": "2015-11-11T21:29:54Z", "region": region, "resources": resources, "detail": detail, } return event def generate_kinesis_event(self, message_bodies: List[bytes], stream_name: str = 'stream-name' ) -> Dict[str, Any]: records = [{ "kinesis": { "kinesisSchemaVersion": "1.0", "partitionKey": "1", "sequenceNumber": "12345", "data": base64.b64encode(body).decode('ascii'), "approximateArrivalTimestamp": 1545084650.987 }, "eventSource": "aws:kinesis", "eventVersion": "1.0", "eventID": "shardId-000000000006:12345", "eventName": "aws:kinesis:record", "invokeIdentityArn": "arn:aws:iam::123:role/lambda-role", "awsRegion": "us-west-2", "eventSourceARN": ( "arn:aws:kinesis:us-east-2:123:stream/%s" % stream_name ) } for body in message_bodies] return {'Records': records} class TestLambdaClient(BaseClient): def __init__(self, app: Chalice, config: Config) -> None: self._app = app self._config = config def invoke(self, function_name: str, payload: Optional[Any] = None) -> InvokeResponse: if payload is None: payload = {} scoped = self._config.scope(self._config.chalice_stage, function_name) lambda_context = LambdaContext( function_name, memory_size=scoped.lambda_memory_size) if function_name not in self._app.handler_map: raise FunctionNotFoundError(function_name) with self._patched_env_vars(scoped.environment_variables): response = self._app.handler_map[function_name]( payload, lambda_context) return InvokeResponse(payload=response) class InvokeResponse(object): def __init__(self, payload: Any) -> None: self.payload = payload ================================================ FILE: chalice/utils.py ================================================ import io import os import zipfile import json import contextlib import tempfile import re import shutil import sys import tarfile from datetime import datetime, timedelta import subprocess from os import PathLike # noqa from collections import OrderedDict # noqa import click from typing import IO, Dict, List, Any, Tuple, Iterator, BinaryIO, Text # noqa from typing import Optional, Union # noqa from typing import MutableMapping, Callable # noqa from typing import cast # noqa import dateutil.parser from dateutil.tz import tzutc from chalice.constants import WELCOME_PROMPT OptInt = Optional[int] OptBytes = Optional[bytes] EnvVars = MutableMapping StrPath = Union[str, 'PathLike[str]'] class AbortedError(Exception): pass def to_cfn_resource_name(name: str) -> str: """Transform a name to a valid cfn name. This will convert the provided name to a CamelCase name. It's possible that the conversion to a CFN resource name can result in name collisions. It's up to the caller to handle name collisions appropriately. """ if not name: raise ValueError("Invalid name: %r" % name) word_separators = ['-', '_'] for word_separator in word_separators: word_parts = [p for p in name.split(word_separator) if p] name = ''.join([w[0].upper() + w[1:] for w in word_parts]) return re.sub(r'[^A-Za-z0-9]+', '', name) def remove_stage_from_deployed_values(key: str, filename: str) -> None: """Delete a top level key from the deployed JSON file.""" final_values: Dict[str, Any] = {} try: with open(filename, 'r') as f: final_values = json.load(f) except IOError: # If there is no file to delete from, then this funciton is a noop. return try: del final_values[key] with open(filename, 'wb') as outfile: data = serialize_to_json(final_values) outfile.write(data.encode('utf-8')) except KeyError: # If they key didn't exist then there is nothing to remove. pass def record_deployed_values( deployed_values: Dict[str, Any], filename: str ) -> None: """Record deployed values to a JSON file. This allows subsequent deploys to lookup previously deployed values. """ final_values: Dict[str, Any] = {} if os.path.isfile(filename): with open(filename, 'r') as f: final_values = json.load(f) final_values.update(deployed_values) with open(filename, 'wb') as outfile: data = serialize_to_json(final_values) outfile.write(data.encode('utf-8')) def serialize_to_json(data: Any) -> str: """Serialize to pretty printed JSON. This includes using 2 space indentation, no trailing whitespace, and including a newline at the end of the JSON document. Useful when you want to serialize JSON to disk. """ return json.dumps(data, indent=2, separators=(',', ': ')) + '\n' class ChaliceZipFile(zipfile.ZipFile): """Support deterministic zipfile generation. Normalizes datetime and permissions. """ compression = 0 # Try to make mypy happy. _default_time_time = (1980, 1, 1, 0, 0, 0) def __init__(self, *args: Any, **kwargs: Any) -> None: self._osutils = cast(OSUtils, kwargs.pop('osutils', OSUtils())) super(ChaliceZipFile, self).__init__(*args, **kwargs) # pylint: disable=W0221 def write( self, filename: StrPath, arcname: Optional[StrPath] = None, compress_type: OptInt = None, compresslevel: OptInt = None, ) -> None: # Only supports files, py2.7 and 3 have different signatures. # We know that in our packager code we never call write() on # directories. zinfo = self._create_zipinfo(filename, arcname, compress_type) with open(filename, 'rb') as f: self.writestr(zinfo, f.read()) def _create_zipinfo( self, filename: StrPath, arcname: Optional[StrPath], compress_type: Optional[int], ) -> zipfile.ZipInfo: # The main thing that prevents deterministic zip file generation # is that the mtime of the file is included in the zip metadata. # We don't actually care what the mtime is when we run on lambda, # so we always set it to the default value (which comes from # zipfile.py). This ensures that as long as the file contents don't # change (or the permissions) then we'll always generate the exact # same zip file bytes. # We also can't use ZipInfo.from_file(), it's only in python3. st = self._osutils.stat(str(filename)) if arcname is None: arcname = filename arcname = self._osutils.normalized_filename(str(arcname)) arcname = arcname.lstrip(os.sep) zinfo = zipfile.ZipInfo(arcname, self._default_time_time) # The external_attr needs the upper 16 bits to be the file mode # so we have to shift it up to the right place. zinfo.external_attr = (st.st_mode & 0xFFFF) << 16 zinfo.file_size = st.st_size zinfo.compress_type = compress_type or self.compression return zinfo def create_zip_file(source_dir: str, outfile: str) -> None: """Create a zip file from a source input directory. This function is intended to be an equivalent to `zip -r`. You give it a source directory, `source_dir`, and it will recursively zip up the files into a zipfile specified by the `outfile` argument. """ with ChaliceZipFile( outfile, 'w', compression=zipfile.ZIP_DEFLATED, osutils=OSUtils() ) as z: for root, _, filenames in os.walk(source_dir): for filename in filenames: full_name = os.path.join(root, filename) archive_name = os.path.relpath(full_name, source_dir) z.write(full_name, archive_name) class OSUtils(object): ZIP_DEFLATED = zipfile.ZIP_DEFLATED def environ(self) -> MutableMapping: return os.environ def open(self, filename: str, mode: str) -> IO: return open(filename, mode) def open_zip( self, filename: str, mode: str, compression: int = ZIP_DEFLATED ) -> zipfile.ZipFile: return ChaliceZipFile( filename, mode, compression=compression, osutils=self ) def remove_file(self, filename: str) -> None: """Remove a file, noop if file does not exist.""" # Unlike os.remove, if the file does not exist, # then this method does nothing. try: os.remove(filename) except OSError: pass def file_exists(self, filename: str) -> bool: return os.path.isfile(filename) def get_file_contents( self, filename: str, binary: bool = True, encoding: Any = 'utf-8' ) -> str: # It looks like the type definition for io.open is wrong. # the encoding arg is unicode, but the actual type is # Optional[Text]. For now we have to use Any to keep mypy happy. if binary: mode = 'rb' # In binary mode the encoding is not used and most be None. encoding = None else: mode = 'r' with io.open(filename, mode, encoding=encoding) as f: return f.read() def set_file_contents( self, filename: str, contents: str, binary: bool = True ) -> None: if binary: mode = 'wb' else: mode = 'w' with open(filename, mode) as f: f.write(contents) def extract_zipfile(self, zipfile_path: str, unpack_dir: str) -> None: with zipfile.ZipFile(zipfile_path, 'r') as z: z.extractall(unpack_dir) def extract_tarfile(self, tarfile_path: str, unpack_dir: str) -> None: with tarfile.open(tarfile_path, 'r:*') as tar: # In Python 3.12+, there's a `filter` arg where passing a # 'data' value will handle this behavior for us. To support older # versions of Python we handle this ourselves. We can't hook # into `extractall` directly so the idea is that we do a separate # validation pass first to ensure there's no files that try # to extract outside of the provided `unpack_dir`. This is roughly # based off of what's done in the `data_filter()` in Python 3.12. self._validate_safe_extract(tar, unpack_dir) tar.extractall(unpack_dir) def _validate_safe_extract( self, tar: tarfile.TarFile, unpack_dir: str ) -> None: for member in tar: self._validate_single_tar_member(member, unpack_dir) def _validate_single_tar_member( self, member: tarfile.TarInfo, unpack_dir: str ) -> None: name = member.name dest_path = os.path.realpath(unpack_dir) if name.startswith(('/', os.sep)): name = member.path.lstrip('/' + os.sep) if os.path.isabs(name): raise RuntimeError(f"Absolute path in tarfile not allowed: {name}") target_path = os.path.realpath(os.path.join(dest_path, name)) # Check we don't escape the destination dir, e.g `../../foo` if os.path.commonpath([target_path, dest_path]) != dest_path: raise RuntimeError( f"Tar member outside destination dir: {target_path}") # If we're dealing with a member that's some type of link, ensure # it doesn't point to anything outside of the destination dir. if member.islnk() or member.issym(): if os.path.abspath(member.linkname): raise RuntimeError(f"Symlink to abspath: {member.linkname}") if member.issym(): target_path = os.path.join( dest_path, os.path.dirname(name), member.linkname, ) else: target_path = os.path.join( dest_path, member.linkname) target_path = os.path.realpath(target_path) if os.path.commonpath([target_path, dest_path]) != dest_path: raise RuntimeError( f"Symlink outside of dest dir: {target_path}") def directory_exists(self, path: str) -> bool: return os.path.isdir(path) def get_directory_contents(self, path: str) -> List[str]: return os.listdir(path) def makedirs(self, path: str) -> None: os.makedirs(path) def dirname(self, path: str) -> str: return os.path.dirname(path) def abspath(self, path: str) -> str: return os.path.abspath(path) def joinpath(self, *args: str) -> str: return os.path.join(*args) def walk( self, path: str, followlinks: bool = False ) -> Iterator[Tuple[str, List[str], List[str]]]: return os.walk(path, followlinks=followlinks) def copytree(self, source: str, destination: str) -> None: if not os.path.exists(destination): self.makedirs(destination) names = self.get_directory_contents(source) for name in names: new_source = os.path.join(source, name) new_destination = os.path.join(destination, name) if os.path.isdir(new_source): self.copytree(new_source, new_destination) else: shutil.copy2(new_source, new_destination) def rmtree(self, directory: str) -> None: shutil.rmtree(directory) def copy(self, source: str, destination: str) -> None: shutil.copy(source, destination) def move(self, source: str, destination: str) -> None: shutil.move(source, destination) @contextlib.contextmanager def tempdir(self) -> Any: tempdir = tempfile.mkdtemp() try: yield tempdir finally: shutil.rmtree(tempdir) def popen( self, command: List[str], stdout: OptInt = None, stderr: OptInt = None, env: Optional[EnvVars] = None, ) -> subprocess.Popen: p = subprocess.Popen(command, stdout=stdout, stderr=stderr, env=env) return p def mtime(self, path: str) -> float: return os.stat(path).st_mtime def stat(self, path: str) -> os.stat_result: return os.stat(path) def normalized_filename(self, path: str) -> str: """Normalize a path into a filename. This will normalize a file and remove any 'drive' component from the path on OSes that support drive specifications. """ return os.path.normpath(os.path.splitdrive(path)[1]) @property def pipe(self) -> int: return subprocess.PIPE def basename(self, path: str) -> str: return os.path.basename(path) def getting_started_prompt(prompter: Any) -> bool: return prompter.prompt(WELCOME_PROMPT) class UI(object): def __init__( self, out: Optional[IO] = None, err: Optional[IO] = None, confirm: Optional[Any] = None, ) -> None: # I tried using a more exact type for the 'confirm' # param, but mypy seems to miss the 'if confirm is None' # check and types _confirm as Union[..., None]. # So for now, we're using Any for this type. if out is None: out = sys.stdout if err is None: err = sys.stderr if confirm is None: confirm = click.confirm self._out = out self._err = err self._confirm = confirm def write(self, msg: str) -> None: self._out.write(msg) def error(self, msg: str) -> None: self._err.write(msg) def confirm( self, msg: str, default: bool = False, abort: bool = False ) -> Any: try: return self._confirm(msg, default, abort) except click.Abort: raise AbortedError() class PipeReader(object): def __init__(self, stream: IO[bytes]) -> None: self._stream = stream def read(self) -> OptBytes: if not self._stream.isatty(): return self._stream.read() return None class TimestampConverter(object): # This is roughly based off of what's used in the AWS CLI. _RELATIVE_TIMESTAMP_REGEX = re.compile( r"(?P\d+)(?Ps|m|h|d|w)$" ) _TO_SECONDS = { 's': 1, 'm': 60, 'h': 3600, 'd': 24 * 3600, 'w': 7 * 24 * 3600, } def __init__(self, now: Optional[Callable[[], datetime]] = None) -> None: if now is None: now = datetime.utcnow self._now = now def timestamp_to_datetime(self, timestamp: str) -> datetime: """Convert a timestamp to a datetime object. This method detects what type of timestamp is provided and parse is appropriately to a timestamp object. """ re_match = self._RELATIVE_TIMESTAMP_REGEX.match(timestamp) if re_match: datetime_value = self._relative_timestamp_to_datetime( int(re_match.group('amount')), re_match.group('unit') ) else: datetime_value = self.parse_iso8601_timestamp(timestamp) return datetime_value def _relative_timestamp_to_datetime( self, amount: int, unit: str ) -> datetime: multiplier = self._TO_SECONDS[unit] return self._now() + timedelta(seconds=amount * multiplier * -1) def parse_iso8601_timestamp(self, timestamp: str) -> datetime: return dateutil.parser.parse(timestamp, tzinfos={'GMT': tzutc()}) ================================================ FILE: chalice/vendored/__init__.py ================================================ ================================================ FILE: chalice/vendored/botocore/__init__.py ================================================ ================================================ FILE: chalice/vendored/botocore/regions.py ================================================ # Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"). You # may not use this file except in compliance with the License. A copy of # the License is located at # # http://aws.amazon.com/apache2.0/ # # or in the "license" file accompanying this file. This file is # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. """Resolves regions and endpoints. This module implements endpoint resolution, including resolving endpoints for a given service and region and resolving the available endpoints for a service in a specific AWS partition. """ import logging import re from botocore.exceptions import NoRegionError LOG = logging.getLogger(__name__) DEFAULT_URI_TEMPLATE = '{service}.{region}.{dnsSuffix}' DEFAULT_SERVICE_DATA = {'endpoints': {}} class BaseEndpointResolver(object): """Resolves regions and endpoints. Must be subclassed.""" def construct_endpoint(self, service_name, region_name=None): """Resolves an endpoint for a service and region combination. :type service_name: string :param service_name: Name of the service to resolve an endpoint for (e.g., s3) :type region_name: string :param region_name: Region/endpoint name to resolve (e.g., us-east-1) if no region is provided, the first found partition-wide endpoint will be used if available. :rtype: dict :return: Returns a dict containing the following keys: - partition: (string, required) Resolved partition name - endpointName: (string, required) Resolved endpoint name - hostname: (string, required) Hostname to use for this endpoint - sslCommonName: (string) sslCommonName to use for this endpoint. - credentialScope: (dict) Signature version 4 credential scope - region: (string) region name override when signing. - service: (string) service name override when signing. - signatureVersions: (list) A list of possible signature versions, including s3, v4, v2, and s3v4 - protocols: (list) A list of supported protocols (e.g., http, https) - ...: Other keys may be included as well based on the metadata """ raise NotImplementedError def get_available_partitions(self): """Lists the partitions available to the endpoint resolver. :return: Returns a list of partition names (e.g., ["aws", "aws-cn"]). """ raise NotImplementedError def get_available_endpoints(self, service_name, partition_name='aws', allow_non_regional=False): """Lists the endpoint names of a particular partition. :type service_name: string :param service_name: Name of a service to list endpoint for (e.g., s3) :type partition_name: string :param partition_name: Name of the partition to limit endpoints to. (e.g., aws for the public AWS endpoints, aws-cn for AWS China endpoints, aws-us-gov for AWS GovCloud (US) Endpoints, etc. :type allow_non_regional: bool :param allow_non_regional: Set to True to include endpoints that are not regional endpoints (e.g., s3-external-1, fips-us-gov-west-1, etc). :return: Returns a list of endpoint names (e.g., ["us-east-1"]). """ raise NotImplementedError class EndpointResolver(BaseEndpointResolver): """Resolves endpoints based on partition endpoint metadata""" def __init__(self, endpoint_data): """ :param endpoint_data: A dict of partition data. """ if 'partitions' not in endpoint_data: raise ValueError('Missing "partitions" in endpoint data') self._endpoint_data = endpoint_data def get_available_partitions(self): result = [] for partition in self._endpoint_data['partitions']: result.append(partition['partition']) return result def get_available_endpoints(self, service_name, partition_name='aws', allow_non_regional=False): result = [] for partition in self._endpoint_data['partitions']: if partition['partition'] != partition_name: continue services = partition['services'] if service_name not in services: continue for endpoint_name in services[service_name]['endpoints']: if allow_non_regional or endpoint_name in partition['regions']: result.append(endpoint_name) return result def construct_endpoint(self, service_name, region_name=None, partition_name=None): if partition_name is not None: valid_partition = None for partition in self._endpoint_data['partitions']: if partition['partition'] == partition_name: valid_partition = partition if valid_partition is not None: result = self._endpoint_for_partition(valid_partition, service_name, region_name, True) return result return None # Iterate over each partition until a match is found. for partition in self._endpoint_data['partitions']: result = self._endpoint_for_partition( partition, service_name, region_name) if result: return result def _endpoint_for_partition(self, partition, service_name, region_name, force_partition=False): # Get the service from the partition, or an empty template. service_data = partition['services'].get( service_name, DEFAULT_SERVICE_DATA) # Use the partition endpoint if no region is supplied. if region_name is None: if 'partitionEndpoint' in service_data: region_name = service_data['partitionEndpoint'] else: raise NoRegionError() # Attempt to resolve the exact region for this partition. if region_name in service_data['endpoints']: return self._resolve( partition, service_name, service_data, region_name) # Check to see if the endpoint provided is valid for the partition. if self._region_match(partition, region_name) or force_partition: # Use the partition endpoint if set and not regionalized. partition_endpoint = service_data.get('partitionEndpoint') is_regionalized = service_data.get('isRegionalized', True) if partition_endpoint and not is_regionalized: LOG.debug('Using partition endpoint for %s, %s: %s', service_name, region_name, partition_endpoint) return self._resolve( partition, service_name, service_data, partition_endpoint) LOG.debug('Creating a regex based endpoint for %s, %s', service_name, region_name) return self._resolve( partition, service_name, service_data, region_name) def _region_match(self, partition, region_name): if region_name in partition['regions']: return True if 'regionRegex' in partition: return re.compile(partition['regionRegex']).match(region_name) return False def _resolve(self, partition, service_name, service_data, endpoint_name): result = service_data['endpoints'].get(endpoint_name, {}) result['partition'] = partition['partition'] result['endpointName'] = endpoint_name # Merge in the service defaults then the partition defaults. self._merge_keys(service_data.get('defaults', {}), result) self._merge_keys(partition.get('defaults', {}), result) hostname = result.get('hostname', DEFAULT_URI_TEMPLATE) result['hostname'] = self._expand_template( partition, result['hostname'], service_name, endpoint_name) if 'sslCommonName' in result: result['sslCommonName'] = self._expand_template( partition, result['sslCommonName'], service_name, endpoint_name) result['dnsSuffix'] = partition['dnsSuffix'] return result def _merge_keys(self, from_data, result): for key in from_data: if key not in result: result[key] = from_data[key] def _expand_template(self, partition, template, service_name, endpoint_name): return template.format( service=service_name, region=endpoint_name, dnsSuffix=partition['dnsSuffix']) ================================================ FILE: docs/Makefile ================================================ # Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = -W SPHINXBUILD = sphinx-build PAPER = BUILDDIR = build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don\'t have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source .PHONY: help help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " applehelp to make an Apple Help Book" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " epub3 to make an epub3" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" @echo " coverage to run coverage check of the documentation (if enabled)" @echo " dummy to check syntax errors of document sources" .PHONY: clean clean: rm -rf $(BUILDDIR)/* .PHONY: html html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." .PHONY: dirhtml dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." .PHONY: singlehtml singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." .PHONY: pickle pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." .PHONY: json json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." .PHONY: htmlhelp htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." .PHONY: qthelp qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Chalice.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Chalice.qhc" .PHONY: applehelp applehelp: $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp @echo @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." @echo "N.B. You won't be able to view it unless you put it in" \ "~/Library/Documentation/Help or install it in your application" \ "bundle." .PHONY: devhelp devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/Chalice" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Chalice" @echo "# devhelp" .PHONY: epub epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." .PHONY: epub3 epub3: $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 @echo @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." .PHONY: latex latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." .PHONY: latexpdf latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." .PHONY: latexpdfja latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." .PHONY: text text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." .PHONY: man man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." .PHONY: texinfo texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." .PHONY: info info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." .PHONY: gettext gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." .PHONY: changes changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." .PHONY: linkcheck linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." .PHONY: doctest doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." .PHONY: coverage coverage: $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage @echo "Testing of coverage in the sources finished, look at the " \ "results in $(BUILDDIR)/coverage/python.txt." .PHONY: xml xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." .PHONY: pseudoxml pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." .PHONY: dummy dummy: $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy @echo @echo "Build finished. Dummy builder generates no files." ================================================ FILE: docs/source/_static/custom.css ================================================ div.body h1 { margin-top: 0; padding-top: 0; font-size: 200%; } div.highlight-python { border-width: 0 0 0 2px; border-style: solid; border-color: #84ad98; } div.highlight-default { border-width: 0 0 0 2px; border-style: solid; border-color: #a9a9a9; } a.reference { border-bottom: 0.5px dotted #faa; } ================================================ FILE: docs/source/_static/fonts/open-sans/stylesheet.css ================================================ /***** Font Definition for Open Sans. This stylesheet comes from qrohlf.com/posts/better-opensans *****/ /* Regular */ @font-face { font-family: 'Open Sans'; src: url('fonts/OpenSans-Regular-webfont.eot'); src: url('fonts/OpenSans-Regular-webfont.eot?#iefix') format('embedded-opentype'), url('fonts/OpenSans-Regular-webfont.woff') format('woff'), url('fonts/OpenSans-Regular-webfont.ttf') format('truetype'), url('fonts/OpenSans-Regular-webfont.svg#OpenSansRegular') format('svg'); font-weight: normal; font-weight: 400; font-style: normal; } /* Italic */ @font-face { font-family: 'Open Sans'; src: url('fonts/OpenSans-Italic-webfont.eot'); src: url('fonts/OpenSans-Italic-webfont.eot?#iefix') format('embedded-opentype'), url('fonts/OpenSans-Italic-webfont.woff') format('woff'), url('fonts/OpenSans-Italic-webfont.ttf') format('truetype'), url('fonts/OpenSans-Italic-webfont.svg#OpenSansItalic') format('svg'); font-weight: normal; font-weight: 400; font-style: italic; } /* Light */ @font-face { font-family: 'Open Sans'; src: url('fonts/OpenSans-Light-webfont.eot'); src: url('fonts/OpenSans-Light-webfont.eot?#iefix') format('embedded-opentype'), url('fonts/OpenSans-Light-webfont.woff') format('woff'), url('fonts/OpenSans-Light-webfont.ttf') format('truetype'), url('fonts/OpenSans-Light-webfont.svg#OpenSansLight') format('svg'); font-weight: 200; font-style: normal; } /* Light Italic */ @font-face { font-family: 'Open Sans'; src: url('fonts/OpenSans-LightItalic-webfont.eot'); src: url('fonts/OpenSans-LightItalic-webfont.eot?#iefix') format('embedded-opentype'), url('fonts/OpenSans-LightItalic-webfont.woff') format('woff'), url('fonts/OpenSans-LightItalic-webfont.ttf') format('truetype'), url('fonts/OpenSans-LightItalic-webfont.svg#OpenSansLightItalic') format('svg'); font-weight: 200; font-style: italic; } /* Semibold */ @font-face { font-family: 'Open Sans'; src: url('fonts/OpenSans-Semibold-webfont.eot'); src: url('fonts/OpenSans-Semibold-webfont.eot?#iefix') format('embedded-opentype'), url('fonts/OpenSans-Semibold-webfont.woff') format('woff'), url('fonts/OpenSans-Semibold-webfont.ttf') format('truetype'), url('fonts/OpenSans-Semibold-webfont.svg#OpenSansSemibold') format('svg'); font-weight: 500; font-style: normal; } /* Semibold Italic */ @font-face { font-family: 'Open Sans'; src: url('fonts/OpenSans-SemiboldItalic-webfont.eot'); src: url('fonts/OpenSans-SemiboldItalic-webfont.eot?#iefix') format('embedded-opentype'), url('fonts/OpenSans-SemiboldItalic-webfont.woff') format('woff'), url('fonts/OpenSans-SemiboldItalic-webfont.ttf') format('truetype'), url('fonts/OpenSans-SemiboldItalic-webfont.svg#OpenSansSemiboldItalic') format('svg'); font-weight: 500; font-style: italic; } /* Bold */ @font-face { font-family: 'Open Sans'; src: url('fonts/OpenSans-Bold-webfont.eot'); src: url('fonts/OpenSans-Bold-webfont.eot?#iefix') format('embedded-opentype'), url('fonts/OpenSans-Bold-webfont.woff') format('woff'), url('fonts/OpenSans-Bold-webfont.ttf') format('truetype'), url('fonts/OpenSans-Bold-webfont.svg#OpenSansBold') format('svg'); font-weight: bold; font-weight: 700; font-style: normal; } /* Bold Italic */ @font-face { font-family: 'Open Sans'; src: url('fonts/OpenSans-BoldItalic-webfont.eot'); src: url('fonts/OpenSans-BoldItalic-webfont.eot?#iefix') format('embedded-opentype'), url('fonts/OpenSans-BoldItalic-webfont.woff') format('woff'), url('fonts/OpenSans-BoldItalic-webfont.ttf') format('truetype'), url('fonts/OpenSans-BoldItalic-webfont.svg#OpenSansBoldItalic') format('svg'); font-weight: bold; font-weight: 700; font-style: italic; } /* Extra Bold */ @font-face { font-family: 'Open Sans'; src: url('fonts/OpenSans-ExtraBold-webfont.eot'); src: url('fonts/OpenSans-ExtraBold-webfont.eot?#iefix') format('embedded-opentype'), url('fonts/OpenSans-ExtraBold-webfont.woff') format('woff'), url('fonts/OpenSans-ExtraBold-webfont.ttf') format('truetype'), url('fonts/OpenSans-ExtraBold-webfont.svg#OpenSansExtrabold') format('svg'); font-weight: 900; font-style: normal; } /* Extra Bold Italic */ @font-face { font-family: 'Open Sans'; src: url('fonts/OpenSans-ExtraBoldItalic-webfont.eot'); src: url('fonts/OpenSans-ExtraBoldItalic-webfont.eot?#iefix') format('embedded-opentype'), url('fonts/OpenSans-ExtraBoldItalic-webfont.woff') format('woff'), url('fonts/OpenSans-ExtraBoldItalic-webfont.ttf') format('truetype'), url('fonts/OpenSans-ExtraBoldItalic-webfont.svg#OpenSansExtraboldItalic') format('svg'); font-weight: 900; font-style: italic; } ================================================ FILE: docs/source/_templates/layout.html ================================================ {%- extends "!layout.html" %} {%- block extrahead %} {{ super() }} {% endblock %} ================================================ FILE: docs/source/api.rst ================================================ Chalice ======= .. class:: Chalice(app_name) This class represents a chalice application. It provides: * The ability to register routes using the :meth:`route` method. * Within a view function, the ability to introspect the current request using the ``current_request`` attribute which is an instance of the :class:`Request` class. .. attribute:: app_name The name of the Chalice app. This corresponds to the value provided when instantiating a ``Chalice`` object. .. attribute:: current_request An object of type :class:`Request`. This value is only set when a view function is being called. This attribute can be used to introspect the current HTTP request. .. attribute:: api An object of type :class:`APIGateway`. This attribute can be used to control how apigateway interprets ``Content-Type`` headers in both requests and responses. .. attribute:: lambda_context A Lambda context object that is passed to the invoked view by AWS Lambda. You can find out more about this object by reading the `lambda context object documentation `_. .. note:: This is only set on ``@app.route`` handlers. For other handlers it will be ``None``. Instead the event parameter will have a ``context`` property. For example :attr:`S3Event.context`. .. attribute:: debug A boolean value that enables debugging. By default, this value is ``False``. If debugging is true, then internal errors are returned back to the client. Additionally, debug log messages generated by the framework will show up in the cloudwatch logs. Example usage: .. code-block:: python from chalice import Chalice app = Chalice(app_name="appname") app.debug = True .. attribute:: websocket_api An object of type :class:`WebsocketAPI`. This attribute can be used to send messages to websocket clients connected through API Gateway. .. method:: route(path, \*\*options) Register a view function for a particular URI path. This method is intended to be used as a decorator for a view function. For example: .. code-block:: python from chalice import Chalice app = Chalice(app_name="appname") @app.route('/resource/{value}', methods=['PUT']) def viewfunction(value): pass :param str path: The path to associate with the view function. The ``path`` should only contain ``[a-zA-Z0-9._-]`` chars and curly braces for parts of the URL you would like to capture. The path should not end in a trailing slash, otherwise a validation error will be raised during deployment. :param list methods: Optional parameter that indicates which HTTP methods this view function should accept. By default, only ``GET`` requests are supported. If you only wanted to support ``POST`` requests, you would specify ``methods=['POST']``. If you support multiple HTTP methods in a single view function (``methods=['GET', 'POST']``), you can check the :attr:`app.current_request.method ` attribute to see which HTTP method was used when making the request. You can provide any HTTP method supported by API Gateway, which includes: ``GET``, ``POST``, ``PUT``, ``PATCH``, ``HEAD``, ``OPTIONS``, and ``DELETE``. :param str name: Optional parameter to specify the name of the view function. You generally do not need to set this value. The name of the view function is used as the default value for the view name. :param Authorizer authorizer: Specify an authorizer to use for this view. Can be an instance of :class:`CognitoUserPoolAuthorizer`, :class:`CustomAuthorizer` or :class:`IAMAuthorizer`. :param str content_types: A list of content types to accept for this view. By default ``application/json`` is accepted. If this value is specified, then chalice will reject any incoming request that does not match the provided list of content types with a 415 Unsupported Media Type response. :param boolean api_key_required: Optional parameter to specify whether the method required a valid API key. :param cors: Specify if CORS is supported for this view. This can either by a boolean value, ``None``, or an instance of :class:`CORSConfig`. Setting this value is set to ``True`` gives similar behavior to enabling CORS in the AWS Console. This includes injecting the ``Access-Control-Allow-Origin`` header to have a value of ``*`` as well as adding an ``OPTIONS`` method to support preflighting requests. If you would like more control over how CORS is configured, you can provide an instance of :class:`CORSConfig`. .. method:: authorizer(name, \*\*options) Register a built-in authorizer. .. code-block:: python from chalice import Chalice, AuthResponse app = Chalice(app_name="appname") @app.authorizer(ttl_seconds=30) def my_auth(auth_request): # Validate auth_request.token, and then: return AuthResponse(routes=['/'], principal_id='username') @app.route('/', authorizer=my_auth) def viewfunction(value): pass :param ttl_seconds: The number of seconds to cache this response. Subsequent requests that require this authorizer will use a cached response if available. The default is 300 seconds. :param execution_role: An optional IAM role to specify when invoking the Lambda function associated with the built-in authorizer. :param header: The header where the auth token will be specified. The default is ``Authorization`` .. method:: schedule(expression, name=None) Register a scheduled event that's invoked on a regular schedule. This will create a lambda function associated with the decorated function. It will also schedule the lambda function to be invoked with a scheduled CloudWatch Event. See :ref:`scheduled-events` for more information. .. code-block:: python @app.schedule('cron(15 10 ? * 6L 2002-2005)') def cron_handler(event): pass @app.schedule('rate(5 minutes)') def rate_handler(event): pass @app.schedule(Rate(5, unit=Rate.MINUTES)) def rate_obj_handler(event): pass @app.schedule(Cron(15, 10, '?', '*', '6L', '2002-2005')) def cron_obj_handler(event): pass :param expression: The schedule expression to use for the CloudWatch event rule. This value can either be a string value or an instance of type ``ScheduleExpression``, which is either a :class:`Cron` or :class:`Rate` object. If a string value is provided, it will be provided directly as the ``ScheduleExpression`` value in the `PutRule `__ API call. :param name: The name of the function to use. This name is combined with the chalice app name as well as the stage name to create the entire lambda function name. This parameter is optional. If it is not provided, the name of the python function will be used. .. method:: on_cw_event(pattern, name=None) Create a lambda function and configure it to be invoked whenever an event that matches the given pattern flows through CloudWatch Events or Event Bridge. :param pattern: The event pattern to use to filter subscribed events. See the CloudWatch Events docs for examples https://amzn.to/2OlqZso :param name: The name of the function to create. This name is combined with the chalice app name as well as the stage name to create the entire lambda function name. This parameter is optional. If it is not provided, the name of the python function will be used. .. method:: on_s3_event(bucket, events=None, prefix=None, suffix=None, name=None) Create a lambda function and configure it to be automatically invoked whenever an event happens on an S3 bucket. .. warning:: You can't use the ``chalice package`` command when using the ``on_s3_event`` decorator. This is because CFN does not support configuring an existing S3 bucket. See :ref:`s3-events` for more information. This example shows how you could implement an image resizer that's triggered whenever an object is uploaded to the ``images/`` prefix of an S3 bucket (e.g ``s3://mybucket/images/house.jpg``). .. code-block:: python @app.on_s3_event('mybucket', events=['s3:ObjectCreated:Put'], prefix='images/', suffix='.jpg') def resize_image(event): with tempfile.NamedTemporaryFile('w') as f: s3.download_file(event.bucket, event.key, f.name) resize_image(f.name) s3.upload_file(event.bucket, 'resized/%s' % event.key, f.name) :param bucket: The name of the S3 bucket. This bucket must already exist. :param events: A list of strings indicating the events that should trigger the lambda function. See `Supported Event Types `__ for the full list of strings you can provide. If this option is not provided, a default of ``['s3:ObjectCreated:*']`` is used, which will configure the lambda function to be invoked whenever a new object is created in the S3 bucket. :param prefix: An optional key prefix. This specifies that the lambda function should only be invoked if the key starts with this prefix (e.g. ``prefix='images/'``). Note that this value is not a glob (e.g. ``images/*``), it is a literal string match for the start of the key. :param suffix: An optional key suffix. This specifies that the lambda function should only be invoked if the key name ends with this suffix (e.g. ``suffix='.jpg'``). Note that this value is not a glob (e.g. ``*.txt``), it is a literal string match for the end of the key. :param name: The name of the function to use. This name is combined with the chalice app name as well as the stage name to create the entire lambda function name. This parameter is optional. If it is not provided, the name of the python function will be used. .. method:: on_sns_message(topic, name=None) Create a lambda function and configure it to be automatically invoked whenever an SNS message is published to the specified topic. See :ref:`sns-events` for more information. This example prints the subject and the contents of the message whenever something publishes to the sns topic of ``mytopic``. In this example, the input parameter is of type :class:`SNSEvent`. .. code-block:: python app.debug = True @app.on_sns_message(topic='mytopic') def handler(event): app.log.info("SNS subject: %s", event.subject) app.log.info("SNS message: %s", event.message) :param topic: The name or ARN of the SNS topic you want to subscribe to. :param name: The name of the function to use. This name is combined with the chalice app name as well as the stage name to create the entire lambda function name. This parameter is optional. If it is not provided, the name of the python function will be used. .. method:: on_sqs_message(queue, batch_size=1, name=None, queue_arn=None, maximum_batching_window_in_seconds=0, maximum_concurrency=None) Create a lambda function and configure it to be automatically invoked whenever a message is published to the specified SQS queue. The lambda function must accept a single parameter which is of type :class:`SQSEvent`. If the decorated function returns without raising any exceptions then Lambda will automatically delete the SQS messages associated with the :class:`SQSEvent`. You don't need to manually delete messages. If any exception is raised, Lambda won't delete any messages, and the messages will become available once the visibility timeout has been reached. Note that for batch sizes of more than one, either the entire batch succeeds and all the messages in the batch are deleted by Lambda, or the entire batch fails. The default batch size is 1. See the `Using AWS Lambda with Amazon SQS `__ for more information on how Lambda integrates with SQS. See the :ref:`sqs-events` topic guide for more information on using SQS in Chalice. .. code-block:: python app.debug = True @app.on_sqs_message(queue='myqueue') def handler(event): app.log.info("Event: %s", event.to_dict()) for record in event: app.log.info("Message body: %s", record.body) :param queue: The name of the SQS queue you want to subscribe to. This is the name of the queue, not the ARN or Queue URL. :param batch_size: The maximum number of messages to retrieve when polling for SQS messages. The event parameter can have multiple SQS messages associated with it. This is why the event parameter passed to the lambda function is iterable. The batch size controls how many messages can be in a single event. :param name: The name of the function to use. This name is combined with the chalice app name as well as the stage name to create the entire lambda function name. This parameter is optional. If it is not provided, the name of the python function will be used. :param queue_arn: The ARN of the SQS queue you want to subscribe to. This argument is mutually exclusive with the ``queue`` parameter. This is useful if you already know the exact ARN or when integrating with the AWS CDK to create your SQS queue. :param maximum_batching_window_in_seconds: The maximum amount of time, in seconds, to gather records before invoking the function. :param maximum_concurrency: The maximum number of concurrent functions that the event source can invoke. .. method:: on_kinesis_record(stream, batch_size=100, starting_position='LATEST', name=None, maximum_batching_window_in_seconds=0) Create a lambda function and configure it to be automatically invoked whenever data is published to the specified Kinesis stream. The lambda function must accept a single parameter which is of type :class:`KinesisEvent`. If the decorated function raises an exception, Lambda retries the batch until processing succeeds or the data expires. See `Using AWS Lambda with Amazon Kinesis `__ for more information on how Lambda integrates with Kinesis. .. code-block:: python app.debug = True @app.on_kinesis_record(stream='mystream') def handler(event): app.log.info("Event: %s", event.to_dict()) for record in event: app.log.info("Message body: %s", record.data) :param stream: The name of the Kinesis stream you want to subscribe to. This is the name of the data stream, not the ARN. :param batch_size: The maximum number of messages to retrieve when polling for Kinesis messages. The event parameter can have multiple Kinesis records associated with it. This is why the event parameter passed to the lambda function is iterable. The batch size controls how many messages can be in a single event. :param starting_position: Specifies where to start processing records. This can have the following values: * ``LATEST`` - Process new records that are added to the stream. * ``TRIM_HORIZON`` - Process all records in the stream. :param name: The name of the function to use. This name is combined with the chalice app name as well as the stage name to create the entire lambda function name. This parameter is optional. If it is not provided, the name of the python function will be used. :param maximum_batching_window_in_seconds: The maximum amount of time, in seconds, to gather records before invoking the function. .. method:: on_dynamodb_record(stream_arn, batch_size=100, starting_position='LATEST', name=None, maximum_batching_window_in_seconds=0) Create a lambda function and configure it to be automatically invoked whenever data is written to a DynamoDB stream. The lambda function must accept a single parameter which is of type :class:`DynamoDBEvent`. If the decorated function raises an exception, Lambda retries the batch until processing succeeds or the data expires. See `Using AWS Lambda with Amazon DynamoDB `__ for more information on how Lambda integrates with DynamoDB Streams. .. code-block:: python app.debug = True @app.on_dynamodb_record(stream_arn='arn:aws:dynamodb:...:stream') def handler(event): app.log.info("Event: %s", event.to_dict()) for record in event: app.log.info("New: %s", record.new_image) :param stream_arn: The name of the DynamoDB stream ARN you want to subscribe to. Note that, unlike other event handlers that accept the resource name, you must provide the stream ARN when subscribing to the DynamoDB stream ARN. :param batch_size: The maximum number of messages to retrieve when polling for DynamoDB messages. The event parameter can have multiple DynamoDB records associated with it. This is why the event parameter passed to the lambda function is iterable. The batch size controls how many messages can be in a single event. :param starting_position: Specifies where to start processing records. This can have the following values: * ``LATEST`` - Process new records that are added to the stream. * ``TRIM_HORIZON`` - Process all records in the stream. :param name: The name of the function to use. This name is combined with the chalice app name as well as the stage name to create the entire lambda function name. This parameter is optional. If it is not provided, the name of the python function will be used. :param maximum_batching_window_in_seconds: The maximum amount of time, in seconds, to gather records before invoking the function. .. method:: lambda_function(name=None) Create a pure lambda function that's not connected to anything. See :doc:`topics/purelambda` for more information. :param name: The name of the function to use. This name is combined with the chalice app name as well as the stage name to create the entire lambda function name. This parameter is optional. If it is not provided, the name of the python function will be used. .. method:: register_blueprint(blueprint, name_prefix=None, url_prefix=None) Register a :class:`Blueprint` to a Chalice app. See :doc:`topics/blueprints` for more information. :param blueprint: The :class:`Blueprint` to register to the app. :param name_prefix: An optional name prefix that's added to all the resources specified in the blueprint. :param url_prefix: An optional url prefix that's added to all the routes defined the Blueprint. This allows you to set the root mount point for all URLs in a Blueprint. .. method:: on_ws_connect(event) Create a Websocket API connect event handler. :param event: The :class:`WebsocketEvent` received to indicate a new connection has been registered with API Gateway. The identifier of this connection is under the :attr:`WebsocketEvent.connection_id` attribute. see :doc:`topics/websockets` for more information. .. method:: on_ws_message(event) Create a Websocket API message event handler. :param event: The :class:`WebsocketEvent` received to indicate API Gateway received a message from a connected client. The identifier of the client that sent the message is under the :attr:`WebsocketEvent.connection_id` attribute. The content of the message is available in the :attr:`WebsocketEvent.body` attribute. see :doc:`topics/websockets` for more information. .. method:: on_ws_disconnect(event) Create a Websocket API disconnect event handler. :param event: The :class:`WebsocketEvent` received to indicate an existing connection has been disconnected from API Gateway. The identifier of this connection is under the :attr:`WebsocketEvent.connection_id` attribute. see :doc:`topics/websockets` for more information. .. method:: middleware(event_type='all') Register a middleware with a Chalice application. This decorator will register a function as Chalice middleware, which will be automatically invoked as part of the request/response cycle for a Lambda invocation. You can provide the ``event_type`` argument to indicate what type of lambda events you want to register with. The default value, ``all``, indicates that the middleware will be called for all Lambda functions defined in your Chalice app. Supported values are: * ``all`` - ``Any`` * ``s3`` - :class:`S3Event` * ``sns`` - :class:`SNSEvent` * ``sqs`` - :class:`SQSEvent` * ``cloudwatch`` - :class:`CloudWatchEvent` * ``scheduled`` - :class:`CloudWatchEvent` * ``websocket`` - :class:`WebsocketEvent` * ``http`` - :class:`Request` * ``pure_lambda`` - :class:`LambdaFunctionEvent` The decorated function must accept two arguments, ``event`` and ``get_response``. The ``event`` is the input event associated with the Lambda invocation, and ``get_response`` is a callable that takes an input event and will invoke the next middleware in the chain, and eventually the original Lambda handler. Below is a noop middleware that shows the minimum needed to write middleware: .. code-block:: python @app.middleware('all') def mymiddleware(event, get_response): return get_response(event) See :doc:`topics/middleware` for more information on writing middleware. .. method:: register_middleware(func, event_type='all') Register a middleware with a Chalice application. This is the same behavior as the :meth:`Chalice.middleware` decorator and is useful if you want to register middleware for pre-existing functions: .. code-block:: python import thirdparty app.register_middleware(thirdparty.func, 'all') .. class:: ConvertToMiddleware(lambda_wrapper) This class is used to convert a function that wraps/proxies a Lambda function into middleware. This allows this wrapper to automatically be applied to every function in your app. For example, if you had the following logging decorator: .. code-block:: python def log_invocation(func): def wrapper(event, context): logger.debug("Before lambda function.") response = func(event, context) logger.debug("After lambda function.") return wrapper @app.lambda_function() @log_invocation def myfunction(event, context): logger.debug("In myfunction().") Rather than decorate every Lambda function with the ``@log_invocation`` decorator, you can instead use ``ConvertToMiddleware`` to automatically apply this wrapper to every Lambda function in your app. .. code-block:: python app.register_middleware(ConvertToMiddleware(log_invoation)) Request ======= .. class:: Request A class that represents the current request. This is mapped to the ``app.current_request`` object. .. code-block:: python @app.route('/objects/{key}', methods=['GET', 'PUT']) def myobject(key): request = app.current_request # type: Request if request.method == 'PUT': # handle PUT request pass elif request.method == 'GET': # handle GET request pass .. attribute:: path The path of the HTTP request. .. attribute:: query_params A MultiDict of the query params for the request. This value is ``None`` if no query params were provided in the request. The MultiDict acts like a normal dictionary except that you can call the method ``getlist()`` to get multiple keys from the same query string parameter .. code-block:: python request = app.current_request # Raises an exception if key doesn't exist, usual Python behavior. single_param = request.query_params['single'] # None if key doesn't exist, usual Python behavior another_param = request.query_params.get('another_param') # A List of all parameters named multi_param, Throws an exception if # key doesn't exist multi_param_list = request.query_params.getlist('multi_param') .. attribute:: headers A dict of the request headers. .. attribute:: uri_params A dict of the captured URI params. This value is ``None`` if no URI params were provided in the request. .. attribute:: method The HTTP method as a string. .. attribute:: json_body The parsed JSON body (``json.loads(raw_body)``). This value will only be non-None if the Content-Type header is ``application/json``, which is the default content type value in chalice. .. attribute:: raw_body The raw HTTP body as bytes. This is useful if you need to calculate a checksum of the HTTP body. .. attribute:: context A dict of additional context information. .. attribute:: stage_vars A dict of configuration for the API Gateway stage. .. attribute:: lambda_context A Lambda context object that is passed to the invoked view by AWS Lambda. You can find out more about this object by reading the `lambda context object documentation `_. .. method:: to_dict() Convert the :class:`Request` object to a dictionary. This is useful for debugging purposes. This dictionary is guaranteed to be JSON serializable so you can return this value from a chalice view. Response ======== .. class:: Response(body, headers=None, status_code=200) A class that represents the response for the view function. You can optionally return an instance of this class from a view function if you want complete control over the returned HTTP response. .. code-block:: python from chalice import Chalice, Response app = Chalice(app_name='custom-response') @app.route('/') def index(): return Response(body='hello world!', status_code=200, headers={'Content-Type': 'text/plain'}) .. versionadded:: 0.6.0 .. attribute:: body The HTTP response body to send back. This value must be a string. .. attribute:: headers An optional dictionary of HTTP headers to send back. This is a dictionary of header name to header value, e.g ``{'Content-Type': 'text/plain'}`` .. attribute:: status_code The integer HTTP status code to send back in the HTTP response. Authorization ============= Each of these classes below can be provided using the ``authorizer`` argument for an ``@app.route(authorizer=...)`` call: .. code-block:: python authorizer = CognitoUserPoolAuthorizer( 'MyPool', header='Authorization', provider_arns=['arn:aws:cognito:...:userpool/name']) @app.route('/user-pools', methods=['GET'], authorizer=authorizer) def authenticated(): return {"secure": True} .. class:: CognitoUserPoolAuthorizer(name, provider_arns, header='Authorization') .. versionadded:: 0.8.1 .. attribute:: name The name of the authorizer. .. attribute:: provider_arns The Cognito User Pool arns to use. .. attribute:: header The header where the auth token will be specified. .. class:: IAMAuthorizer() .. versionadded:: 0.8.3 .. class:: CustomAuthorizer(name, authorizer_uri, ttl_seconds, header='Authorization') .. versionadded:: 0.8.1 .. attribute:: name The name of the authorizer. .. attribute:: authorizer_uri The URI of the lambda function to use for the custom authorizer. This usually has the form ``arn:aws:apigateway:{region}:lambda:path/2015-03-31/functions/{lambda_arn}/invocations``. .. attribute:: ttl_seconds The number of seconds to cache the returned policy from a custom authorizer. .. attribute:: header The header where the auth token will be specified. Built-in Authorizers -------------------- These classes are used when defining built-in authorizers in Chalice. .. class:: AuthRequest(auth_type, token, method_arn) An instance of this class is passed as the first argument to an authorizer defined via ``@app.authorizer()``. You generally do not instantiate this class directly. .. attribute:: auth_type The type of authentication .. attribute:: token The authorization token. This is usually the value of the ``Authorization`` header. .. attribute:: method_arn The ARN of the API gateway being authorized. .. class:: AuthResponse(routes, principal_id, context=None) .. attribute:: routes A list of authorized routes. Each element in the list can either by a string route such as `"/foo/bar"` or an instance of ``AuthRoute``. If you specify the URL as a string, then all supported HTTP methods will be authorized. If you want to specify which HTTP methods are allowed, you can use ``AuthRoute``. If you want to specify that all routes and HTTP methods are supported you can use the wildcard value of ``"*"``: ``AuthResponse(routes=['*'], ...)`` .. attribute:: principal_id The principal id of the user. .. attribute:: context An optional dictionary of key value pairs. This dictionary will be accessible in the ``app.current_request.context`` in all subsequent authorized requests for this user. .. class:: AuthRoute(path, methods) This class be used in the ``routes`` attribute of a :class:`AuthResponse` instance to get fine grained control over which HTTP methods are allowed for a given route. .. attribute:: path The allowed route specified as a string .. attribute:: methods A list of allowed HTTP methods. APIGateway ========== .. class:: APIGateway() This class is used to control how API Gateway interprets ``Content-Type`` headers in both requests and responses. There is a single instance of this class attached to each :class:`Chalice` object under the ``api`` attribute. .. attribute:: cors Global cors configuration. If a route-level cors configuration is not provided, or is ``None`` then this configuration will be used. By default it is set to ``False``. This can either be ``True``, ``False``, or an instance of the ``CORSConfig`` class. This makes it easy to enable CORS for your entire application by setting ``app.api.cors = True``. .. versionadded:: 1.12.1 .. attribute:: default_binary_types The value of ``default_binary_types`` are the ``Content-Types`` that are considered binary by default. This value should not be changed, instead you should modify the ``binary_types`` list to change the behavior of a content type. Its value is: ``application/octet-stream``, ``application/x-tar``, ``application/zip``, ``audio/basic``, ``audio/ogg``, ``audio/mp4``, ``audio/mpeg``, ``audio/wav``, ``audio/webm``, ``image/png``, ``image/jpg``, ``image/jpeg``, ``image/gif``, ``video/ogg``, ``video/mpeg``, ``video/webm``. .. attribute:: binary_types The value of ``binary_types`` controls how API Gateway interprets requests and responses as detailed below. If an incoming request has a ``Content-Type`` header value that is present in the ``binary_types`` list it will be assumed that its body is a sequence of raw bytes. You can access these bytes by accessing the ``app.current_request.raw_body`` property. If an outgoing response from ``Chalice`` has a header ``Content-Type`` that matches one of the ``binary_types`` its body must be a ``bytes`` type object. It is important to note that originating request must have the ``Accept`` header for the same type as the ``Content-Type`` on the response. Otherwise a ``400`` error will be returned. This value can be modified to change what types API Gateway treats as binary. The easiest way to do this is to simply append new types to the list. .. code-block:: python app.api.binary_types.append('application/my-binary-data') Keep in mind that there can only be a total of 25 binary types at a time and Chalice by default has a list of 16 types. It is recommended if you are going to make extensive use of binary types to reset the list to the exact set of content types you will be using. This can easily be done by reassigning the whole list. .. code-block:: python app.api.binary_types = [ 'application/octet-stream', 'application/my-binary-data', ] **Implementation Note**: API Gateway and Lambda communicate through a JSON event which is encoded using ``UTF-8``. The raw bytes are temporarily encoded using base64 when being passed between API Gateway and Lambda. In the worst case this encoding can cause the binary body to be inflated up to ``4/3`` its original size. Lambda only accepts an event up to ``6mb``, which means even if your binary data was not quite at that limit, with the base64 encoding it may exceed that limit. This will manifest as a ``502`` Bad Gateway error. WebsocketAPI ============ .. class:: WebsocketAPI This class is used to send messages to websocket clients connected to an API Gateway Websocket API. .. attribute:: session A boto3 Session that will be used to send websocket messages to clients. Any custom configuration can be set through a botocore ``session``. This **must** be manually set before websocket features can be used. .. code-block:: python import botocore from boto3.session import Session from chalice import Chalice app = Chalice('example') session = botocore.session.Session() session.set_config_variable('retries', {'max_attempts': 0}) app.websocket_api.session = Session(botocore_session=session) .. method:: configure(domain_name, stage) Configure prepares the :class:`WebsocketAPI` to call the :meth:`send` method. Without first calling this method calls to :meth:`send` will fail with the message ``WebsocketAPI needs to be configured before sending messages.``. This is because a boto3 ``apigatewaymanagementapi`` client must be created from the :attr:`session` with a custom endpoint in order to properly communicate with our API Gateway WebsocketAPI. This method is called on your behalf before each of the websocket handlers: ``on_ws_connect``, ``on_ws_message``, ``on_ws_disconnect``. This ensures that the :meth:`send` method is available in each of those handlers. .. _websocket-send: .. method:: send(connection_id, message) *requires* ``boto3>=1.9.91`` Method to send a message to a client. The ``connection_id`` is the unique identifier of the socket to send the ``message`` to. The ``message`` must be a utf-8 string. If the socket is disconnected it raises a :class:`WebsocketDisconnectedError` error. .. method:: close(connection_id) *requires* ``boto3>=1.9.221`` Method to close a WebSocket connection. The ``connection_id`` is the unique identifier of the socket to close. If the socket is already disconnected it raises a :class:`WebsocketDisconnectedError` error. .. method:: info(connection_id) *requires* ``boto3>=1.9.221`` Method to get info about a WebSocket. The ``connection_id`` is the unique identifier of the socket to get info about. The following is an example of the format this method returns:: { 'ConnectedAt': datetime(2015, 1, 1), 'Identity': { 'SourceIp': 'string', 'UserAgent': 'string' }, 'LastActiveAt': datetime(2015, 1, 1) } If the socket is disconnected it raises a :class:`WebsocketDisconnectedError` error. .. class:: WebsocketDisconnectedError An exception raised when a message is sent to a websocket that has disconnected. .. attribute:: connection_id The unique identifier of the websocket that was disconnected. CORS ==== .. class:: CORSConfig(allow_origin='*', allow_headers=None, expose_headers=None, max_age=None, allow_credentials=None) CORS configuration to attach to a route, or globally on ``app.api.cors``. .. code-block:: python from chalice import CORSConfig cors_config = CORSConfig( allow_origin='https://foo.example.com', allow_headers=['X-Special-Header'], max_age=600, expose_headers=['X-Special-Header'], allow_credentials=True ) @app.route('/custom_cors', methods=['GET'], cors=cors_config) def supports_custom_cors(): return {'cors': True} .. versionadded:: 0.8.1 .. attribute:: allow_origin The value of the ``Access-Control-Allow-Origin`` to send in the response. Keep in mind that even though the ``Access-Control-Allow-Origin`` header can be set to a string that is a space separated list of origins, this behavior does not work on all clients that implement CORS. You should only supply a single origin to the ``CORSConfig`` object. If you need to supply multiple origins you will need to define a custom handler for it that accepts ``OPTIONS`` requests and matches the ``Origin`` header against a whitelist of origins. If the match is successful then return just their ``Origin`` back to them in the ``Access-Control-Allow-Origin`` header. .. attribute:: allow_headers The list of additional allowed headers. This list is added to list of built in allowed headers: ``Content-Type``, ``X-Amz-Date``, ``Authorization``, ``X-Api-Key``, ``X-Amz-Security-Token``. .. attribute:: expose_headers A list of values to return for the ``Access-Control-Expose-Headers``: .. attribute:: max_age The value for the ``Access-Control-Max-Age`` .. attribute:: allow_credentials A boolean value that sets the value of ``Access-Control-Allow-Credentials``. Event Sources ============= .. versionadded:: 1.0.0b1 .. class:: Rate(value, unit) An instance of this class can be used as the ``expression`` value in the :meth:`Chalice.schedule` method: .. code-block:: python @app.schedule(Rate(5, unit=Rate.MINUTES)) def handler(event): pass Examples: .. code-block:: python # Run every minute. Rate(1, unit=Rate.MINUTES) # Run every 2 hours. Rate(2, unit=Rate.HOURS) .. attribute:: value An integer value that presents the amount of time to wait between invocations of the scheduled event. .. attribute:: unit The unit of the provided ``value`` attribute. This can be either ``Rate.MINUTES``, ``Rate.HOURS``, or ``Rate.DAYS``. .. attribute:: MINUTES, HOURS, DAYS These values should be used for the ``unit`` attribute. .. class:: Cron(minutes, hours, day_of_month, month, day_of_week, year) An instance of this class can be used as the ``expression`` value in the :meth:`Chalice.schedule` method. .. code-block:: python @app.schedule(Cron(15, 10, '?', '*', '6L', '2002-2005')) def handler(event): pass It provides more capabilities than the :class:`Rate` class. There are a few limits: * You can't specify ``day_of_month`` and ``day_of_week`` fields in the same Cron expression. If you specify a value in one of the fields, you must use a ``?`` in the other. * Cron expressions that lead to rates faster than 1 minute are not supported. For more information, see the API `docs page `__. Examples: .. code-block:: python # Run at 10:00am (UTC) every day. Cron(0, 10, '*', '*', '?', '*') # Run at 12:15pm (UTC) every day. Cron(15, 12, '*', '*', '?', '*') # Run at 06:00pm (UTC) every Monday through Friday. Cron(0, 18, '?', '*', 'MON-FRI', '*') # Run at 08:00am (UTC) every 1st day of the month. Cron(0, 8, 1, '*', '?', '*') # Run every 15 minutes. Cron('0/15', '*', '*', '*', '?', '*') # Run every 10 minutes Monday through Friday. Cron('0/10', '*', '?', '*', 'MON-FRI', '*') # Run every 5 minutes Monday through Friday between # 08:00am and 5:55pm (UTC). Cron('0/5', '8-17', '?', '*', 'MON-FRI', '*') .. class:: CloudWatchEvent() This is the input argument for a scheduled or CloudWatch events. .. code-block:: python @app.schedule('rate(1 hour)') def every_hour(event: CloudWatchEvent): pass In the code example above, the ``event`` argument is of type ``CloudWatchEvent``, which will have the following attributes. .. attribute:: version By default, this is set to 0 (zero) in all events. .. attribute:: account The 12-digit number identifying an AWS account. .. attribute:: region Identifies the AWS region where the event originated. .. attribute:: detail For CloudWatch events this will be the event payload. For scheduled events, this will be an empty dictionary. .. attribute:: detail_type For scheduled events, this value will be ``"Scheduled Event"``. .. attribute:: source Identifies the service that sourced the event. All events sourced from within AWS will begin with "aws." Customer-generated events can have any value here as long as it doesn't begin with "aws." We recommend the use of java package-name style reverse domain-name strings. For scheduled events, this will be ``aws.events``. .. attribute:: time The event timestamp, which can be specified by the service originating the event. If the event spans a time interval, the service might choose to report the start time, so this value can be noticeably before the time the event is actually received. .. attribute:: event_id A unique value is generated for every event. This can be helpful in tracing events as they move through rules to targets, and are processed. .. attribute:: resources This JSON array contains ARNs that identify resources that are involved in the event. Inclusion of these ARNs is at the discretion of the service. For scheduled events, this will include the ARN of the CloudWatch rule that triggered this event. .. attribute:: context A `Lambda context object `_ that is passed to the handler by AWS Lambda. This is useful if you need the AWS request ID for tracing, or any other data in the context object. .. method:: to_dict() Return the original event dictionary provided from Lambda. This is useful if you need direct access to the lambda event, for example if a new key is added to the lambda event that has not been mapped as an attribute to the ``CloudWatchEvent`` object. Example:: {'account': '123457940291', 'detail': {}, 'detail-type': 'Scheduled Event', 'id': '12345678-b9f1-4667-9c5e-39f98e9a6113', 'region': 'us-west-2', 'resources': ['arn:aws:events:us-west-2:123457940291:rule/testevents-dev-every_minute'], 'source': 'aws.events', 'time': '2017-06-30T23:28:38Z', 'version': '0'} .. class:: S3Event() This is the input argument for an S3 event. .. code-block:: python @app.on_s3_event(bucket='mybucket') def event_handler(event: S3Event): app.log.info("Event received for bucket: %s, key %s", event.bucket, event.key) In the code example above, the ``event`` argument is of type ``S3Event``, which will have the following attributes. .. attribute:: bucket The S3 bucket associated with the event. .. attribute:: key The S3 key name associated with the event. The original key name in the S3 event payload is URL encoded. However, this ``key`` attribute automatically URL decodes the key name for you. If you need access to the original URL encoded key name, you can access it through the ``to_dict()`` method. .. attribute:: context A `Lambda context object `_ that is passed to the handler by AWS Lambda. This is useful if you need the AWS request ID for tracing, or any other data in the context object. .. method:: to_dict() Return the original event dictionary provided from Lambda. This is useful if you need direct access to the lambda event, for example if a new key is added to the lambda event that has not been mapped as an attribute to the ``S3Event`` object. Note that this event is not modified in any way. This means that the key name of the S3 object is URL encoded, which is the way that S3 sends this value to Lambda. .. class:: SNSEvent() This is the input argument for an SNS event handler. .. code-block:: python @app.on_sns_message(topic='mytopic') def event_handler(event: SNSEvent): app.log.info("Message received with subject: %s, message: %s", event.subject, event.message) In the code example above, the ``event`` argument is of type ``SNSEvent``, which will have the following attributes. .. attribute:: subject The subject of the SNS message that was published. .. attribute:: message The string value of the SNS message that was published. .. attribute:: context A `Lambda context object `_ that is passed to the handler by AWS Lambda. This is useful if you need the AWS request ID for tracing, or any other data in the context object. .. method:: to_dict() Return the original event dictionary provided from Lambda. This is useful if you need direct access to the lambda event, for example if a new key is added to the lambda event that has not been mapped as an attribute to the ``SNSEvent`` object. .. class:: SQSEvent() This is the input argument for an SQS event handler. .. code-block:: python @app.on_sqs_message(queue='myqueue') def event_handler(event: SQSEvent): app.log.info("Event: %s", event.to_dict()) In the code example above, the ``event`` argument is of type ``SQSEvent``. An ``SQSEvent`` can have multiple sqs messages associated with it. To access the multiple messages, you can iterate over the ``SQSEvent``. .. method:: __iter__() Iterate over individual SQS messages associated with the event. Each element in the iterable is of type :class:`SQSRecord`. .. attribute:: context A `Lambda context object `_ that is passed to the handler by AWS Lambda. This is useful if you need the AWS request ID for tracing, or any other data in the context object. .. method:: to_dict() Return the original event dictionary provided from Lambda. This is useful if you need direct access to the lambda event, for example if a new key is added to the lambda event that has not been mapped as an attribute to the ``SQSEvent`` object. .. class:: SQSRecord() Represents a single SQS record within an :class:`SQSEvent`. .. attribute:: body The body of the SQS message. .. attribute:: receipt_handle The receipt handle associated with the message. This is useful if you need to manually delete an SQS message to account for partial failures. .. attribute:: context A `Lambda context object `_ that is passed to the handler by AWS Lambda. .. method:: to_dict() Return the original dictionary associated with the given message. This is useful if you need direct access to the lambda event. .. class:: KinesisEvent() This is the input argument for a Kinesis data stream event handler. .. code-block:: python @app.on_kinesis_record(stream='mystream') def event_handler(event: KinesisEvent): app.log.info("Event: %s", event.to_dict()) In the code example above, the ``event`` argument is of type ``KinesisEvent``. A ``KinesisEvent`` can have multiple messages associated with it. To access the multiple messages, you can iterate over the ``KinesisEvent``. .. method:: __iter__() Iterate over individual Kinesis records associated with the event. Each element in the iterable is of type :class:`KinesisRecord`. .. attribute:: context A `Lambda context object `_ that is passed to the handler by AWS Lambda. This is useful if you need the AWS request ID for tracing, or any other data in the context object. .. method:: to_dict() Return the original event dictionary provided from Lambda. This is useful if you need direct access to the lambda event, for example if a new key is added to the lambda event that has not been mapped as an attribute to the ``SQSEvent`` object. .. class:: KinesisRecord() Represents a single Kinesis record within a :class:`KinesisEvent`. .. attribute:: data The payload data for the Kinesis record. This data is automatically base64 decoded for you and will be a ``bytes`` type. .. attribute:: sequence_number The unique identifier of the record within its shard. .. attribute:: partition_key Identifies which shard in the stream the data record is assigned to. .. attribute:: schema_version Schema version for the record. .. attribute:: timestamp The approximate time that the record was inserted into the stream. This is automatically converted to a ``datetime.datetime`` object. .. attribute:: context A `Lambda context object `_ that is passed to the handler by AWS Lambda. .. method:: to_dict() Return the original dictionary associated with the given message. This is useful if you need direct access to the lambda event. .. class:: DynamoDBEvent() This is the input argument for a DynamoDB stream event handler. .. code-block:: python @app.on_dynamodb_record(stream_arn='arn:aws:us-west-2:.../stream') def event_handler(event: DynamoDBEvent): app.log.info("Event: %s", event.to_dict()) for record in event: app.log.info(record.to_dict()) In the code example above, the ``event`` argument is of type ``DynamoDBEvent``. A ``DynamoDBEvent`` can have multiple messages associated with it. To access the multiple messages, you can iterate over the ``DynamoDBEvent``. .. method:: __iter__() Iterate over individual DynamoDB records associated with the event. Each element in the iterable is of type :class:`DynamoDBRecord`. .. attribute:: context A `Lambda context object `_ that is passed to the handler by AWS Lambda. This is useful if you need the AWS request ID for tracing, or any other data in the context object. .. method:: to_dict() Return the original event dictionary provided from Lambda. This is useful if you need direct access to the lambda event, for example if a new key is added to the lambda event that has not been mapped as an attribute to the ``SQSEvent`` object. .. class:: DynamoDBRecord() Represents a single DynamoDB record within a :class:`DynamoDBEvent`. .. attribute:: timestamp The approximate time that the record was the stream record was created. This is automatically converted to a ``datetime.datetime`` object. .. attribute:: keys The primary key attribute(s) for the DynamoDB item that was modified. .. attribute:: new_image The item in the DynamoDB table as it appeared after it was modified. .. attribute:: old_image The item in the DynamoDB table as it appeared before it was modified. .. attribute:: sequence_number The sequence number of the stream record. .. attribute:: size_bytes The size of the stream record, in bytes. .. attribute:: stream_view_type The type of data from the modified DynamoDB item. .. attribute:: aws_region The region associated with the event. .. attribute:: event_id A unique identifier for the event. .. attribute:: event_name The type of data modification that was performed on the DynamoDB table. This can be: ``INSERT``, ``MODIFY``, or ``DELETE``. .. attribute:: event_source_arn The ARN of the DynamoDB stream. .. attribute:: table_name The name of the DynamoDB table associated with the stream. This value is computed from the ``event_source_arn`` parameter and will be an empty string if Chalice is unable to parse the table name from the event source ARN. .. attribute:: context A `Lambda context object `_ that is passed to the handler by AWS Lambda. .. method:: to_dict() Return the original dictionary associated with the given message. This is useful if you need direct access to the lambda event. .. class:: LambdaFunctionEvent() This is the input argument of middleware registered to a pure Lambda function (``@app.lambda_function()``). .. attribute:: event The original input event dictionary. .. attribute:: context A `Lambda context object `_ that is passed to the handler by AWS Lambda. Blueprints ========== .. class:: Blueprint(import_name) An object used for grouping related handlers together. This is primarily used as a mechanism for organizing your lambda handlers. Any decorator methods defined in the :class:`Chalice` object are also defined on a ``Blueprint`` object. You can register a blueprint to a Chalice app using the :meth:`Chalice.register_blueprint` method. The ``import_name`` is the module in which the Blueprint is defined. It is used to construct the appropriate handler string when creating the Lambda functions associated with a Blueprint. This is typically the `__name__` attribute:``mybp = Blueprint(__name__)``. See :doc:`topics/blueprints` for more information. .. code-block:: python # In ./app.py from chalice import Chalice from chalicelib import myblueprint app = Chalice(app_name='blueprints') app.register_blueprint(myblueprint) # In chalicelib/myblueprint.py from chalice import Blueprint myblueprint = Blueprint(__name__) @myblueprint.route('/') def index(): return {'hello': 'world'} Websockets ========== .. _websocket-api: .. class:: WebsocketEvent() Event object event that is passed as the sole arugment to any handler function decorated with one of the three websocket related handlers: ``on_ws_connect``, ``on_ws_disconnect``, ``on_ws_message``. .. attribute:: domain_name The domain name of the endpoint for the API Gateway Websocket API. .. attribute:: stage The API Gateway stage of the Websocket API. .. attribute:: connection_id A handle that uniquely identifies a connection with API Gateway. .. attribute:: body The message body received. This is only populated on the ``on_ws_message`` otherwise it will be set to ``None``. .. attribute:: json_body The parsed JSON body (``json.loads(body)``) of the message. If the body is not JSON parsable then using this attribute will raise a ``ValueError``. See :doc:`topics/websockets` for more information. .. _testing-api: Testing ======= .. class:: Client(app, stage_name='dev', project_dir='.') A test client used to write tests for Chalice apps. It allows you to test Lambda function invocation as well as REST APIs. Depending on what you want to test, you'll access the various attributes of this class. You can use this class as a context manager. When entering the context manager, any environment variables specified for your function will be set. The original environment variables are put back when the block is exited: .. code-block:: python from chalice.test import Client with Client(app) as client: result = client.http.post("/my-data") See the :doc:`topics/testing` documentation for more details on testing your Chalice app. .. attribute:: lambda_ Returns the Lambda test client :class:`TestLambdaClient`. .. attribute:: http Returns the test client for REST APIs :class:`TestHTTPClient`. .. attribute:: events Returns the test client for generating Lambda events :class:`TestEventsClient`. .. class:: TestLambdaClient(import_name) Test client for invoking Lambda functions. This class should not be instantiated directly, and instead should be accessed via the ``Client.lambda_`` attribute: .. code-block:: python @app.lambda_function() def myfunction(event, context): return {"hello": "world"} with Client(app) as client: result = client.lambda_.invoke("myfunction") assert result.payload == {"hello": "world"} .. method:: invoke(function_name, payload=None) Invoke a Lambda function by name. The name should match the resource name of the function. This is typically the name of the python function unless an explicit ``name=`` kwarg is provided when registering the function. Returns an :class:`InvokeResponse` instance. .. class:: TestHTTPClient(import_name) Test client for REST APIs. This class should not be instantiated directly, and instead should be accessed via the ``Client.http`` attribute: .. code-block:: python with Client(app) as client: response = client.http.get("/my-route") .. method:: request(method, path, headers=None, body=b'') Makes a test HTTP request to your REST API. Returns an :class:`HTTPResponse`. You can also use the methods below to make a request with a specific HTTP method instead of using this method directly, e.g. ``client.http.get("/foo")`` instead of ``client.http.request("GET", "/foo")``. .. method:: get(path, \*\*kwargs) Makes an HTTP GET request. .. method:: post(path, \*\*kwargs) Makes an HTTP POST request. .. method:: put(path, \*\*kwargs) Makes an HTTP PUT request. .. method:: patch(path, \*\*kwargs) Makes an HTTP PATCH request. .. method:: options(path, \*\*kwargs) Makes an HTTP OPTIONS request. .. method:: delete(path, \*\*kwargs) Makes an HTTP DELETE request. .. method:: head(path, \*\*kwargs) Makes an HTTP HEAD request. .. class:: TestEventsClient(import_name) Test client for generating Lambda events. This class should not be instantiated directly, and instead should be accessed via the ``Client.events`` attribute: .. code-block:: python with Client(app) as client: result = client.lambda_.invoke( "my_sns_handler", client.events.generate_sns_event("Hello world") ) .. method:: generate_sns_event(message, subject='') Generates a sample SNS event. .. method:: generate_s3_event(bucket, key, event_name='ObjectCreated:Put') Generates a sample S3 event. .. method:: generate_sqs_event(message_bodies, queue_name='queue-name') Generates a sample SQS event. .. method:: generate_cw_event(source, detail_type, detail, resources, region='us-west-2') Generates a sample CloudWatch event. .. method:: generate_kinesis_event(message_bodies, stream_name='stream-name') Generates a Kinesis event. .. class:: HTTPResponse() .. attribute:: body The body of the HTTP response, in ``bytes``. .. attribute:: headers A dictionary of HTTP headers in the resopnse. .. attribute:: status_code The status code of the HTTP response. .. class:: InvokeResponse(payload) .. attribute:: payload The response payload of Lambda invocation. .. _cdk-api: AWS CDK ======= The Chalice CDK construct is available in the ``chalice.cdk`` namespace. For more details on using the AWS CDK with Chalice, see :doc:`tutorials/cdk`. .. code-block:: python from chalice import cdk .. class:: cdk.Chalice(scope, id, source_dir, stage_config=None, preserve_logical_ids=True, \*\*kwargs) A test client used to write tests for Chalice apps. It allows you to :param scope: The CDK scope that the construct is created within. :param str id: The identifier for the construct. Must be unique within the scope in which it's created. :param str source_dir: Path to Chalice application source code. :param dict stage_config: Chalice stage configuration. The configuration object should have the same structure as Chalice JSON stage configuration. :param bool preserve_logical_ids: Whether the resources should have the same logical IDs in the resulting CDK template as they did in the original CloudFormation template file. If you're vending a Construct using cdk-chalice, make sure to pass this as ``False``. Note: regardless of whether this option is true or false, the :attr:`sam_template`'s ``get_resource`` and related methods always uses the original logical ID of the resource/element, as specified in the template file. :raises `ChaliceError`: Error packaging the Chalice application. .. code-block:: python chalice = Chalice( self, 'ChaliceApp', source_dir='../runtime', stage_config={ 'environment_variables': { 'MY_ENV_VAR': 'FOO' } } ) .. method:: get_resource(resource_name) Returns a low-level CfnResource from the resources in a Chalice app with the given resource name. The resource name corresponds to the logical ID of the underlying resource in the SAM template. Any modifications performed on that resource will be reflected in the resulting CDK template. :param str resource_name: The logical ID of the resource in the SAM template. :rtype: aws_cdk.cdk.CfnResource .. method:: get_role(resource_name) Returns an ``IRole`` for an underlying SAM template resource. This is useful if you want to grant additional permissions to an IAM role constructed by Chalice. .. code-block:: python dynamodb_table = dynamodb.Table(...) chalice = Chalice(scope, 'ChaliceApp', ....) dynamodb_table.grant_read_write_data(chalice.get_role('DefaultRole')) :param str resource_name: The logical ID of the resource in the SAM template. :rtype: aws_cdk.aws_iam.IRole .. method:: get_function(resource_name) Returns an ``IFunction`` for an underlying SAM template resource. :param str resource_name: The logical ID of the resource in the SAM template. :rtype: aws_cdk.aws_lambda.IFunction .. method:: add_environment_variable(key, value, function_name) Convenience function to add environment variables to a single Lambda function constructed by Chalice. You can also add environment variables to Lambda functions using the ``stage_config`` parameter when creating the ``Chalice()`` construct. :param str key: The environment variable key name. :param str value: The value of the environment variable. :param str function_name: The logical ID of the Lambda function resource. ================================================ FILE: docs/source/chalicedocs.py ================================================ # This code is based on the sphinxcontrib.youtube # code, but with support for python3 and a few other # changes. import re from docutils import nodes from docutils.parsers.rst import directives, Directive CONTROL_HEIGHT = 30 def get_size(d, key): if key not in d: return None m = re.match("(\d+)(|%|px)$", d[key]) if not m: raise ValueError("invalid size %r" % d[key]) return int(m.group(1)), m.group(2) or "px" def css(d): return "; ".join(sorted("%s: %s" % kv for kv in d.items())) class youtube(nodes.General, nodes.Element): pass def visit_youtube_node(self, node): aspect = node["aspect"] width = node["width"] height = node["height"] if aspect is None: aspect = 16, 9 div_style = {} if (height is None) and (width is not None) and (width[1] == "%"): div_style = { "padding-top": "%dpx" % CONTROL_HEIGHT, "padding-bottom": "%f%%" % (width[0] * aspect[1] / aspect[0]), "width": "%d%s" % width, "position": "relative", "margin": "0 auto 30px auto", } style = { "position": "absolute", "top": "0", "left": "0", "width": "100%", "height": "100%", "border": "0", } attrs = { "src": "https://www.youtube.com/embed/%s" % node["id"], "style": css(style), } else: if width is None: if height is None: width = 560, "px" else: width = height[0] * aspect[0] / aspect[1], "px" if height is None: height = width[0] * aspect[1] / aspect[0], "px" style = { "width": "%d%s" % width, "height": "%d%s" % (height[0] + CONTROL_HEIGHT, height[1]), "border": "0", } attrs = { "src": "https://www.youtube.com/embed/%s" % node["id"], "style": css(style), } attrs["allowfullscreen"] = "true" div_attrs = { "CLASS": "youtube-wrapper", "style": css(div_style), } self.body.append(self.starttag(node, "div", **div_attrs)) self.body.append(self.starttag(node, "iframe", **attrs)) self.body.append("") def depart_youtube_node(self, node): pass class YouTube(Directive): has_content = True required_arguments = 1 optional_arguments = 0 final_argument_whitespace = False option_spec = { "width": directives.unchanged, "height": directives.unchanged, "aspect": directives.unchanged, } def run(self): if "aspect" in self.options: aspect = self.options.get("aspect") m = re.match("(\d+):(\d+)", aspect) if m is None: raise ValueError("invalid aspect ratio %r" % aspect) aspect = tuple(int(x) for x in m.groups()) else: aspect = None width = get_size(self.options, "width") height = get_size(self.options, "height") return [ youtube(id=self.arguments[0], aspect=aspect, width=width, height=height) ] def unsupported_visit_youtube(self, node): self.builder.warn('youtube: unsupported output format (node skipped)') raise nodes.SkipNode _NODE_VISITORS = { 'html': (visit_youtube_node, depart_youtube_node), 'latex': (unsupported_visit_youtube, None), 'man': (unsupported_visit_youtube, None), 'texinfo': (unsupported_visit_youtube, None), 'text': (unsupported_visit_youtube, None) } def setup(app): app.add_node(youtube, **_NODE_VISITORS) app.add_directive("youtube", YouTube) ================================================ FILE: docs/source/conf.py ================================================ # -*- coding: utf-8 -*- # # Chalice documentation build configuration file, created by # sphinx-quickstart on Tue May 17 14:09:17 2016. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys import os _ROOT_SOURCE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, _ROOT_SOURCE) # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'chalicedocs', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # source_suffix = ['.rst', '.md'] source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'AWS Chalice' copyright = u'2016, James Saryerwinnie' author = u'James Saryerwinnie' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = u'1.32' # The full version, including alpha/beta/rc tags. release = u'1.32.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path exclude_patterns = [] # The reST default role (used for this markup: `text`) to use for all # documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. #keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False # -- Options for HTML output ---------------------------------------------- # Our theme is based off the smithy theme but keep in mind that we heavily # modify parts of the theme. html_theme = 'smithy' html_theme_path = ['./theme'] html_theme_options = {'ga_id': os.environ.get('_CHALICE_GA_ID', '')} # The name for this set of Sphinx documents. # " v documentation" by default. html_title = 'AWS Chalice' # A shorter title for the navigation bar. Default is the same as html_title. html_short_title = 'Chalice' # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (relative to this directory) to use as a favicon of # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. #html_extra_path = [] # If not None, a 'Last updated on:' timestamp is inserted at every page # bottom, using the given strftime format. # The empty string is equivalent to '%b %d, %Y'. #html_last_updated_fmt = None # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh' #html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # 'ja' uses this config value. # 'zh' user can custom change `jieba` dictionary path. #html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. #html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. htmlhelp_basename = 'Chalicedoc' primary_domain = 'py' linkcheck_retries = 5 # The AWS Doc site now uses javascript to dynamically render the content, # so the anchors aren't going to exist in the static html content. This # will fail the anchor checker so we have to disable this. linkcheck_anchors = False ================================================ FILE: docs/source/faq.rst ================================================ Frequently Asked Questions ========================== **Q: What is AWS Chalice?** AWS Chalice is a framework for writing serverless apps in python. It consists of a CLI, a declarative python API for connecting events to AWS Lambda functions, and a runtime component that provides APIs accessible to your Lambda functions. **Q: Why should I use AWS Chalice?** Chalice is designed for a seamless getting started experience that can get you up and running quickly. It handles all the boilerplate and low level details of creating a serverless application, allowing you to focus on the business logic of your application. It also provides deep integration with various AWS services allowing you to take advantage of the features available in each service. **Q: How does Chalice compare to AWS SAM and the AWS CDK?** Chalice is designed to work together with AWS SAM. SAM focus on provisioning the resources needed for your application, and not necessarily on the application code itself. Chalice provides a set of APIs to help you write your application code, including a routing layer for REST and Websocket APIs, and decorators to connect various AWS event sources to Lambda functions. It then can integrate with AWS SAM by offloading the deployment to AWS CloudFormation. **Q: How does Chalice compare to other similiar frameworks?** The biggest difference between Chalice and other frameworks is that Chalice is focused on using a familiar, decorator-based API to write serverless python applications that run on AWS Lambda. Its goal is to make writing and deploying these types of applications as simple as possible specifically for Python developers. It was designed from the ground up to run in a serverless environment. To achieve this goal, it has to make certain tradeoffs. Chalice makes assumptions about how applications will be deployed, and it has restrictions on how an application can be structured. The feature set is purposefully small. ================================================ FILE: docs/source/index.rst ================================================ Documentation ============= Getting Started --------------- .. toctree:: :maxdepth: 2 quickstart Tutorials --------- .. toctree:: :maxdepth: 2 tutorials/index Topics ------ .. toctree:: :maxdepth: 2 topics/index API Reference ------------- .. toctree:: :maxdepth: 2 api Sample Apps ----------- .. toctree:: :maxdepth: 2 samples/index Upgrade Notes ------------- .. toctree:: :maxdepth: 2 upgrading FAQ --- .. toctree:: :maxdepth: 1 faq Indices and tables ================== * :ref:`genindex` * :ref:`search` ================================================ FILE: docs/source/main.rst ================================================ :orphan: .. include:: ./index.rst ================================================ FILE: docs/source/quickstart.rst ================================================ Quickstart ========== .. include:: ../../README.rst :start-after: quick-start-begin :end-before: quick-start-end Cleaning Up ----------- If you're done experimenting with Chalice and you'd like to cleanup, you can use the ``chalice delete`` command, and Chalice will delete all the resources it created when running the ``chalice deploy`` command. :: $ chalice delete Deleting Rest API: abcd4kwyl4 Deleting function aws:arn:lambda:region:123456789:helloworld-dev Deleting IAM Role helloworld-dev Next Steps ---------- At this point, there are several tutorials you can follow based on what you're interested in: * :doc:`Creating REST APIs ` - Dive into more detail on how to create a REST API using Chalice. We'll explore URL parameters, error messages, content types, CORs, and more. * :doc:`Event Sources ` - In this tutorial, we'll focus on difference event sources you can connect with a Lambda function other than a REST API with Amazon API Gateway. * :doc:`Websockets ` - In this tutorial, we'll show you how to create a websocket API and create a sample chat application. You can also jump into specific :doc:`topic guides `. These are more detailed than the tutorials, and provide more reference style documentation on specific features of Chalice. And finally, you can look at the :doc:`API Reference ` for detailed API documentation for Chalice. This is useful if you know exactly what feature you're using but need to lookup a specific parameter name or return value. ================================================ FILE: docs/source/samples/index.rst ================================================ Sample Applications =================== Below are a collection of Chalice sample applications. They show you how you can write more real-world serverless applications. The code for all of these sample applications is available in the `Chalice repository on GitHub `__. For each of these sample apps, we'll cover what is does, the architecture of the app, how to deploy and test the app, and we'll walk through the key parts of the application code. :doc:`todo-app/index` This app is a REST API that manages Todo items. These items are stored in an Amazon DynamoDB database. The REST API is protected with JWT auth. We show you how to implement auth and login with Chalice's :ref:`builtin-authorizers`. :doc:`media-query/index` This app shows how to create an image processing pipeline that can analyze images and videos to detect real world objects. The results of this analysis are then stored in a database and exposed through a queryable REST API. .. toctree:: :hidden: :glob: ./*/index ================================================ FILE: docs/source/samples/media-query/code/.chalice/config.json ================================================ { "version": "2.0", "app_name": "media-query", "stages": { "dev": { "api_gateway_stage": "api", "autogen_policy": false } } } ================================================ FILE: docs/source/samples/media-query/code/.chalice/policy-dev.json ================================================ { "Version": "2012-10-17", "Statement": [ { "Action": [ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents" ], "Resource": "arn:aws:logs:*:*:*", "Effect": "Allow" }, { "Action": [ "dynamodb:PutItem", "dynamodb:DeleteItem", "dynamodb:UpdateItem", "dynamodb:GetItem", "dynamodb:Scan", "dynamodb:Query" ], "Resource": [ "arn:aws:dynamodb:*:*:table/media-query-*" ], "Effect": "Allow" }, { "Action": [ "rekognition:DetectLabels", "rekognition:StartLabelDetection", "rekognition:GetLabelDetection" ], "Resource": "*", "Effect": "Allow" }, { "Action": [ "s3:GetObject" ], "Resource": "arn:aws:s3:::media-query*", "Effect": "Allow" }, { "Action": [ "iam:PassRole" ], "Resource": "arn:aws:iam::*:role/media-query-*", "Effect": "Allow" } ] } ================================================ FILE: docs/source/samples/media-query/code/app.py ================================================ import json import os import boto3 from chalice import Chalice from chalice import NotFoundError from chalicelib import db from chalicelib import rekognition app = Chalice(app_name='media-query') _MEDIA_DB = None _REKOGNITION_CLIENT = None _SUPPORTED_IMAGE_EXTENSIONS = ( '.jpg', '.png', ) _SUPPORTED_VIDEO_EXTENSIONS = ( '.mp4', '.flv', '.mov', ) def get_media_db(): global _MEDIA_DB if _MEDIA_DB is None: _MEDIA_DB = db.DynamoMediaDB( boto3.resource('dynamodb').Table( os.environ['MEDIA_TABLE_NAME'])) return _MEDIA_DB def get_rekognition_client(): global _REKOGNITION_CLIENT if _REKOGNITION_CLIENT is None: _REKOGNITION_CLIENT = rekognition.RekognitonClient( boto3.client('rekognition')) return _REKOGNITION_CLIENT # Start of Event Handlers. @app.on_s3_event(bucket=os.environ['MEDIA_BUCKET_NAME'], events=['s3:ObjectCreated:*']) def handle_object_created(event): if _is_image(event.key): _handle_created_image(bucket=event.bucket, key=event.key) elif _is_video(event.key): _handle_created_video(bucket=event.bucket, key=event.key) @app.on_s3_event(bucket=os.environ['MEDIA_BUCKET_NAME'], events=['s3:ObjectRemoved:*']) def handle_object_removed(event): if _is_image(event.key) or _is_video(event.key): get_media_db().delete_media_file(event.key) @app.on_sns_message(topic=os.environ['VIDEO_TOPIC_NAME']) def add_video_file(event): message = json.loads(event.message) labels = get_rekognition_client().get_video_job_labels(message['JobId']) get_media_db().add_media_file( name=message['Video']['S3ObjectName'], media_type=db.VIDEO_TYPE, labels=labels) @app.route('/') def list_media_files(): params = {} if app.current_request.query_params: params = _extract_db_list_params(app.current_request.query_params) return get_media_db().list_media_files(**params) @app.route('/{name}') def get_media_file(name): item = get_media_db().get_media_file(name) if item is None: raise NotFoundError('Media file (%s) not found' % name) return item # End of Event Handlers. def _extract_db_list_params(query_params): valid_query_params = [ 'startswith', 'media-type', 'label' ] return { k.replace('-', '_'): v for k, v in query_params.items() if k in valid_query_params } def _is_image(key): return key.endswith(_SUPPORTED_IMAGE_EXTENSIONS) def _handle_created_image(bucket, key): labels = get_rekognition_client().get_image_labels(bucket=bucket, key=key) get_media_db().add_media_file(key, media_type=db.IMAGE_TYPE, labels=labels) def _is_video(key): return key.endswith(_SUPPORTED_VIDEO_EXTENSIONS) def _handle_created_video(bucket, key): get_rekognition_client().start_video_label_job( bucket=bucket, key=key, topic_arn=os.environ['VIDEO_TOPIC_ARN'], role_arn=os.environ['VIDEO_ROLE_ARN'] ) ================================================ FILE: docs/source/samples/media-query/code/chalicelib/__init__.py ================================================ ================================================ FILE: docs/source/samples/media-query/code/chalicelib/db.py ================================================ from boto3.dynamodb.conditions import Attr IMAGE_TYPE = 'image' VIDEO_TYPE = 'video' class MediaDB(object): def list_media_files(self, label=None): pass def add_media_file(self, name, media_type, labels=None): pass def get_media_file(self, name): pass def delete_media_file(self, name): pass class DynamoMediaDB(MediaDB): def __init__(self, table_resource): self._table = table_resource def list_media_files(self, startswith=None, media_type=None, label=None): scan_params = {} filter_expression = None if startswith is not None: filter_expression = self._add_to_filter_expression( filter_expression, Attr('name').begins_with(startswith) ) if media_type is not None: filter_expression = self._add_to_filter_expression( filter_expression, Attr('type').eq(media_type) ) if label is not None: filter_expression = self._add_to_filter_expression( filter_expression, Attr('labels').contains(label) ) if filter_expression: scan_params['FilterExpression'] = filter_expression response = self._table.scan(**scan_params) return response['Items'] def add_media_file(self, name, media_type, labels=None): if labels is None: labels = [] self._table.put_item( Item={ 'name': name, 'type': media_type, 'labels': labels, } ) def get_media_file(self, name): response = self._table.get_item( Key={ 'name': name, }, ) return response.get('Item') def delete_media_file(self, name): self._table.delete_item( Key={ 'name': name, } ) def _add_to_filter_expression(self, expression, condition): if expression is None: return condition return expression & condition ================================================ FILE: docs/source/samples/media-query/code/chalicelib/rekognition.py ================================================ import uuid class RekognitonClient(object): def __init__(self, boto3_client): self._boto3_client = boto3_client def get_image_labels(self, bucket, key): response = self._boto3_client.detect_labels( Image={ 'S3Object': { 'Bucket': bucket, 'Name': key }, }, MinConfidence=50.0 ) return [label['Name'] for label in response['Labels']] def start_video_label_job(self, bucket, key, topic_arn, role_arn): response = self._boto3_client.start_label_detection( Video={ 'S3Object': { 'Bucket': bucket, 'Name': key } }, ClientRequestToken=str(uuid.uuid4()), NotificationChannel={ 'SNSTopicArn': topic_arn, 'RoleArn': role_arn }, MinConfidence=50.0 ) return response['JobId'] def get_video_job_labels(self, job_id): labels = set() client_kwargs = { 'JobId': job_id, } response = self._boto3_client.get_label_detection(**client_kwargs) self._collect_video_labels(labels, response) while 'NextToken' in response: client_kwargs['NextToken'] = response['NextToken'] response = self._boto3_client.get_label_detection(**client_kwargs) self._collect_video_labels(labels, response) return list(labels) def _collect_video_labels(self, labels, response): for label in response['Labels']: label_name = label['Label']['Name'] labels.add(label_name) ================================================ FILE: docs/source/samples/media-query/code/recordresources.py ================================================ import argparse import json import os import boto3 from botocore import xform_name def record_as_env_var(stack_name, stage): cloudformation = boto3.client('cloudformation') response = cloudformation.describe_stacks( StackName=stack_name ) outputs = response['Stacks'][0]['Outputs'] with open(os.path.join('.chalice', 'config.json')) as f: data = json.load(f) data['stages'].setdefault(stage, {}).setdefault( 'environment_variables', {} ) for output in outputs: data['stages'][stage]['environment_variables'][ _to_env_var_name(output['OutputKey'])] = output['OutputValue'] with open(os.path.join('.chalice', 'config.json'), 'w') as f: serialized = json.dumps(data, indent=2, separators=(',', ': ')) f.write(serialized + '\n') def _to_env_var_name(name): return xform_name(name).upper() def main(): parser = argparse.ArgumentParser() parser.add_argument('-s', '--stage', default='dev') parser.add_argument('--stack-name', required=True) args = parser.parse_args() record_as_env_var(stack_name=args.stack_name, stage=args.stage) if __name__ == '__main__': main() ================================================ FILE: docs/source/samples/media-query/code/requirements.txt ================================================ boto3<1.15.0 ================================================ FILE: docs/source/samples/media-query/code/resources.json ================================================ { "Outputs": { "MediaBucketName": { "Value": { "Ref": "MediaBucket" } }, "MediaTableName": { "Value": { "Ref": "MediaTable" } }, "VideoTopicArn": { "Value": { "Ref": "VideoTopic" } }, "VideoTopicName": { "Value": { "Fn::GetAtt": [ "VideoTopic", "TopicName" ] } }, "VideoRoleArn": { "Value": { "Fn::GetAtt": [ "VideoRole", "Arn" ] } } }, "Resources": { "MediaBucket": { "Type": "AWS::S3::Bucket" }, "MediaTable": { "Properties": { "AttributeDefinitions": [ { "AttributeName": "name", "AttributeType": "S" } ], "KeySchema": [ { "AttributeName": "name", "KeyType": "HASH" } ], "ProvisionedThroughput": { "ReadCapacityUnits": 5, "WriteCapacityUnits": 5 } }, "Type": "AWS::DynamoDB::Table" }, "VideoTopic": { "Type": "AWS::SNS::Topic" }, "VideoRole": { "Properties": { "AssumeRolePolicyDocument": { "Statement": [ { "Effect": "Allow", "Action": [ "sts:AssumeRole" ], "Principal": { "Service": [ "rekognition.amazonaws.com" ] } } ] }, "Policies": [ { "PolicyName": "RekognitionPublish", "PolicyDocument": { "Statement": [ { "Effect": "Allow", "Action": [ "sns:Publish" ], "Resource": [ { "Ref": "VideoTopic" } ] } ] } } ] }, "Type": "AWS::IAM::Role" } } } ================================================ FILE: docs/source/samples/media-query/index.rst ================================================ ======================= Media Query Application ======================= .. youtube:: UCZXJpI1dKw :width: 75% This sample application shows how to combine multiple event handlers in Chalice to create an image processing pipeline. It takes as input any image or video and it will identify objects, people, text, scenes, and activities. This results of this analysis can then be queried with a REST API. .. image:: docs/assets/appexample.jpg :width: 100% :alt: Application Example There are several components of this application. The first part is an image processing pipeline. The application is registered to automatically process any media that's uploaded to an Amazon S3 bucket. The application will then use Amazon Rekognition to automatically detect labels in either the image or the video. The returned labels are then stored in an Amazon DynamoDB table. For videos, an asynchronous job is started. This is because the analysis for videos takes longer than analyzing images so we don't want our Lambda function to block until the job is complete. To handle this asynchronous job, we subscribe to an Amazon SNS topic. When the asynchronous job is finished analyzing our uploaded video, an event handler is called that will retrieve the results and store the labels in Amazon DynamoDB. The final component is the REST API. This allows users to query for labels associated with the media that has been uploaded. You can find the full source code for this application in our `samples directory on GitHub `__. :: $ git clone git://github.com/aws/chalice $ cd chalice/docs/source/samples/media-query/code We'll now walk through the architecture of the application, how to deploy and use the application, and go over the application code. .. note:: This sample application is also available as a `workshop `__. The main difference between the sample apps here and the Chalice workshops is that the workshop is a detailed step by step process for how to create this application from scratch. You build the app by gradually adding each feature piece by piece. It takes several hours to work through all the workshop material. In this document we review the architecture, the deployment process, then walk through the main sections of the code. Architecture ============ Below is the architecture for the application. .. image:: docs/assets/architecture.png :width: 100% :alt: Architecture diagram The main components of the application are as follows: * ``handle_object_created``: A Lambda function that is triggered when an object is uploaded to a S3 bucket. If the object is an image, it will call Amazon Rekognition's ``DetectLabels`` API to detect objects in the image. With the detected objects, the Lambda function will then add the object to an Amazon DynamoDB table. If the object is a video, it will call Rekognition's ``StartLabelDetection`` API to initiate an asynchronous job to detect labels in the video. When the job is completed, a completion notification is pushed to an SNS topic. * ``handle_object_deleted``: A Lambda function that removes the object from the DynamoDB table if the object is deleted from the S3 bucket. * ``add_video_labels``: A Lambda function that is triggered on video label detection SNS messages. On invocation, it will call Rekognition's ``GetLabelDetection`` API to retrieve all detected objects from the video. It then adds the video with its labels to the DynamoDB Table * ``api_handler``: A Lambda function that is invoked by HTTP requests to Amazon API Gateway. On invocation, it will query the database based on the received HTTP request and return the results to the user through API Gateway. Deployment ========== First, we'll setup our development environment by cloning the Chalice GitHub repository and copying the sample code in a new directory:: $ git clone git://github.com/aws/chalice $ mkdir /tmp/demo $ cp -r chalice/docs/source/samples/media-query/code/ /tmp/demo/media-query $ cd /tmp/demo/media-query/ Next configure a virtual environment that uses Python 3. In this example we're using Python 3.7. :: $ python3 -m venv /tmp/venv37 $ . /tmp/venv37/bin/activate To deploy the application, first install the necessary requirements and install Chalice:: $ pip install -r requirements.txt $ pip install chalice We'll also be using the AWS CLI to help deploy our application, you can follow the `installation instructions `__ if you don't have the AWS CLI installed. Next, we'll use the AWS CLI to deploy a CloudFormation stack containing the S3 bucket, DynamoDB table, and SNS topic needed to run this application:: $ aws cloudformation deploy --template-file resources.json \ --stack-name media-query --capabilities CAPABILITY_IAM Record the deployed resources as environment variables in the Chalice application by running the `recordresources.py` script:: $ python recordresources.py --stack-name media-query You can see these values by looking at the ``.chalice/config.json`` file. Once those resources are created and recorded, deploy the Chalice application:: $ chalice deploy Using the Application ===================== Once the application is deployed, use the AWS CLI to fetch the name of the bucket that is storing the media files:: $ aws cloudformation describe-stacks --stack-name media-query \ --query "Stacks[0].Outputs[?OutputKey=='MediaBucketName'].OutputValue" \ --output text media-query-mediabucket-xtrhd3c4b59 Upload some sample media files to your Amazon S3 bucket so the system populates information about the media files in your DynamoDB table. If you need sample media files, you can use the included samples from the corresponding `Chalice workshop `__ assets `here `__. :: $ aws s3 cp assets/sample.jpg s3://media-query-mediabucket-xtrhd3c4b59/sample.jpg $ aws s3 cp assets/sample.mp4 s3://media-query-mediabucket-xtrhd3c4b59/sample.mp4 Wait about a minute for the media files to be populated in the database and then install HTTPie:: $ pip install httpie Then, list out all if the media files using the application's API with HTTPie:: $ chalice url https://qi5hf4djdg.execute-api.us-west-2.amazonaws.com/api/ $ http https://qi5hf4djdg.execute-api.us-west-2.amazonaws.com/api/ HTTP/1.1 200 OK Connection: keep-alive Content-Length: 279 Content-Type: application/json Date: Tue, 10 Jul 2018 17:58:40 GMT Via: 1.1 fa751ee53e2bf18781ae98b293ff9375.cloudfront.net (CloudFront) X-Amz-Cf-Id: sNnrzvbdvgj1ZraySJvfSUbHthC_fok8l5GJ7glV4QcED_M1c8tlvg== X-Amzn-Trace-Id: Root=1-5b44f3d0-4546157e8f5e35a008d06d88;Sampled=0 X-Cache: Miss from cloudfront x-amz-apigw-id: J0sIlHs3vHcFj9g= x-amzn-RequestId: e0aaf4e1-846a-11e8-b756-99d52d342d60 [ { "labels": [ "Animal", "Canine", "Dog", "German Shepherd", "Mammal", "Pet", "Collie" ], "name": "sample.jpg", "type": "image" }, { "labels": [ "Human", "Clothing", "Dog", "Nest", "Person", "Footwear", "Bird Nest", "People", "Animal", "Husky" ], "name": "sample.mp4", "type": "video" } ] You can include query string parameters as well to query all objects based on what the file name starts with, the type of the media file, and the detected objects in the media file:: $ http https://qi5hf4djdg.execute-api.us-west-2.amazonaws.com/api/ startswith==sample.m HTTP/1.1 200 OK Connection: keep-alive Content-Length: 153 Content-Type: application/json Date: Tue, 10 Jul 2018 19:20:02 GMT Via: 1.1 aa42484f82c16d99015c599631def20c.cloudfront.net (CloudFront) X-Amz-Cf-Id: euqlOlWN5k5V_zKCJy4SL988Vcje6W5jDR88GrWr5uYGH-_ZvN4arg== X-Amzn-Trace-Id: Root=1-5b4506e0-db041a3492ee56e8f3d9457c;Sampled=0 X-Cache: Miss from cloudfront x-amz-apigw-id: J04DHE92PHcF--Q= x-amzn-RequestId: 3d82319d-8476-11e8-86d9-a1e4585e5c26 [ { "labels": [ "Human", "Clothing", "Dog", "Nest", "Person", "Footwear", "Bird Nest", "People", "Animal", "Husky" ], "name": "sample.mp4", "type": "video" } ] $ http https://qi5hf4djdg.execute-api.us-west-2.amazonaws.com/api/ media-type==image HTTP/1.1 200 OK Connection: keep-alive Content-Length: 126 Content-Type: application/json Date: Tue, 10 Jul 2018 19:20:53 GMT Via: 1.1 88eb066576c1b47cd896ab0019b9f25f.cloudfront.net (CloudFront) X-Amz-Cf-Id: rwuOwzLKDM4KgcSBXFihWeNNsYSpZDYVpc8IXdT0xOu8qz8aA2Pj3w== X-Amzn-Trace-Id: Root=1-5b450715-de71cf04ca2900b839ff1194;Sampled=0 X-Cache: Miss from cloudfront x-amz-apigw-id: J04LaE6YPHcF3VA= x-amzn-RequestId: 5d29d59a-8476-11e8-a347-ebb5d5f47789 [ { "labels": [ "Animal", "Canine", "Dog", "German Shepherd", "Mammal", "Pet", "Collie" ], "name": "sample.jpg", "type": "image" } ] $ http https://qi5hf4djdg.execute-api.us-west-2.amazonaws.com/api/ label==Person HTTP/1.1 200 OK Connection: keep-alive Content-Length: 153 Content-Type: application/json Date: Tue, 10 Jul 2018 19:20:02 GMT Via: 1.1 aa42484f82c16d99015c599631def20c.cloudfront.net (CloudFront) X-Amz-Cf-Id: euqlOlWN5k5V_zKCJy4SL988Vcje6W5jDR88GrWr5uYGH-_ZvN4arg== X-Amzn-Trace-Id: Root=1-5b4506e0-db041a3492ee56e8f3d9457c;Sampled=0 X-Cache: Miss from cloudfront x-amz-apigw-id: J04DHE92PHcF--Q= x-amzn-RequestId: 3d82319d-8476-11e8-86d9-a1e4585e5c26 [ { "labels": [ "Human", "Clothing", "Dog", "Nest", "Person", "Footwear", "Bird Nest", "People", "Animal", "Husky" ], "name": "sample.mp4", "type": "video" } ] You can also query for a specific object:: $ http https://qi5hf4djdg.execute-api.us-west-2.amazonaws.com/api/sample.jpg HTTP/1.1 200 OK Connection: keep-alive Content-Length: 126 Content-Type: application/json Date: Tue, 10 Jul 2018 19:20:53 GMT Via: 1.1 88eb066576c1b47cd896ab0019b9f25f.cloudfront.net (CloudFront) X-Amz-Cf-Id: rwuOwzLKDM4KgcSBXFihWeNNsYSpZDYVpc8IXdT0xOu8qz8aA2Pj3w== X-Amzn-Trace-Id: Root=1-5b450715-de71cf04ca2900b839ff1194;Sampled=0 X-Cache: Miss from cloudfront x-amz-apigw-id: J04LaE6YPHcF3VA= x-amzn-RequestId: 5d29d59a-8476-11e8-a347-ebb5d5f47789 [ { "labels": [ "Animal", "Canine", "Dog", "German Shepherd", "Mammal", "Pet", "Collie" ], "name": "sample.jpg", "type": "image" } ] Code Walkthrough ================ We'll take a top-down approach with this application and start with the main entry point, the ``app.py`` file. The source code for this application is split between the ``app.py`` as well as supporting code in ``chalicelib/``. Event Handlers -------------- In the ``app.py`` we see four different decorator types, each corresponding to Lambda functions that are triggered by different events. Note that the line numbers correspond to the line numbers in the ``app.py`` file. .. literalinclude:: code/app.py :lineno-match: :start-after: # Start of Event Handlers :end-before: # End of Event Handlers The first two decorators use ``@app.on_s3_event`` and are specifying that these two Lambda functions should be invoked when an object is created or deleted from S3, respectively. The name of the S3 bucket is not hardcoded in the ``app.py`` file but instead pulled from the environment variable ``MEDIA_BUCKET_NAME``. The ``recordresources.py`` script that was run as part of the deployment process described above automatically created these resources and updated the Chalice config file (``.chalice/config.json``) with these values. If you look at the contents of your ``.chalice/config.json`` file, it should look something like this: .. code-block:: json { "version": "2.0", "app_name": "media-query", "stages": { "dev": { "api_gateway_stage": "api", "autogen_policy": false, "environment_variables": { "MEDIA_TABLE_NAME": "media-query-MediaTable-10QEPR0O8DOT4", "MEDIA_BUCKET_NAME": "media-query-mediabucket-fb8oddjbslv1", "VIDEO_TOPIC_NAME": "media-query-VideoTopic-KU38EEHIIUV1", "VIDEO_ROLE_ARN": "arn:aws:iam::123456789123:role/media-query-VideoRole-1GKK0CA30VCAD", "VIDEO_TOPIC_ARN": "arn:aws:sns:us-west-2:123456789123:media-query-VideoTopic-KU38EEHIIUV1" } } } } Next, the ``@app.on_sns_message`` is used to connect an SNS topic to our Lambda function. This is only used for video processing with Rekognition. Because of the longer processing times of video compared to images, video analysis is performed by first starting a "video label job". When you start this asynchronous job, we can specify an SNS topic that Rekognition will publish to when the job is complete, as shown in the ``_handle_created_video`` function below. .. literalinclude:: code/app.py :linenos: :lineno-match: :pyobject: _handle_created_video The ``add_video_file()`` function will then query for the results of the job (the ``JobId`` is provided as part of the SNS message that's published) and store the results in the DynamoDB table. The final two decorators of this app creates a REST API with Amazon API Gateway and defines two routes: ``/`` and ``/{name}``. Requesting the root URL of ``/`` is equivalent to a "List" API call that will return all the media files that have been analyzed so far. Request ``/{name}``, where ``{name}`` is the name of the media file that was uploaded to S3 will return the detected labels for that single resource. This is equivalent to a "Get" API call. .. note:: This sample application returns all analyzed media files in its List API call. In practice, you should paginate your List API calls to ensure you don't return unbounded results. Supporting Files ---------------- The event handlers described in the previous section interact with Rekognition and DynamoDB through clients that are accessed through ``get_rekognition_client()`` and ``get_rekognition_client()`` respectively. These clients are high level wrappers to the corresponding boto3 clients/resources for these services. The code for these high level clients are in ``chalicelib/rekognition.py`` and ``chalicelib/db.py``. If we look at the ``DynamoMediaDB.add_media_file()`` method in the ``chalicelib/db.py`` file, we see that it's a small wrapper around the ``put_item()`` operation of the underlying DynamoDB API: .. literalinclude:: code/chalicelib/db.py :linenos: :lineno-match: :pyobject: DynamoMediaDB.add_media_file We see a similar pattern in ``chalicelib/rekognition.py``. Here's the ``start_video_label_job`` job that starts the asynchronous processing discussed in the previous section. .. literalinclude:: code/chalicelib/rekognition.py :linenos: :lineno-match: :pyobject: RekognitonClient.start_video_label_job As you can see, it's a small wrapper around the ``start_label_detection`` operation of the underlying Rekognition API. We encourage you to look through the rest of the ``chalicelib/`` directory to see how these high level clients are implemented. Cleaning Up =========== If you're done experimenting with this sample app, you can run these commands to delete this app. 1. Delete the chalice application:: $ chalice delete Deleting Rest API: kyfn3gqcf0 Deleting function: arn:aws:lambda:us-west-2:123456789123:function:media-query-dev Deleting IAM role: media-query-dev-api_handler Deleting function: arn:aws:lambda:us-west-2:123456789123:function:media-query-dev-add_video_file Deleting IAM role: media-query-dev-add_video_file Deleting function: arn:aws:lambda:us-west-2:123456789123:function:media-query-dev-handle_object_removed Deleting IAM role: media-query-dev-handle_object_removed Deleting function: arn:aws:lambda:us-west-2:123456789123:function:media-query-dev-handle_object_created Deleting IAM role: media-query-dev-handle_object_created 2. Delete all objects in your S3 bucket:: $ aws s3 rm s3://$MEDIA_BUCKET_NAME --recursive delete: s3://media-query-mediabucket-4b1h8anboxpa/sample.jpg delete: s3://media-query-mediabucket-4b1h8anboxpa/sample.mp4 3. Delete the CloudFormation stack containing the additional AWS resources:: $ aws cloudformation delete-stack --stack-name media-query ================================================ FILE: docs/source/samples/todo-app/code/.chalice/config.json ================================================ { "stages": { "dev": { "autogen_policy": false, "api_gateway_stage": "api" } }, "version": "2.0", "app_name": "mytodo" } ================================================ FILE: docs/source/samples/todo-app/code/.chalice/policy-dev.json ================================================ { "Version": "2012-10-17", "Statement": [ { "Action": [ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents" ], "Resource": "arn:aws:logs:*:*:*", "Effect": "Allow" }, { "Action": [ "dynamodb:PutItem", "dynamodb:DeleteItem", "dynamodb:UpdateItem", "dynamodb:GetItem", "dynamodb:Scan", "dynamodb:Query" ], "Resource": [ "arn:aws:dynamodb:*:*:table/todo-app-*", "arn:aws:dynamodb:*:*:table/users-app-*" ], "Effect": "Allow" }, { "Action": [ "ssm:GetParameter" ], "Resource": [ "arn:aws:ssm:*:*:parameter/todo-sample-app/auth-key" ], "Effect": "Allow" } ] } ================================================ FILE: docs/source/samples/todo-app/code/.gitignore ================================================ .chalice/deployments/ .chalice/venv/ ================================================ FILE: docs/source/samples/todo-app/code/app.py ================================================ import os import base64 import boto3 from chalice import Chalice, AuthResponse from chalicelib import auth, db app = Chalice(app_name='mytodo') app.debug = True _DB = None _USER_DB = None _AUTH_KEY = None _SSM_AUTH_KEY_NAME = '/todo-sample-app/auth-key' @app.route('/login', methods=['POST']) def login(): body = app.current_request.json_body record = get_users_db().get_item( Key={'username': body['username']})['Item'] jwt_token = auth.get_jwt_token( body['username'], body['password'], record, get_auth_key()) return {'token': jwt_token} @app.authorizer() def jwt_auth(auth_request): token = auth_request.token decoded = auth.decode_jwt_token(token, get_auth_key()) return AuthResponse(routes=['*'], principal_id=decoded['sub']) def get_auth_key(): global _AUTH_KEY if _AUTH_KEY is None: base64_key = boto3.client('ssm').get_parameter( Name=_SSM_AUTH_KEY_NAME, WithDecryption=True )['Parameter']['Value'] _AUTH_KEY = base64.b64decode(base64_key) return _AUTH_KEY def get_users_db(): global _USER_DB if _USER_DB is None: _USER_DB = boto3.resource('dynamodb').Table( os.environ['USERS_TABLE_NAME']) return _USER_DB def get_app_db(): global _DB if _DB is None: _DB = db.DynamoDBTodo( boto3.resource('dynamodb').Table( os.environ['APP_TABLE_NAME']) ) return _DB def get_authorized_username(current_request): return current_request.context['authorizer']['principalId'] @app.route('/todos', methods=['GET'], authorizer=jwt_auth) def list_todos(): username = get_authorized_username(app.current_request) return get_app_db().list_items(username=username) @app.route('/todos', methods=['POST'], authorizer=jwt_auth) def create_todo(): body = app.current_request.json_body username = get_authorized_username(app.current_request) return get_app_db().add_item( username=username, description=body['description'], metadata=body.get('metadata'), ) @app.route('/todos/{uid}', methods=['GET'], authorizer=jwt_auth) def get_todo(uid): username = get_authorized_username(app.current_request) return get_app_db().get_item(uid, username=username) @app.route('/todos/{uid}', methods=['PUT'], authorizer=jwt_auth) def update_todo(uid): body = app.current_request.json_body username = get_authorized_username(app.current_request) get_app_db().update_item( uid, description=body.get('description'), state=body.get('state'), metadata=body.get('metadata'), username=username) @app.route('/todos/{uid}', methods=['DELETE'], authorizer=jwt_auth) def delete_todo(uid): username = get_authorized_username(app.current_request) return get_app_db().delete_item(uid, username=username) ================================================ FILE: docs/source/samples/todo-app/code/chalicelib/__init__.py ================================================ ================================================ FILE: docs/source/samples/todo-app/code/chalicelib/auth.py ================================================ import hashlib import hmac import datetime from uuid import uuid4 import jwt from chalice import UnauthorizedError def get_jwt_token(username, password, record, secret): actual = hashlib.pbkdf2_hmac( record['hash'], password.encode('utf-8'), record['salt'].value, int(record['rounds']) ) expected = record['hashed'].value if hmac.compare_digest(actual, expected): now = datetime.datetime.utcnow() unique_id = str(uuid4()) payload = { 'sub': username, 'iat': now, 'nbf': now, 'jti': unique_id, # NOTE: We can also add 'exp' if we want tokens to expire. } return jwt.encode(payload, secret, algorithm='HS256') raise UnauthorizedError('Invalid password') def decode_jwt_token(token, secret): return jwt.decode(token, secret, algorithms=['HS256']) ================================================ FILE: docs/source/samples/todo-app/code/chalicelib/db.py ================================================ from uuid import uuid4 from boto3.dynamodb.conditions import Key DEFAULT_USERNAME = 'default' class TodoDB(object): def list_items(self): pass def add_item(self, description, metadata=None): pass def get_item(self, uid): pass def delete_item(self, uid): pass def update_item(self, uid, description=None, state=None, metadata=None): pass class InMemoryTodoDB(TodoDB): def __init__(self, state=None): if state is None: state = {} self._state = state def list_all_items(self): all_items = [] for username in self._state: all_items.extend(self.list_items(username)) return all_items def list_items(self, username=DEFAULT_USERNAME): return list(self._state.get(username, {}).values()) def add_item(self, description, metadata=None, username=DEFAULT_USERNAME): if username not in self._state: self._state[username] = {} uid = str(uuid4()) self._state[username][uid] = { 'uid': uid, 'description': description, 'state': 'unstarted', 'metadata': metadata if metadata is not None else {}, 'username': username } return uid def get_item(self, uid, username=DEFAULT_USERNAME): return self._state[username][uid] def delete_item(self, uid, username=DEFAULT_USERNAME): del self._state[username][uid] def update_item(self, uid, description=None, state=None, metadata=None, username=DEFAULT_USERNAME): item = self._state[username][uid] if description is not None: item['description'] = description if state is not None: item['state'] = state if metadata is not None: item['metadata'] = metadata class DynamoDBTodo(TodoDB): def __init__(self, table_resource): self._table = table_resource def list_all_items(self): response = self._table.scan() return response['Items'] def list_items(self, username=DEFAULT_USERNAME): response = self._table.query( KeyConditionExpression=Key('username').eq(username) ) return response['Items'] def add_item(self, description, metadata=None, username=DEFAULT_USERNAME): uid = str(uuid4()) self._table.put_item( Item={ 'username': username, 'uid': uid, 'description': description, 'state': 'unstarted', 'metadata': metadata if metadata is not None else {}, } ) return uid def get_item(self, uid, username=DEFAULT_USERNAME): response = self._table.get_item( Key={ 'username': username, 'uid': uid, }, ) return response['Item'] def delete_item(self, uid, username=DEFAULT_USERNAME): self._table.delete_item( Key={ 'username': username, 'uid': uid, } ) def update_item(self, uid, description=None, state=None, metadata=None, username=DEFAULT_USERNAME): # We could also use update_item() with an UpdateExpression. item = self.get_item(uid, username) if description is not None: item['description'] = description if state is not None: item['state'] = state if metadata is not None: item['metadata'] = metadata self._table.put_item(Item=item) ================================================ FILE: docs/source/samples/todo-app/code/create-resources.py ================================================ import os import uuid import json import argparse import base64 import boto3 AUTH_KEY_PARAM_NAME = '/todo-sample-app/auth-key' TABLES = { 'app': { 'prefix': 'todo-app', 'env_var': 'APP_TABLE_NAME', 'hash_key': 'username', 'range_key': 'uid' }, 'users': { 'prefix': 'users-app', 'env_var': 'USERS_TABLE_NAME', 'hash_key': 'username', } } def create_table(table_name_prefix, hash_key, range_key=None): table_name = '%s-%s' % (table_name_prefix, str(uuid.uuid4())) client = boto3.client('dynamodb') key_schema = [ { 'AttributeName': hash_key, 'KeyType': 'HASH', } ] attribute_definitions = [ { 'AttributeName': hash_key, 'AttributeType': 'S', } ] if range_key is not None: key_schema.append({'AttributeName': range_key, 'KeyType': 'RANGE'}) attribute_definitions.append( {'AttributeName': range_key, 'AttributeType': 'S'}) client.create_table( TableName=table_name, KeySchema=key_schema, AttributeDefinitions=attribute_definitions, ProvisionedThroughput={ 'ReadCapacityUnits': 5, 'WriteCapacityUnits': 5, } ) waiter = client.get_waiter('table_exists') waiter.wait(TableName=table_name, WaiterConfig={'Delay': 1}) return table_name def record_as_env_var(key, value, stage): with open(os.path.join('.chalice', 'config.json')) as f: data = json.load(f) data['stages'].setdefault(stage, {}).setdefault( 'environment_variables', {} )[key] = value with open(os.path.join('.chalice', 'config.json'), 'w') as f: serialized = json.dumps(data, indent=2, separators=(',', ': ')) f.write(serialized + '\n') def _already_in_config(env_var, stage): with open(os.path.join('.chalice', 'config.json')) as f: return env_var in json.load(f)['stages'].get( stage, {}).get('environment_variables', {}) def create_auth_key_if_needed(stage): ssm = boto3.client('ssm') try: ssm.get_parameter(Name=AUTH_KEY_PARAM_NAME) except ssm.exceptions.ParameterNotFound: print(f"Generating auth key.") kms = boto3.client('kms') random_bytes = kms.generate_random(NumberOfBytes=32)['Plaintext'] encoded_random_bytes = base64.b64encode(random_bytes).decode() ssm.put_parameter(Name=AUTH_KEY_PARAM_NAME, Value=encoded_random_bytes, Type='SecureString') def create_resources(args): for table_config in TABLES.values(): # We assume if it a value is recorded in the Chalice config # file, the table already exists. if _already_in_config(table_config['env_var'], args.stage): continue print(f"Creating table: {table_config['prefix']}") table_name = create_table( table_config['prefix'], table_config['hash_key'], table_config.get('range_key') ) record_as_env_var(table_config['env_var'], table_name, args.stage) create_auth_key_if_needed(args.stage) def cleanup_resources(args): ddb = boto3.client('dynamodb') ssm = boto3.client('ssm') with open(os.path.join('.chalice', 'config.json')) as f: config = json.load(f) env_vars = config['stages'].get(args.stage, {}).get( 'environment_variables', {}) for key in list(env_vars): value = env_vars.pop(key) if key.endswith('_TABLE_NAME'): print(f"Deleting table: {value}") ddb.delete_table(TableName=value) if not env_vars: del config['stages'][args.stage]['environment_variables'] try: print(f"Deleting SSM param: {AUTH_KEY_PARAM_NAME}") ssm.delete_parameter(Name=AUTH_KEY_PARAM_NAME) except Exception: pass with open(os.path.join('.chalice', 'config.json'), 'w') as f: serialized = json.dumps(config, indent=2, separators=(',', ': ')) f.write(serialized + '\n') print("Resources deleted. If you haven't already, be " "sure to run 'chalice delete' to delete your Chalice application.") def main(): parser = argparse.ArgumentParser() parser.add_argument('-s', '--stage', default='dev') parser.add_argument('-c', '--cleanup', action='store_true') # app - stores the todo items # users - stores the user data. args = parser.parse_args() if args.cleanup: cleanup_resources(args) else: create_resources(args) if __name__ == '__main__': main() ================================================ FILE: docs/source/samples/todo-app/code/requirements-dev.txt ================================================ chalice==1.29.0 pytest==7.4.0 ================================================ FILE: docs/source/samples/todo-app/code/requirements.txt ================================================ boto3==1.27.0 botocore==1.30.0 PyJWT==2.7.0 ================================================ FILE: docs/source/samples/todo-app/code/tests/__init__.py ================================================ ================================================ FILE: docs/source/samples/todo-app/code/tests/test_db.py ================================================ import os import unittest import boto3 from uuid import uuid4 from chalicelib.db import InMemoryTodoDB from chalicelib.db import DynamoDBTodo class TestTodoDB(unittest.TestCase): def setUp(self): self.db_dict = {} self.db = InMemoryTodoDB(self.db_dict) def tearDown(self): response = self.db.list_all_items() for item in response: self.db.delete_item(item['uid'], username=item['username']) def test_can_add_and_retrieve_data(self): todo_id = self.db.add_item('First item') must_contain = {'description': 'First item', 'state': 'unstarted', 'metadata': {}} full_record = self.db.get_item(todo_id) assert dict(full_record, **must_contain) == full_record def test_can_add_and_list_data(self): todo_id = self.db.add_item('First item') todos = self.db.list_items() self.assertEqual(len(todos), 1) self.assertEqual(todos[0]['uid'], todo_id) def test_can_add_and_delete_data(self): todo_id = self.db.add_item('First item') self.assertEqual(len(self.db.list_items()), 1) self.db.delete_item(todo_id) self.assertEqual(len(self.db.list_items()), 0) def test_can_add_and_update_data(self): todo_id = self.db.add_item('First item') self.db.update_item(todo_id, state='started') self.assertEqual(self.db.get_item(todo_id)['state'], 'started') def test_can_add_and_retrieve_data_with_specified_username(self): username = 'myusername' todo_id = self.db.add_item('First item', username=username) must_contain = { 'description': 'First item', 'state': 'unstarted', 'metadata': {}, 'username': username } full_record = self.db.get_item(todo_id, username=username) assert dict(full_record, **must_contain) == full_record def test_can_add_and_list_data_with_specified_username(self): username = 'myusername' todo_id = self.db.add_item('First item', username=username) todos = self.db.list_items(username=username) self.assertEqual(len(todos), 1) self.assertEqual(todos[0]['uid'], todo_id) self.assertEqual(todos[0]['username'], username) def test_can_add_and_delete_data_with_specified_username(self): username = 'myusername' todo_id = self.db.add_item('First item', username=username) self.assertEqual(len(self.db.list_items(username=username)), 1) self.db.delete_item(todo_id, username=username) self.assertEqual(len(self.db.list_items(username=username)), 0) def test_can_add_and_update_data_with_specified_username(self): username = 'myusername' todo_id = self.db.add_item('First item', username=username) self.db.update_item(todo_id, state='started', username=username) self.assertEqual(self.db.get_item( todo_id, username=username)['state'], 'started') def test_list_all_items(self): todo_id = self.db.add_item('First item', username='user') other_todo_id = self.db.add_item('First item', username='otheruser') all_todos = self.db.list_all_items() self.assertEqual(len(all_todos), 2) users = [todo['username'] for todo in all_todos] todo_ids = [todo['uid'] for todo in all_todos] self.assertCountEqual(['user', 'otheruser'], users) self.assertCountEqual([todo_id, other_todo_id], todo_ids) @unittest.skipUnless(os.environ.get('RUN_INTEG_TESTS', False), "Skipping integ tests (RUN_INTEG_TESTS) not test.") class TestDynamoDB(TestTodoDB): @classmethod def setUpClass(cls): cls.TABLE_NAME = 'todo-integ-%s' % str(uuid4()) client = boto3.client('dynamodb') client.create_table( TableName=cls.TABLE_NAME, KeySchema=[ { 'AttributeName': 'username', 'KeyType': 'HASH' }, { 'AttributeName': 'uid', 'KeyType': 'RANGE', } ], AttributeDefinitions=[ { 'AttributeName': 'username', 'AttributeType': 'S', }, { 'AttributeName': 'uid', 'AttributeType': 'S', } ], ProvisionedThroughput={ 'ReadCapacityUnits': 5, 'WriteCapacityUnits': 5, } ) waiter = client.get_waiter('table_exists') waiter.wait(TableName=cls.TABLE_NAME, WaiterConfig={'Delay': 1}) @classmethod def tearDownClass(cls): client = boto3.client('dynamodb') client.delete_table(TableName=cls.TABLE_NAME) waiter = client.get_waiter('table_not_exists') waiter.wait(TableName=cls.TABLE_NAME, WaiterConfig={'Delay': 1}) def setUp(self): resource = boto3.resource('dynamodb') self.table = resource.Table(self.TABLE_NAME) self.db = DynamoDBTodo(self.table) ================================================ FILE: docs/source/samples/todo-app/code/users.py ================================================ import os import json import getpass import argparse import hashlib import hmac import base64 import boto3 from boto3.dynamodb.types import Binary def get_table_name(stage): # We might want to user the chalice modules to # load the config. For now we'll just load it directly. with open(os.path.join('.chalice', 'config.json')) as f: data = json.load(f) return data['stages'][stage]['environment_variables']['USERS_TABLE_NAME'] def create_user(stage): table_name = get_table_name(stage) table = boto3.resource('dynamodb').Table(table_name) username = input('Username: ').strip() password = getpass.getpass('Password: ').strip() password_fields = encode_password(password) item = { 'username': username, 'hash': password_fields['hash'], 'salt': Binary(password_fields['salt']), 'rounds': password_fields['rounds'], 'hashed': Binary(password_fields['hashed']), } table.put_item(Item=item) def encode_password(password, salt=None): if salt is None: salt = os.urandom(32) rounds = 100000 hashed = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt, rounds) return { 'hash': 'sha256', 'salt': salt, 'rounds': rounds, 'hashed': hashed, } def list_users(stage): table_name = get_table_name(stage) table = boto3.resource('dynamodb').Table(table_name) for item in table.scan()['Items']: print(item['username']) def get_user(username, stage): table_name = get_table_name(stage) table = boto3.resource('dynamodb').Table(table_name) user_record = table.get_item(Key={'username': username}).get('Item') if user_record is not None: print(f"Entry for user: {username}") for key, value in user_record.items(): if isinstance(value, Binary): value = base64.b64encode(value.value).decode() print(f" {key:10}: {value}") def test_password(stage): username = input('Username: ').strip() password = getpass.getpass('Password: ').strip() table_name = get_table_name(stage) table = boto3.resource('dynamodb').Table(table_name) item = table.get_item(Key={'username': username})['Item'] encoded = encode_password(password, salt=item['salt'].value) if hmac.compare_digest(encoded['hashed'], item['hashed'].value): print("Password verified.") else: print("Password verification failed.") def main(): parser = argparse.ArgumentParser() parser.add_argument('-c', '--create-user', action='store_true') parser.add_argument('-t', '--test-password', action='store_true') parser.add_argument('-g', '--get-user') parser.add_argument('-s', '--stage', default='dev') parser.add_argument('-l', '--list-users', action='store_true') args = parser.parse_args() if args.create_user: create_user(args.stage) elif args.list_users: list_users(args.stage) elif args.test_password: test_password(args.stage) elif args.get_user is not None: get_user(args.get_user, args.stage) if __name__ == '__main__': main() ================================================ FILE: docs/source/samples/todo-app/index.rst ================================================ ================ Todo Application ================ This is a sample application that allows you to manage Todo items. This tutorial will walk through creating a serverless web API to create, update, get, and delete Todos, managing Todos in a database, and adding authorization with JWT. AWS services covered include AWS Lambda, Amazon API Gateway, Amazon DynamoDB, AWS CodeBuild, and AWS Systems Manager. You can find the full source code for this application in our `samples directory on GitHub `__. :: $ git clone git://github.com/aws/chalice $ cd chalice/docs/source/samples/todo-app/code We'll now walk through the architecture of this application, how to deploy and use the application, and finally we'll go over the main components of the application code. .. note:: This sample application is also available as a `workshop `__. The main difference between the sample apps here and the Chalice workshops is that the workshop is a detailed step by step process for how to create this application from scratch. You build the app by gradually adding each feature piece by piece. In the workshop, we first create a REST API with no authentication or data store. Then we introduce DynamoDB, then JWT auth, etc. The workshop also shows you how to set up a CI/CD pipeline to automatically deploy your application whenever you push to your git repository. It takes several hours to work through all the workshop material. In this document we review the architecture, the deployment process, then walk through the main sections of the final version of this application. Architecture ============ The main component of this application is a REST API backed by Amazon API Gateway and AWS Lambda. The rest API lets you manage a Todo list. It lets you create a new Todo list as well as check off existing Todo items. In order to see a list of your Todo items, you must first log in. Information about our users is stored in an Amazon DynamoDB table. The authentication is done using a builtin authorizer. This lets you define a Lambda function to perform your custom auth process. For this sample app, we're using JSON Web Tokens (JWT). The Todo items are stored in a separate DynamoDB table. Below is an architecture diagram of our sample app. It shows the API Gateway REST API, along with a Lambda function for our authorizer, a Lambda function for our REST API, and two DynamoDB tables. .. image:: docs/assets/architecture.jpg :width: 100% :alt: Architecture diagram .. _todo-sample-rest-api: REST API -------- The REST API supports the following resources: * GET - ``/todos/`` - Gets a list of all todo items * POST - ``/todos/`` - Creates a new Todo item * GET - ``/todos/{id}`` - Gets a specific todo item * DELETE - ``/todos/{id}`` - Deletes a specific todo item * PUT - ``/todos/{id}`` - Updates the state of a todo item A todo item has this schema:: { "description": {"type": "str"}, "uid": {"type: "str"}, "state": {"type: "str", "enum": ["unstarted", "started", "completed"]}, "metadata": { "type": "object" } } Deployment ========== To run and deploy this application, first create a virtual environment and install the dependencies. Python 3.7 is used for this sample app. :: $ python3 -m /tmp/venv37 $ . /tmp/venv37/bin/activate $ pip install ./requirements-dev.txt $ pip install ./requirements.txt As part of this application, there are additional resources that are created that are used by this application, including two DynamoDB tables as well as an SSM parameter used to store our secret key used in our JWT auth. To create these resources, you can run:: $ python create-resources.py This will also update your ``.chalice/config.json`` file with environment variables containing the name of the DynamoDB tables that were created. At this point, you can either test the application by running ``chalice local``. This will start a local HTTP server on port 8000 that emulates API Gateway so that you can test without having to deploy your application to AWS. You can also run ``chalice deploy`` to deploy your application to AWS, which allows you to test on an actual API Gateway REST API:: $ chalice deploy Creating deployment package. Creating IAM role: mytodo-dev-api_handler Creating lambda function: mytodo-dev Creating IAM role: mytodo-dev-jwt_auth Creating lambda function: mytodo-dev-jwt_auth Creating Rest API Resources deployed: - Lambda ARN: arn:aws:lambda:us-east-1:12345:function:mytodo-dev - Lambda ARN: arn:aws:lambda:us-east-1:12345:function:mytodo-dev-jwt_auth - Rest API URL: https://abcd.execute-api.us-west-2.amazonaws.com/api/ Using the Application ===================== If you've deployed your application using ``chalice deploy``, you can test the REST API by making requests to the ``Rest API URL``, shown in the output of ``chalice deploy``, in our example that would be ``https://abcd.execute-api.us-west-2.amazonaws.com/api/``. If you're using ``chalice local``, you'll make requests to ``http://localhost:8000/``. Before we can make requests we need to authenticate with the API. In order to authenticate with the API we need to create user accounts. A helper script, ``users.py`` is included in the repository to help you manage users. The first thing we'll need to do is create a user:: $ python users.py --create-user Username: myusername Password: This will create a new entry in our users DynamoDB table. You can then test that the password verification works by running:: $ python users.py --test-password Username: myusername Password: Password verified. Once we've created a test user, we can now login by sending a POST request to the ``/login`` URL:: $ echo '{"username": "myusername", "password": "mypassword"}' | \ http POST https://abcd.execute-api.us-west-2.amazonaws.com/api/login/ { "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJteXVzZXJuYW1lIiwiaWF0IjoxNTk1NDU3Njg5LCJuYmYiOjE1OTU0NTc2ODksImp0aSI6IjMxNjc4YzFkLTdkZjEtNGEzOC04YmZiLTllZjZiMGM1YzAyNyJ9.w46RdtzZdk_P0LAh_St3wjsqgh-k-Hp1ykTpbDqad2k", } .. note:: We're using the HTTPie command line tool instead of cURL. You can install this tool by running ``pip install httpie``. Now whenever we make any requests to our REST API, we need to include the token value in the output above as the value of our ``Authorization`` header. For example, we can list all of our Todos, which is initially empty:: $ http https://abcd.execute-api.us-west-2.amazonaws.com/api/todos/ 'Authorization: my.jwt.token' HTTP/1.1 200 OK Content-Length: 2 Content-Type: application/json [] If you omit the ``Authorization`` header, you'll see this error response:: $ http https://abcd.execute-api.us-west-2.amazonaws.com/api/todos/ HTTP/1.1 401 Unauthorized Content-Length: 26 Content-Type: application/json x-amzn-ErrorType: UnauthorizedException { "message": "Unauthorized" } We can create a new Todo:: $ echo '{"description": "My first Todo", "metadata": {}}' \ | http POST https://abcd.execute-api.us-west-2.amazonaws.com/api/todos/ \ 'Authorization: my.jwt.token' HTTP/1.1 200 OK Content-Length: 36 Content-Type: application/json e25643f7-0b18-47d2-b124-4e6713ab527c Now when we list our Todos, we'll see our new entry we created:: $ http https://abcd.execute-api.us-west-2.amazonaws.com/api/todos/ 'Authorization: my.jwt.token' HTTP/1.1 200 OK Content-Length: 136 Content-Type: application/json [ { "description": "My first Todo", "metadata": {}, "state": "unstarted", "uid": "e25643f7-0b18-47d2-b124-4e6713ab527c", "username": "myusername" } ] We can update our Todo and mark it completed:: $ echo '{"state": "completed"}' | \ http PUT https://abcd.execute-api.us-west-2.amazonaws.com/api/todos/e25643f7-0b18-47d2-b124-4e6713ab527c \ 'Authorization: my.jwt.token' HTTP/1.1 200 OK Content-Length: 4 Content-Type: application/json null And we can now verify that the Todo item shows up as completed:: $ http https://abcd.execute-api.us-west-2.amazonaws.com/api/todos/e25643f7-0b18-47d2-b124-4e6713ab527c \ 'Authorization: my.jwt.token' HTTP/1.1 200 OK Content-Length: 134 Content-Type: application/json { "description": "My first Todo", "metadata": {}, "state": "completed", "uid": "e25643f7-0b18-47d2-b124-4e6713ab527c", "username": "myusername" } Code Walkthrough ================ .. _todo-app-rest-api: Rest API -------- Below is the code for the five routes defined in the :ref:`todo-sample-rest-api` section defined in the ``app.py`` file: .. literalinclude:: code/app.py :caption: app.py :linenos: :lineno-match: :lines: 67-105 The first thing all of these routes do is extract the current username from the request. This is done by examining the context associated with the current request. This will include the ``principalId``, or the current username, which is discussed in more detail in the :ref:`todo-app-jwt-auth` section below. Each of these routes then makes a call into the data storage layer, and either retrieves or updates data in the ``Todo`` DynamoDB table. This is discussed in the next section on data storage. The application DB is tracked as a module level variable that is retrieved through the ``get_app_db()`` function. The name of the DynamoDB table is provided through the ``APP_TABLE_NAME`` environment variable, which is specified in your ``.chalice/config.json`` file. This was automatically filled in for you when you ran the ``create-resources.py`` script. User input is extracted from both the URL (the ``uid`` associated with a Todo item is provided as part of the URL) as well as the JSON request body. A key takeaway from these routes is that there's minimal logic in the route definitions themselves. They're primarily about extracting user input and then delegating the heavy lifting to other objects that are independent of any routing information. Data Storage ------------ Each route in this sample application app makes a call to the data storage layer, which is backed by a DynamoDB table. This interface is defined by the ``TodoDB`` interface, which is defined in the ``chalicelib/db.py`` file: .. literalinclude:: code/chalicelib/db.py :caption: chalicelib/db.py :linenos: :lineno-match: :pyobject: TodoDB There are two different implementations of this interface. The first one, ``InMemoryTodoDB``, is an in-memory implementation of this interface where all data is stored within the process. The purpose of this implementation is for testing purposes when you don't want to work with the real DynamoDB service. This allows you to develop your application locally and test using ``chalice local``. The other implementation of ``TodoDB`` interface is ``DynamoDBTodo``, which communicates with the actual DynamoDB service to store and retrieve Todo items. It uses the Table resource of ``boto3``, created via ``boto3.resource('dynamodb').Table(TABLE_NAME)``. This allows us to use the `high level querying interface of boto3 `__. The implementation is shown below. .. literalinclude:: code/chalicelib/db.py :caption: chalicelib/db.py :linenos: :lineno-match: :pyobject: DynamoDBTodo .. _todo-app-jwt-auth: JWT Authentication ------------------ .. note:: This example is for illustration purposes and does not necessarily represent best practices. Its intent is to show how custom authentication can be implemented in a Chalice app. Our REST API for our Todo items requires that you send an appropriate ``Authorization`` header when making HTTP requests. You can retrieve a auth token by making a request to the ``/login`` route with your user name and password. The underlying mechanism used to handle our auth functionality is through issuing a `JWT `__ when you login. Users Table ~~~~~~~~~~~ In order to login, we need a way to store and retrieve user information. This is done through our ``Users`` DynamoDB table. This was created when you ran the ``create-resoureces.py`` file. Each user record stores their username and information about their password. We're using PBKDF2 as our key derivation function for password hashing, which is available in Python's standard library through the `hashlib.pbkdf2_hmac `__ function. The parameters needed by ``pbkdf2_hmac`` are stored in each user's record, including the password hash, salt, number of rounds, and the hash used for PBKDF2 (sha256 in our example). These user entries were created and stored in the ``Users`` DynamoDB table when you ran the ``python users.py --create-user`` command. You can see the fields for a specific user by using the ``--get-user`` option to the ``users.py`` script:: $ python users.py --get-user myusername Entry for user: myusername hash : sha256 username : myusername hashed : Hym8Ss6WIArus+aZ6BucZ3sz6Wu5w8Tc3lPUivTuUi4= salt : rXMPBx8ZriKU3SQTh58BlxQQtpcLHfmITTB2tpRs/sM= rounds : 100000 Login Flow ~~~~~~~~~~ Below is the code for the ``/login`` route: .. literalinclude:: code/app.py :caption: app.py :linenos: :lineno-match: :pyobject: login In this login view, we first lookup the user record fom our users DB, and then try to generate a JWT token for this entry. The ``auth.get_jwt_token`` will first verify that the password hash matches what's stored in our users DB, and then generate a JWT token for this user as shown in the code below: .. literalinclude:: code/chalicelib/auth.py :caption: chalicelib/auth.py :linenos: :lineno-match: :pyobject: get_jwt_token The call to ``jwt.encode()`` requires a payload and a secret. This secret is a value that is only known to our application and is used in our built-in authorizer to verify the JWT is valid. This secret value is stored as an SSM parameter. A random secret was automatically generated and stored in SSM for you when running the ``create-resources.py`` script. When we call ``auth.get_jwt_token`` we first retrieve this value from SSM as shown in the ``get_auth_key()`` function defined in our ``app.py`` file: .. literalinclude:: code/app.py :caption: app.py :linenos: :lineno-match: :pyobject: get_auth_key Once we've generated a JWT token, we return the token back to the caller. They must then provide that same token in the ``Authorization`` header whenever they make API calls to the REST API. Custom Authorizer ~~~~~~~~~~~~~~~~~ In order to require that a specific route requires proper authorization, we must first create an authorizer, and then associate it with any routes that require auth. Chalice supports different types of :doc:`../../topics/authorizers`, and in this example we're using the :ref:`builtin-authorizers` type provided by Chalice. This lets us write our custom authorization logic as part of our Chalice app. To do this, we decorate our auth function with the ``@app.authorizer`` decorator. Our custom authorizer logic takes the JWT token (accessible through the ``auth_request.token`` attribute, and verifies the token is valid using our secret key retrieved via ``get_auth_key()``. The custom authorizer is shown below: .. literalinclude:: code/app.py :caption: app.py :linenos: :lineno-match: :pyobject: jwt_auth Once we verify that JWT token is valid, we return an ``AuthResponse`` that specifies what routes the user is allowed to access. In our example, we're giving them access to all routes, denoted by a ``*``. Now that we have our authorizer, we can associate with a route by providing the function as the value of the ``authorizer=`` parameter. We saw this in the :ref:`todo-app-rest-api` section above. For example, note that the ``@app.route()`` decorator is being provided an ``authorizer`` function: .. literalinclude:: code/app.py :caption: app.py :linenos: :lineno-match: :pyobject: list_todos Cleaning Up =========== Once you're finished experimenting with this sample app, you can cleanup your resources by deleting the Chalice app and deleting any additional resources associated with this app. To do this, first delete your Chalice app:: $ chalice delete Deleting Rest API: q7dc49grhk Deleting function: arn:aws:lambda:us-west-w:12345:function:mytodo-dev-jwt_auth Deleting IAM role: mytodo-dev-jwt_auth Deleting function: arn:aws:lambda:us-west-w:12345:function:mytodo-dev Deleting IAM role: mytodo-dev-api_handler Then to cleanup the remaining resources, rerun the ``create-resources.py`` script with the ``--cleanup`` flag. This will delete the DynamoDB tables and the SSM parameter, along with any additional resources created as part of your Chalice app:: $ python create-resources.py --cleanup Deleting table: todo-app-632a558c-8355-4c2d-a46e-24350f371389 Deleting table: users-app-05b34fa2-1ae6-4d81-95d1-7ced59878a2b Deleting SSM param: /todo-sample-app/auth-key Resources deleted. If you haven't already, be sure to run 'chalice delete' to delete your Chalice application. ================================================ FILE: docs/source/theme/smithy/globaltoc.html ================================================ {# basic/globaltoc.html ~~~~~~~~~~~~~~~~~~~~ #} ================================================ FILE: docs/source/theme/smithy/landing.html ================================================
A framework for writing serverless applications
Get started
[full example]
from chalice import Chalice

app = Chalice(app_name="helloworld")

@app.route("/")
def index():
    return {"hello": "world"}

@app.schedule(Rate(5, unit=Rate.MINUTES))
def periodic_task(event):
    return {"hello": "world"}

@app.on_s3_event(bucket='mybucket')
def s3_handler(event):
    print(event.bucket, event.key)

Coding

Focus on writing your application code

Focus on writing your application code instead of the resources or services needed to deploy your application. Chalice automatically determines how to provision the necessary resources for your application.

Coding

A familiar decorator based API

Chalice's API for writing serverless application uses a familiar decorator-based syntax used in frameworks such as Flask, bottle, and FastAPI. Skip the learning curve and get up and running quickly.

Coding

Supports multiple deployment systems

Chalice supports multiple tools to deploy your application including AWS CloudFormation, Terraform, and its own built-in deployer based on the AWS SDK for Python. Use the deployment tools and services you're already familiar with.

Quickstart

Up and running in minutes

Chalice lets you quickly create and deploy python applications that use AWS Lambda. Using the Chalice CLI, you can have a REST API deployed to Amazon API Gateway and AWS Lambda in minutes.

Features

Native python packaging

Chalice has built-in support for python packaging tools. It will automatically package your application and install 3rd party dependencies specified in your requirements.txt file.

AWS SAM and Terraform integration

You can use Chalice's included deployer that's built using the AWS SDK for Python (boto3) or you can have Chalice generate packages that can be deployed with AWS CloudFormation or Terraform.

CI/CD pipeline generation

Automatically generate a deployment pipeline that's built with AWS CodePipeline and AWS CodeBuild. Deploy your application whenever you push changes to your Git repository.

Local testing support

Test your REST API using the local test server. This gives you a quicker feedback loop and let's you test your code before deploying to AWS.

Websocket APIs

Create Websocket APIs with API Gateway. Includes runtime APIs to send messages back to connected clients.

Automatic policy generation

Automatically generate policies for your application based on scanning your source code.

Learning Resources

Tutorials

See step-by-step tutorials that show you how to use various features of Chalice. These are designed to quickly get you up and running if you're new to Chalice. These are perfect for new users of Chalice.

See more.

Sample Applications

Our sample applications are complete examples that are larger in scope than our tutorials. Learn how you can combine multiple features of Chalice to create more real-world serverless applications. We walk through the architecture, deployment, and code for all of our sample applications.

See more.
================================================ FILE: docs/source/theme/smithy/layout.html ================================================ {%- extends "basic/layout.html" %} {%- block extrahead %} {% endblock %} {%- block scripts %} {{ super() }} {% if theme_ga_id %} {% endif %} {%- endblock %} {%- block css -%} {{ super() }} {% if pagename == "index" %} {% endif %} {% endblock -%}
{%- block header %}
{% endblock -%} {%- block relbar1 %}{% endblock %} {% block content %} {% if pagename == "index" %} {%- include 'landing.html' with context %} {% endif %} {% if pagename != "index" or builder == "singlehtml" %}
{% if parents %} {% endif %} {% block body %} {% endblock %} {% if prev or next %}
{% if prev %} {% endif %} {%- if next and next.title != '<no title>' %} {{ next.title }} → {%- endif %}
{% endif %} {%- block content_footer %}{%- endblock %}
{%- if pagename not in ('search', 'contents', 'index', '404') -%} {%- endif -%}
{% endif %} {% endblock %}
{%- block relbar2 %}{% endblock %} {%- block footer %}
{%- endblock -%} ================================================ FILE: docs/source/theme/smithy/static/asciinema-player.css ================================================ .asciinema-player-wrapper { position: relative; text-align: center; outline: none; } .asciinema-player-wrapper .title-bar { display: none; top: -78px; transition: top 0.15s linear; position: absolute; left: 0; right: 0; box-sizing: content-box; font-size: 20px; line-height: 1em; padding: 15px; font-family: sans-serif; color: white; background-color: rgba(0, 0, 0, 0.8); } .asciinema-player-wrapper .title-bar img { vertical-align: middle; height: 48px; margin-right: 16px; } .asciinema-player-wrapper .title-bar a { color: white; text-decoration: underline; } .asciinema-player-wrapper .title-bar a:hover { text-decoration: none; } .asciinema-player-wrapper:fullscreen { background-color: #000; width: 100%; height: 100%; display: -webkit-flex; display: -ms-flexbox; display: flex; -webkit-justify-content: center; justify-content: center; -webkit-align-items: center; align-items: center; } .asciinema-player-wrapper:fullscreen .asciinema-player { position: static; } .asciinema-player-wrapper:fullscreen .title-bar { display: initial; } .asciinema-player-wrapper:fullscreen.hud .title-bar { top: 0; } .asciinema-player-wrapper:-webkit-full-screen { background-color: #000; width: 100%; height: 100%; display: -webkit-flex; display: -ms-flexbox; display: flex; -webkit-justify-content: center; justify-content: center; -webkit-align-items: center; align-items: center; } .asciinema-player-wrapper:-webkit-full-screen .asciinema-player { position: static; } .asciinema-player-wrapper:-webkit-full-screen .title-bar { display: initial; } .asciinema-player-wrapper:-webkit-full-screen.hud .title-bar { top: 0; } .asciinema-player-wrapper:-moz-full-screen { background-color: #000; width: 100%; height: 100%; display: -webkit-flex; display: -ms-flexbox; display: flex; -webkit-justify-content: center; justify-content: center; -webkit-align-items: center; align-items: center; } .asciinema-player-wrapper:-moz-full-screen .asciinema-player { position: static; } .asciinema-player-wrapper:-moz-full-screen .title-bar { display: initial; } .asciinema-player-wrapper:-moz-full-screen.hud .title-bar { top: 0; } .asciinema-player-wrapper:-ms-fullscreen { background-color: #000; width: 100%; height: 100%; display: -webkit-flex; display: -ms-flexbox; display: flex; -webkit-justify-content: center; justify-content: center; -webkit-align-items: center; align-items: center; } .asciinema-player-wrapper:-ms-fullscreen .asciinema-player { position: static; } .asciinema-player-wrapper:-ms-fullscreen .title-bar { display: initial; } .asciinema-player-wrapper:-ms-fullscreen.hud .title-bar { top: 0; } .asciinema-player-wrapper .asciinema-player { text-align: left; display: inline-block; padding: 0px; position: relative; box-sizing: content-box; -moz-box-sizing: content-box; -webkit-box-sizing: content-box; overflow: hidden; max-width: 100%; } .asciinema-terminal { box-sizing: content-box; -moz-box-sizing: content-box; -webkit-box-sizing: content-box; overflow: hidden; padding: 0; margin: 0px; display: block; white-space: pre; border: 0; word-wrap: normal; word-break: normal; border-radius: 0; border-style: solid; cursor: text; border-width: 0.5em; font-family: Consolas, Menlo, 'Bitstream Vera Sans Mono', monospace, 'Powerline Symbols'; line-height: 1.3333333333em; } .asciinema-terminal .line { letter-spacing: normal; overflow: hidden; height: 1.3333333333em; } .asciinema-terminal .line span { padding: 0; display: inline-block; height: 1.3333333333em; } .asciinema-terminal .line { display: block; width: 200%; } .asciinema-terminal .bright { font-weight: bold; } .asciinema-terminal .underline { text-decoration: underline; } .asciinema-terminal .italic { font-style: italic; } .asciinema-terminal.font-small { font-size: 12px; } .asciinema-terminal.font-medium { font-size: 18px; } .asciinema-terminal.font-big { font-size: 24px; } .asciinema-player .control-bar { width: 100%; height: 32px; background: rgba(0, 0, 0, 0.8); /* no gradient fallback */ background: -moz-linear-gradient(top, rgba(0, 0, 0, 0.5) 0%, #000000 25%, #000000 100%); /* FF3.6-15 */ background: -webkit-linear-gradient(top, rgba(0, 0, 0, 0.5) 0%, #000000 25%, #000000 100%); /* Chrome10-25,Safari5.1-6 */ background: linear-gradient(to bottom, rgba(0, 0, 0, 0.5) 0%, #000000 25%, #000000 100%); /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */ color: #bbbbbb; box-sizing: content-box; line-height: 1; position: absolute; bottom: -35px; left: 0; transition: bottom 0.15s linear; } .asciinema-player .control-bar * { box-sizing: inherit; font-size: 0; } .asciinema-player .control-bar svg.icon path { fill: #bbbbbb; } .asciinema-player .control-bar .playback-button { display: block; float: left; cursor: pointer; height: 12px; width: 12px; padding: 10px; } .asciinema-player .control-bar .playback-button svg { height: 12px; width: 12px; } .asciinema-player .control-bar .timer { display: block; float: left; width: 50px; height: 100%; text-align: center; font-family: Helvetica, Arial, sans-serif; font-size: 11px; font-weight: bold; line-height: 32px; cursor: default; } .asciinema-player .control-bar .timer span { display: inline-block; font-size: inherit; } .asciinema-player .control-bar .timer .time-remaining { display: none; } .asciinema-player .control-bar .timer:hover .time-elapsed { display: none; } .asciinema-player .control-bar .timer:hover .time-remaining { display: inline; } .asciinema-player .control-bar .progressbar { display: block; overflow: hidden; height: 100%; padding: 0 10px; } .asciinema-player .control-bar .progressbar .bar { display: block; cursor: pointer; height: 100%; padding-top: 15px; font-size: 0; } .asciinema-player .control-bar .progressbar .bar .gutter { display: block; height: 3px; background-color: #333; } .asciinema-player .control-bar .progressbar .bar .gutter span { display: inline-block; height: 100%; background-color: #bbbbbb; border-radius: 3px; } .asciinema-player .control-bar.live .progressbar .bar { cursor: default; } .asciinema-player .control-bar .fullscreen-button { display: block; float: right; width: 14px; height: 14px; padding: 9px; cursor: pointer; } .asciinema-player .control-bar .fullscreen-button svg { width: 14px; height: 14px; } .asciinema-player .control-bar .fullscreen-button svg:first-child { display: inline; } .asciinema-player .control-bar .fullscreen-button svg:last-child { display: none; } .asciinema-player-wrapper.hud .control-bar { bottom: 0px; } .asciinema-player-wrapper:fullscreen .fullscreen-button svg:first-child { display: none; } .asciinema-player-wrapper:fullscreen .fullscreen-button svg:last-child { display: inline; } .asciinema-player-wrapper:-webkit-full-screen .fullscreen-button svg:first-child { display: none; } .asciinema-player-wrapper:-webkit-full-screen .fullscreen-button svg:last-child { display: inline; } .asciinema-player-wrapper:-moz-full-screen .fullscreen-button svg:first-child { display: none; } .asciinema-player-wrapper:-moz-full-screen .fullscreen-button svg:last-child { display: inline; } .asciinema-player-wrapper:-ms-fullscreen .fullscreen-button svg:first-child { display: none; } .asciinema-player-wrapper:-ms-fullscreen .fullscreen-button svg:last-child { display: inline; } .asciinema-player .loading { z-index: 10; background-repeat: no-repeat; background-position: center; position: absolute; top: 0; left: 0; right: 0; bottom: 32px; background-color: rgba(0, 0, 0, 0.5); } .asciinema-player .start-prompt { z-index: 10; background-repeat: no-repeat; background-position: center; position: absolute; top: 0; left: 0; right: 0; bottom: 32px; z-index: 20; cursor: pointer; } .asciinema-player .start-prompt .play-button { font-size: 0px; } .asciinema-player .start-prompt .play-button { position: absolute; left: 0; top: 0; right: 0; bottom: 0; text-align: center; color: white; display: table; width: 100%; height: 100%; } .asciinema-player .start-prompt .play-button div { vertical-align: middle; display: table-cell; } .asciinema-player .start-prompt .play-button div span { width: 96px; height: 96px; display: inline-block; } @-webkit-keyframes expand { 0% { -webkit-transform: scale(0); } 50% { -webkit-transform: scale(1); } 100% { z-index: 1; } } @-moz-keyframes expand { 0% { -moz-transform: scale(0); } 50% { -moz-transform: scale(1); } 100% { z-index: 1; } } @-o-keyframes expand { 0% { -o-transform: scale(0); } 50% { -o-transform: scale(1); } 100% { z-index: 1; } } @keyframes expand { 0% { transform: scale(0); } 50% { transform: scale(1); } 100% { z-index: 1; } } .loader { position: absolute; left: 50%; top: 50%; margin: -20px 0 0 -20px; background-color: white; border-radius: 50%; box-shadow: 0 0 0 6.66667px #141414; width: 40px; height: 40px; } .loader:before, .loader:after { content: ""; position: absolute; left: 50%; top: 50%; display: block; margin: -21px 0 0 -21px; border-radius: 50%; z-index: 2; width: 42px; height: 42px; } .loader:before { background-color: #141414; -webkit-animation: expand 1.6s linear infinite both; -moz-animation: expand 1.6s linear infinite both; animation: expand 1.6s linear infinite both; } .loader:after { background-color: white; -webkit-animation: expand 1.6s linear 0.8s infinite both; -moz-animation: expand 1.6s linear 0.8s infinite both; animation: expand 1.6s linear 0.8s infinite both; } .asciinema-terminal .fg-16 { color: #000000; } .asciinema-terminal .bg-16 { background-color: #000000; } .asciinema-terminal .fg-17 { color: #00005f; } .asciinema-terminal .bg-17 { background-color: #00005f; } .asciinema-terminal .fg-18 { color: #000087; } .asciinema-terminal .bg-18 { background-color: #000087; } .asciinema-terminal .fg-19 { color: #0000af; } .asciinema-terminal .bg-19 { background-color: #0000af; } .asciinema-terminal .fg-20 { color: #0000d7; } .asciinema-terminal .bg-20 { background-color: #0000d7; } .asciinema-terminal .fg-21 { color: #0000ff; } .asciinema-terminal .bg-21 { background-color: #0000ff; } .asciinema-terminal .fg-22 { color: #005f00; } .asciinema-terminal .bg-22 { background-color: #005f00; } .asciinema-terminal .fg-23 { color: #005f5f; } .asciinema-terminal .bg-23 { background-color: #005f5f; } .asciinema-terminal .fg-24 { color: #005f87; } .asciinema-terminal .bg-24 { background-color: #005f87; } .asciinema-terminal .fg-25 { color: #005faf; } .asciinema-terminal .bg-25 { background-color: #005faf; } .asciinema-terminal .fg-26 { color: #005fd7; } .asciinema-terminal .bg-26 { background-color: #005fd7; } .asciinema-terminal .fg-27 { color: #005fff; } .asciinema-terminal .bg-27 { background-color: #005fff; } .asciinema-terminal .fg-28 { color: #008700; } .asciinema-terminal .bg-28 { background-color: #008700; } .asciinema-terminal .fg-29 { color: #00875f; } .asciinema-terminal .bg-29 { background-color: #00875f; } .asciinema-terminal .fg-30 { color: #008787; } .asciinema-terminal .bg-30 { background-color: #008787; } .asciinema-terminal .fg-31 { color: #0087af; } .asciinema-terminal .bg-31 { background-color: #0087af; } .asciinema-terminal .fg-32 { color: #0087d7; } .asciinema-terminal .bg-32 { background-color: #0087d7; } .asciinema-terminal .fg-33 { color: #0087ff; } .asciinema-terminal .bg-33 { background-color: #0087ff; } .asciinema-terminal .fg-34 { color: #00af00; } .asciinema-terminal .bg-34 { background-color: #00af00; } .asciinema-terminal .fg-35 { color: #00af5f; } .asciinema-terminal .bg-35 { background-color: #00af5f; } .asciinema-terminal .fg-36 { color: #00af87; } .asciinema-terminal .bg-36 { background-color: #00af87; } .asciinema-terminal .fg-37 { color: #00afaf; } .asciinema-terminal .bg-37 { background-color: #00afaf; } .asciinema-terminal .fg-38 { color: #00afd7; } .asciinema-terminal .bg-38 { background-color: #00afd7; } .asciinema-terminal .fg-39 { color: #00afff; } .asciinema-terminal .bg-39 { background-color: #00afff; } .asciinema-terminal .fg-40 { color: #00d700; } .asciinema-terminal .bg-40 { background-color: #00d700; } .asciinema-terminal .fg-41 { color: #00d75f; } .asciinema-terminal .bg-41 { background-color: #00d75f; } .asciinema-terminal .fg-42 { color: #00d787; } .asciinema-terminal .bg-42 { background-color: #00d787; } .asciinema-terminal .fg-43 { color: #00d7af; } .asciinema-terminal .bg-43 { background-color: #00d7af; } .asciinema-terminal .fg-44 { color: #00d7d7; } .asciinema-terminal .bg-44 { background-color: #00d7d7; } .asciinema-terminal .fg-45 { color: #00d7ff; } .asciinema-terminal .bg-45 { background-color: #00d7ff; } .asciinema-terminal .fg-46 { color: #00ff00; } .asciinema-terminal .bg-46 { background-color: #00ff00; } .asciinema-terminal .fg-47 { color: #00ff5f; } .asciinema-terminal .bg-47 { background-color: #00ff5f; } .asciinema-terminal .fg-48 { color: #00ff87; } .asciinema-terminal .bg-48 { background-color: #00ff87; } .asciinema-terminal .fg-49 { color: #00ffaf; } .asciinema-terminal .bg-49 { background-color: #00ffaf; } .asciinema-terminal .fg-50 { color: #00ffd7; } .asciinema-terminal .bg-50 { background-color: #00ffd7; } .asciinema-terminal .fg-51 { color: #00ffff; } .asciinema-terminal .bg-51 { background-color: #00ffff; } .asciinema-terminal .fg-52 { color: #5f0000; } .asciinema-terminal .bg-52 { background-color: #5f0000; } .asciinema-terminal .fg-53 { color: #5f005f; } .asciinema-terminal .bg-53 { background-color: #5f005f; } .asciinema-terminal .fg-54 { color: #5f0087; } .asciinema-terminal .bg-54 { background-color: #5f0087; } .asciinema-terminal .fg-55 { color: #5f00af; } .asciinema-terminal .bg-55 { background-color: #5f00af; } .asciinema-terminal .fg-56 { color: #5f00d7; } .asciinema-terminal .bg-56 { background-color: #5f00d7; } .asciinema-terminal .fg-57 { color: #5f00ff; } .asciinema-terminal .bg-57 { background-color: #5f00ff; } .asciinema-terminal .fg-58 { color: #5f5f00; } .asciinema-terminal .bg-58 { background-color: #5f5f00; } .asciinema-terminal .fg-59 { color: #5f5f5f; } .asciinema-terminal .bg-59 { background-color: #5f5f5f; } .asciinema-terminal .fg-60 { color: #5f5f87; } .asciinema-terminal .bg-60 { background-color: #5f5f87; } .asciinema-terminal .fg-61 { color: #5f5faf; } .asciinema-terminal .bg-61 { background-color: #5f5faf; } .asciinema-terminal .fg-62 { color: #5f5fd7; } .asciinema-terminal .bg-62 { background-color: #5f5fd7; } .asciinema-terminal .fg-63 { color: #5f5fff; } .asciinema-terminal .bg-63 { background-color: #5f5fff; } .asciinema-terminal .fg-64 { color: #5f8700; } .asciinema-terminal .bg-64 { background-color: #5f8700; } .asciinema-terminal .fg-65 { color: #5f875f; } .asciinema-terminal .bg-65 { background-color: #5f875f; } .asciinema-terminal .fg-66 { color: #5f8787; } .asciinema-terminal .bg-66 { background-color: #5f8787; } .asciinema-terminal .fg-67 { color: #5f87af; } .asciinema-terminal .bg-67 { background-color: #5f87af; } .asciinema-terminal .fg-68 { color: #5f87d7; } .asciinema-terminal .bg-68 { background-color: #5f87d7; } .asciinema-terminal .fg-69 { color: #5f87ff; } .asciinema-terminal .bg-69 { background-color: #5f87ff; } .asciinema-terminal .fg-70 { color: #5faf00; } .asciinema-terminal .bg-70 { background-color: #5faf00; } .asciinema-terminal .fg-71 { color: #5faf5f; } .asciinema-terminal .bg-71 { background-color: #5faf5f; } .asciinema-terminal .fg-72 { color: #5faf87; } .asciinema-terminal .bg-72 { background-color: #5faf87; } .asciinema-terminal .fg-73 { color: #5fafaf; } .asciinema-terminal .bg-73 { background-color: #5fafaf; } .asciinema-terminal .fg-74 { color: #5fafd7; } .asciinema-terminal .bg-74 { background-color: #5fafd7; } .asciinema-terminal .fg-75 { color: #5fafff; } .asciinema-terminal .bg-75 { background-color: #5fafff; } .asciinema-terminal .fg-76 { color: #5fd700; } .asciinema-terminal .bg-76 { background-color: #5fd700; } .asciinema-terminal .fg-77 { color: #5fd75f; } .asciinema-terminal .bg-77 { background-color: #5fd75f; } .asciinema-terminal .fg-78 { color: #5fd787; } .asciinema-terminal .bg-78 { background-color: #5fd787; } .asciinema-terminal .fg-79 { color: #5fd7af; } .asciinema-terminal .bg-79 { background-color: #5fd7af; } .asciinema-terminal .fg-80 { color: #5fd7d7; } .asciinema-terminal .bg-80 { background-color: #5fd7d7; } .asciinema-terminal .fg-81 { color: #5fd7ff; } .asciinema-terminal .bg-81 { background-color: #5fd7ff; } .asciinema-terminal .fg-82 { color: #5fff00; } .asciinema-terminal .bg-82 { background-color: #5fff00; } .asciinema-terminal .fg-83 { color: #5fff5f; } .asciinema-terminal .bg-83 { background-color: #5fff5f; } .asciinema-terminal .fg-84 { color: #5fff87; } .asciinema-terminal .bg-84 { background-color: #5fff87; } .asciinema-terminal .fg-85 { color: #5fffaf; } .asciinema-terminal .bg-85 { background-color: #5fffaf; } .asciinema-terminal .fg-86 { color: #5fffd7; } .asciinema-terminal .bg-86 { background-color: #5fffd7; } .asciinema-terminal .fg-87 { color: #5fffff; } .asciinema-terminal .bg-87 { background-color: #5fffff; } .asciinema-terminal .fg-88 { color: #870000; } .asciinema-terminal .bg-88 { background-color: #870000; } .asciinema-terminal .fg-89 { color: #87005f; } .asciinema-terminal .bg-89 { background-color: #87005f; } .asciinema-terminal .fg-90 { color: #870087; } .asciinema-terminal .bg-90 { background-color: #870087; } .asciinema-terminal .fg-91 { color: #8700af; } .asciinema-terminal .bg-91 { background-color: #8700af; } .asciinema-terminal .fg-92 { color: #8700d7; } .asciinema-terminal .bg-92 { background-color: #8700d7; } .asciinema-terminal .fg-93 { color: #8700ff; } .asciinema-terminal .bg-93 { background-color: #8700ff; } .asciinema-terminal .fg-94 { color: #875f00; } .asciinema-terminal .bg-94 { background-color: #875f00; } .asciinema-terminal .fg-95 { color: #875f5f; } .asciinema-terminal .bg-95 { background-color: #875f5f; } .asciinema-terminal .fg-96 { color: #875f87; } .asciinema-terminal .bg-96 { background-color: #875f87; } .asciinema-terminal .fg-97 { color: #875faf; } .asciinema-terminal .bg-97 { background-color: #875faf; } .asciinema-terminal .fg-98 { color: #875fd7; } .asciinema-terminal .bg-98 { background-color: #875fd7; } .asciinema-terminal .fg-99 { color: #875fff; } .asciinema-terminal .bg-99 { background-color: #875fff; } .asciinema-terminal .fg-100 { color: #878700; } .asciinema-terminal .bg-100 { background-color: #878700; } .asciinema-terminal .fg-101 { color: #87875f; } .asciinema-terminal .bg-101 { background-color: #87875f; } .asciinema-terminal .fg-102 { color: #878787; } .asciinema-terminal .bg-102 { background-color: #878787; } .asciinema-terminal .fg-103 { color: #8787af; } .asciinema-terminal .bg-103 { background-color: #8787af; } .asciinema-terminal .fg-104 { color: #8787d7; } .asciinema-terminal .bg-104 { background-color: #8787d7; } .asciinema-terminal .fg-105 { color: #8787ff; } .asciinema-terminal .bg-105 { background-color: #8787ff; } .asciinema-terminal .fg-106 { color: #87af00; } .asciinema-terminal .bg-106 { background-color: #87af00; } .asciinema-terminal .fg-107 { color: #87af5f; } .asciinema-terminal .bg-107 { background-color: #87af5f; } .asciinema-terminal .fg-108 { color: #87af87; } .asciinema-terminal .bg-108 { background-color: #87af87; } .asciinema-terminal .fg-109 { color: #87afaf; } .asciinema-terminal .bg-109 { background-color: #87afaf; } .asciinema-terminal .fg-110 { color: #87afd7; } .asciinema-terminal .bg-110 { background-color: #87afd7; } .asciinema-terminal .fg-111 { color: #87afff; } .asciinema-terminal .bg-111 { background-color: #87afff; } .asciinema-terminal .fg-112 { color: #87d700; } .asciinema-terminal .bg-112 { background-color: #87d700; } .asciinema-terminal .fg-113 { color: #87d75f; } .asciinema-terminal .bg-113 { background-color: #87d75f; } .asciinema-terminal .fg-114 { color: #87d787; } .asciinema-terminal .bg-114 { background-color: #87d787; } .asciinema-terminal .fg-115 { color: #87d7af; } .asciinema-terminal .bg-115 { background-color: #87d7af; } .asciinema-terminal .fg-116 { color: #87d7d7; } .asciinema-terminal .bg-116 { background-color: #87d7d7; } .asciinema-terminal .fg-117 { color: #87d7ff; } .asciinema-terminal .bg-117 { background-color: #87d7ff; } .asciinema-terminal .fg-118 { color: #87ff00; } .asciinema-terminal .bg-118 { background-color: #87ff00; } .asciinema-terminal .fg-119 { color: #87ff5f; } .asciinema-terminal .bg-119 { background-color: #87ff5f; } .asciinema-terminal .fg-120 { color: #87ff87; } .asciinema-terminal .bg-120 { background-color: #87ff87; } .asciinema-terminal .fg-121 { color: #87ffaf; } .asciinema-terminal .bg-121 { background-color: #87ffaf; } .asciinema-terminal .fg-122 { color: #87ffd7; } .asciinema-terminal .bg-122 { background-color: #87ffd7; } .asciinema-terminal .fg-123 { color: #87ffff; } .asciinema-terminal .bg-123 { background-color: #87ffff; } .asciinema-terminal .fg-124 { color: #af0000; } .asciinema-terminal .bg-124 { background-color: #af0000; } .asciinema-terminal .fg-125 { color: #af005f; } .asciinema-terminal .bg-125 { background-color: #af005f; } .asciinema-terminal .fg-126 { color: #af0087; } .asciinema-terminal .bg-126 { background-color: #af0087; } .asciinema-terminal .fg-127 { color: #af00af; } .asciinema-terminal .bg-127 { background-color: #af00af; } .asciinema-terminal .fg-128 { color: #af00d7; } .asciinema-terminal .bg-128 { background-color: #af00d7; } .asciinema-terminal .fg-129 { color: #af00ff; } .asciinema-terminal .bg-129 { background-color: #af00ff; } .asciinema-terminal .fg-130 { color: #af5f00; } .asciinema-terminal .bg-130 { background-color: #af5f00; } .asciinema-terminal .fg-131 { color: #af5f5f; } .asciinema-terminal .bg-131 { background-color: #af5f5f; } .asciinema-terminal .fg-132 { color: #af5f87; } .asciinema-terminal .bg-132 { background-color: #af5f87; } .asciinema-terminal .fg-133 { color: #af5faf; } .asciinema-terminal .bg-133 { background-color: #af5faf; } .asciinema-terminal .fg-134 { color: #af5fd7; } .asciinema-terminal .bg-134 { background-color: #af5fd7; } .asciinema-terminal .fg-135 { color: #af5fff; } .asciinema-terminal .bg-135 { background-color: #af5fff; } .asciinema-terminal .fg-136 { color: #af8700; } .asciinema-terminal .bg-136 { background-color: #af8700; } .asciinema-terminal .fg-137 { color: #af875f; } .asciinema-terminal .bg-137 { background-color: #af875f; } .asciinema-terminal .fg-138 { color: #af8787; } .asciinema-terminal .bg-138 { background-color: #af8787; } .asciinema-terminal .fg-139 { color: #af87af; } .asciinema-terminal .bg-139 { background-color: #af87af; } .asciinema-terminal .fg-140 { color: #af87d7; } .asciinema-terminal .bg-140 { background-color: #af87d7; } .asciinema-terminal .fg-141 { color: #af87ff; } .asciinema-terminal .bg-141 { background-color: #af87ff; } .asciinema-terminal .fg-142 { color: #afaf00; } .asciinema-terminal .bg-142 { background-color: #afaf00; } .asciinema-terminal .fg-143 { color: #afaf5f; } .asciinema-terminal .bg-143 { background-color: #afaf5f; } .asciinema-terminal .fg-144 { color: #afaf87; } .asciinema-terminal .bg-144 { background-color: #afaf87; } .asciinema-terminal .fg-145 { color: #afafaf; } .asciinema-terminal .bg-145 { background-color: #afafaf; } .asciinema-terminal .fg-146 { color: #afafd7; } .asciinema-terminal .bg-146 { background-color: #afafd7; } .asciinema-terminal .fg-147 { color: #afafff; } .asciinema-terminal .bg-147 { background-color: #afafff; } .asciinema-terminal .fg-148 { color: #afd700; } .asciinema-terminal .bg-148 { background-color: #afd700; } .asciinema-terminal .fg-149 { color: #afd75f; } .asciinema-terminal .bg-149 { background-color: #afd75f; } .asciinema-terminal .fg-150 { color: #afd787; } .asciinema-terminal .bg-150 { background-color: #afd787; } .asciinema-terminal .fg-151 { color: #afd7af; } .asciinema-terminal .bg-151 { background-color: #afd7af; } .asciinema-terminal .fg-152 { color: #afd7d7; } .asciinema-terminal .bg-152 { background-color: #afd7d7; } .asciinema-terminal .fg-153 { color: #afd7ff; } .asciinema-terminal .bg-153 { background-color: #afd7ff; } .asciinema-terminal .fg-154 { color: #afff00; } .asciinema-terminal .bg-154 { background-color: #afff00; } .asciinema-terminal .fg-155 { color: #afff5f; } .asciinema-terminal .bg-155 { background-color: #afff5f; } .asciinema-terminal .fg-156 { color: #afff87; } .asciinema-terminal .bg-156 { background-color: #afff87; } .asciinema-terminal .fg-157 { color: #afffaf; } .asciinema-terminal .bg-157 { background-color: #afffaf; } .asciinema-terminal .fg-158 { color: #afffd7; } .asciinema-terminal .bg-158 { background-color: #afffd7; } .asciinema-terminal .fg-159 { color: #afffff; } .asciinema-terminal .bg-159 { background-color: #afffff; } .asciinema-terminal .fg-160 { color: #d70000; } .asciinema-terminal .bg-160 { background-color: #d70000; } .asciinema-terminal .fg-161 { color: #d7005f; } .asciinema-terminal .bg-161 { background-color: #d7005f; } .asciinema-terminal .fg-162 { color: #d70087; } .asciinema-terminal .bg-162 { background-color: #d70087; } .asciinema-terminal .fg-163 { color: #d700af; } .asciinema-terminal .bg-163 { background-color: #d700af; } .asciinema-terminal .fg-164 { color: #d700d7; } .asciinema-terminal .bg-164 { background-color: #d700d7; } .asciinema-terminal .fg-165 { color: #d700ff; } .asciinema-terminal .bg-165 { background-color: #d700ff; } .asciinema-terminal .fg-166 { color: #d75f00; } .asciinema-terminal .bg-166 { background-color: #d75f00; } .asciinema-terminal .fg-167 { color: #d75f5f; } .asciinema-terminal .bg-167 { background-color: #d75f5f; } .asciinema-terminal .fg-168 { color: #d75f87; } .asciinema-terminal .bg-168 { background-color: #d75f87; } .asciinema-terminal .fg-169 { color: #d75faf; } .asciinema-terminal .bg-169 { background-color: #d75faf; } .asciinema-terminal .fg-170 { color: #d75fd7; } .asciinema-terminal .bg-170 { background-color: #d75fd7; } .asciinema-terminal .fg-171 { color: #d75fff; } .asciinema-terminal .bg-171 { background-color: #d75fff; } .asciinema-terminal .fg-172 { color: #d78700; } .asciinema-terminal .bg-172 { background-color: #d78700; } .asciinema-terminal .fg-173 { color: #d7875f; } .asciinema-terminal .bg-173 { background-color: #d7875f; } .asciinema-terminal .fg-174 { color: #d78787; } .asciinema-terminal .bg-174 { background-color: #d78787; } .asciinema-terminal .fg-175 { color: #d787af; } .asciinema-terminal .bg-175 { background-color: #d787af; } .asciinema-terminal .fg-176 { color: #d787d7; } .asciinema-terminal .bg-176 { background-color: #d787d7; } .asciinema-terminal .fg-177 { color: #d787ff; } .asciinema-terminal .bg-177 { background-color: #d787ff; } .asciinema-terminal .fg-178 { color: #d7af00; } .asciinema-terminal .bg-178 { background-color: #d7af00; } .asciinema-terminal .fg-179 { color: #d7af5f; } .asciinema-terminal .bg-179 { background-color: #d7af5f; } .asciinema-terminal .fg-180 { color: #d7af87; } .asciinema-terminal .bg-180 { background-color: #d7af87; } .asciinema-terminal .fg-181 { color: #d7afaf; } .asciinema-terminal .bg-181 { background-color: #d7afaf; } .asciinema-terminal .fg-182 { color: #d7afd7; } .asciinema-terminal .bg-182 { background-color: #d7afd7; } .asciinema-terminal .fg-183 { color: #d7afff; } .asciinema-terminal .bg-183 { background-color: #d7afff; } .asciinema-terminal .fg-184 { color: #d7d700; } .asciinema-terminal .bg-184 { background-color: #d7d700; } .asciinema-terminal .fg-185 { color: #d7d75f; } .asciinema-terminal .bg-185 { background-color: #d7d75f; } .asciinema-terminal .fg-186 { color: #d7d787; } .asciinema-terminal .bg-186 { background-color: #d7d787; } .asciinema-terminal .fg-187 { color: #d7d7af; } .asciinema-terminal .bg-187 { background-color: #d7d7af; } .asciinema-terminal .fg-188 { color: #d7d7d7; } .asciinema-terminal .bg-188 { background-color: #d7d7d7; } .asciinema-terminal .fg-189 { color: #d7d7ff; } .asciinema-terminal .bg-189 { background-color: #d7d7ff; } .asciinema-terminal .fg-190 { color: #d7ff00; } .asciinema-terminal .bg-190 { background-color: #d7ff00; } .asciinema-terminal .fg-191 { color: #d7ff5f; } .asciinema-terminal .bg-191 { background-color: #d7ff5f; } .asciinema-terminal .fg-192 { color: #d7ff87; } .asciinema-terminal .bg-192 { background-color: #d7ff87; } .asciinema-terminal .fg-193 { color: #d7ffaf; } .asciinema-terminal .bg-193 { background-color: #d7ffaf; } .asciinema-terminal .fg-194 { color: #d7ffd7; } .asciinema-terminal .bg-194 { background-color: #d7ffd7; } .asciinema-terminal .fg-195 { color: #d7ffff; } .asciinema-terminal .bg-195 { background-color: #d7ffff; } .asciinema-terminal .fg-196 { color: #ff0000; } .asciinema-terminal .bg-196 { background-color: #ff0000; } .asciinema-terminal .fg-197 { color: #ff005f; } .asciinema-terminal .bg-197 { background-color: #ff005f; } .asciinema-terminal .fg-198 { color: #ff0087; } .asciinema-terminal .bg-198 { background-color: #ff0087; } .asciinema-terminal .fg-199 { color: #ff00af; } .asciinema-terminal .bg-199 { background-color: #ff00af; } .asciinema-terminal .fg-200 { color: #ff00d7; } .asciinema-terminal .bg-200 { background-color: #ff00d7; } .asciinema-terminal .fg-201 { color: #ff00ff; } .asciinema-terminal .bg-201 { background-color: #ff00ff; } .asciinema-terminal .fg-202 { color: #ff5f00; } .asciinema-terminal .bg-202 { background-color: #ff5f00; } .asciinema-terminal .fg-203 { color: #ff5f5f; } .asciinema-terminal .bg-203 { background-color: #ff5f5f; } .asciinema-terminal .fg-204 { color: #ff5f87; } .asciinema-terminal .bg-204 { background-color: #ff5f87; } .asciinema-terminal .fg-205 { color: #ff5faf; } .asciinema-terminal .bg-205 { background-color: #ff5faf; } .asciinema-terminal .fg-206 { color: #ff5fd7; } .asciinema-terminal .bg-206 { background-color: #ff5fd7; } .asciinema-terminal .fg-207 { color: #ff5fff; } .asciinema-terminal .bg-207 { background-color: #ff5fff; } .asciinema-terminal .fg-208 { color: #ff8700; } .asciinema-terminal .bg-208 { background-color: #ff8700; } .asciinema-terminal .fg-209 { color: #ff875f; } .asciinema-terminal .bg-209 { background-color: #ff875f; } .asciinema-terminal .fg-210 { color: #ff8787; } .asciinema-terminal .bg-210 { background-color: #ff8787; } .asciinema-terminal .fg-211 { color: #ff87af; } .asciinema-terminal .bg-211 { background-color: #ff87af; } .asciinema-terminal .fg-212 { color: #ff87d7; } .asciinema-terminal .bg-212 { background-color: #ff87d7; } .asciinema-terminal .fg-213 { color: #ff87ff; } .asciinema-terminal .bg-213 { background-color: #ff87ff; } .asciinema-terminal .fg-214 { color: #ffaf00; } .asciinema-terminal .bg-214 { background-color: #ffaf00; } .asciinema-terminal .fg-215 { color: #ffaf5f; } .asciinema-terminal .bg-215 { background-color: #ffaf5f; } .asciinema-terminal .fg-216 { color: #ffaf87; } .asciinema-terminal .bg-216 { background-color: #ffaf87; } .asciinema-terminal .fg-217 { color: #ffafaf; } .asciinema-terminal .bg-217 { background-color: #ffafaf; } .asciinema-terminal .fg-218 { color: #ffafd7; } .asciinema-terminal .bg-218 { background-color: #ffafd7; } .asciinema-terminal .fg-219 { color: #ffafff; } .asciinema-terminal .bg-219 { background-color: #ffafff; } .asciinema-terminal .fg-220 { color: #ffd700; } .asciinema-terminal .bg-220 { background-color: #ffd700; } .asciinema-terminal .fg-221 { color: #ffd75f; } .asciinema-terminal .bg-221 { background-color: #ffd75f; } .asciinema-terminal .fg-222 { color: #ffd787; } .asciinema-terminal .bg-222 { background-color: #ffd787; } .asciinema-terminal .fg-223 { color: #ffd7af; } .asciinema-terminal .bg-223 { background-color: #ffd7af; } .asciinema-terminal .fg-224 { color: #ffd7d7; } .asciinema-terminal .bg-224 { background-color: #ffd7d7; } .asciinema-terminal .fg-225 { color: #ffd7ff; } .asciinema-terminal .bg-225 { background-color: #ffd7ff; } .asciinema-terminal .fg-226 { color: #ffff00; } .asciinema-terminal .bg-226 { background-color: #ffff00; } .asciinema-terminal .fg-227 { color: #ffff5f; } .asciinema-terminal .bg-227 { background-color: #ffff5f; } .asciinema-terminal .fg-228 { color: #ffff87; } .asciinema-terminal .bg-228 { background-color: #ffff87; } .asciinema-terminal .fg-229 { color: #ffffaf; } .asciinema-terminal .bg-229 { background-color: #ffffaf; } .asciinema-terminal .fg-230 { color: #ffffd7; } .asciinema-terminal .bg-230 { background-color: #ffffd7; } .asciinema-terminal .fg-231 { color: #ffffff; } .asciinema-terminal .bg-231 { background-color: #ffffff; } .asciinema-terminal .fg-232 { color: #080808; } .asciinema-terminal .bg-232 { background-color: #080808; } .asciinema-terminal .fg-233 { color: #121212; } .asciinema-terminal .bg-233 { background-color: #121212; } .asciinema-terminal .fg-234 { color: #1c1c1c; } .asciinema-terminal .bg-234 { background-color: #1c1c1c; } .asciinema-terminal .fg-235 { color: #262626; } .asciinema-terminal .bg-235 { background-color: #262626; } .asciinema-terminal .fg-236 { color: #303030; } .asciinema-terminal .bg-236 { background-color: #303030; } .asciinema-terminal .fg-237 { color: #3a3a3a; } .asciinema-terminal .bg-237 { background-color: #3a3a3a; } .asciinema-terminal .fg-238 { color: #444444; } .asciinema-terminal .bg-238 { background-color: #444444; } .asciinema-terminal .fg-239 { color: #4e4e4e; } .asciinema-terminal .bg-239 { background-color: #4e4e4e; } .asciinema-terminal .fg-240 { color: #585858; } .asciinema-terminal .bg-240 { background-color: #585858; } .asciinema-terminal .fg-241 { color: #626262; } .asciinema-terminal .bg-241 { background-color: #626262; } .asciinema-terminal .fg-242 { color: #6c6c6c; } .asciinema-terminal .bg-242 { background-color: #6c6c6c; } .asciinema-terminal .fg-243 { color: #767676; } .asciinema-terminal .bg-243 { background-color: #767676; } .asciinema-terminal .fg-244 { color: #808080; } .asciinema-terminal .bg-244 { background-color: #808080; } .asciinema-terminal .fg-245 { color: #8a8a8a; } .asciinema-terminal .bg-245 { background-color: #8a8a8a; } .asciinema-terminal .fg-246 { color: #949494; } .asciinema-terminal .bg-246 { background-color: #949494; } .asciinema-terminal .fg-247 { color: #9e9e9e; } .asciinema-terminal .bg-247 { background-color: #9e9e9e; } .asciinema-terminal .fg-248 { color: #a8a8a8; } .asciinema-terminal .bg-248 { background-color: #a8a8a8; } .asciinema-terminal .fg-249 { color: #b2b2b2; } .asciinema-terminal .bg-249 { background-color: #b2b2b2; } .asciinema-terminal .fg-250 { color: #bcbcbc; } .asciinema-terminal .bg-250 { background-color: #bcbcbc; } .asciinema-terminal .fg-251 { color: #c6c6c6; } .asciinema-terminal .bg-251 { background-color: #c6c6c6; } .asciinema-terminal .fg-252 { color: #d0d0d0; } .asciinema-terminal .bg-252 { background-color: #d0d0d0; } .asciinema-terminal .fg-253 { color: #dadada; } .asciinema-terminal .bg-253 { background-color: #dadada; } .asciinema-terminal .fg-254 { color: #e4e4e4; } .asciinema-terminal .bg-254 { background-color: #e4e4e4; } .asciinema-terminal .fg-255 { color: #eeeeee; } .asciinema-terminal .bg-255 { background-color: #eeeeee; } .asciinema-theme-asciinema .asciinema-terminal { color: #cccccc; background-color: #121314; border-color: #121314; } .asciinema-theme-asciinema .fg-bg { color: #121314; } .asciinema-theme-asciinema .bg-fg { background-color: #cccccc; } .asciinema-theme-asciinema .fg-0 { color: #000000; } .asciinema-theme-asciinema .bg-0 { background-color: #000000; } .asciinema-theme-asciinema .fg-1 { color: #dd3c69; } .asciinema-theme-asciinema .bg-1 { background-color: #dd3c69; } .asciinema-theme-asciinema .fg-2 { color: #4ebf22; } .asciinema-theme-asciinema .bg-2 { background-color: #4ebf22; } .asciinema-theme-asciinema .fg-3 { color: #ddaf3c; } .asciinema-theme-asciinema .bg-3 { background-color: #ddaf3c; } .asciinema-theme-asciinema .fg-4 { color: #26b0d7; } .asciinema-theme-asciinema .bg-4 { background-color: #26b0d7; } .asciinema-theme-asciinema .fg-5 { color: #b954e1; } .asciinema-theme-asciinema .bg-5 { background-color: #b954e1; } .asciinema-theme-asciinema .fg-6 { color: #54e1b9; } .asciinema-theme-asciinema .bg-6 { background-color: #54e1b9; } .asciinema-theme-asciinema .fg-7 { color: #d9d9d9; } .asciinema-theme-asciinema .bg-7 { background-color: #d9d9d9; } .asciinema-theme-asciinema .fg-8 { color: #4d4d4d; } .asciinema-theme-asciinema .bg-8 { background-color: #4d4d4d; } .asciinema-theme-asciinema .fg-9 { color: #dd3c69; } .asciinema-theme-asciinema .bg-9 { background-color: #dd3c69; } .asciinema-theme-asciinema .fg-10 { color: #4ebf22; } .asciinema-theme-asciinema .bg-10 { background-color: #4ebf22; } .asciinema-theme-asciinema .fg-11 { color: #ddaf3c; } .asciinema-theme-asciinema .bg-11 { background-color: #ddaf3c; } .asciinema-theme-asciinema .fg-12 { color: #26b0d7; } .asciinema-theme-asciinema .bg-12 { background-color: #26b0d7; } .asciinema-theme-asciinema .fg-13 { color: #b954e1; } .asciinema-theme-asciinema .bg-13 { background-color: #b954e1; } .asciinema-theme-asciinema .fg-14 { color: #54e1b9; } .asciinema-theme-asciinema .bg-14 { background-color: #54e1b9; } .asciinema-theme-asciinema .fg-15 { color: #ffffff; } .asciinema-theme-asciinema .bg-15 { background-color: #ffffff; } .asciinema-theme-asciinema .fg-8, .asciinema-theme-asciinema .fg-9, .asciinema-theme-asciinema .fg-10, .asciinema-theme-asciinema .fg-11, .asciinema-theme-asciinema .fg-12, .asciinema-theme-asciinema .fg-13, .asciinema-theme-asciinema .fg-14, .asciinema-theme-asciinema .fg-15 { font-weight: bold; } .asciinema-theme-tango .asciinema-terminal { color: #cccccc; background-color: #121314; border-color: #121314; } .asciinema-theme-tango .fg-bg { color: #121314; } .asciinema-theme-tango .bg-fg { background-color: #cccccc; } .asciinema-theme-tango .fg-0 { color: #000000; } .asciinema-theme-tango .bg-0 { background-color: #000000; } .asciinema-theme-tango .fg-1 { color: #cc0000; } .asciinema-theme-tango .bg-1 { background-color: #cc0000; } .asciinema-theme-tango .fg-2 { color: #4e9a06; } .asciinema-theme-tango .bg-2 { background-color: #4e9a06; } .asciinema-theme-tango .fg-3 { color: #c4a000; } .asciinema-theme-tango .bg-3 { background-color: #c4a000; } .asciinema-theme-tango .fg-4 { color: #3465a4; } .asciinema-theme-tango .bg-4 { background-color: #3465a4; } .asciinema-theme-tango .fg-5 { color: #75507b; } .asciinema-theme-tango .bg-5 { background-color: #75507b; } .asciinema-theme-tango .fg-6 { color: #06989a; } .asciinema-theme-tango .bg-6 { background-color: #06989a; } .asciinema-theme-tango .fg-7 { color: #d3d7cf; } .asciinema-theme-tango .bg-7 { background-color: #d3d7cf; } .asciinema-theme-tango .fg-8 { color: #555753; } .asciinema-theme-tango .bg-8 { background-color: #555753; } .asciinema-theme-tango .fg-9 { color: #ef2929; } .asciinema-theme-tango .bg-9 { background-color: #ef2929; } .asciinema-theme-tango .fg-10 { color: #8ae234; } .asciinema-theme-tango .bg-10 { background-color: #8ae234; } .asciinema-theme-tango .fg-11 { color: #fce94f; } .asciinema-theme-tango .bg-11 { background-color: #fce94f; } .asciinema-theme-tango .fg-12 { color: #729fcf; } .asciinema-theme-tango .bg-12 { background-color: #729fcf; } .asciinema-theme-tango .fg-13 { color: #ad7fa8; } .asciinema-theme-tango .bg-13 { background-color: #ad7fa8; } .asciinema-theme-tango .fg-14 { color: #34e2e2; } .asciinema-theme-tango .bg-14 { background-color: #34e2e2; } .asciinema-theme-tango .fg-15 { color: #eeeeec; } .asciinema-theme-tango .bg-15 { background-color: #eeeeec; } .asciinema-theme-tango .fg-8, .asciinema-theme-tango .fg-9, .asciinema-theme-tango .fg-10, .asciinema-theme-tango .fg-11, .asciinema-theme-tango .fg-12, .asciinema-theme-tango .fg-13, .asciinema-theme-tango .fg-14, .asciinema-theme-tango .fg-15 { font-weight: bold; } .asciinema-theme-solarized-dark .asciinema-terminal { color: #839496; background-color: #002b36; border-color: #002b36; } .asciinema-theme-solarized-dark .fg-bg { color: #002b36; } .asciinema-theme-solarized-dark .bg-fg { background-color: #839496; } .asciinema-theme-solarized-dark .fg-0 { color: #073642; } .asciinema-theme-solarized-dark .bg-0 { background-color: #073642; } .asciinema-theme-solarized-dark .fg-1 { color: #dc322f; } .asciinema-theme-solarized-dark .bg-1 { background-color: #dc322f; } .asciinema-theme-solarized-dark .fg-2 { color: #859900; } .asciinema-theme-solarized-dark .bg-2 { background-color: #859900; } .asciinema-theme-solarized-dark .fg-3 { color: #b58900; } .asciinema-theme-solarized-dark .bg-3 { background-color: #b58900; } .asciinema-theme-solarized-dark .fg-4 { color: #268bd2; } .asciinema-theme-solarized-dark .bg-4 { background-color: #268bd2; } .asciinema-theme-solarized-dark .fg-5 { color: #d33682; } .asciinema-theme-solarized-dark .bg-5 { background-color: #d33682; } .asciinema-theme-solarized-dark .fg-6 { color: #2aa198; } .asciinema-theme-solarized-dark .bg-6 { background-color: #2aa198; } .asciinema-theme-solarized-dark .fg-7 { color: #eee8d5; } .asciinema-theme-solarized-dark .bg-7 { background-color: #eee8d5; } .asciinema-theme-solarized-dark .fg-8 { color: #002b36; } .asciinema-theme-solarized-dark .bg-8 { background-color: #002b36; } .asciinema-theme-solarized-dark .fg-9 { color: #cb4b16; } .asciinema-theme-solarized-dark .bg-9 { background-color: #cb4b16; } .asciinema-theme-solarized-dark .fg-10 { color: #586e75; } .asciinema-theme-solarized-dark .bg-10 { background-color: #586e75; } .asciinema-theme-solarized-dark .fg-11 { color: #657b83; } .asciinema-theme-solarized-dark .bg-11 { background-color: #657b83; } .asciinema-theme-solarized-dark .fg-12 { color: #839496; } .asciinema-theme-solarized-dark .bg-12 { background-color: #839496; } .asciinema-theme-solarized-dark .fg-13 { color: #6c71c4; } .asciinema-theme-solarized-dark .bg-13 { background-color: #6c71c4; } .asciinema-theme-solarized-dark .fg-14 { color: #93a1a1; } .asciinema-theme-solarized-dark .bg-14 { background-color: #93a1a1; } .asciinema-theme-solarized-dark .fg-15 { color: #fdf6e3; } .asciinema-theme-solarized-dark .bg-15 { background-color: #fdf6e3; } .asciinema-theme-solarized-light .asciinema-terminal { color: #657b83; background-color: #fdf6e3; border-color: #fdf6e3; } .asciinema-theme-solarized-light .fg-bg { color: #fdf6e3; } .asciinema-theme-solarized-light .bg-fg { background-color: #657b83; } .asciinema-theme-solarized-light .fg-0 { color: #073642; } .asciinema-theme-solarized-light .bg-0 { background-color: #073642; } .asciinema-theme-solarized-light .fg-1 { color: #dc322f; } .asciinema-theme-solarized-light .bg-1 { background-color: #dc322f; } .asciinema-theme-solarized-light .fg-2 { color: #859900; } .asciinema-theme-solarized-light .bg-2 { background-color: #859900; } .asciinema-theme-solarized-light .fg-3 { color: #b58900; } .asciinema-theme-solarized-light .bg-3 { background-color: #b58900; } .asciinema-theme-solarized-light .fg-4 { color: #268bd2; } .asciinema-theme-solarized-light .bg-4 { background-color: #268bd2; } .asciinema-theme-solarized-light .fg-5 { color: #d33682; } .asciinema-theme-solarized-light .bg-5 { background-color: #d33682; } .asciinema-theme-solarized-light .fg-6 { color: #2aa198; } .asciinema-theme-solarized-light .bg-6 { background-color: #2aa198; } .asciinema-theme-solarized-light .fg-7 { color: #eee8d5; } .asciinema-theme-solarized-light .bg-7 { background-color: #eee8d5; } .asciinema-theme-solarized-light .fg-8 { color: #002b36; } .asciinema-theme-solarized-light .bg-8 { background-color: #002b36; } .asciinema-theme-solarized-light .fg-9 { color: #cb4b16; } .asciinema-theme-solarized-light .bg-9 { background-color: #cb4b16; } .asciinema-theme-solarized-light .fg-10 { color: #586e75; } .asciinema-theme-solarized-light .bg-10 { background-color: #586e75; } .asciinema-theme-solarized-light .fg-11 { color: #657c83; } .asciinema-theme-solarized-light .bg-11 { background-color: #657c83; } .asciinema-theme-solarized-light .fg-12 { color: #839496; } .asciinema-theme-solarized-light .bg-12 { background-color: #839496; } .asciinema-theme-solarized-light .fg-13 { color: #6c71c4; } .asciinema-theme-solarized-light .bg-13 { background-color: #6c71c4; } .asciinema-theme-solarized-light .fg-14 { color: #93a1a1; } .asciinema-theme-solarized-light .bg-14 { background-color: #93a1a1; } .asciinema-theme-solarized-light .fg-15 { color: #fdf6e3; } .asciinema-theme-solarized-light .bg-15 { background-color: #fdf6e3; } .asciinema-theme-seti .asciinema-terminal { color: #cacecd; background-color: #111213; border-color: #111213; } .asciinema-theme-seti .fg-bg { color: #111213; } .asciinema-theme-seti .bg-fg { background-color: #cacecd; } .asciinema-theme-seti .fg-0 { color: #323232; } .asciinema-theme-seti .bg-0 { background-color: #323232; } .asciinema-theme-seti .fg-1 { color: #c22832; } .asciinema-theme-seti .bg-1 { background-color: #c22832; } .asciinema-theme-seti .fg-2 { color: #8ec43d; } .asciinema-theme-seti .bg-2 { background-color: #8ec43d; } .asciinema-theme-seti .fg-3 { color: #e0c64f; } .asciinema-theme-seti .bg-3 { background-color: #e0c64f; } .asciinema-theme-seti .fg-4 { color: #43a5d5; } .asciinema-theme-seti .bg-4 { background-color: #43a5d5; } .asciinema-theme-seti .fg-5 { color: #8b57b5; } .asciinema-theme-seti .bg-5 { background-color: #8b57b5; } .asciinema-theme-seti .fg-6 { color: #8ec43d; } .asciinema-theme-seti .bg-6 { background-color: #8ec43d; } .asciinema-theme-seti .fg-7 { color: #eeeeee; } .asciinema-theme-seti .bg-7 { background-color: #eeeeee; } .asciinema-theme-seti .fg-8 { color: #323232; } .asciinema-theme-seti .bg-8 { background-color: #323232; } .asciinema-theme-seti .fg-9 { color: #c22832; } .asciinema-theme-seti .bg-9 { background-color: #c22832; } .asciinema-theme-seti .fg-10 { color: #8ec43d; } .asciinema-theme-seti .bg-10 { background-color: #8ec43d; } .asciinema-theme-seti .fg-11 { color: #e0c64f; } .asciinema-theme-seti .bg-11 { background-color: #e0c64f; } .asciinema-theme-seti .fg-12 { color: #43a5d5; } .asciinema-theme-seti .bg-12 { background-color: #43a5d5; } .asciinema-theme-seti .fg-13 { color: #8b57b5; } .asciinema-theme-seti .bg-13 { background-color: #8b57b5; } .asciinema-theme-seti .fg-14 { color: #8ec43d; } .asciinema-theme-seti .bg-14 { background-color: #8ec43d; } .asciinema-theme-seti .fg-15 { color: #ffffff; } .asciinema-theme-seti .bg-15 { background-color: #ffffff; } .asciinema-theme-seti .fg-8, .asciinema-theme-seti .fg-9, .asciinema-theme-seti .fg-10, .asciinema-theme-seti .fg-11, .asciinema-theme-seti .fg-12, .asciinema-theme-seti .fg-13, .asciinema-theme-seti .fg-14, .asciinema-theme-seti .fg-15 { font-weight: bold; } /* Based on Monokai from base16 collection - https://github.com/chriskempson/base16 */ .asciinema-theme-monokai .asciinema-terminal { color: #f8f8f2; background-color: #272822; border-color: #272822; } .asciinema-theme-monokai .fg-bg { color: #272822; } .asciinema-theme-monokai .bg-fg { background-color: #f8f8f2; } .asciinema-theme-monokai .fg-0 { color: #272822; } .asciinema-theme-monokai .bg-0 { background-color: #272822; } .asciinema-theme-monokai .fg-1 { color: #f92672; } .asciinema-theme-monokai .bg-1 { background-color: #f92672; } .asciinema-theme-monokai .fg-2 { color: #a6e22e; } .asciinema-theme-monokai .bg-2 { background-color: #a6e22e; } .asciinema-theme-monokai .fg-3 { color: #f4bf75; } .asciinema-theme-monokai .bg-3 { background-color: #f4bf75; } .asciinema-theme-monokai .fg-4 { color: #66d9ef; } .asciinema-theme-monokai .bg-4 { background-color: #66d9ef; } .asciinema-theme-monokai .fg-5 { color: #ae81ff; } .asciinema-theme-monokai .bg-5 { background-color: #ae81ff; } .asciinema-theme-monokai .fg-6 { color: #a1efe4; } .asciinema-theme-monokai .bg-6 { background-color: #a1efe4; } .asciinema-theme-monokai .fg-7 { color: #f8f8f2; } .asciinema-theme-monokai .bg-7 { background-color: #f8f8f2; } .asciinema-theme-monokai .fg-8 { color: #75715e; } .asciinema-theme-monokai .bg-8 { background-color: #75715e; } .asciinema-theme-monokai .fg-9 { color: #f92672; } .asciinema-theme-monokai .bg-9 { background-color: #f92672; } .asciinema-theme-monokai .fg-10 { color: #a6e22e; } .asciinema-theme-monokai .bg-10 { background-color: #a6e22e; } .asciinema-theme-monokai .fg-11 { color: #f4bf75; } .asciinema-theme-monokai .bg-11 { background-color: #f4bf75; } .asciinema-theme-monokai .fg-12 { color: #66d9ef; } .asciinema-theme-monokai .bg-12 { background-color: #66d9ef; } .asciinema-theme-monokai .fg-13 { color: #ae81ff; } .asciinema-theme-monokai .bg-13 { background-color: #ae81ff; } .asciinema-theme-monokai .fg-14 { color: #a1efe4; } .asciinema-theme-monokai .bg-14 { background-color: #a1efe4; } .asciinema-theme-monokai .fg-15 { color: #f9f8f5; } .asciinema-theme-monokai .bg-15 { background-color: #f9f8f5; } .asciinema-theme-monokai .fg-8, .asciinema-theme-monokai .fg-9, .asciinema-theme-monokai .fg-10, .asciinema-theme-monokai .fg-11, .asciinema-theme-monokai .fg-12, .asciinema-theme-monokai .fg-13, .asciinema-theme-monokai .fg-14, .asciinema-theme-monokai .fg-15 { font-weight: bold; } ================================================ FILE: docs/source/theme/smithy/static/asciinema-player.js ================================================ /** * asciinema-player v2.6.1 * * Copyright 2011-2018, Marcin Kulik * */ // CustomEvent polyfill from MDN (https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent) (function () { if (typeof window.CustomEvent === "function") return false; function CustomEvent ( event, params ) { params = params || { bubbles: false, cancelable: false, detail: undefined }; var evt = document.createEvent( 'CustomEvent'); evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail); return evt; } CustomEvent.prototype = window.Event.prototype; window.CustomEvent = CustomEvent; })(); /** * @license * Copyright (c) 2014 The Polymer Project Authors. All rights reserved. * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt * Code distributed by Google as part of the polymer project is also * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt */ // @version 0.7.22 "undefined"==typeof WeakMap&&!function(){var e=Object.defineProperty,t=Date.now()%1e9,n=function(){this.name="__st"+(1e9*Math.random()>>>0)+(t++ +"__")};n.prototype={set:function(t,n){var o=t[this.name];return o&&o[0]===t?o[1]=n:e(t,this.name,{value:[t,n],writable:!0}),this},get:function(e){var t;return(t=e[this.name])&&t[0]===e?t[1]:void 0},"delete":function(e){var t=e[this.name];return t&&t[0]===e?(t[0]=t[1]=void 0,!0):!1},has:function(e){var t=e[this.name];return t?t[0]===e:!1}},window.WeakMap=n}(),function(e){function t(e){E.push(e),b||(b=!0,w(o))}function n(e){return window.ShadowDOMPolyfill&&window.ShadowDOMPolyfill.wrapIfNeeded(e)||e}function o(){b=!1;var e=E;E=[],e.sort(function(e,t){return e.uid_-t.uid_});var t=!1;e.forEach(function(e){var n=e.takeRecords();r(e),n.length&&(e.callback_(n,e),t=!0)}),t&&o()}function r(e){e.nodes_.forEach(function(t){var n=v.get(t);n&&n.forEach(function(t){t.observer===e&&t.removeTransientObservers()})})}function i(e,t){for(var n=e;n;n=n.parentNode){var o=v.get(n);if(o)for(var r=0;r0){var r=n[o-1],i=p(r,e);if(i)return void(n[o-1]=i)}else t(this.observer);n[o]=e},addListeners:function(){this.addListeners_(this.target)},addListeners_:function(e){var t=this.options;t.attributes&&e.addEventListener("DOMAttrModified",this,!0),t.characterData&&e.addEventListener("DOMCharacterDataModified",this,!0),t.childList&&e.addEventListener("DOMNodeInserted",this,!0),(t.childList||t.subtree)&&e.addEventListener("DOMNodeRemoved",this,!0)},removeListeners:function(){this.removeListeners_(this.target)},removeListeners_:function(e){var t=this.options;t.attributes&&e.removeEventListener("DOMAttrModified",this,!0),t.characterData&&e.removeEventListener("DOMCharacterDataModified",this,!0),t.childList&&e.removeEventListener("DOMNodeInserted",this,!0),(t.childList||t.subtree)&&e.removeEventListener("DOMNodeRemoved",this,!0)},addTransientObserver:function(e){if(e!==this.target){this.addListeners_(e),this.transientObservedNodes.push(e);var t=v.get(e);t||v.set(e,t=[]),t.push(this)}},removeTransientObservers:function(){var e=this.transientObservedNodes;this.transientObservedNodes=[],e.forEach(function(e){this.removeListeners_(e);for(var t=v.get(e),n=0;n=0)){n.push(e);for(var o,r=e.querySelectorAll("link[rel="+a+"]"),d=0,s=r.length;s>d&&(o=r[d]);d++)o["import"]&&i(o["import"],t,n);t(e)}}var a=window.HTMLImports?window.HTMLImports.IMPORT_LINK_TYPE:"none";e.forDocumentTree=r,e.forSubtree=t}),window.CustomElements.addModule(function(e){function t(e,t){return n(e,t)||o(e,t)}function n(t,n){return e.upgrade(t,n)?!0:void(n&&a(t))}function o(e,t){b(e,function(e){return n(e,t)?!0:void 0})}function r(e){N.push(e),y||(y=!0,setTimeout(i))}function i(){y=!1;for(var e,t=N,n=0,o=t.length;o>n&&(e=t[n]);n++)e();N=[]}function a(e){_?r(function(){d(e)}):d(e)}function d(e){e.__upgraded__&&!e.__attached&&(e.__attached=!0,e.attachedCallback&&e.attachedCallback())}function s(e){u(e),b(e,function(e){u(e)})}function u(e){_?r(function(){c(e)}):c(e)}function c(e){e.__upgraded__&&e.__attached&&(e.__attached=!1,e.detachedCallback&&e.detachedCallback())}function l(e){for(var t=e,n=window.wrap(document);t;){if(t==n)return!0;t=t.parentNode||t.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&t.host}}function f(e){if(e.shadowRoot&&!e.shadowRoot.__watched){g.dom&&console.log("watching shadow-root for: ",e.localName);for(var t=e.shadowRoot;t;)w(t),t=t.olderShadowRoot}}function p(e,n){if(g.dom){var o=n[0];if(o&&"childList"===o.type&&o.addedNodes&&o.addedNodes){for(var r=o.addedNodes[0];r&&r!==document&&!r.host;)r=r.parentNode;var i=r&&(r.URL||r._URL||r.host&&r.host.localName)||"";i=i.split("/?").shift().split("/").pop()}console.group("mutations (%d) [%s]",n.length,i||"")}var a=l(e);n.forEach(function(e){"childList"===e.type&&(M(e.addedNodes,function(e){e.localName&&t(e,a)}),M(e.removedNodes,function(e){e.localName&&s(e)}))}),g.dom&&console.groupEnd()}function m(e){for(e=window.wrap(e),e||(e=window.wrap(document));e.parentNode;)e=e.parentNode;var t=e.__observer;t&&(p(e,t.takeRecords()),i())}function w(e){if(!e.__observer){var t=new MutationObserver(p.bind(this,e));t.observe(e,{childList:!0,subtree:!0}),e.__observer=t}}function v(e){e=window.wrap(e),g.dom&&console.group("upgradeDocument: ",e.baseURI.split("/").pop());var n=e===window.wrap(document);t(e,n),w(e),g.dom&&console.groupEnd()}function h(e){E(e,v)}var g=e.flags,b=e.forSubtree,E=e.forDocumentTree,_=window.MutationObserver._isPolyfilled&&g["throttle-attached"];e.hasPolyfillMutations=_,e.hasThrottledAttached=_;var y=!1,N=[],M=Array.prototype.forEach.call.bind(Array.prototype.forEach),O=Element.prototype.createShadowRoot;O&&(Element.prototype.createShadowRoot=function(){var e=O.call(this);return window.CustomElements.watchShadow(this),e}),e.watchShadow=f,e.upgradeDocumentTree=h,e.upgradeDocument=v,e.upgradeSubtree=o,e.upgradeAll=t,e.attached=a,e.takeRecords=m}),window.CustomElements.addModule(function(e){function t(t,o){if("template"===t.localName&&window.HTMLTemplateElement&&HTMLTemplateElement.decorate&&HTMLTemplateElement.decorate(t),!t.__upgraded__&&t.nodeType===Node.ELEMENT_NODE){var r=t.getAttribute("is"),i=e.getRegisteredDefinition(t.localName)||e.getRegisteredDefinition(r);if(i&&(r&&i.tag==t.localName||!r&&!i["extends"]))return n(t,i,o)}}function n(t,n,r){return a.upgrade&&console.group("upgrade:",t.localName),n.is&&t.setAttribute("is",n.is),o(t,n),t.__upgraded__=!0,i(t),r&&e.attached(t),e.upgradeSubtree(t,r),a.upgrade&&console.groupEnd(),t}function o(e,t){Object.__proto__?e.__proto__=t.prototype:(r(e,t.prototype,t["native"]),e.__proto__=t.prototype)}function r(e,t,n){for(var o={},r=t;r!==n&&r!==HTMLElement.prototype;){for(var i,a=Object.getOwnPropertyNames(r),d=0;i=a[d];d++)o[i]||(Object.defineProperty(e,i,Object.getOwnPropertyDescriptor(r,i)),o[i]=1);r=Object.getPrototypeOf(r)}}function i(e){e.createdCallback&&e.createdCallback()}var a=e.flags;e.upgrade=t,e.upgradeWithDefinition=n,e.implementPrototype=o}),window.CustomElements.addModule(function(e){function t(t,o){var s=o||{};if(!t)throw new Error("document.registerElement: first argument `name` must not be empty");if(t.indexOf("-")<0)throw new Error("document.registerElement: first argument ('name') must contain a dash ('-'). Argument provided was '"+String(t)+"'.");if(r(t))throw new Error("Failed to execute 'registerElement' on 'Document': Registration failed for type '"+String(t)+"'. The type name is invalid.");if(u(t))throw new Error("DuplicateDefinitionError: a type with name '"+String(t)+"' is already registered");return s.prototype||(s.prototype=Object.create(HTMLElement.prototype)),s.__name=t.toLowerCase(),s["extends"]&&(s["extends"]=s["extends"].toLowerCase()),s.lifecycle=s.lifecycle||{},s.ancestry=i(s["extends"]),a(s),d(s),n(s.prototype),c(s.__name,s),s.ctor=l(s),s.ctor.prototype=s.prototype,s.prototype.constructor=s.ctor,e.ready&&v(document),s.ctor}function n(e){if(!e.setAttribute._polyfilled){var t=e.setAttribute;e.setAttribute=function(e,n){o.call(this,e,n,t)};var n=e.removeAttribute;e.removeAttribute=function(e){o.call(this,e,null,n)},e.setAttribute._polyfilled=!0}}function o(e,t,n){e=e.toLowerCase();var o=this.getAttribute(e);n.apply(this,arguments);var r=this.getAttribute(e);this.attributeChangedCallback&&r!==o&&this.attributeChangedCallback(e,o,r)}function r(e){for(var t=0;t<_.length;t++)if(e===_[t])return!0}function i(e){var t=u(e);return t?i(t["extends"]).concat([t]):[]}function a(e){for(var t,n=e["extends"],o=0;t=e.ancestry[o];o++)n=t.is&&t.tag;e.tag=n||e.__name,n&&(e.is=e.__name)}function d(e){if(!Object.__proto__){var t=HTMLElement.prototype;if(e.is){var n=document.createElement(e.tag);t=Object.getPrototypeOf(n)}for(var o,r=e.prototype,i=!1;r;)r==t&&(i=!0),o=Object.getPrototypeOf(r),o&&(r.__proto__=o),r=o;i||console.warn(e.tag+" prototype not found in prototype chain for "+e.is),e["native"]=t}}function s(e){return g(M(e.tag),e)}function u(e){return e?y[e.toLowerCase()]:void 0}function c(e,t){y[e]=t}function l(e){return function(){return s(e)}}function f(e,t,n){return e===N?p(t,n):O(e,t)}function p(e,t){e&&(e=e.toLowerCase()),t&&(t=t.toLowerCase());var n=u(t||e);if(n){if(e==n.tag&&t==n.is)return new n.ctor;if(!t&&!n.is)return new n.ctor}var o;return t?(o=p(e),o.setAttribute("is",t),o):(o=M(e),e.indexOf("-")>=0&&b(o,HTMLElement),o)}function m(e,t){var n=e[t];e[t]=function(){var e=n.apply(this,arguments);return h(e),e}}var w,v=(e.isIE,e.upgradeDocumentTree),h=e.upgradeAll,g=e.upgradeWithDefinition,b=e.implementPrototype,E=e.useNative,_=["annotation-xml","color-profile","font-face","font-face-src","font-face-uri","font-face-format","font-face-name","missing-glyph"],y={},N="http://www.w3.org/1999/xhtml",M=document.createElement.bind(document),O=document.createElementNS.bind(document);w=Object.__proto__||E?function(e,t){return e instanceof t}:function(e,t){if(e instanceof t)return!0;for(var n=e;n;){if(n===t.prototype)return!0;n=n.__proto__}return!1},m(Node.prototype,"cloneNode"),m(document,"importNode"),document.registerElement=t,document.createElement=p,document.createElementNS=f,e.registry=y,e["instanceof"]=w,e.reservedTagList=_,e.getRegisteredDefinition=u,document.register=document.registerElement}),function(e){function t(){i(window.wrap(document)),window.CustomElements.ready=!0;var e=window.requestAnimationFrame||function(e){setTimeout(e,16)};e(function(){setTimeout(function(){window.CustomElements.readyTime=Date.now(),window.HTMLImports&&(window.CustomElements.elapsed=window.CustomElements.readyTime-window.HTMLImports.readyTime),document.dispatchEvent(new CustomEvent("WebComponentsReady",{bubbles:!0}))})})}var n=e.useNative,o=e.initializeModules;e.isIE;if(n){var r=function(){};e.watchShadow=r,e.upgrade=r,e.upgradeAll=r,e.upgradeDocumentTree=r,e.upgradeSubtree=r,e.takeRecords=r,e["instanceof"]=function(e,t){return e instanceof t}}else o();var i=e.upgradeDocumentTree,a=e.upgradeDocument;if(window.wrap||(window.ShadowDOMPolyfill?(window.wrap=window.ShadowDOMPolyfill.wrapIfNeeded,window.unwrap=window.ShadowDOMPolyfill.unwrapIfNeeded):window.wrap=window.unwrap=function(e){return e}),window.HTMLImports&&(window.HTMLImports.__importsParsingHook=function(e){e["import"]&&a(wrap(e["import"]))}),"complete"===document.readyState||e.flags.eager)t();else if("interactive"!==document.readyState||window.attachEvent||window.HTMLImports&&!window.HTMLImports.ready){var d=window.HTMLImports&&!window.HTMLImports.ready?"HTMLImportsLoaded":"DOMContentLoaded";window.addEventListener(d,t)}else t()}(window.CustomElements); if(typeof Math.imul == "undefined" || (Math.imul(0xffffffff,5) == 0)) { Math.imul = function (a, b) { var ah = (a >>> 16) & 0xffff; var al = a & 0xffff; var bh = (b >>> 16) & 0xffff; var bl = b & 0xffff; // the shift by 0 fixes the sign on the high part // the final |0 converts the unsigned value into a signed value return ((al * bl) + (((ah * bl + al * bh) << 16) >>> 0)|0); } } /** * React v15.5.4 * * Copyright 2013-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. * */ !function(t){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var e;e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,e.React=t()}}(function(){return function t(e,n,r){function o(u,a){if(!n[u]){if(!e[u]){var s="function"==typeof require&&require;if(!a&&s)return s(u,!0);if(i)return i(u,!0);var c=new Error("Cannot find module '"+u+"'");throw c.code="MODULE_NOT_FOUND",c}var l=n[u]={exports:{}};e[u][0].call(l.exports,function(t){var n=e[u][1][t];return o(n||t)},l,l.exports,t,e,n,r)}return n[u].exports}for(var i="function"==typeof require&&require,u=0;u1){for(var y=Array(d),h=0;h1){for(var m=Array(v),b=0;b8&&C<=11),x=32,w=String.fromCharCode(x),T={beforeInput:{phasedRegistrationNames:{bubbled:"onBeforeInput",captured:"onBeforeInputCapture"},dependencies:["topCompositionEnd","topKeyPress","topTextInput","topPaste"]},compositionEnd:{phasedRegistrationNames:{bubbled:"onCompositionEnd",captured:"onCompositionEndCapture"},dependencies:["topBlur","topCompositionEnd","topKeyDown","topKeyPress","topKeyUp","topMouseDown"]},compositionStart:{phasedRegistrationNames:{bubbled:"onCompositionStart",captured:"onCompositionStartCapture"},dependencies:["topBlur","topCompositionStart","topKeyDown","topKeyPress","topKeyUp","topMouseDown"]},compositionUpdate:{phasedRegistrationNames:{bubbled:"onCompositionUpdate",captured:"onCompositionUpdateCapture"},dependencies:["topBlur","topCompositionUpdate","topKeyDown","topKeyPress","topKeyUp","topMouseDown"]}},k=!1,P=null,S={eventTypes:T,extractEvents:function(e,t,n,r){return[u(e,t,n,r),p(e,t,n,r)]}};t.exports=S},{123:123,19:19,20:20,78:78,82:82}],4:[function(e,t,n){"use strict";function r(e,t){return e+t.charAt(0).toUpperCase()+t.substring(1)}var o={animationIterationCount:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridRow:!0,gridColumn:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},i=["Webkit","ms","Moz","O"];Object.keys(o).forEach(function(e){i.forEach(function(t){o[r(t,e)]=o[e]})});var a={background:{backgroundAttachment:!0,backgroundColor:!0,backgroundImage:!0,backgroundPositionX:!0,backgroundPositionY:!0,backgroundRepeat:!0},backgroundPosition:{backgroundPositionX:!0,backgroundPositionY:!0},border:{borderWidth:!0,borderStyle:!0,borderColor:!0},borderBottom:{borderBottomWidth:!0,borderBottomStyle:!0,borderBottomColor:!0},borderLeft:{borderLeftWidth:!0,borderLeftStyle:!0,borderLeftColor:!0},borderRight:{borderRightWidth:!0,borderRightStyle:!0,borderRightColor:!0},borderTop:{borderTopWidth:!0,borderTopStyle:!0,borderTopColor:!0},font:{fontStyle:!0,fontVariant:!0,fontWeight:!0,fontSize:!0,lineHeight:!0,fontFamily:!0},outline:{outlineWidth:!0,outlineStyle:!0,outlineColor:!0}},s={isUnitlessNumber:o,shorthandPropertyExpansions:a};t.exports=s},{}],5:[function(e,t,n){"use strict";var r=e(4),o=e(123),i=(e(58),e(125),e(94)),a=e(136),s=e(140),u=(e(142),s(function(e){return a(e)})),l=!1,c="cssFloat";if(o.canUseDOM){var p=document.createElement("div").style;try{p.font=""}catch(e){l=!0}void 0===document.documentElement.style.cssFloat&&(c="styleFloat")}var d={createMarkupForStyles:function(e,t){var n="";for(var r in e)if(e.hasOwnProperty(r)){var o=e[r];null!=o&&(n+=u(r)+":",n+=i(r,o,t)+";")}return n||null},setValueForStyles:function(e,t,n){var o=e.style;for(var a in t)if(t.hasOwnProperty(a)){var s=i(a,t[a],n);if("float"!==a&&"cssFloat"!==a||(a=c),s)o[a]=s;else{var u=l&&r.shorthandPropertyExpansions[a];if(u)for(var p in u)o[p]="";else o[a]=""}}}};t.exports=d},{123:123,125:125,136:136,140:140,142:142,4:4,58:58,94:94}],6:[function(e,t,n){"use strict";function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}var o=e(112),i=e(24),a=(e(137),function(){function e(t){r(this,e),this._callbacks=null,this._contexts=null,this._arg=t}return e.prototype.enqueue=function(e,t){this._callbacks=this._callbacks||[],this._callbacks.push(e),this._contexts=this._contexts||[],this._contexts.push(t)},e.prototype.notifyAll=function(){var e=this._callbacks,t=this._contexts,n=this._arg;if(e&&t){e.length!==t.length&&o("24"),this._callbacks=null,this._contexts=null;for(var r=0;r8));var A=!1;b.canUseDOM&&(A=k("input")&&(!document.documentMode||document.documentMode>11));var D={get:function(){return O.get.call(this)},set:function(e){I=""+e,O.set.call(this,e)}},L={eventTypes:S,extractEvents:function(e,t,n,o){var i,a,s=t?E.getNodeFromInstance(t):window;if(r(s)?R?i=u:a=l:P(s)?A?i=f:(i=m,a=h):v(s)&&(i=g),i){var c=i(e,t);if(c){var p=w.getPooled(S.change,c,n,o);return p.type="change",C.accumulateTwoPhaseDispatches(p),p}}a&&a(e,s,t),"topBlur"===e&&y(t,s)}};t.exports=L},{102:102,109:109,110:110,123:123,16:16,19:19,33:33,71:71,80:80}],8:[function(e,t,n){"use strict";function r(e,t){return Array.isArray(t)&&(t=t[1]),t?t.nextSibling:e.firstChild}function o(e,t,n){c.insertTreeBefore(e,t,n)}function i(e,t,n){Array.isArray(t)?s(e,t[0],t[1],n):m(e,t,n)}function a(e,t){if(Array.isArray(t)){var n=t[1];t=t[0],u(e,t,n),e.removeChild(n)}e.removeChild(t)}function s(e,t,n,r){for(var o=t;;){var i=o.nextSibling;if(m(e,o,r),o===n)break;o=i}}function u(e,t,n){for(;;){var r=t.nextSibling;if(r===n)break;e.removeChild(r)}}function l(e,t,n){var r=e.parentNode,o=e.nextSibling;o===t?n&&m(r,document.createTextNode(n),o):n?(h(o,n),u(r,o,t)):u(r,e,t)}var c=e(9),p=e(13),d=(e(33),e(58),e(93)),f=e(114),h=e(115),m=d(function(e,t,n){e.insertBefore(t,n)}),v=p.dangerouslyReplaceNodeWithMarkup,g={dangerouslyReplaceNodeWithMarkup:v,replaceDelimitedText:l,processUpdates:function(e,t){for(var n=0;n-1||a("96",e),!l.plugins[n]){t.extractEvents||a("97",e),l.plugins[n]=t;var r=t.eventTypes;for(var i in r)o(r[i],t,i)||a("98",i,e)}}}function o(e,t,n){l.eventNameDispatchConfigs.hasOwnProperty(n)&&a("99",n),l.eventNameDispatchConfigs[n]=e;var r=e.phasedRegistrationNames;if(r){for(var o in r)if(r.hasOwnProperty(o)){var s=r[o];i(s,t,n)}return!0}return!!e.registrationName&&(i(e.registrationName,t,n),!0)}function i(e,t,n){l.registrationNameModules[e]&&a("100",e),l.registrationNameModules[e]=t,l.registrationNameDependencies[e]=t.eventTypes[n].dependencies}var a=e(112),s=(e(137),null),u={},l={plugins:[],eventNameDispatchConfigs:{},registrationNameModules:{},registrationNameDependencies:{},possibleRegistrationNames:null,injectEventPluginOrder:function(e){s&&a("101"),s=Array.prototype.slice.call(e),r()},injectEventPluginsByName:function(e){var t=!1;for(var n in e)if(e.hasOwnProperty(n)){var o=e[n];u.hasOwnProperty(n)&&u[n]===o||(u[n]&&a("102",n),u[n]=o,t=!0)}t&&r()},getPluginModuleForEvent:function(e){var t=e.dispatchConfig;if(t.registrationName)return l.registrationNameModules[t.registrationName]||null;if(void 0!==t.phasedRegistrationNames){var n=t.phasedRegistrationNames;for(var r in n)if(n.hasOwnProperty(r)){var o=l.registrationNameModules[n[r]];if(o)return o}}return null},_resetEventPlugins:function(){s=null;for(var e in u)u.hasOwnProperty(e)&&delete u[e];l.plugins.length=0;var t=l.eventNameDispatchConfigs;for(var n in t)t.hasOwnProperty(n)&&delete t[n];var r=l.registrationNameModules;for(var o in r)r.hasOwnProperty(o)&&delete r[o]}};t.exports=l},{112:112,137:137}],18:[function(e,t,n){"use strict";function r(e){return"topMouseUp"===e||"topTouchEnd"===e||"topTouchCancel"===e}function o(e){return"topMouseMove"===e||"topTouchMove"===e}function i(e){return"topMouseDown"===e||"topTouchStart"===e}function a(e,t,n,r){var o=e.type||"unknown-event";e.currentTarget=g.getNodeFromInstance(r),t?m.invokeGuardedCallbackWithCatch(o,n,e):m.invokeGuardedCallback(o,n,e),e.currentTarget=null}function s(e,t){var n=e._dispatchListeners,r=e._dispatchInstances;if(Array.isArray(n))for(var o=0;o1?1-t:void 0;return this._fallbackText=o.slice(e,s),this._fallbackText}}),i.addPoolingTo(r),t.exports=r},{106:106,143:143,24:24}],21:[function(e,t,n){"use strict";var r=e(11),o=r.injection.MUST_USE_PROPERTY,i=r.injection.HAS_BOOLEAN_VALUE,a=r.injection.HAS_NUMERIC_VALUE,s=r.injection.HAS_POSITIVE_NUMERIC_VALUE,u=r.injection.HAS_OVERLOADED_BOOLEAN_VALUE,l={isCustomAttribute:RegExp.prototype.test.bind(new RegExp("^(data|aria)-["+r.ATTRIBUTE_NAME_CHAR+"]*$")),Properties:{accept:0,acceptCharset:0,accessKey:0,action:0,allowFullScreen:i,allowTransparency:0,alt:0,as:0,async:i,autoComplete:0,autoPlay:i,capture:i,cellPadding:0,cellSpacing:0,charSet:0,challenge:0,checked:o|i,cite:0,classID:0,className:0,cols:s,colSpan:0,content:0,contentEditable:0,contextMenu:0,controls:i,coords:0,crossOrigin:0,data:0,dateTime:0,default:i,defer:i,dir:0,disabled:i,download:u,draggable:0,encType:0,form:0,formAction:0,formEncType:0,formMethod:0,formNoValidate:i,formTarget:0,frameBorder:0,headers:0,height:0,hidden:i,high:0,href:0,hrefLang:0,htmlFor:0,httpEquiv:0,icon:0,id:0,inputMode:0,integrity:0,is:0,keyParams:0,keyType:0,kind:0,label:0,lang:0,list:0,loop:i,low:0,manifest:0,marginHeight:0,marginWidth:0,max:0,maxLength:0,media:0,mediaGroup:0,method:0,min:0,minLength:0,multiple:o|i,muted:o|i,name:0,nonce:0,noValidate:i,open:i,optimum:0,pattern:0,placeholder:0,playsInline:i,poster:0,preload:0,profile:0,radioGroup:0,readOnly:i,referrerPolicy:0,rel:0,required:i,reversed:i,role:0,rows:s,rowSpan:a,sandbox:0,scope:0,scoped:i,scrolling:0,seamless:i,selected:o|i,shape:0,size:s,sizes:0,span:s,spellCheck:0,src:0,srcDoc:0,srcLang:0,srcSet:0,start:a,step:0,style:0,summary:0,tabIndex:0,target:0,title:0,type:0,useMap:0,value:0,width:0,wmode:0,wrap:0,about:0,datatype:0,inlist:0,prefix:0,property:0,resource:0,typeof:0,vocab:0,autoCapitalize:0,autoCorrect:0,autoSave:0,color:0,itemProp:0,itemScope:i,itemType:0,itemID:0,itemRef:0,results:0,security:0,unselectable:0},DOMAttributeNames:{acceptCharset:"accept-charset",className:"class",htmlFor:"for",httpEquiv:"http-equiv"},DOMPropertyNames:{},DOMMutationMethods:{value:function(e,t){if(null==t)return e.removeAttribute("value");"number"!==e.type||!1===e.hasAttribute("value")?e.setAttribute("value",""+t):e.validity&&!e.validity.badInput&&e.ownerDocument.activeElement!==e&&e.setAttribute("value",""+t)}}};t.exports=l},{11:11}],22:[function(e,t,n){"use strict";function r(e){var t={"=":"=0",":":"=2"};return"$"+(""+e).replace(/[=:]/g,function(e){return t[e]})}function o(e){var t={"=0":"=","=2":":"};return(""+("."===e[0]&&"$"===e[1]?e.substring(2):e.substring(1))).replace(/(=0|=2)/g,function(e){return t[e]})}var i={escape:r,unescape:o};t.exports=i},{}],23:[function(e,t,n){"use strict";function r(e){null!=e.checkedLink&&null!=e.valueLink&&s("87")}function o(e){r(e),(null!=e.value||null!=e.onChange)&&s("88")}function i(e){r(e),(null!=e.checked||null!=e.onChange)&&s("89")}function a(e){if(e){var t=e.getName();if(t)return" Check the render method of `"+t+"`."}return""}var s=e(112),u=e(64),l=e(145),c=e(120),p=l(c.isValidElement),d=(e(137),e(142),{button:!0,checkbox:!0,image:!0,hidden:!0,radio:!0,reset:!0,submit:!0}),f={value:function(e,t,n){return!e[t]||d[e.type]||e.onChange||e.readOnly||e.disabled?null:new Error("You provided a `value` prop to a form field without an `onChange` handler. This will render a read-only field. If the field should be mutable use `defaultValue`. Otherwise, set either `onChange` or `readOnly`.")},checked:function(e,t,n){return!e[t]||e.onChange||e.readOnly||e.disabled?null:new Error("You provided a `checked` prop to a form field without an `onChange` handler. This will render a read-only field. If the field should be mutable use `defaultChecked`. Otherwise, set either `onChange` or `readOnly`.")},onChange:p.func},h={},m={checkPropTypes:function(e,t,n){for(var r in f){if(f.hasOwnProperty(r))var o=f[r](t,r,e,"prop",null,u);o instanceof Error&&!(o.message in h)&&(h[o.message]=!0,a(n))}},getValue:function(e){return e.valueLink?(o(e),e.valueLink.value):e.value},getChecked:function(e){return e.checkedLink?(i(e),e.checkedLink.value):e.checked},executeOnChange:function(e,t){return e.valueLink?(o(e),e.valueLink.requestChange(t.target.value)):e.checkedLink?(i(e),e.checkedLink.requestChange(t.target.checked)):e.onChange?e.onChange.call(void 0,t):void 0}};t.exports=m},{112:112,120:120,137:137,142:142,145:145,64:64}],24:[function(e,t,n){"use strict";var r=e(112),o=(e(137),function(e){var t=this;if(t.instancePool.length){var n=t.instancePool.pop();return t.call(n,e),n}return new t(e)}),i=function(e,t){var n=this;if(n.instancePool.length){var r=n.instancePool.pop();return n.call(r,e,t),r}return new n(e,t)},a=function(e,t,n){var r=this;if(r.instancePool.length){var o=r.instancePool.pop();return r.call(o,e,t,n),o}return new r(e,t,n)},s=function(e,t,n,r){var o=this;if(o.instancePool.length){var i=o.instancePool.pop();return o.call(i,e,t,n,r),i}return new o(e,t,n,r)},u=function(e){var t=this;e instanceof t||r("25"),e.destructor(),t.instancePool.length=0||null!=t.is}function h(e){var t=e.type;d(t),this._currentElement=e,this._tag=t.toLowerCase(),this._namespaceURI=null,this._renderedChildren=null,this._previousStyle=null,this._previousStyleCopy=null,this._hostNode=null,this._hostParent=null,this._rootNodeID=0,this._domID=0,this._hostContainerInfo=null,this._wrapperState=null,this._topLevelWrapper=null,this._flags=0}var m=e(112),v=e(143),g=e(2),y=e(5),_=e(9),C=e(10),b=e(11),E=e(12),x=e(16),w=e(17),T=e(25),k=e(32),P=e(33),S=e(38),N=e(39),M=e(40),I=e(43),O=(e(58),e(61)),R=e(68),A=(e(129),e(95)),D=(e(137),e(109),e(141),e(118),e(142),k),L=x.deleteListener,U=P.getNodeFromInstance,F=T.listenTo,j=w.registrationNameModules,V={string:!0,number:!0},B="__html",W={children:null,dangerouslySetInnerHTML:null,suppressContentEditableWarning:null},H=11,q={topAbort:"abort",topCanPlay:"canplay",topCanPlayThrough:"canplaythrough",topDurationChange:"durationchange",topEmptied:"emptied",topEncrypted:"encrypted",topEnded:"ended",topError:"error",topLoadedData:"loadeddata",topLoadedMetadata:"loadedmetadata",topLoadStart:"loadstart",topPause:"pause",topPlay:"play",topPlaying:"playing",topProgress:"progress",topRateChange:"ratechange",topSeeked:"seeked",topSeeking:"seeking",topStalled:"stalled",topSuspend:"suspend",topTimeUpdate:"timeupdate",topVolumeChange:"volumechange",topWaiting:"waiting"},K={area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0},z={listing:!0,pre:!0,textarea:!0},Y=v({menuitem:!0},K),X=/^[a-zA-Z][a-zA-Z:_\.\-\d]*$/,Q={},G={}.hasOwnProperty,$=1;h.displayName="ReactDOMComponent",h.Mixin={mountComponent:function(e,t,n,r){this._rootNodeID=$++,this._domID=n._idCounter++,this._hostParent=t,this._hostContainerInfo=n;var i=this._currentElement.props;switch(this._tag){case"audio":case"form":case"iframe":case"img":case"link":case"object":case"source":case"video":this._wrapperState={listeners:null},e.getReactMountReady().enqueue(c,this);break;case"input":S.mountWrapper(this,i,t),i=S.getHostProps(this,i),e.getReactMountReady().enqueue(c,this);break;case"option":N.mountWrapper(this,i,t),i=N.getHostProps(this,i);break;case"select":M.mountWrapper(this,i,t),i=M.getHostProps(this,i),e.getReactMountReady().enqueue(c,this);break;case"textarea":I.mountWrapper(this,i,t),i=I.getHostProps(this,i),e.getReactMountReady().enqueue(c,this)}o(this,i);var a,p;null!=t?(a=t._namespaceURI,p=t._tag):n._tag&&(a=n._namespaceURI,p=n._tag),(null==a||a===C.svg&&"foreignobject"===p)&&(a=C.html),a===C.html&&("svg"===this._tag?a=C.svg:"math"===this._tag&&(a=C.mathml)),this._namespaceURI=a;var d;if(e.useCreateElement){var f,h=n._ownerDocument;if(a===C.html)if("script"===this._tag){var m=h.createElement("div"),v=this._currentElement.type;m.innerHTML="<"+v+">",f=m.removeChild(m.firstChild)}else f=i.is?h.createElement(this._currentElement.type,i.is):h.createElement(this._currentElement.type);else f=h.createElementNS(a,this._currentElement.type);P.precacheNode(this,f),this._flags|=D.hasCachedChildNodes,this._hostParent||E.setAttributeForRoot(f),this._updateDOMProperties(null,i,e);var y=_(f);this._createInitialChildren(e,i,r,y),d=y}else{var b=this._createOpenTagMarkupAndPutListeners(e,i),x=this._createContentMarkup(e,i,r);d=!x&&K[this._tag]?b+"/>":b+">"+x+""}switch(this._tag){case"input":e.getReactMountReady().enqueue(s,this),i.autoFocus&&e.getReactMountReady().enqueue(g.focusDOMComponent,this);break;case"textarea":e.getReactMountReady().enqueue(u,this),i.autoFocus&&e.getReactMountReady().enqueue(g.focusDOMComponent,this);break;case"select":case"button":i.autoFocus&&e.getReactMountReady().enqueue(g.focusDOMComponent,this);break;case"option":e.getReactMountReady().enqueue(l,this)}return d},_createOpenTagMarkupAndPutListeners:function(e,t){var n="<"+this._currentElement.type;for(var r in t)if(t.hasOwnProperty(r)){var o=t[r];if(null!=o)if(j.hasOwnProperty(r))o&&i(this,r,o,e);else{"style"===r&&(o&&(o=this._previousStyleCopy=v({},t.style)),o=y.createMarkupForStyles(o,this));var a=null;null!=this._tag&&f(this._tag,t)?W.hasOwnProperty(r)||(a=E.createMarkupForCustomAttribute(r,o)):a=E.createMarkupForProperty(r,o),a&&(n+=" "+a)}}return e.renderToStaticMarkup?n:(this._hostParent||(n+=" "+E.createMarkupForRoot()),n+=" "+E.createMarkupForID(this._domID))},_createContentMarkup:function(e,t,n){var r="",o=t.dangerouslySetInnerHTML;if(null!=o)null!=o.__html&&(r=o.__html);else{var i=V[typeof t.children]?t.children:null,a=null!=i?null:t.children;if(null!=i)r=A(i);else if(null!=a){var s=this.mountChildren(a,e,n);r=s.join("")}}return z[this._tag]&&"\n"===r.charAt(0)?"\n"+r:r},_createInitialChildren:function(e,t,n,r){var o=t.dangerouslySetInnerHTML;if(null!=o)null!=o.__html&&_.queueHTML(r,o.__html);else{var i=V[typeof t.children]?t.children:null,a=null!=i?null:t.children;if(null!=i)""!==i&&_.queueText(r,i);else if(null!=a)for(var s=this.mountChildren(a,e,n),u=0;u"},receiveComponent:function(){},getHostNode:function(){return i.getNodeFromInstance(this)},unmountComponent:function(){i.uncacheNode(this)}}),t.exports=a},{143:143,33:33,9:9}],36:[function(e,t,n){"use strict";var r={useCreateElement:!0,useFiber:!1};t.exports=r},{}],37:[function(e,t,n){"use strict";var r=e(8),o=e(33),i={dangerouslyProcessChildrenUpdates:function(e,t){var n=o.getNodeFromInstance(e);r.processUpdates(n,t)}};t.exports=i},{33:33,8:8}],38:[function(e,t,n){"use strict";function r(){this._rootNodeID&&d.updateWrapper(this)}function o(e){return"checkbox"===e.type||"radio"===e.type?null!=e.checked:null!=e.value}function i(e){var t=this._currentElement.props,n=l.executeOnChange(t,e);p.asap(r,this);var o=t.name;if("radio"===t.type&&null!=o){for(var i=c.getNodeFromInstance(this),s=i;s.parentNode;)s=s.parentNode;for(var u=s.querySelectorAll("input[name="+JSON.stringify(""+o)+'][type="radio"]'),d=0;dt.end?(n=t.end,r=t.start):(n=t.start,r=t.end),o.moveToElementText(e),o.moveStart("character",n),o.setEndPoint("EndToStart",o),o.moveEnd("character",r-n),o.select()}function s(e,t){if(window.getSelection){var n=window.getSelection(),r=e[c()].length,o=Math.min(t.start,r),i=void 0===t.end?o:Math.min(t.end,r);if(!n.extend&&o>i){var a=i;i=o,o=a}var s=l(e,o),u=l(e,i);if(s&&u){var p=document.createRange();p.setStart(s.node,s.offset),n.removeAllRanges(),o>i?(n.addRange(p),n.extend(u.node,u.offset)):(p.setEnd(u.node,u.offset),n.addRange(p))}}}var u=e(123),l=e(105),c=e(106),p=u.canUseDOM&&"selection"in document&&!("getSelection"in window),d={getOffsets:p?o:i,setOffsets:p?a:s};t.exports=d},{105:105,106:106,123:123}],42:[function(e,t,n){"use strict";var r=e(112),o=e(143),i=e(8),a=e(9),s=e(33),u=e(95),l=(e(137),e(118),function(e){this._currentElement=e,this._stringText=""+e, this._hostNode=null,this._hostParent=null,this._domID=0,this._mountIndex=0,this._closingComment=null,this._commentNodes=null});o(l.prototype,{mountComponent:function(e,t,n,r){var o=n._idCounter++,i=" react-text: "+o+" ";if(this._domID=o,this._hostParent=t,e.useCreateElement){var l=n._ownerDocument,c=l.createComment(i),p=l.createComment(" /react-text "),d=a(l.createDocumentFragment());return a.queueChild(d,a(c)),this._stringText&&a.queueChild(d,a(l.createTextNode(this._stringText))),a.queueChild(d,a(p)),s.precacheNode(this,c),this._closingComment=p,d}var f=u(this._stringText);return e.renderToStaticMarkup?f:""+f+""},receiveComponent:function(e,t){if(e!==this._currentElement){this._currentElement=e;var n=""+e;if(n!==this._stringText){this._stringText=n;var r=this.getHostNode();i.replaceDelimitedText(r[0],r[1],n)}}},getHostNode:function(){var e=this._commentNodes;if(e)return e;if(!this._closingComment)for(var t=s.getNodeFromInstance(this),n=t.nextSibling;;){if(null==n&&r("67",this._domID),8===n.nodeType&&" /react-text "===n.nodeValue){this._closingComment=n;break}n=n.nextSibling}return e=[this._hostNode,this._closingComment],this._commentNodes=e,e},unmountComponent:function(){this._closingComment=null,this._commentNodes=null,s.uncacheNode(this)}}),t.exports=l},{112:112,118:118,137:137,143:143,33:33,8:8,9:9,95:95}],43:[function(e,t,n){"use strict";function r(){this._rootNodeID&&c.updateWrapper(this)}function o(e){var t=this._currentElement.props,n=s.executeOnChange(t,e);return l.asap(r,this),n}var i=e(112),a=e(143),s=e(23),u=e(33),l=e(71),c=(e(137),e(142),{getHostProps:function(e,t){return null!=t.dangerouslySetInnerHTML&&i("91"),a({},t,{value:void 0,defaultValue:void 0,children:""+e._wrapperState.initialValue,onChange:e._wrapperState.onChange})},mountWrapper:function(e,t){var n=s.getValue(t),r=n;if(null==n){var a=t.defaultValue,u=t.children;null!=u&&(null!=a&&i("92"),Array.isArray(u)&&(u.length<=1||i("93"),u=u[0]),a=""+u),null==a&&(a=""),r=a}e._wrapperState={initialValue:""+r,listeners:null,onChange:o.bind(e)}},updateWrapper:function(e){var t=e._currentElement.props,n=u.getNodeFromInstance(e),r=s.getValue(t);if(null!=r){var o=""+r;o!==n.value&&(n.value=o),null==t.defaultValue&&(n.defaultValue=o)}null!=t.defaultValue&&(n.defaultValue=t.defaultValue)},postMountWrapper:function(e){var t=u.getNodeFromInstance(e),n=t.textContent;n===e._wrapperState.initialValue&&(t.value=n)}});t.exports=c},{112:112,137:137,142:142,143:143,23:23,33:33,71:71}],44:[function(e,t,n){"use strict";function r(e,t){"_hostNode"in e||u("33"),"_hostNode"in t||u("33");for(var n=0,r=e;r;r=r._hostParent)n++;for(var o=0,i=t;i;i=i._hostParent)o++;for(;n-o>0;)e=e._hostParent,n--;for(;o-n>0;)t=t._hostParent,o--;for(var a=n;a--;){if(e===t)return e;e=e._hostParent,t=t._hostParent}return null}function o(e,t){"_hostNode"in e||u("35"),"_hostNode"in t||u("35");for(;t;){if(t===e)return!0;t=t._hostParent}return!1}function i(e){return"_hostNode"in e||u("36"),e._hostParent}function a(e,t,n){for(var r=[];e;)r.push(e),e=e._hostParent;var o;for(o=r.length;o-- >0;)t(r[o],"captured",n);for(o=0;o0;)n(u[l],"captured",i)}var u=e(112);e(137);t.exports={isAncestor:o,getLowestCommonAncestor:r,getParentInstance:i,traverseTwoPhase:a,traverseEnterLeave:s}},{112:112,137:137}],45:[function(e,t,n){"use strict";var r=e(120),o=e(30),i=o;r.addons&&(r.__SECRET_INJECTED_REACT_DOM_DO_NOT_USE_OR_YOU_WILL_BE_FIRED=i),t.exports=i},{120:120,30:30}],46:[function(e,t,n){"use strict";function r(){this.reinitializeTransaction()}var o=e(143),i=e(71),a=e(89),s=e(129),u={initialize:s,close:function(){d.isBatchingUpdates=!1}},l={initialize:s,close:i.flushBatchedUpdates.bind(i)},c=[l,u];o(r.prototype,a,{getTransactionWrappers:function(){return c}});var p=new r,d={isBatchingUpdates:!1,batchedUpdates:function(e,t,n,r,o,i){var a=d.isBatchingUpdates;return d.isBatchingUpdates=!0,a?e(t,n,r,o,i):p.perform(e,null,t,n,r,o,i)}};t.exports=d},{129:129,143:143,71:71,89:89}],47:[function(e,t,n){"use strict";function r(){x||(x=!0,y.EventEmitter.injectReactEventListener(g),y.EventPluginHub.injectEventPluginOrder(s),y.EventPluginUtils.injectComponentTree(d),y.EventPluginUtils.injectTreeTraversal(h),y.EventPluginHub.injectEventPluginsByName({SimpleEventPlugin:E,EnterLeaveEventPlugin:u,ChangeEventPlugin:a,SelectEventPlugin:b,BeforeInputEventPlugin:i}),y.HostComponent.injectGenericComponentClass(p),y.HostComponent.injectTextComponentClass(m),y.DOMProperty.injectDOMPropertyConfig(o),y.DOMProperty.injectDOMPropertyConfig(l),y.DOMProperty.injectDOMPropertyConfig(C),y.EmptyComponent.injectEmptyComponentFactory(function(e){return new f(e)}),y.Updates.injectReconcileTransaction(_),y.Updates.injectBatchingStrategy(v),y.Component.injectEnvironment(c))}var o=e(1),i=e(3),a=e(7),s=e(14),u=e(15),l=e(21),c=e(27),p=e(31),d=e(33),f=e(35),h=e(44),m=e(42),v=e(46),g=e(52),y=e(55),_=e(65),C=e(73),b=e(74),E=e(75),x=!1;t.exports={inject:r}},{1:1,14:14,15:15,21:21,27:27,3:3,31:31,33:33,35:35,42:42,44:44,46:46,52:52,55:55,65:65,7:7,73:73,74:74,75:75}],48:[function(e,t,n){"use strict";var r="function"==typeof Symbol&&Symbol.for&&Symbol.for("react.element")||60103;t.exports=r},{}],49:[function(e,t,n){"use strict";var r,o={injectEmptyComponentFactory:function(e){r=e}},i={create:function(e){return r(e)}};i.injection=o,t.exports=i},{}],50:[function(e,t,n){"use strict";function r(e,t,n){try{t(n)}catch(e){null===o&&(o=e)}}var o=null,i={invokeGuardedCallback:r,invokeGuardedCallbackWithCatch:r,rethrowCaughtError:function(){if(o){var e=o;throw o=null,e}}};t.exports=i},{}],51:[function(e,t,n){"use strict";function r(e){o.enqueueEvents(e),o.processEventQueue(!1)}var o=e(16),i={handleTopLevel:function(e,t,n,i){r(o.extractEvents(e,t,n,i))}};t.exports=i},{16:16}],52:[function(e,t,n){"use strict";function r(e){for(;e._hostParent;)e=e._hostParent;var t=p.getNodeFromInstance(e),n=t.parentNode;return p.getClosestInstanceFromNode(n)}function o(e,t){this.topLevelType=e,this.nativeEvent=t,this.ancestors=[]}function i(e){var t=f(e.nativeEvent),n=p.getClosestInstanceFromNode(t),o=n;do{e.ancestors.push(o),o=o&&r(o)}while(o);for(var i=0;i/," "+i.CHECKSUM_ATTR_NAME+'="'+t+'"$&')},canReuseMarkup:function(e,t){var n=t.getAttribute(i.CHECKSUM_ATTR_NAME);return n=n&&parseInt(n,10),r(e)===n}};t.exports=i},{92:92}],60:[function(e,t,n){"use strict";function r(e,t){for(var n=Math.min(e.length,t.length),r=0;r.":"function"==typeof t?" Instead of passing a class like Foo, pass React.createElement(Foo) or .":null!=t&&void 0!==t.props?" This may be caused by unintentionally loading two independent copies of React.":"");var a,s=v.createElement(F,{child:t});if(e){var u=E.get(e);a=u._processChildContext(u._context)}else a=P;var c=d(n);if(c){var p=c._currentElement,h=p.props.child;if(M(h,t)){var m=c._renderedComponent.getPublicInstance(),g=r&&function(){r.call(m)};return j._updateRootComponent(c,s,a,n,g),m}j.unmountComponentAtNode(n)}var y=o(n),_=y&&!!i(y),C=l(n),b=_&&!c&&!C,x=j._renderNewRootComponent(s,n,b,a)._renderedComponent.getPublicInstance();return r&&r.call(x),x},render:function(e,t,n){return j._renderSubtreeIntoContainer(null,e,t,n)},unmountComponentAtNode:function(e){c(e)||f("40");var t=d(e);return t?(delete L[t._instance.rootID],k.batchedUpdates(u,t,e,!1),!0):(l(e),1===e.nodeType&&e.hasAttribute(O),!1)},_mountImageIntoNode:function(e,t,n,i,a){if(c(t)||f("41"),i){var s=o(t);if(x.canReuseMarkup(e,s))return void y.precacheNode(n,s);var u=s.getAttribute(x.CHECKSUM_ATTR_NAME);s.removeAttribute(x.CHECKSUM_ATTR_NAME);var l=s.outerHTML;s.setAttribute(x.CHECKSUM_ATTR_NAME,u);var p=e,d=r(p,l),m=" (client) "+p.substring(d-20,d+20)+"\n (server) "+l.substring(d-20,d+20);t.nodeType===A&&f("42",m)}if(t.nodeType===A&&f("43"),a.useCreateElement){for(;t.lastChild;)t.removeChild(t.lastChild);h.insertTreeBefore(t,e,null)}else N(t,e),y.precacheNode(n,t.firstChild)}};t.exports=j},{108:108,11:11,112:112,114:114,116:116,119:119,120:120,130:130,137:137,142:142,25:25,33:33,34:34,36:36,53:53,57:57,58:58,59:59,66:66,70:70,71:71,9:9}],61:[function(e,t,n){"use strict";function r(e,t,n){return{type:"INSERT_MARKUP",content:e,fromIndex:null,fromNode:null,toIndex:n,afterNode:t}}function o(e,t,n){return{type:"MOVE_EXISTING",content:null,fromIndex:e._mountIndex,fromNode:d.getHostNode(e),toIndex:n,afterNode:t}}function i(e,t){return{type:"REMOVE_NODE",content:null,fromIndex:e._mountIndex,fromNode:t,toIndex:null,afterNode:null}}function a(e){return{type:"SET_MARKUP",content:e,fromIndex:null,fromNode:null,toIndex:null,afterNode:null}}function s(e){return{type:"TEXT_CONTENT",content:e,fromIndex:null,fromNode:null,toIndex:null,afterNode:null}}function u(e,t){return t&&(e=e||[],e.push(t)),e}function l(e,t){p.processChildrenUpdates(e,t)}var c=e(112),p=e(28),d=(e(57),e(58),e(119),e(66)),f=e(26),h=(e(129),e(97)),m=(e(137),{Mixin:{_reconcilerInstantiateChildren:function(e,t,n){return f.instantiateChildren(e,t,n)},_reconcilerUpdateChildren:function(e,t,n,r,o,i){var a;return a=h(t,0),f.updateChildren(e,a,n,r,o,this,this._hostContainerInfo,i,0),a},mountChildren:function(e,t,n){var r=this._reconcilerInstantiateChildren(e,t,n);this._renderedChildren=r;var o=[],i=0;for(var a in r)if(r.hasOwnProperty(a)){var s=r[a],u=d.mountComponent(s,t,this,this._hostContainerInfo,n,0);s._mountIndex=i++,o.push(u)}return o},updateTextContent:function(e){var t=this._renderedChildren;f.unmountChildren(t,!1);for(var n in t)t.hasOwnProperty(n)&&c("118");l(this,[s(e)])},updateMarkup:function(e){var t=this._renderedChildren;f.unmountChildren(t,!1);for(var n in t)t.hasOwnProperty(n)&&c("118");l(this,[a(e)])},updateChildren:function(e,t,n){this._updateChildren(e,t,n)},_updateChildren:function(e,t,n){var r=this._renderedChildren,o={},i=[],a=this._reconcilerUpdateChildren(r,e,i,o,t,n);if(a||r){var s,c=null,p=0,f=0,h=0,m=null;for(s in a)if(a.hasOwnProperty(s)){var v=r&&r[s],g=a[s];v===g?(c=u(c,this.moveChild(v,m,p,f)),f=Math.max(v._mountIndex,f),v._mountIndex=p):(v&&(f=Math.max(v._mountIndex,f)),c=u(c,this._mountChildAtIndex(g,i[h],m,p,t,n)),h++),p++,m=d.getHostNode(g)}for(s in o)o.hasOwnProperty(s)&&(c=u(c,this._unmountChild(r[s],o[s])));c&&l(this,c),this._renderedChildren=a}},unmountChildren:function(e){var t=this._renderedChildren;f.unmountChildren(t,e),this._renderedChildren=null},moveChild:function(e,t,n,r){if(e._mountIndex0&&r.length<20?n+" (keys: "+r.join(", ")+")":n}function i(e,t){var n=s.get(e);return n||null}var a=e(112),s=(e(119),e(57)),u=(e(58),e(71)),l=(e(137),e(142),{isMounted:function(e){var t=s.get(e);return!!t&&!!t._renderedComponent},enqueueCallback:function(e,t,n){l.validateCallback(t,n);var o=i(e);if(!o)return null;o._pendingCallbacks?o._pendingCallbacks.push(t):o._pendingCallbacks=[t],r(o)},enqueueCallbackInternal:function(e,t){e._pendingCallbacks?e._pendingCallbacks.push(t):e._pendingCallbacks=[t],r(e)},enqueueForceUpdate:function(e){var t=i(e,"forceUpdate");t&&(t._pendingForceUpdate=!0,r(t))},enqueueReplaceState:function(e,t,n){var o=i(e,"replaceState");o&&(o._pendingStateQueue=[t],o._pendingReplaceState=!0,void 0!==n&&null!==n&&(l.validateCallback(n,"replaceState"),o._pendingCallbacks?o._pendingCallbacks.push(n):o._pendingCallbacks=[n]),r(o))},enqueueSetState:function(e,t){var n=i(e,"setState");n&&((n._pendingStateQueue||(n._pendingStateQueue=[])).push(t),r(n))},enqueueElementInternal:function(e,t,n){e._pendingElement=t,e._context=n,r(e)},validateCallback:function(e,t){e&&"function"!=typeof e&&a("122",t,o(e))}});t.exports=l},{112:112,119:119,137:137,142:142,57:57,58:58,71:71}],71:[function(e,t,n){"use strict";function r(){P.ReactReconcileTransaction&&b||c("123")}function o(){this.reinitializeTransaction(),this.dirtyComponentsLength=null,this.callbackQueue=d.getPooled(),this.reconcileTransaction=P.ReactReconcileTransaction.getPooled(!0)}function i(e,t,n,o,i,a){return r(),b.batchedUpdates(e,t,n,o,i,a)}function a(e,t){return e._mountOrder-t._mountOrder}function s(e){var t=e.dirtyComponentsLength;t!==g.length&&c("124",t,g.length),g.sort(a),y++;for(var n=0;n]/;t.exports=o},{}],96:[function(e,t,n){"use strict";function r(e){if(null==e)return null;if(1===e.nodeType)return e;var t=a.get(e);if(t)return t=s(t),t?i.getNodeFromInstance(t):null;"function"==typeof e.render?o("44"):o("45",Object.keys(e))}var o=e(112),i=(e(119),e(33)),a=e(57),s=e(103);e(137),e(142);t.exports=r},{103:103,112:112,119:119,137:137,142:142,33:33,57:57}],97:[function(e,t,n){(function(n){"use strict";function r(e,t,n,r){if(e&&"object"==typeof e){var o=e;void 0===o[n]&&null!=t&&(o[n]=t)}}function o(e,t){if(null==e)return e;var n={};return i(e,r,n),n}var i=(e(22),e(117));e(142);void 0!==n&&n.env,t.exports=o}).call(this,void 0)},{117:117,142:142,22:22}],98:[function(e,t,n){"use strict";function r(e,t,n){Array.isArray(e)?e.forEach(t,n):e&&t.call(n,e)}t.exports=r},{}],99:[function(e,t,n){"use strict";function r(e){var t,n=e.keyCode;return"charCode"in e?0===(t=e.charCode)&&13===n&&(t=13):t=n,t>=32||13===t?t:0}t.exports=r},{}],100:[function(e,t,n){"use strict";function r(e){if(e.key){var t=i[e.key]||e.key;if("Unidentified"!==t)return t}if("keypress"===e.type){var n=o(e);return 13===n?"Enter":String.fromCharCode(n)}return"keydown"===e.type||"keyup"===e.type?a[e.keyCode]||"Unidentified":""}var o=e(99),i={Esc:"Escape",Spacebar:" ",Left:"ArrowLeft",Up:"ArrowUp",Right:"ArrowRight",Down:"ArrowDown",Del:"Delete",Win:"OS",Menu:"ContextMenu",Apps:"ContextMenu",Scroll:"ScrollLock",MozPrintableKey:"Unidentified"},a={8:"Backspace",9:"Tab",12:"Clear",13:"Enter",16:"Shift",17:"Control",18:"Alt",19:"Pause",20:"CapsLock",27:"Escape",32:" ",33:"PageUp",34:"PageDown",35:"End",36:"Home",37:"ArrowLeft",38:"ArrowUp",39:"ArrowRight",40:"ArrowDown",45:"Insert",46:"Delete",112:"F1",113:"F2",114:"F3",115:"F4",116:"F5",117:"F6",118:"F7",119:"F8",120:"F9",121:"F10",122:"F11",123:"F12",144:"NumLock",145:"ScrollLock",224:"Meta"};t.exports=r},{99:99}],101:[function(e,t,n){"use strict";function r(e){var t=this,n=t.nativeEvent;if(n.getModifierState)return n.getModifierState(e);var r=i[e];return!!r&&!!n[r]}function o(e){return r}var i={Alt:"altKey",Control:"ctrlKey",Meta:"metaKey",Shift:"shiftKey"};t.exports=o},{}],102:[function(e,t,n){"use strict";function r(e){var t=e.target||e.srcElement||window;return t.correspondingUseElement&&(t=t.correspondingUseElement),3===t.nodeType?t.parentNode:t}t.exports=r},{}],103:[function(e,t,n){"use strict";function r(e){for(var t;(t=e._renderedNodeType)===o.COMPOSITE;)e=e._renderedComponent;return t===o.HOST?e._renderedComponent:t===o.EMPTY?null:void 0}var o=e(62);t.exports=r},{62:62}],104:[function(e,t,n){"use strict";function r(e){var t=e&&(o&&e[o]||e[i]);if("function"==typeof t)return t}var o="function"==typeof Symbol&&Symbol.iterator,i="@@iterator";t.exports=r},{}],105:[function(e,t,n){"use strict";function r(e){for(;e&&e.firstChild;)e=e.firstChild;return e}function o(e){for(;e;){if(e.nextSibling)return e.nextSibling;e=e.parentNode}}function i(e,t){for(var n=r(e),i=0,a=0;n;){if(3===n.nodeType){if(a=i+n.textContent.length,i<=t&&a>=t)return{node:n,offset:t-i};i=a}n=r(o(n))}}t.exports=i},{}],106:[function(e,t,n){"use strict";function r(){return!i&&o.canUseDOM&&(i="textContent"in document.documentElement?"textContent":"innerText"),i}var o=e(123),i=null;t.exports=r},{123:123}],107:[function(e,t,n){"use strict";function r(e,t){var n={};return n[e.toLowerCase()]=t.toLowerCase(),n["Webkit"+e]="webkit"+t,n["Moz"+e]="moz"+t,n["ms"+e]="MS"+t,n["O"+e]="o"+t.toLowerCase(),n}function o(e){if(s[e])return s[e];if(!a[e])return e;var t=a[e];for(var n in t)if(t.hasOwnProperty(n)&&n in u)return s[e]=t[n];return""}var i=e(123),a={animationend:r("Animation","AnimationEnd"),animationiteration:r("Animation","AnimationIteration"),animationstart:r("Animation","AnimationStart"),transitionend:r("Transition","TransitionEnd")},s={},u={};i.canUseDOM&&(u=document.createElement("div").style,"AnimationEvent"in window||(delete a.animationend.animation,delete a.animationiteration.animation,delete a.animationstart.animation),"TransitionEvent"in window||delete a.transitionend.transition),t.exports=o},{123:123}],108:[function(e,t,n){"use strict";function r(e){if(e){var t=e.getName();if(t)return" Check the render method of `"+t+"`."}return""}function o(e){return"function"==typeof e&&void 0!==e.prototype&&"function"==typeof e.prototype.mountComponent&&"function"==typeof e.prototype.receiveComponent}function i(e,t){var n;if(null===e||!1===e)n=l.create(i);else if("object"==typeof e){var s=e,u=s.type;if("function"!=typeof u&&"string"!=typeof u){var d="";d+=r(s._owner),a("130",null==u?u:typeof u,d)}"string"==typeof s.type?n=c.createInternalComponent(s):o(s.type)?(n=new s.type(s),n.getHostNode||(n.getHostNode=n.getNativeNode)):n=new p(s)}else"string"==typeof e||"number"==typeof e?n=c.createInstanceForText(e):a("131",typeof e);return n._mountIndex=0,n._mountImage=null,n}var a=e(112),s=e(143),u=e(29),l=e(49),c=e(54),p=(e(121),e(137),e(142),function(e){this.construct(e)});s(p.prototype,u,{_instantiateReactComponent:i}),t.exports=i},{112:112,121:121,137:137,142:142,143:143,29:29,49:49,54:54}],109:[function(e,t,n){"use strict";function r(e,t){if(!i.canUseDOM||t&&!("addEventListener"in document))return!1;var n="on"+e,r=n in document;if(!r){var a=document.createElement("div");a.setAttribute(n,"return;"),r="function"==typeof a[n]}return!r&&o&&"wheel"===e&&(r=document.implementation.hasFeature("Events.wheel","3.0")),r}var o,i=e(123);i.canUseDOM&&(o=document.implementation&&document.implementation.hasFeature&&!0!==document.implementation.hasFeature("","")),t.exports=r},{123:123}],110:[function(e,t,n){"use strict";function r(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return"input"===t?!!o[e.type]:"textarea"===t}var o={color:!0,date:!0,datetime:!0,"datetime-local":!0,email:!0,month:!0,number:!0,password:!0,range:!0,search:!0,tel:!0,text:!0,time:!0,url:!0,week:!0};t.exports=r},{}],111:[function(e,t,n){"use strict";function r(e){return'"'+o(e)+'"'}var o=e(95);t.exports=r},{95:95}],112:[function(e,t,n){"use strict";function r(e){for(var t=arguments.length-1,n="Minified React error #"+e+"; visit http://facebook.github.io/react/docs/error-decoder.html?invariant="+e,r=0;r]/,u=e(93),l=u(function(e,t){if(e.namespaceURI!==i.svg||"innerHTML"in e)e.innerHTML=t;else{r=r||document.createElement("div"),r.innerHTML=""+t+"";for(var n=r.firstChild;n.firstChild;)e.appendChild(n.firstChild)}});if(o.canUseDOM){var c=document.createElement("div");c.innerHTML=" ",""===c.innerHTML&&(l=function(e,t){if(e.parentNode&&e.parentNode.replaceChild(e,e),a.test(t)||"<"===t[0]&&s.test(t)){e.innerHTML=String.fromCharCode(65279)+t;var n=e.firstChild;1===n.data.length?e.removeChild(n):n.deleteData(0,1)}else e.innerHTML=t}),c=null}t.exports=l},{10:10,123:123,93:93}],115:[function(e,t,n){"use strict";var r=e(123),o=e(95),i=e(114),a=function(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&3===n.nodeType)return void(n.nodeValue=t)}e.textContent=t};r.canUseDOM&&("textContent"in document.documentElement||(a=function(e,t){if(3===e.nodeType)return void(e.nodeValue=t);i(e,o(t))})),t.exports=a},{114:114,123:123,95:95}],116:[function(e,t,n){"use strict";function r(e,t){var n=null===e||!1===e,r=null===t||!1===t;if(n||r)return n===r;var o=typeof e,i=typeof t;return"string"===o||"number"===o?"string"===i||"number"===i:"object"===i&&e.type===t.type&&e.key===t.key}t.exports=r},{}],117:[function(e,t,n){"use strict";function r(e,t){return e&&"object"==typeof e&&null!=e.key?l.escape(e.key):t.toString(36)}function o(e,t,n,i){var d=typeof e;if("undefined"!==d&&"boolean"!==d||(e=null),null===e||"string"===d||"number"===d||"object"===d&&e.$$typeof===s)return n(i,e,""===t?c+r(e,0):t),1;var f,h,m=0,v=""===t?c:t+p;if(Array.isArray(e))for(var g=0;g":"<"+e+">",s[e]=!a.firstChild),s[e]?d[e]:null}var o=e(123),i=e(137),a=o.canUseDOM?document.createElement("div"):null,s={},u=[1,'"],l=[1,"","
"],c=[3,"","
"],p=[1,'',""],d={"*":[1,"?
","
"],area:[1,"",""],col:[2,"","
"],legend:[1,"
","
"],param:[1,"",""],tr:[2,"","
"],optgroup:u,option:u,caption:l,colgroup:l,tbody:l,tfoot:l,thead:l,td:c,th:c};["circle","clipPath","defs","ellipse","g","image","line","linearGradient","mask","path","pattern","polygon","polyline","radialGradient","rect","stop","text","tspan"].forEach(function(e){d[e]=p,s[e]=!0}),t.exports=r},{123:123,137:137}],134:[function(e,t,n){"use strict";function r(e){return e.Window&&e instanceof e.Window?{x:e.pageXOffset||e.document.documentElement.scrollLeft,y:e.pageYOffset||e.document.documentElement.scrollTop}:{x:e.scrollLeft,y:e.scrollTop}}t.exports=r},{}],135:[function(e,t,n){"use strict";function r(e){return e.replace(o,"-$1").toLowerCase()}var o=/([A-Z])/g;t.exports=r},{}],136:[function(e,t,n){"use strict";function r(e){return o(e).replace(i,"-ms-")}var o=e(135),i=/^ms-/;t.exports=r},{135:135}],137:[function(e,t,n){"use strict";function r(e,t,n,r,i,a,s,u){if(o(t),!e){var l;if(void 0===t)l=new Error("Minified exception occurred; use the non-minified dev environment for the full error message and additional helpful warnings.");else{var c=[n,r,i,a,s,u],p=0;l=new Error(t.replace(/%s/g,function(){return c[p++]})),l.name="Invariant Violation"}throw l.framesToPop=1,l}}var o=function(e){};t.exports=r},{}],138:[function(e,t,n){"use strict";function r(e){var t=e?e.ownerDocument||e:document,n=t.defaultView||window;return!(!e||!("function"==typeof n.Node?e instanceof n.Node:"object"==typeof e&&"number"==typeof e.nodeType&&"string"==typeof e.nodeName))}t.exports=r},{}],139:[function(e,t,n){"use strict";function r(e){return o(e)&&3==e.nodeType}var o=e(138);t.exports=r},{138:138}],140:[function(e,t,n){"use strict";function r(e){var t={};return function(n){return t.hasOwnProperty(n)||(t[n]=e.call(this,n)),t[n]}}t.exports=r},{}],141:[function(e,t,n){"use strict";function r(e,t){return e===t?0!==e||0!==t||1/e==1/t:e!==e&&t!==t}function o(e,t){if(r(e,t))return!0;if("object"!=typeof e||null===e||"object"!=typeof t||null===t)return!1;var n=Object.keys(e),o=Object.keys(t);if(n.length!==o.length)return!1;for(var a=0;a 0x10FFFF || // not a valid Unicode code point floor(codePoint) != codePoint // not an integer ) { throw RangeError('Invalid code point: ' + codePoint); } if (codePoint <= 0xFFFF) { // BMP code point codeUnits.push(codePoint); } else { // Astral code point; split in surrogate halves // http://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae codePoint -= 0x10000; highSurrogate = (codePoint >> 10) + 0xD800; lowSurrogate = (codePoint % 0x400) + 0xDC00; codeUnits.push(highSurrogate, lowSurrogate); } if (index + 1 == length || codeUnits.length > MAX_SIZE) { result += stringFromCharCode.apply(null, codeUnits); codeUnits.length = 0; } } return result; }; if (defineProperty) { defineProperty(String, 'fromCodePoint', { 'value': fromCodePoint, 'configurable': true, 'writable': true }); } else { String.fromCodePoint = fromCodePoint; } }()); } /*! http://mths.be/codepointat v0.1.0 by @mathias */ if (!String.prototype.codePointAt) { (function() { 'use strict'; // needed to support `apply`/`call` with `undefined`/`null` var codePointAt = function(position) { if (this == null) { throw TypeError(); } var string = String(this); var size = string.length; // `ToInteger` var index = position ? Number(position) : 0; if (index != index) { // better `isNaN` index = 0; } // Account for out-of-bounds indices: if (index < 0 || index >= size) { return undefined; } // Get the first code unit var first = string.charCodeAt(index); var second; if ( // check if it’s the start of a surrogate pair first >= 0xD800 && first <= 0xDBFF && // high surrogate size > index + 1 // there is a next code unit ) { second = string.charCodeAt(index + 1); if (second >= 0xDC00 && second <= 0xDFFF) { // low surrogate // http://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae return (first - 0xD800) * 0x400 + second - 0xDC00 + 0x10000; } } return first; }; if (Object.defineProperty) { Object.defineProperty(String.prototype, 'codePointAt', { 'value': codePointAt, 'configurable': true, 'writable': true }); } else { String.prototype.codePointAt = codePointAt; } }()); } function registerAsciinemaPlayerElement() { var AsciinemaPlayerProto = Object.create(HTMLElement.prototype); function merge() { var merged = {}; for (var i=0; i>>0),ma=0;function na(a,b,c){return a.call.apply(a.bind,arguments)} function oa(a,b,c){if(!a)throw Error();if(2b?1:0};var ua=Array.prototype.indexOf?function(a,b,c){return Array.prototype.indexOf.call(a,b,c)}:function(a,b,c){c=null==c?0:0>c?Math.max(0,a.length+c):c;if(ca(a))return ca(b)&&1==b.length?a.indexOf(b,c):-1;for(;cb?null:ca(a)?a.charAt(b):a[b]}function ya(a,b){var c=ua(a,b),d;(d=0<=c)&&Array.prototype.splice.call(a,c,1);return d}function za(a,b){a.sort(b||Aa)}function Ca(a,b){for(var c=Array(a.length),d=0;db?1:a2*this.Fc&&Na(this),!0):!1};function Na(a){if(a.Fc!=a.ib.length){for(var b=0,c=0;ba){var b=Ra[a];if(b)return b}b=new Qa([a|0],0>a?-1:0);-128<=a&&128>a&&(Ra[a]=b);return b}function Ta(a){if(isNaN(a)||!isFinite(a))return Ua;if(0>a)return Ta(-a).kb();for(var b=[],c=1,d=0;a>=c;d++)b[d]=a/c|0,c*=Va;return new Qa(b,0)}var Va=4294967296,Ua=Sa(0),Wa=Sa(1),Xa=Sa(16777216);g=Qa.prototype; g.Of=function(){return 0a||36>>0).toString(a);c=e;if(c.hc())return f+d;for(;6>f.length;)f="0"+f;d=""+f+d}};function Ya(a,b){return 0>b?0:bthis.compare(Xa)};g.Ve=function(a){return 0>=this.compare(a)};g.compare=function(a){a=this.ze(a);return a.Eb()?-1:a.hc()?0:1};g.kb=function(){return this.Hf().add(Wa)}; g.add=function(a){for(var b=Math.max(this.Ma.length,a.Ma.length),c=[],d=0,e=0;e<=b;e++){var f=d+(Ya(this,e)&65535)+(Ya(a,e)&65535),h=(f>>>16)+(Ya(this,e)>>>16)+(Ya(a,e)>>>16);d=h>>>16;f&=65535;h&=65535;c[e]=h<<16|f}return new Qa(c,c[c.length-1]&-2147483648?-1:0)};g.ze=function(a){return this.add(a.kb())}; g.multiply=function(a){if(this.hc()||a.hc())return Ua;if(this.Eb())return a.Eb()?this.kb().multiply(a.kb()):this.kb().multiply(a).kb();if(a.Eb())return this.multiply(a.kb()).kb();if(this.Ue()&&a.Ue())return Ta(this.vd()*a.vd());for(var b=this.Ma.length+a.Ma.length,c=[],d=0;d<2*b;d++)c[d]=0;for(d=0;d>>16,h=Ya(this,d)&65535,k=Ya(a,e)>>>16,l=Ya(a,e)&65535;c[2*d+2*e]+=h*l;ab(c,2*d+2*e);c[2*d+2*e+1]+=f*l;ab(c,2*d+2*e+1);c[2*d+2*e+1]+= h*k;ab(c,2*d+2*e+1);c[2*d+2*e+2]+=f*k;ab(c,2*d+2*e+2)}for(d=0;d>>16,a[b]&=65535,b++} function Za(a,b){if(b.hc())throw Error("division by zero");if(a.hc())return Ua;if(a.Eb())return b.Eb()?Za(a.kb(),b.kb()):Za(a.kb(),b).kb();if(b.Eb())return Za(a,b.kb()).kb();if(30=f?1:Math.pow(2,f-48);h=Ta(e);for(var k=h.multiply(b);k.Eb()||k.xf(d);)e-=f,h=Ta(e),k=h.multiply(b);h.hc()&&(h=Wa);c=c.add(h);d=d.ze(k)}return c}g.Hf=function(){for(var a=this.Ma.length,b=[],c=0;c>5;a%=32;for(var c=this.Ma.length+b+(0>>32-a:Ya(this,e-b);return new Qa(d,this.Lc)}; g.ad=function(a){var b=a>>5;a%=32;for(var c=this.Ma.length-b,d=[],e=0;e>>a|Ya(this,e+b+1)<<32-a:Ya(this,e+b);return new Qa(d,this.Lc)};function cb(a,b){null!=a&&this.append.apply(this,arguments)}g=cb.prototype;g.xc="";g.set=function(a){this.xc=""+a};g.append=function(a,b,c){this.xc+=String(a);if(null!=b)for(var d=1;d>>16&65535)*d+c*(b>>>16&65535)<<16>>>0)|0};function hd(a){a=gd(a|0,-862048943);return gd(a<<15|a>>>-15,461845907)} function id(a,b){var c=(a|0)^(b|0);return gd(c<<13|c>>>-13,5)+-430675100|0}function jd(a,b){var c=(a|0)^b;c=gd(c^c>>>16,-2048144789);c=gd(c^c>>>13,-1028477387);return c^c>>>16}function kd(a){a:{var b=1;for(var c=0;;)if(b>2)}function qd(a){return a instanceof rd} function sd(a,b){if(a.Zb===b.Zb)return 0;var c=wb(a.fb);if(t(c?b.fb:c))return-1;if(t(a.fb)){if(wb(b.fb))return 1;c=Aa(a.fb,b.fb);return 0===c?Aa(a.name,b.name):c}return Aa(a.name,b.name)}function rd(a,b,c,d,e){this.fb=a;this.name=b;this.Zb=c;this.Oc=d;this.hb=e;this.m=2154168321;this.J=4096}g=rd.prototype;g.toString=function(){return this.Zb};g.equiv=function(a){return this.K(null,a)};g.K=function(a,b){return b instanceof rd?this.Zb===b.Zb:!1}; g.call=function(){var a=null;a=function(a,c,d){switch(arguments.length){case 2:return D.c(c,this);case 3:return D.l(c,this,d)}throw Error("Invalid arity: "+(arguments.length-1));};a.c=function(a,c){return D.c(c,this)};a.l=function(a,c,d){return D.l(c,this,d)};return a}();g.apply=function(a,b){return this.call.apply(this,[this].concat(Gb(b)))};g.h=function(a){return D.c(a,this)};g.c=function(a,b){return D.l(a,this,b)};g.P=function(){return this.hb}; g.T=function(a,b){return new rd(this.fb,this.name,this.Zb,this.Oc,b)};g.U=function(){var a=this.Oc;return null!=a?a:this.Oc=a=pd(kd(this.name),nd(this.fb))};g.hd=function(){return this.name};g.jd=function(){return this.fb};g.R=function(a,b){return Jc(b,this.Zb)};var td=function td(a){switch(arguments.length){case 1:return td.h(arguments[0]);case 2:return td.c(arguments[0],arguments[1]);default:throw Error(["Invalid arity: ",v.h(arguments.length)].join(""));}}; td.h=function(a){if(a instanceof rd)return a;var b=a.indexOf("/");return 1>b?td.c(null,a):td.c(a.substring(0,b),a.substring(b+1,a.length))};td.c=function(a,b){var c=null!=a?[v.h(a),"/",v.h(b)].join(""):b;return new rd(a,b,c,null,null)};td.L=2;function ud(a){return null!=a?a.J&131072||q===a.Tf?!0:a.J?!1:Ab(cd,a):Ab(cd,a)} function E(a){if(null==a)return null;if(null!=a&&(a.m&8388608||q===a.Pe))return a.S(null);if(vb(a)||"string"===typeof a)return 0===a.length?null:new Jb(a,0,null);if(Ab(Bc,a))return Cc(a);throw Error([v.h(a)," is not ISeqable"].join(""));}function y(a){if(null==a)return null;if(null!=a&&(a.m&64||q===a.G))return a.Ia(null);a=E(a);return null==a?null:Wb(a)}function vd(a){return null!=a?null!=a&&(a.m&64||q===a.G)?a.bb(null):(a=E(a))?Yb(a):wd:wd} function z(a){return null==a?null:null!=a&&(a.m&128||q===a.Id)?a.Ka(null):E(vd(a))}var G=function G(a){switch(arguments.length){case 1:return G.h(arguments[0]);case 2:return G.c(arguments[0],arguments[1]);default:for(var c=[],d=arguments.length,e=0;;)if(e=d)return-1;!(0c&&(c+=d,c=0>c?0:c);for(;;)if(cc?d+c:c;for(;;)if(0<=c){if(G.c(Vd(a,c),b))return c;--c}else return-1}function Yd(a,b){this.o=a;this.i=b} Yd.prototype.ja=function(){return this.ia?0:a};g.Rc=function(){var a=this.W(null);return 0d)c=1;else if(0===c)c=0;else a:for(d=0;;){var e=Ke(Vd(a,d),Vd(b,d));if(0===e&&d+1>1&1431655765;a=(a&858993459)+(a>>2&858993459);return 16843009*(a+(a>>4)&252645135)>>24} var v=function v(a){switch(arguments.length){case 0:return v.B();case 1:return v.h(arguments[0]);default:for(var c=[],d=arguments.length,e=0;;)if(ed:e))c[d]=a.next(),d+=1;else return qf(new nf(c,0,d),Rf.h?Rf.h(a):Rf.call(null,a))}else return null},null,null)};function Sf(a,b,c,d,e,f){this.buffer=a;this.ub=b;this.pe=c;this.Rb=d;this.ye=e;this.Gf=f} Sf.prototype.step=function(){if(this.ub!==Nf)return!0;for(;;)if(this.ub===Nf)if(this.buffer.Td()){if(this.pe)return!1;if(this.ye.ja()){if(this.Gf)var a=P(this.Rb,ae(null,this.ye.next()));else a=this.ye.next(),a=this.Rb.c?this.Rb.c(null,a):this.Rb.call(null,null,a);Hd(a)&&(this.Rb.h?this.Rb.h(null):this.Rb.call(null,null),this.pe=!0)}else this.Rb.h?this.Rb.h(null):this.Rb.call(null,null),this.pe=!0}else this.ub=this.buffer.remove();else return!0};Sf.prototype.ja=function(){return this.step()}; Sf.prototype.next=function(){if(this.ja()){var a=this.ub;this.ub=Nf;return a}throw Error("No such element");};Sf.prototype.remove=function(){return Error("Unsupported operation")};Sf.prototype[Fb]=function(){return yd(this)}; function Tf(a,b){var c=new Sf(Qf,Nf,!1,null,b,!1);c.Rb=function(){var b=function(a){return function(){function b(b,c){a.buffer=a.buffer.add(c);return b}var c=null;c=function(a,c){switch(arguments.length){case 0:return null;case 1:return a;case 2:return b.call(this,a,c)}throw Error("Invalid arity: "+(arguments.length-1));};c.B=function(){return null};c.h=function(a){return a};c.c=b;return c}()}(c);return a.h?a.h(b):a.call(null,b)}();return c} function Uf(a,b){var c=Kf(b);c=Tf(a,c);c=Rf(c);return t(c)?c:wd}function Vf(a,b){for(;;){if(null==E(b))return!0;var c=y(b);c=a.h?a.h(c):a.call(null,c);if(t(c)){c=a;var d=z(b);a=c;b=d}else return!1}}function Wf(a,b){for(;;)if(E(b)){var c=y(b);c=a.h?a.h(c):a.call(null,c);if(t(c))return c;c=a;var d=z(b);a=c;b=d}else return null}function Xf(a){if(Ge(a))return 0===(a&1);throw Error(["Argument must be an integer: ",v.h(a)].join(""));} function Yf(a){return function(){function b(b,c){return wb(a.c?a.c(b,c):a.call(null,b,c))}function c(b){return wb(a.h?a.h(b):a.call(null,b))}function d(){return wb(a.B?a.B():a.call(null))}var e=null,f=function(){function b(a,b,d){var e=null;if(2a?0:a-1>>>5<<5}function Jg(a,b,c){for(;;){if(0===b)return c;var d=Gg(a);d.o[0]=c;c=d;b-=5}} var Kg=function Kg(a,b,c,d){var f=Hg(c),h=a.F-1>>>b&31;5===b?f.o[h]=d:(c=c.o[h],null!=c?(b-=5,a=Kg.M?Kg.M(a,b,c,d):Kg.call(null,a,b,c,d)):a=Jg(null,b-5,d),f.o[h]=a);return f};function Lg(a,b){throw Error(["No item ",v.h(a)," in vector of length ",v.h(b)].join(""));}function Mg(a,b){if(b>=Ig(a))return a.fa;for(var c=a.root,d=a.shift;;)if(0>>d&31];d=e}else return c.o} var Ng=function Ng(a,b,c,d,e){var h=Hg(c);if(0===b)h.o[d&31]=e;else{var k=d>>>b&31;b-=5;c=c.o[k];a=Ng.Z?Ng.Z(a,b,c,d,e):Ng.call(null,a,b,c,d,e);h.o[k]=a}return h},Og=function Og(a,b,c){var e=a.F-2>>>b&31;if(5=this.F)a=new Jb(this.fa,0,null);else{a:{a=this.root;for(var b=this.shift;;)if(0this.F-Ig(this)){for(var c=this.fa.length,d=Array(c+1),e=0;;)if(e>>5>1<b)return new R(null,b,5,T,a,null);for(var c=32,d=(new R(null,32,5,T,a.slice(0,32),null)).Pc(null);;)if(cb||this.end<=this.start+b?Lg(b,this.end-this.start):A.c(this.Ja,this.start+b)};g.ka=function(a,b,c){return 0>b||this.end<=this.start+b?c:A.l(this.Ja,this.start+b,c)}; g.dc=function(a,b,c){a=this.start+b;if(0>b||this.end+1<=a)throw Error(["Index ",v.h(b)," out of bounds [0,",v.h(this.W(null)),"]"].join(""));b=this.meta;c=K.l(this.Ja,a,c);var d=this.end;a+=1;return Zg(b,c,this.start,d>a?d:a,null)};g.ba=function(){return null!=this.Ja&&q===this.Ja.fe?Qg(this.Ja,this.start,this.end):new Jf(Hf,this)};g.P=function(){return this.meta};g.W=function(){return this.end-this.start};g.Ac=function(){return A.c(this.Ja,this.end-1)}; g.Bc=function(){if(this.start===this.end)throw Error("Can't pop empty vector");return Zg(this.meta,this.Ja,this.start,this.end-1,null)};g.Rc=function(){return this.start!==this.end?new Zd(this,this.end-this.start-1,null):null};g.U=function(){var a=this.w;return null!=a?a:this.w=a=Ad(this)};g.K=function(a,b){return $d(this,b)};g.oa=function(){return tc(he,this.meta)};g.Fa=function(a,b){return null!=this.Ja&&q===this.Ja.fe?Rg(this.Ja,b,this.start,this.end):Kd(this,b)}; g.Ga=function(a,b,c){return null!=this.Ja&&q===this.Ja.fe?Sg(this.Ja,b,c,this.start,this.end):Ld(this,b,c)};g.O=function(a,b,c){if("number"===typeof b)return this.dc(null,b,c);throw Error("Subvec's key for assoc must be a number.");};g.S=function(){var a=this;return function(b){return function e(d){return d===a.end?null:ae(A.c(a.Ja,d),new kf(null,function(){return function(){return e(d+1)}}(b),null,null))}}(this)(a.start)};g.T=function(a,b){return Zg(b,this.Ja,this.start,this.end,this.w)}; g.X=function(a,b){return Zg(this.meta,qc(this.Ja,this.end,b),this.start,this.end+1,null)};g.call=function(){var a=null;a=function(a,c,d){switch(arguments.length){case 2:return this.$(null,c);case 3:return this.ka(null,c,d)}throw Error("Invalid arity: "+(arguments.length-1));};a.c=function(a,c){return this.$(null,c)};a.l=function(a,c,d){return this.ka(null,c,d)};return a}();g.apply=function(a,b){return this.call.apply(this,[this].concat(Gb(b)))};g.h=function(a){return this.$(null,a)}; g.c=function(a,b){return this.ka(null,a,b)};Yg.prototype[Fb]=function(){return yd(this)};function Zg(a,b,c,d,e){for(;;)if(b instanceof Yg)c=b.start+c,d=b.start+d,b=b.Ja;else{if(!ze(b))throw Error("v must satisfy IVector");var f=H(b);if(0>c||0>d||c>f||d>f)throw Error("Index out of bounds");return new Yg(a,b,c,d,e)}}function $g(a,b){return a===b.la?b:new Fg(a,Gb(b.o))} var ah=function ah(a,b,c,d){c=$g(a.root.la,c);var f=a.F-1>>>b&31;if(5===b)a=d;else{var h=c.o[f];null!=h?(b-=5,a=ah.M?ah.M(a,b,h,d):ah.call(null,a,b,h,d)):a=Jg(a.root.la,b-5,d)}c.o[f]=a;return c};function Tg(a,b,c,d){this.F=a;this.shift=b;this.root=c;this.fa=d;this.J=88;this.m=275}g=Tg.prototype; g.Dc=function(a,b){if(this.root.la){if(32>this.F-Ig(this))this.fa[this.F&31]=b;else{var c=new Fg(this.root.la,this.fa),d=[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null];d[0]=b;this.fa=d;if(this.F>>>5>1<>>d&31,m=k(d-5,f.o[p]);f.o[p]=m}return f}}(a)(a.shift,a.root)}();a.root=d}return a}if(b===a.F)return a.Dc(null,c);throw Error(["Index ",v.h(b)," out of bounds for TransientVector of length",v.h(a.F)].join(""));}throw Error("assoc! after persistent!");} g.W=function(){if(this.root.la)return this.F;throw Error("count after persistent!");};g.$=function(a,b){if(this.root.la)return(0<=b&&b=c)return new r(this.meta,this.F-1,d,null);G.c(b,this.o[e])||(d[f]=this.o[e],d[f+1]=this.o[e+1],f+=2);e+=2}}else return this}; g.O=function(a,b,c){a=ih(this.o,b);if(-1===a){if(this.Fb?4:2*(b+1));Be(this.o,0,c,0,2*b);return new xh(a,this.na,c)};g.qd=function(){return yh(this.o,0,null)};g.Jc=function(a,b){return vh(this.o,a,b)};g.sc=function(a,b,c,d){var e=1<<(b>>>a&31);if(0===(this.na&e))return d;var f=$e(this.na&e-1);e=this.o[2*f];f=this.o[2*f+1];return null==e?f.sc(a+5,b,c,d):rh(c,e)?f:d}; g.Kb=function(a,b,c,d,e,f){var h=1<<(c>>>b&31),k=$e(this.na&h-1);if(0===(this.na&h)){var l=$e(this.na);if(2*l>>b&31]=zh.Kb(a,b+5,c,d,e,f);for(e=d=0;;)if(32>d)0!== (this.na>>>d&1)&&(k[d]=null!=this.o[e]?zh.Kb(a,b+5,od(this.o[e]),this.o[e],this.o[e+1],f):this.o[e+1],e+=2),d+=1;else break;return new Ah(a,l+1,k)}b=Array(2*(l+4));Be(this.o,0,b,0,2*k);b[2*k]=d;b[2*k+1]=e;Be(this.o,2*k,b,2*(k+1),2*(l-k));f.H=!0;a=this.Gc(a);a.o=b;a.na|=h;return a}l=this.o[2*k];h=this.o[2*k+1];if(null==l)return l=h.Kb(a,b+5,c,d,e,f),l===h?this:uh(this,a,2*k+1,l);if(rh(d,l))return e===h?this:uh(this,a,2*k+1,e);f.H=!0;f=b+5;b=od(l);if(b===c)e=new Bh(null,b,2,[l,h,d,e]);else{var p=new qh; e=zh.Kb(a,f,b,l,h,p).Kb(a,f,c,d,e,p)}d=2*k;k=2*k+1;a=this.Gc(a);a.o[d]=null;a.o[k]=e;return a}; g.Jb=function(a,b,c,d,e){var f=1<<(b>>>a&31),h=$e(this.na&f-1);if(0===(this.na&f)){var k=$e(this.na);if(16<=k){h=[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null];h[b>>>a&31]=zh.Jb(a+5,b,c,d,e);for(d=c=0;;)if(32>c)0!==(this.na>>>c&1)&&(h[c]=null!=this.o[d]?zh.Jb(a+5,od(this.o[d]),this.o[d],this.o[d+1],e):this.o[d+1],d+=2),c+=1;else break;return new Ah(null,k+1,h)}a=Array(2*(k+1));Be(this.o, 0,a,0,2*h);a[2*h]=c;a[2*h+1]=d;Be(this.o,2*h,a,2*(h+1),2*(k-h));e.H=!0;return new xh(null,this.na|f,a)}var l=this.o[2*h];f=this.o[2*h+1];if(null==l)return k=f.Jb(a+5,b,c,d,e),k===f?this:new xh(null,this.na,sh(this.o,2*h+1,k));if(rh(c,l))return d===f?this:new xh(null,this.na,sh(this.o,2*h+1,d));e.H=!0;e=this.na;k=this.o;a+=5;var p=od(l);if(p===b)c=new Bh(null,p,2,[l,f,c,d]);else{var m=new qh;c=zh.Jb(a,p,l,f,m).Jb(a,b,c,d,m)}a=2*h;h=2*h+1;d=Gb(k);d[a]=null;d[h]=c;return new xh(null,e,d)}; g.rd=function(a,b,c){var d=1<<(b>>>a&31);if(0===(this.na&d))return this;var e=$e(this.na&d-1),f=this.o[2*e],h=this.o[2*e+1];return null==f?(a=h.rd(a+5,b,c),a===h?this:null!=a?new xh(null,this.na,sh(this.o,2*e+1,a)):this.na===d?null:new xh(null,this.na^d,th(this.o,e))):rh(c,f)?new xh(null,this.na^d,th(this.o,e)):this};g.ba=function(){return new wh(this.o,0,null,null)};var zh=new xh(null,0,[]);function Ch(a,b,c){this.o=a;this.i=b;this.Lb=c} Ch.prototype.ja=function(){for(var a=this.o.length;;){if(null!=this.Lb&&this.Lb.ja())return!0;if(this.i>>a&31];return null!=e?e.sc(a+5,b,c,d):d};g.Kb=function(a,b,c,d,e,f){var h=c>>>b&31,k=this.o[h];if(null==k)return a=uh(this,a,h,zh.Kb(a,b+5,c,d,e,f)),a.F+=1,a;b=k.Kb(a,b+5,c,d,e,f);return b===k?this:uh(this,a,h,b)}; g.Jb=function(a,b,c,d,e){var f=b>>>a&31,h=this.o[f];if(null==h)return new Ah(null,this.F+1,sh(this.o,f,zh.Jb(a+5,b,c,d,e)));a=h.Jb(a+5,b,c,d,e);return a===h?this:new Ah(null,this.F,sh(this.o,f,a))}; g.rd=function(a,b,c){var d=b>>>a&31,e=this.o[d];if(null!=e){a=e.rd(a+5,b,c);if(a===e)d=this;else if(null==a)if(8>=this.F)a:{e=this.o;a=e.length;b=Array(2*(this.F-1));c=0;for(var f=1,h=0;;)if(ca?d:rh(c,this.o[a])?this.o[a+1]:d}; g.Kb=function(a,b,c,d,e,f){if(c===this.ec){b=Eh(this.o,this.F,d);if(-1===b){if(this.o.length>2*this.F)return b=2*this.F,c=2*this.F+1,a=this.Gc(a),a.o[b]=d,a.o[c]=e,f.H=!0,a.F+=1,a;c=this.o.length;b=Array(c+2);Be(this.o,0,b,0,c);b[c]=d;b[c+1]=e;f.H=!0;d=this.F+1;a===this.la?(this.o=b,this.F=d,a=this):a=new Bh(this.la,this.ec,d,b);return a}return this.o[b+1]===e?this:uh(this,a,b+1,e)}return(new xh(a,1<<(this.ec>>>b&31),[null,this,null,null])).Kb(a,b,c,d,e,f)}; g.Jb=function(a,b,c,d,e){return b===this.ec?(a=Eh(this.o,this.F,c),-1===a?(a=2*this.F,b=Array(a+2),Be(this.o,0,b,0,a),b[a]=c,b[a+1]=d,e.H=!0,new Bh(null,this.ec,this.F+1,b)):G.c(this.o[a+1],d)?this:new Bh(null,this.ec,this.F,sh(this.o,a+1,d))):(new xh(null,1<<(this.ec>>>a&31),[null,this])).Jb(a,b,c,d,e)};g.rd=function(a,b,c){a=Eh(this.o,this.F,c);return-1===a?this:1===this.F?null:new Bh(null,this.ec,this.F-1,th(this.o,Ze(a)))};g.ba=function(){return new wh(this.o,0,null,null)}; function Fh(a,b,c,d,e){this.meta=a;this.Mb=b;this.i=c;this.s=d;this.w=e;this.m=32374988;this.J=0}g=Fh.prototype;g.toString=function(){return fd(this)};g.equiv=function(a){return this.K(null,a)};g.indexOf=function(){var a=null;a=function(a,c){switch(arguments.length){case 1:return Ud(this,a,0);case 2:return Ud(this,a,c)}throw Error("Invalid arity: "+(arguments.length-1));};a.h=function(a){return Ud(this,a,0)};a.c=function(a,c){return Ud(this,a,c)};return a}(); g.lastIndexOf=function(){function a(a){return Xd(this,a,H(this))}var b=null;b=function(b,d){switch(arguments.length){case 1:return a.call(this,b);case 2:return Xd(this,b,d)}throw Error("Invalid arity: "+(arguments.length-1));};b.h=a;b.c=function(a,b){return Xd(this,a,b)};return b}();g.P=function(){return this.meta};g.Ka=function(){return null==this.s?yh(this.Mb,this.i+2,null):yh(this.Mb,this.i,z(this.s))};g.U=function(){var a=this.w;return null!=a?a:this.w=a=Ad(this)}; g.K=function(a,b){return $d(this,b)};g.oa=function(){return tc(wd,this.meta)};g.Fa=function(a,b){return ce(b,this)};g.Ga=function(a,b,c){return de(b,c,this)};g.Ia=function(){return null==this.s?new R(null,2,5,T,[this.Mb[this.i],this.Mb[this.i+1]],null):y(this.s)};g.bb=function(){var a=null==this.s?yh(this.Mb,this.i+2,null):yh(this.Mb,this.i,z(this.s));return null!=a?a:wd};g.S=function(){return this};g.T=function(a,b){return new Fh(b,this.Mb,this.i,this.s,this.w)};g.X=function(a,b){return ae(b,this)}; Fh.prototype[Fb]=function(){return yd(this)};function yh(a,b,c){if(null==c)for(c=a.length;;)if(bthis.F?H(z(this))+1:this.F};g.U=function(){var a=this.w;return null!=a?a:this.w=a=Ad(this)};g.K=function(a,b){return $d(this,b)};g.oa=function(){return tc(wd,this.meta)};g.Fa=function(a,b){return ce(b,this)};g.Ga=function(a,b,c){return de(b,c,this)};g.Ia=function(){var a=this.stack;return null==a?null:nc(a)};g.bb=function(){var a=y(this.stack);a=Mh(this.vc?a.right:a.left,z(this.stack),this.vc);return null!=a?new Nh(null,a,this.vc,this.F-1,null):wd};g.S=function(){return this}; g.T=function(a,b){return new Nh(b,this.stack,this.vc,this.F,this.w)};g.X=function(a,b){return ae(b,this)};Nh.prototype[Fb]=function(){return yd(this)};function Oh(a,b,c){return new Nh(null,Mh(a,null,b),b,c,null)} function Ph(a,b,c,d){return c instanceof Qh?c.left instanceof Qh?new Qh(c.key,c.H,c.left.bc(),new Rh(a,b,c.right,d,null),null):c.right instanceof Qh?new Qh(c.right.key,c.right.H,new Rh(c.key,c.H,c.left,c.right.left,null),new Rh(a,b,c.right.right,d,null),null):new Rh(a,b,c,d,null):new Rh(a,b,c,d,null)} function Sh(a,b,c,d){return d instanceof Qh?d.right instanceof Qh?new Qh(d.key,d.H,new Rh(a,b,c,d.left,null),d.right.bc(),null):d.left instanceof Qh?new Qh(d.left.key,d.left.H,new Rh(a,b,c,d.left.left,null),new Rh(d.key,d.H,d.left.right,d.right,null),null):new Rh(a,b,c,d,null):new Rh(a,b,c,d,null)} function Th(a,b,c,d){if(c instanceof Qh)return new Qh(a,b,c.bc(),d,null);if(d instanceof Rh)return Sh(a,b,c,d.ud());if(d instanceof Qh&&d.left instanceof Rh)return new Qh(d.left.key,d.left.H,new Rh(a,b,c,d.left.left,null),Sh(d.key,d.H,d.left.right,d.right.ud()),null);throw Error("red-black tree invariant violation");} function Uh(a,b,c,d){if(d instanceof Qh)return new Qh(a,b,c,d.bc(),null);if(c instanceof Rh)return Ph(a,b,c.ud(),d);if(c instanceof Qh&&c.right instanceof Rh)return new Qh(c.right.key,c.right.H,Ph(c.key,c.H,c.left.ud(),c.right.left),new Rh(a,b,c.right.right,d,null),null);throw Error("red-black tree invariant violation");} var Vh=function Vh(a,b,c){var e=null!=a.left?function(){var e=a.left;return Vh.l?Vh.l(e,b,c):Vh.call(null,e,b,c)}():c;if(Hd(e))return e;var f=function(){var c=a.key,f=a.H;return b.l?b.l(e,c,f):b.call(null,e,c,f)}();if(Hd(f))return f;if(null!=a.right){var h=a.right;return Vh.l?Vh.l(h,b,f):Vh.call(null,h,b,f)}return f};function Rh(a,b,c,d,e){this.key=a;this.H=b;this.left=c;this.right=d;this.w=e;this.m=32402207;this.J=0}g=Rh.prototype; g.lastIndexOf=function(){function a(a){return Xd(this,a,H(this))}var b=null;b=function(b,d){switch(arguments.length){case 1:return a.call(this,b);case 2:return Xd(this,b,d)}throw Error("Invalid arity: "+(arguments.length-1));};b.h=a;b.c=function(a,b){return Xd(this,a,b)};return b}(); g.indexOf=function(){var a=null;a=function(a,c){switch(arguments.length){case 1:return Ud(this,a,0);case 2:return Ud(this,a,c)}throw Error("Invalid arity: "+(arguments.length-1));};a.h=function(a){return Ud(this,a,0)};a.c=function(a,c){return Ud(this,a,c)};return a}();g.Ee=function(a){return a.He(this)};g.ud=function(){return new Qh(this.key,this.H,this.left,this.right,null)};g.bc=function(){return this};g.De=function(a){return a.Ge(this)};g.replace=function(a,b,c,d){return new Rh(a,b,c,d,null)}; g.Ge=function(a){return new Rh(a.key,a.H,this,a.right,null)};g.He=function(a){return new Rh(a.key,a.H,a.left,this,null)};g.Jc=function(a,b){return Vh(this,a,b)};g.V=function(a,b){return this.ka(null,b,null)};g.I=function(a,b,c){return this.ka(null,b,c)};g.$=function(a,b){if(0===b)return this.key;if(1===b)return this.H;throw Error("Index out of bounds");};g.ka=function(a,b,c){return 0===b?this.key:1===b?this.H:c};g.dc=function(a,b,c){return(new R(null,2,5,T,[this.key,this.H],null)).dc(null,b,c)}; g.P=function(){return null};g.W=function(){return 2};g.fd=function(){return this.key};g.gd=function(){return this.H};g.Ac=function(){return this.H};g.Bc=function(){return new R(null,1,5,T,[this.key],null)};g.U=function(){var a=this.w;return null!=a?a:this.w=a=Ad(this)};g.K=function(a,b){return $d(this,b)};g.oa=function(){return he};g.Fa=function(a,b){return Kd(this,b)};g.Ga=function(a,b,c){return Ld(this,b,c)};g.O=function(a,b,c){return K.l(new R(null,2,5,T,[this.key,this.H],null),b,c)}; g.yc=function(a,b){return 0===b||1===b};g.S=function(){var a=this.key;return Tb(Tb(wd,this.H),a)};g.T=function(a,b){return tc(new R(null,2,5,T,[this.key,this.H],null),b)};g.X=function(a,b){return new R(null,3,5,T,[this.key,this.H,b],null)}; g.call=function(){var a=null;a=function(a,c,d){switch(arguments.length){case 2:return this.$(null,c);case 3:return this.ka(null,c,d)}throw Error("Invalid arity: "+(arguments.length-1));};a.c=function(a,c){return this.$(null,c)};a.l=function(a,c,d){return this.ka(null,c,d)};return a}();g.apply=function(a,b){return this.call.apply(this,[this].concat(Gb(b)))};g.h=function(a){return this.$(null,a)};g.c=function(a,b){return this.ka(null,a,b)};Rh.prototype[Fb]=function(){return yd(this)}; function Qh(a,b,c,d,e){this.key=a;this.H=b;this.left=c;this.right=d;this.w=e;this.m=32402207;this.J=0}g=Qh.prototype;g.lastIndexOf=function(){function a(a){return Xd(this,a,H(this))}var b=null;b=function(b,d){switch(arguments.length){case 1:return a.call(this,b);case 2:return Xd(this,b,d)}throw Error("Invalid arity: "+(arguments.length-1));};b.h=a;b.c=function(a,b){return Xd(this,a,b)};return b}(); g.indexOf=function(){var a=null;a=function(a,c){switch(arguments.length){case 1:return Ud(this,a,0);case 2:return Ud(this,a,c)}throw Error("Invalid arity: "+(arguments.length-1));};a.h=function(a){return Ud(this,a,0)};a.c=function(a,c){return Ud(this,a,c)};return a}();g.Ee=function(a){return new Qh(this.key,this.H,this.left,a,null)};g.ud=function(){throw Error("red-black tree invariant violation");};g.bc=function(){return new Rh(this.key,this.H,this.left,this.right,null)}; g.De=function(a){return new Qh(this.key,this.H,a,this.right,null)};g.replace=function(a,b,c,d){return new Qh(a,b,c,d,null)};g.Ge=function(a){return this.left instanceof Qh?new Qh(this.key,this.H,this.left.bc(),new Rh(a.key,a.H,this.right,a.right,null),null):this.right instanceof Qh?new Qh(this.right.key,this.right.H,new Rh(this.key,this.H,this.left,this.right.left,null),new Rh(a.key,a.H,this.right.right,a.right,null),null):new Rh(a.key,a.H,this,a.right,null)}; g.He=function(a){return this.right instanceof Qh?new Qh(this.key,this.H,new Rh(a.key,a.H,a.left,this.left,null),this.right.bc(),null):this.left instanceof Qh?new Qh(this.left.key,this.left.H,new Rh(a.key,a.H,a.left,this.left.left,null),new Rh(this.key,this.H,this.left.right,this.right,null),null):new Rh(a.key,a.H,a.left,this,null)};g.Jc=function(a,b){return Vh(this,a,b)};g.V=function(a,b){return this.ka(null,b,null)};g.I=function(a,b,c){return this.ka(null,b,c)}; g.$=function(a,b){if(0===b)return this.key;if(1===b)return this.H;throw Error("Index out of bounds");};g.ka=function(a,b,c){return 0===b?this.key:1===b?this.H:c};g.dc=function(a,b,c){return(new R(null,2,5,T,[this.key,this.H],null)).dc(null,b,c)};g.P=function(){return null};g.W=function(){return 2};g.fd=function(){return this.key};g.gd=function(){return this.H};g.Ac=function(){return this.H};g.Bc=function(){return new R(null,1,5,T,[this.key],null)}; g.U=function(){var a=this.w;return null!=a?a:this.w=a=Ad(this)};g.K=function(a,b){return $d(this,b)};g.oa=function(){return he};g.Fa=function(a,b){return Kd(this,b)};g.Ga=function(a,b,c){return Ld(this,b,c)};g.O=function(a,b,c){return K.l(new R(null,2,5,T,[this.key,this.H],null),b,c)};g.yc=function(a,b){return 0===b||1===b};g.S=function(){var a=this.key;return Tb(Tb(wd,this.H),a)};g.T=function(a,b){return tc(new R(null,2,5,T,[this.key,this.H],null),b)}; g.X=function(a,b){return new R(null,3,5,T,[this.key,this.H,b],null)};g.call=function(){var a=null;a=function(a,c,d){switch(arguments.length){case 2:return this.$(null,c);case 3:return this.ka(null,c,d)}throw Error("Invalid arity: "+(arguments.length-1));};a.c=function(a,c){return this.$(null,c)};a.l=function(a,c,d){return this.ka(null,c,d)};return a}();g.apply=function(a,b){return this.call.apply(this,[this].concat(Gb(b)))};g.h=function(a){return this.$(null,a)}; g.c=function(a,b){return this.ka(null,a,b)};Qh.prototype[Fb]=function(){return yd(this)}; var Wh=function Wh(a,b,c,d,e){if(null==b)return new Qh(c,d,null,null,null);var h=function(){var d=b.key;return a.c?a.c(c,d):a.call(null,c,d)}();if(0===h)return e[0]=b,null;if(0>h)return h=function(){var h=b.left;return Wh.Z?Wh.Z(a,h,c,d,e):Wh.call(null,a,h,c,d,e)}(),null!=h?b.De(h):null;h=function(){var h=b.right;return Wh.Z?Wh.Z(a,h,c,d,e):Wh.call(null,a,h,c,d,e)}();return null!=h?b.Ee(h):null},Xh=function Xh(a,b){if(null==a)return b;if(null==b)return a;if(a instanceof Qh){if(b instanceof Qh){var d= function(){var d=a.right,f=b.left;return Xh.c?Xh.c(d,f):Xh.call(null,d,f)}();return d instanceof Qh?new Qh(d.key,d.H,new Qh(a.key,a.H,a.left,d.left,null),new Qh(b.key,b.H,d.right,b.right,null),null):new Qh(a.key,a.H,a.left,new Qh(b.key,b.H,d,b.right,null),null)}return new Qh(a.key,a.H,a.left,function(){var d=a.right;return Xh.c?Xh.c(d,b):Xh.call(null,d,b)}(),null)}if(b instanceof Qh)return new Qh(b.key,b.H,function(){var d=b.left;return Xh.c?Xh.c(a,d):Xh.call(null,a,d)}(),b.right,null);d=function(){var d= a.right,f=b.left;return Xh.c?Xh.c(d,f):Xh.call(null,d,f)}();return d instanceof Qh?new Qh(d.key,d.H,new Rh(a.key,a.H,a.left,d.left,null),new Rh(b.key,b.H,d.right,b.right,null),null):Th(a.key,a.H,a.left,new Rh(b.key,b.H,d,b.right,null))},Yh=function Yh(a,b,c,d){if(null!=b){var f=function(){var d=b.key;return a.c?a.c(c,d):a.call(null,c,d)}();if(0===f)return d[0]=b,Xh(b.left,b.right);if(0>f)return f=function(){var f=b.left;return Yh.M?Yh.M(a,f,c,d):Yh.call(null,a,f,c,d)}(),null!=f||null!=d[0]?b.left instanceof Rh?Th(b.key,b.H,f,b.right):new Qh(b.key,b.H,f,b.right,null):null;f=function(){var f=b.right;return Yh.M?Yh.M(a,f,c,d):Yh.call(null,a,f,c,d)}();return null!=f||null!=d[0]?b.right instanceof Rh?Uh(b.key,b.H,b.left,f):new Qh(b.key,b.H,b.left,f,null):null}return null},Zh=function Zh(a,b,c,d){var f=b.key,h=a.c?a.c(c,f):a.call(null,c,f);return 0===h?b.replace(f,d,b.left,b.right):0>h?b.replace(f,b.H,function(){var f=b.left;return Zh.M?Zh.M(a,f,c,d):Zh.call(null,a,f,c,d)}(),b.right):b.replace(f,b.H,b.left, function(){var f=b.right;return Zh.M?Zh.M(a,f,c,d):Zh.call(null,a,f,c,d)}())};function $h(a,b,c,d,e){this.Bb=a;this.mc=b;this.F=c;this.meta=d;this.w=e;this.m=418776847;this.J=8192}g=$h.prototype;g.forEach=function(a){for(var b=E(this),c=null,d=0,e=0;;)if(ed?c.left:c.right}else return null}g.has=function(a){return He(this,a)};g.V=function(a,b){return this.I(null,b,null)}; g.I=function(a,b,c){a=ai(this,b);return null!=a?a.H:c};g.Qc=function(a,b,c){return null!=this.mc?Jd(Vh(this.mc,b,c)):c};g.P=function(){return this.meta};g.W=function(){return this.F};g.Rc=function(){return 0(a.h?a.h(c):a.call(null,c))?b:c};Ai.A=function(a,b,c,d){return Mb(function(b,c){return Ai.l(a,b,c)},Ai.l(a,b,c),d)};Ai.N=function(a){var b=y(a),c=z(a);a=y(c);var d=z(c);c=y(d);d=z(d);return Ai.A(b,a,c,d)};Ai.L=3;function Bi(a,b){return new kf(null,function(){var c=E(b);if(c){var d=y(c);d=a.h?a.h(d):a.call(null,d);c=t(d)?ae(y(c),Bi(a,vd(c))):null}else c=null;return c},null,null)}function Di(a,b,c){this.i=a;this.end=b;this.step=c} Di.prototype.ja=function(){return 0this.end};Di.prototype.next=function(){var a=this.i;this.i+=this.step;return a};function Ei(a,b,c,d,e){this.meta=a;this.start=b;this.end=c;this.step=d;this.w=e;this.m=32375006;this.J=139264}g=Ei.prototype;g.toString=function(){return fd(this)};g.equiv=function(a){return this.K(null,a)}; g.indexOf=function(){var a=null;a=function(a,c){switch(arguments.length){case 1:return Ud(this,a,0);case 2:return Ud(this,a,c)}throw Error("Invalid arity: "+(arguments.length-1));};a.h=function(a){return Ud(this,a,0)};a.c=function(a,c){return Ud(this,a,c)};return a}(); g.lastIndexOf=function(){function a(a){return Xd(this,a,H(this))}var b=null;b=function(b,d){switch(arguments.length){case 1:return a.call(this,b);case 2:return Xd(this,b,d)}throw Error("Invalid arity: "+(arguments.length-1));};b.h=a;b.c=function(a,b){return Xd(this,a,b)};return b}();g.$=function(a,b){if(0<=b&&bthis.end&&0===this.step)return this.start;throw Error("Index out of bounds");}; g.ka=function(a,b,c){return 0<=b&&bthis.end&&0===this.step?this.start:c};g.ba=function(){return new Di(this.start,this.end,this.step)};g.P=function(){return this.meta};g.Ka=function(){return 0this.end?new Ei(this.meta,this.start+this.step,this.end,this.step,null):null}; g.W=function(){return wb(this.S(null))?0:Math.ceil((this.end-this.start)/this.step)};g.U=function(){var a=this.w;return null!=a?a:this.w=a=Ad(this)};g.K=function(a,b){return $d(this,b)};g.oa=function(){return tc(wd,this.meta)};g.Fa=function(a,b){return Kd(this,b)};g.Ga=function(a,b,c){for(a=this.start;;)if(0this.end){c=b.c?b.c(c,a):b.call(null,c,a);if(Hd(c))return B(c);a+=this.step}else return c};g.Ia=function(){return null==this.S(null)?null:this.start}; g.bb=function(){return null!=this.S(null)?new Ei(this.meta,this.start+this.step,this.end,this.step,null):wd};g.S=function(){return 0this.step?this.start>this.end?this:null:this.start===this.end?null:this};g.T=function(a,b){return new Ei(b,this.start,this.end,this.step,this.w)};g.X=function(a,b){return ae(b,this)};Ei.prototype[Fb]=function(){return yd(this)};function Fi(a,b,c){return new Ei(null,a,b,c,null)} function Gi(a,b){return new R(null,2,5,T,[Bi(a,b),ng(a,b)],null)} function Hi(a){var b=y;return function(){function c(c,d,e){return new R(null,2,5,T,[b.l?b.l(c,d,e):b.call(null,c,d,e),a.l?a.l(c,d,e):a.call(null,c,d,e)],null)}function d(c,d){return new R(null,2,5,T,[b.c?b.c(c,d):b.call(null,c,d),a.c?a.c(c,d):a.call(null,c,d)],null)}function e(c){return new R(null,2,5,T,[b.h?b.h(c):b.call(null,c),a.h?a.h(c):a.call(null,c)],null)}function f(){return new R(null,2,5,T,[b.B?b.B():b.call(null),a.B?a.B():a.call(null)],null)}var h=null,k=function(){function c(a,b,c,e){var f= null;if(3lb)return Jc(a,"#");Jc(a,c);if(0===tb.h(f))E(h)&&Jc(a,function(){var a=Ki.h(f);return t(a)?a:"..."}());else{if(E(h)){var l=y(h);b.l?b.l(l,a,f):b.call(null,l,a,f)}for(var p=z(h),m=tb.h(f)-1;;)if(!p||null!=m&&0===m){E(p)&&0===m&&(Jc(a,d),Jc(a,function(){var a=Ki.h(f);return t(a)?a:"..."}()));break}else{Jc(a,d);var u=y(p);c=a;h=f;b.l?b.l(u,c,h):b.call(null,u,c,h);var w=z(p);c=m-1;p=w;m=c}}return Jc(a,e)}finally{lb=k}} function Li(a,b){for(var c=E(b),d=null,e=0,f=0;;)if(fH(a)?a.toUpperCase():[v.h(a.substring(0,1).toUpperCase()),v.h(a.substring(1))].join("")} function Qo(a){if("string"===typeof a)return a;a=jf(a);var b=Fo(a,/-/),c=E(b);b=y(c);c=z(c);return t(Oo.h?Oo.h(b):Oo.call(null,b))?a:Kb(v,b,ig.c(Po,c))}function Ro(a){var b=function(){var b=function(){var b=me(a);return b?(b=a.displayName,t(b)?b:a.name):b}();if(t(b))return b;b=function(){var b=null!=a?a.J&4096||q===a.Oe?!0:!1:!1;return b?jf(a):b}();if(t(b))return b;b=qe(a);return xe(b)?Tk.h(b):null}();return Do(""+v.h(b),"$",".")}var So=!1;if("undefined"===typeof To)var To=0;function Uo(a){return setTimeout(a,16)}var Vo="undefined"===typeof window||null==window.document?Uo:function(){var a=window,b=a.requestAnimationFrame;if(t(b))return b;b=a.webkitRequestAnimationFrame;if(t(b))return b;b=a.mozRequestAnimationFrame;if(t(b))return b;a=a.msRequestAnimationFrame;return t(a)?a:Uo}();function Wo(a,b){return a.cljsMountOrder-b.cljsMountOrder}if("undefined"===typeof Xo)var Xo=function(){return null};function Yo(a){this.Yd=a} function Zo(a,b){var c=a[b];if(null==c)return null;a[b]=null;for(var d=c.length,e=0;;)if(e=d&&a.push(gq(c));return a}}(e),[b,c],a))}};if("undefined"===typeof jq)var jq=null;function kq(){if(null!=jq)return jq;if("undefined"!==typeof ReactDOM)return jq=ReactDOM;if("undefined"!==typeof require){var a=jq=require("react-dom");if(t(a))return a;throw Error("require('react-dom') failed");}throw Error("js/ReactDOM is missing");}if("undefined"===typeof lq)var lq=dg.h(Ef); function mq(a,b,c){var d=So;So=!0;try{return kq().render(a.B?a.B():a.call(null),b,function(){return function(){var d=So;So=!1;try{return gg.M(lq,K,b,new R(null,2,5,T,[a,b],null)),Zo(bp,"afterRender"),null!=c?c.B?c.B():c.call(null):null}finally{So=d}}}(d))}finally{So=d}}function nq(a,b){return mq(a,b,null)}function oq(a,b,c){qp();return mq(function(){return gq(me(a)?a.B?a.B():a.call(null):a)},b,c)}Wp=function(a){return kq().findDOMNode(a)};function pq(a){switch(arguments.length){case 2:return oq(arguments[0],arguments[1],null);case 3:return oq(arguments[0],arguments[1],arguments[2]);default:throw Error(["Invalid arity: ",v.h(arguments.length)].join(""));}}function qq(a,b){return oq(a,b,null)} da("reagent.core.force_update_all",function(){qp();qp();for(var a=E(mh(B(lq))),b=null,c=0,d=0;;)if(d=Number(c)?a:a=-1Number(a)?"-":0<=b.indexOf("+")?"+":0<=b.indexOf(" ")?" ":"";0<=Number(a)&&(d=f+d);if(isNaN(c)||d.length>=Number(c))return d;d=isNaN(e)?Math.abs(Number(a)).toString():Math.abs(Number(a)).toFixed(e);a=Number(c)-d.length-f.length;0<=b.indexOf("-",0)?d=f+d+sa(" ",a):(b=0<=b.indexOf("0",0)?"0":" ",d=f+sa(b,a)+d);return d};yq.fc.d=function(a,b,c,d,e,f,h,k){return yq.fc.f(parseInt(a,10),b,c,d,0,f,h,k)}; yq.fc.i=yq.fc.d;yq.fc.u=yq.fc.d;function zq(a){var b=be([Vk,null]);return wg.c(t(a)?a:Ef,function(){return function e(a){return new kf(null,function(){for(var b=a;;)if(b=E(b)){if(Ae(b)){var d=Wc(b),k=H(d),l=of(k);a:for(var p=0;;)if(p=H(h)&&Vf(function(){return function(a){return!(a instanceof Xq)}}(b,c,d,e,f,h),h)))throw Error(Bq("%s is not a valid sequence schema; %s%s%s",be([a,"a valid sequence schema consists of zero or more `one` elements, ","followed by zero or more `optional` elements, followed by an optional ", "schema that will match the remaining elements."])));return new R(null,2,5,T,[O.c(c,f),y(h)],null)} R.prototype.xb=function(){var a=this,b=Zq(a),c=J(b,0,null),d=J(b,1,null);return Wg(O.c(function(){return function(a,b,c,d){return function m(e){return new kf(null,function(){return function(){for(;;){var a=E(e);if(a){if(Ae(a)){var b=Wc(a),c=H(b),d=of(c);return function(){for(var a=0;;)if(ac?f:c;return $r(a,ea?0:a}():function(){var a=e-b;return f>a?f:a}())} function gs(a,b){var c=null!=a&&(a.m&64||q===a.G)?P(U,a):a,d=D.c(c,pl);d=null!=d&&(d.m&64||q===d.G)?P(U,d):d;var e=D.c(d,Aj),f=D.c(c,Yj),h=D.c(c,no);return $r(c,e>f?function(){var a=h-1,c=e+b;return a=a}}(l,p,a,c,c,d,e,f,h,k),h),l,p);return Zr(c,d)} function it(a,b){var c=null!=a&&(a.m&64||q===a.G)?P(U,a):a,d=D.c(c,pl),e=null!=d&&(d.m&64||q===d.G)?P(U,d):d,f=D.c(e,zn),h=D.c(c,tk),k=D.c(c,fl),l=b-1;d=J(cf(Bi(function(a,b,c,d,e,f,h){return function(a){return h>a}}(l,a,c,c,d,e,f,h,k),h)),l,0);return Zr(c,d)}function jt(a){return K.l(a,im,Ve)}function kt(a){return K.l(a,im,Hr)}function lt(a,b,c){return K.l(a,b,c)}function mt(a,b,c){return Wg(O.A(jg(b,a),new R(null,1,5,T,[c],null),be([jg(H(a)-b-1,kg(b,a))])))} function nt(a,b){var c=null!=a&&(a.m&64||q===a.G)?P(U,a):a,d=D.c(c,pl),e=null!=d&&(d.m&64||q===d.G)?P(U,d):d;d=D.c(e,zn);e=D.c(e,Aj);var f=D.c(c,fl);D.c(c,no);var h=D.c(c,Oj),k=D.c(c,Rj),l=D.c(c,$l),p=D.c(c,im);p=95b?p.h?p.h(b):p.call(null,b):b;h=tr(p,h);return G.c(f,d+1)?t(k)?K.l(Yr(zg(c,new R(null,3,5,T,[il,e,d],null),h),d+1),vk,!0):zg(c,new R(null,3,5,T,[il,e,d],null),h):Yr(Ag.Z(c,new R(null,2,5,T,[il,e],null),t(l)?mt:lt,d,h),d+1)} function ot(a,b){var c=null!=a&&(a.m&64||q===a.G)?P(U,a):a,d=D.c(c,Rj),e=D.c(c,vk);t(t(d)?e:d)&&(c=null!=c&&(c.m&64||q===c.G)?P(U,c):c,d=D.c(c,pl),d=null!=d&&(d.m&64||q===d.G)?P(U,d):d,d=D.c(d,Aj),e=D.c(c,no),c=Yr(c,0),c=G.c(e,d+1)?Tr.h(c):$r(c,d+1));return c=nt(c,b)}function pt(a){a=null!=a&&(a.m&64||q===a.G)?P(U,a):a;var b=D.c(a,fl),c=D.c(a,no);return K.l(a,il,Wg(qg(c,Wg(qg(b,new R(null,2,5,T,[69,Ef],null))))))} function qt(a){a=null!=a&&(a.m&64||q===a.G)?P(U,a):a;var b=D.c(a,pl);b=null!=b&&(b.m&64||q===b.G)?P(U,b):b;b=D.c(b,Aj);var c=D.c(a,fl),d=D.c(a,Oj);return zg(a,new R(null,2,5,T,[il,b],null),gr.c(c,d))}function rt(a,b,c){return Wg(O.c(jg(b,a),qg(H(a)-b,vr(c))))}function st(a,b,c){return Wg(O.c(qg(b+1,vr(c)),kg(b+1,a)))} function tt(a){a=null!=a&&(a.m&64||q===a.G)?P(U,a):a;var b=D.c(a,pl),c=null!=b&&(b.m&64||q===b.G)?P(U,b):b;b=D.c(c,zn);c=D.c(c,Aj);var d=D.c(a,fl),e=D.c(a,Oj);--d;return Ag.Z(a,new R(null,2,5,T,[il,c],null),rt,b=k?Zr(c,k-1):c,m=Mb(D,p,new R(null,2,5,T,[pl,zn],null));return Ag.l(p,new R(null,2,5,T,[il,h],null),function(a,b,c,d,e,f,h,k,m,l,p,Q){return function(a){return Wg(O.A(jg(b,a),kg(b+c,a),be([qg(c,vr(Q))])))}}(p,m,function(){var a=k-m;return b=a}}(c,b)(b)}()))return Gu(a,b+64);throw Jt;}catch(h){if(h instanceof Error){var d=h;if(d===Jt)try{if(55===b)return Bg(a,V,ms);throw Jt;}catch(k){if(k instanceof Error){var e=k;if(e===Jt)try{if(56===b)return Bg(a,V,ns);throw Jt;}catch(l){if(l instanceof Error){var f=l;if(f===Jt)try{if(99===b)return du(a); throw Jt;}catch(p){if(p instanceof Error){d=p;if(d===Jt)throw Jt;throw d;}throw p;}else throw f;}else throw l;}else throw e;}else throw k;}else throw d;}else throw h;}else throw Jt;}catch(h){if(h instanceof Error)if(d=h,d===Jt)try{if(35===c)try{if(56===b)return Bg(a,V,pt);throw Jt;}catch(k){if(k instanceof Error){e=k;if(e===Jt)throw Jt;throw e;}throw k;}else throw Jt;}catch(k){if(k instanceof Error)if(e=k,e===Jt)try{if(40===c)try{if(48===b)return Zt(a);throw Jt;}catch(l){if(l instanceof Error){f= l;if(f===Jt)return $t(a);throw f;}throw l;}else throw Jt;}catch(l){if(l instanceof Error){f=l;if(f===Jt)return a;throw f;}throw l;}else throw e;else throw k;}else throw d;else throw h;}},function(a){return a},function(a){return a},Gu,function(a,b){return Cg(a,V,ot,b)},function(a,b){var c=function(){switch(b){case 64:return eu;case 65:return fu;case 66:return gu;case 67:return hu;case 68:return iu;case 69:return ju;case 70:return ku;case 71:return lu;case 72:return mu;case 73:return nu;case 74:return ou; case 75:return pu;case 76:return su;case 77:return tu;case 80:return uu;case 83:return qu;case 84:return ru;case 87:return vu;case 88:return wu;case 90:return xu;case 96:return lu;case 97:return hu;case 100:return Du;case 101:return fu;case 102:return mu;case 103:return yu;case 104:return zu;case 108:return Au;case 109:return Cu;case 112:return Eu;case 114:return Fu;default:return null}}();return t(c)?c.h?c.h(a):c.call(null,a):a},function(a){return a},function(a,b){return K.l(a,kk,ge.c(kk.h(a),b))}, function(a){return a},function(a,b){return K.l(a,rk,ge.c(rk.h(a),b))},function(a){return a},function(a){return a},function(a){return K.A(a,rk,he,be([kk,he]))}]);function Iu(a,b){for(var c=a,d=Tl.h(c),e=b;;){var f=y(e);if(t(f)){var h=160<=f?65:f;h=D.c(d.h?d.h(xq):d.call(null,xq),h);d=J(h,0,null);h=J(h,1,null);a:for(;;)if(E(h)){var k=y(h);k=Hu.h?Hu.h(k):Hu.call(null,k);c=k.c?k.c(c,f):k.call(null,c,f);h=z(h)}else break a;e=vd(e)}else return K.l(c,Tl,d)}} function Ju(a,b){var c=xg(function(a){return a.codePointAt(0)},b);return Iu(a,c)} function Ku(a,b){try{if(ze(b)&&3===H(b)){var c=Vd(b,0),d=Vd(b,1),e=Vd(b,2);return[v.h(a+8),";2;",v.h(c),";",v.h(d),";",v.h(e)].join("")}throw Jt;}catch(k){if(k instanceof Error){var f=k;if(f===Jt)try{if(t(function(){return function(){return function(a){return 8>a}}(f)(b)}()))return""+v.h(a+b);throw Jt;}catch(l){if(l instanceof Error){var h=l;if(h===Jt)try{if(t(function(){return function(){return function(a){return 16>a}}(h,f)(b)}()))return""+v.h(a+52+b);throw Jt;}catch(p){if(p instanceof Error){c= p;if(c===Jt)return[v.h(a+8),";5;",v.h(b)].join("");throw c;}throw p;}else throw h;}else throw l;}else throw f;}else throw k;}}ag.c(Ku,30);ag.c(Ku,40);var Lu=function Lu(a){if(null!=a&&null!=a.yd)return a.yd(a);var c=Lu[n(null==a?null:a)];if(null!=c)return c.h?c.h(a):c.call(null,a);c=Lu._;if(null!=c)return c.h?c.h(a):c.call(null,a);throw Cb("Screen.lines",a);},Mu=function Mu(a){if(null!=a&&null!=a.xd)return a.xd(a);var c=Mu[n(null==a?null:a)];if(null!=c)return c.h?c.h(a):c.call(null,a);c=Mu._;if(null!=c)return c.h?c.h(a):c.call(null,a);throw Cb("Screen.cursor",a);};function Nu(a,b){var c=0parseFloat(Iv)){Hv=String(Kv);break a}}Hv=Iv}var gb={}; function Lv(a){return fb(a,function(){for(var b=0,c=ra(String(Hv)).split("."),d=ra(String(a)).split("."),e=Math.max(c.length,d.length),f=0;0==b&&f=a.keyCode)a.keyCode=-1}catch(b){}};var Uv="closure_listenable_"+(1E6*Math.random()|0),Vv=0;function Wv(a,b,c,d,e){this.listener=a;this.Xd=null;this.src=b;this.type=c;this.capture=!!d;this.Ub=e;this.key=++Vv;this.$c=this.Fd=!1}function Xv(a){a.$c=!0;a.listener=null;a.Xd=null;a.src=null;a.Ub=null};function Yv(a){this.src=a;this.rb={};this.wd=0}Yv.prototype.add=function(a,b,c,d,e){var f=a.toString();a=this.rb[f];a||(a=this.rb[f]=[],this.wd++);var h=Zv(a,b,d,e);-1e.keyCode||void 0!=e.returnValue)){a:{var f=!1;if(0==e.keyCode)try{e.keyCode=-1;break a}catch(l){f=!0}if(f||void 0==e.returnValue)e.returnValue=!0}e=[];for(f=c.currentTarget;f;f=f.parentNode)e.push(f);f=a.type;for(var h=e.length-1;!c.Kc&&0<=h;h--){c.currentTarget=e[h];var k=nw(e[h],f,!0,c);d=d&&k}for(h=0;!c.Kc&& h>>0);function fw(a){if(ha(a))return a;a[pw]||(a[pw]=function(b){return a.handleEvent(b)});return a[pw]};function qw(){wv.call(this);this.Ib=new Yv(this);this.ff=this;this.ve=null}qa(qw,wv);qw.prototype[Uv]=!0;g=qw.prototype;g.addEventListener=function(a,b,c,d){dw(this,a,b,c,d)};g.removeEventListener=function(a,b,c,d){lw(this,a,b,c,d)}; g.dispatchEvent=function(a){var b,c=this.ve;if(c)for(b=[];c;c=c.ve)b.push(c);c=this.ff;var d=a.type||a;if(ca(a))a=new Sv(a,c);else if(a instanceof Sv)a.target=a.target||c;else{var e=a;a=new Sv(d,c);Ia(a,e)}e=!0;if(b)for(var f=b.length-1;!a.Kc&&0<=f;f--){var h=a.currentTarget=b[f];e=rw(h,d,!0,a)&&e}a.Kc||(h=a.currentTarget=c,e=rw(h,d,!0,a)&&e,a.Kc||(e=rw(h,d,!1,a)&&e));if(b)for(f=0;!a.Kc&&fthis.head?(Yw(this.o,this.fa,a,0,this.o.length-this.fa),Yw(this.o,0,a,this.o.length-this.fa,this.head),this.fa=0,this.head=this.length,this.o=a):this.fa===this.head?(this.head=this.fa=0,this.o=a):null};function ax(a,b){for(var c=a.length,d=0;;)if(da)){a+=1;continue}break}hx=!1;return 0c)return a;a:for(;;){var e=cMath.random()&&15>d)d+=1;else break a;if(d>this.level){for(var e=this.level+1;;)if(e<=d+1)c[e]=this.header,e+=1;else break;this.level=d}for(d=Ex(a,b,Array(d));;)return 0<=this.level?(c=c[0].forward,d.forward[0]=c[0],c[0]=d):null}; Gx.prototype.remove=function(a){var b=Array(15),c=Fx(this.header,a,this.level,b);c=0===c.forward.length?null:c.forward[0];if(null!=c&&c.key===a){for(a=0;;)if(a<=this.level){var d=b[a].forward;c===(ad)return c===b.header?null:c;var e;a:for(e=c;;){e=d=a)break a}null!=e?(--d,c=e):--d}}Gx.prototype.S=function(){return function(a){return function d(c){return new kf(null,function(){return function(){return null==c?null:ae(new R(null,2,5,T,[c.key,c.H],null),d(c.forward[0]))}}(a),null,null)}}(this)(this.header.forward[0])}; Gx.prototype.R=function(a,b,c){return Y(b,function(){return function(a){return Y(b,Qi,""," ","",c,a)}}(this),"{",", ","}",c,this)};var Ix=new Gx(Ex(null,null,0),0);function Jx(a){var b=(new Date).valueOf()+a,c=Hx(b),d=t(t(c)?c.keya:b)?a+8:a,[v.h(c),v.h(a)].join("")):null} function Vy(a){var b=J(a,0,null),c=J(a,1,null);a=J(a,2,null);return["rgb(",v.h(b),",",v.h(c),",",v.h(a),")"].join("")} var Wy=hj(function(a){a=null!=a&&(a.m&64||q===a.G)?P(U,a):a;var b=D.c(a,Nk),c=D.c(a,pl);a=K.l(a,Nk,t(c)?wb(b):b);var d=null!=a&&(a.m&64||q===a.G)?P(U,a):a,e=D.c(d,Ok),f=D.c(d,Tn);b=D.c(d,Kj);var h=D.c(d,dk);c=D.c(d,Vl);var k=D.c(d,Nk),l=D.c(d,Yn);d=D.c(d,pl);var p=t(k)?t(e)?e:"fg":f;e=Uy(t(k)?t(f)?f:"bg":e,b,"fg-");h=Uy(p,h,"bg-");c=vg(ub,new R(null,6,5,T,[e,h,t(b)?"bright":null,t(l)?"italic":null,t(c)?"underline":null,t(d)?"cursor":null],null));if(E(c))a:for(b=new cb,c=E(c);;)if(null!=c)b.append(""+ v.h(y(c))),c=z(c),null!=c&&b.append(" ");else{b=b.toString();break a}else b=null;l=null!=a&&(a.m&64||q===a.G)?P(U,a):a;a=D.c(l,Ok);c=D.c(l,Tn);h=D.c(l,Nk);l=t(h)?c:a;a=t(h)?a:c;a=hi.A(be([t(ze.h?ze.h(l):ze.call(null,l))?new r(null,1,[ik,Vy(l)],null):null,t(ze.h?ze.h(a):ze.call(null,a))?new r(null,1,[al,Vy(a)],null):null]));return hi.A(be([t(b)?new r(null,1,[vn,b],null):null,t(a)?new r(null,1,[fm,a],null):null]))}); function Xy(a,b){var c=J(a,0,null),d=J(a,1,null);d=Bg(d,pl,function(){return function(a){return t(a)?B(b):a}}(a,c,d));return new R(null,3,5,T,[ro,Wy.h?Wy.h(d):Wy.call(null,d),c],null)}function Yy(a,b){var c=J(a,0,null),d=J(a,1,null),e=jg(b,c);e=E(e)?new R(null,2,5,T,[Eo(e),d],null):null;var f=K.l(d,pl,!0);f=new R(null,2,5,T,[Vd(c,b),f],null);c=kg(b+1,c);d=E(c)?new R(null,2,5,T,[Eo(c),d],null):null;return vg(ub,new R(null,3,5,T,[e,f,d],null))} function Zy(a,b){for(var c=he,d=a,e=b;;)if(E(d)){var f=y(d),h=J(f,0,null);J(f,1,null);h=H(h);if(h<=e)c=ge.c(c,f),d=vd(d),e-=h;else return O.A(c,Yy(f,e),be([vd(d)]))}else return c}function $y(a,b,c){a=t(B(b))?Zy(B(a),B(b)):B(a);return new R(null,2,5,T,[Lm,Ii(bg(function(){return function(a,b){return pe(new R(null,3,5,T,[Xy,b,c],null),new r(null,1,[mk,a],null))}}(a),a))],null)}var qA=new ti(null,new r(null,3,["small",null,"medium",null,"big",null],null),null); function rA(a,b,c,d,e){var f=yp(function(){var a=B(c);return t(qA.h?qA.h(a):qA.call(null,a))?["font-",v.h(a)].join(""):null}),h=yp(function(){return function(){var d=B(a),e=B(b),f=B(c);f=t(qA.h?qA.h(f):qA.call(null,f))?null:new r(null,1,[wk,f],null);return hi.A(be([new r(null,2,[fl,[v.h(d),"ch"].join(""),no,[v.h(1.3333333333*e),"em"].join("")],null),f]))}}(f)),k=yp(function(){return function(){return Lu(B(d))}}(f,h)),l=yp(function(a,c,d){return function(){return xg(function(a,b,c){return function(d){return yp(function(a, b,c){return function(){return D.c(B(c),d)}}(a,b,c))}}(a,c,d),Fi(0,B(b),1))}}(f,h,k)),p=yp(function(){return function(){return Mu(B(d))}}(f,h,k,l)),m=yp(function(a,b,c,d,e){return function(){return zn.h(B(e))}}(f,h,k,l,p)),u=yp(function(a,b,c,d,e){return function(){return Aj.h(B(e))}}(f,h,k,l,p,m)),w=yp(function(a,b,c,d,e){return function(){return On.h(B(e))}}(f,h,k,l,p,m,u));return function(a,b,c,d,f,h,k,l){return function(){return new R(null,3,5,T,[Gm,new r(null,2,[vn,B(a),fm,B(b)],null),bg(function(a, b,c,d,f,h,k,l){return function(m,p){var u=yp(function(a,b,c,d,e,f,h,k){return function(){var a=B(k);return t(a)?(a=G.c(m,B(h)))?B(f):a:a}}(a,b,c,d,f,h,k,l));return pe(new R(null,4,5,T,[$y,p,u,e],null),new r(null,1,[mk,m],null))}}(a,b,c,d,f,h,k,l),B(d))],null)}}(f,h,k,l,p,m,u,w)} function sA(){return new R(null,2,5,T,[Ym,new r(null,4,[Mn,"1.1",Fl,"0 0 866.0254037844387 866.0254037844387",vn,"icon",mo,new r(null,1,[An,'\x3cdefs\x3e \x3cmask id\x3d"small-triangle-mask"\x3e \x3crect width\x3d"100%" height\x3d"100%" fill\x3d"white"/\x3e \x3cpolygon points\x3d"508.01270189221935 433.01270189221935, 208.0127018922194 259.8076211353316, 208.01270189221927 606.217782649107" fill\x3d"black"\x3e\x3c/polygon\x3e \x3c/mask\x3e \x3c/defs\x3e \x3cpolygon points\x3d"808.0127018922194 433.01270189221935, 58.01270189221947 -1.1368683772161603e-13, 58.01270189221913 866.0254037844386" mask\x3d"url(#small-triangle-mask)" fill\x3d"white"\x3e\x3c/polygon\x3e \x3cpolyline points\x3d"481.2177826491071 333.0127018922194, 134.80762113533166 533.0127018922194" stroke\x3d"white" stroke-width\x3d"90"\x3e\x3c/polyline\x3e'],null)], null)],null)}function tA(){return new R(null,3,5,T,[Ym,new r(null,3,[Mn,"1.1",Fl,"0 0 12 12",vn,"icon"],null),new R(null,2,5,T,[Fj,new r(null,1,[pn,"M1,0 L11,6 L1,12 Z"],null)],null)],null)}function uA(){return new R(null,4,5,T,[Ym,new r(null,3,[Mn,"1.1",Fl,"0 0 12 12",vn,"icon"],null),new R(null,2,5,T,[Fj,new r(null,1,[pn,"M1,0 L4,0 L4,12 L1,12 Z"],null)],null),new R(null,2,5,T,[Fj,new r(null,1,[pn,"M8,0 L11,0 L11,12 L8,12 Z"],null)],null)],null)} function vA(){return new R(null,4,5,T,[Ym,new r(null,3,[Mn,"1.1",Fl,"0 0 12 12",vn,"icon"],null),new R(null,2,5,T,[Fj,new r(null,1,[pn,"M12,0 L7,0 L9,2 L7,4 L8,5 L10,3 L12,5 Z"],null)],null),new R(null,2,5,T,[Fj,new r(null,1,[pn,"M0,12 L0,7 L2,9 L4,7 L5,8 L3,10 L5,12 Z"],null)],null)],null)} function wA(){return new R(null,4,5,T,[Ym,new r(null,3,[Mn,"1.1",Fl,"0 0 12 12",vn,"icon"],null),new R(null,2,5,T,[Fj,new r(null,1,[pn,"M7,5 L7,0 L9,2 L11,0 L12,1 L10,3 L12,5 Z"],null)],null),new R(null,2,5,T,[Fj,new r(null,1,[pn,"M5,7 L0,7 L2,9 L0,11 L1,12 L3,10 L5,12 Z"],null)],null)],null)}function xA(a,b){return function(b){return function(){return new R(null,3,5,T,[cl,new r(null,1,[Sl,b],null),new R(null,1,5,T,[t(B(a))?uA:tA],null)],null)}}(Ty(b,new fy(null,null,null)))} function yA(a){return 10>a?["0",v.h(a)].join(""):a}function zA(a){var b=Math.floor((a%60+60)%60);return[v.h(yA(Math.floor(a/60))),":",v.h(yA(b))].join("")}function AA(a,b){var c=T,d=new R(null,2,5,T,[Yk,zA(B(a))],null),e=T;var f=B(a);var h=B(b);f=["-",v.h(zA(h-f))].join("");return new R(null,3,5,c,[Ml,d,new R(null,2,5,e,[co,f],null)],null)} function BA(){function a(a){a.preventDefault();return Ry(a.currentTarget.parentNode.parentNode.parentNode)}return function(){return new R(null,4,5,T,[un,new r(null,1,[Sl,a],null),new R(null,1,5,T,[vA],null),new R(null,1,5,T,[wA],null)],null)}} function CA(a,b){var c=Sy(b,function(a){var b=a.currentTarget.offsetWidth,c=a.currentTarget.getBoundingClientRect();return cy(Nu(a.clientX-c.left,b)/b)}),d=yp(function(){return function(){return[v.h(100*B(a)),"%"].join("")}}(c));return function(a,b){return function(){return new R(null,2,5,T,[Vj,new R(null,3,5,T,[Bl,new r(null,1,[Ql,a],null),new R(null,2,5,T,[Cj,new R(null,2,5,T,[ro,new r(null,1,[fm,new r(null,1,[fl,B(b)],null)],null)],null)],null)],null)],null)}}(c,d)} function DA(a,b,c,d){return function(e){return function(){return new R(null,5,5,T,[Kk,new R(null,3,5,T,[xA,a,d],null),new R(null,3,5,T,[AA,b,c],null),new R(null,1,5,T,[BA],null),new R(null,3,5,T,[CA,e,d],null)],null)}}(yp(function(){return B(b)/B(c)}))} function EA(a){return function(a){return function(){return new R(null,3,5,T,[ol,new r(null,1,[Sl,a],null),new R(null,2,5,T,[Xk,new R(null,2,5,T,[km,new R(null,2,5,T,[ro,new R(null,1,5,T,[sA],null)],null)],null)],null)],null)}}(Ty(a,new fy(null,null,null)))}function FA(){return new R(null,2,5,T,[Ek,new R(null,1,5,T,[xn],null)],null)}function GA(a){return Wf(function(b){return a[b]},new R(null,4,5,T,["altKey","shiftKey","metaKey","ctrlKey"],null))} function HA(a){var b=t(GA(a))?null:function(){switch(a.key){case " ":return new fy(null,null,null);case "f":return bm;case "0":return cy(0);case "1":return cy(.1);case "2":return cy(.2);case "3":return cy(.3);case "4":return cy(.4);case "5":return cy(.5);case "6":return cy(.6);case "7":return cy(.7);case "8":return cy(.8);case "9":return cy(.9);default:return null}}();if(t(b))return b;switch(a.key){case "\x3e":return new ey(null,null,null);case "\x3c":return new dy(null,null,null);default:return null}} function IA(a){if(t(GA(a)))return null;switch(a.which){case 37:return new ay(null,null,null);case 39:return new $x(null,null,null);default:return null}}function JA(a){var b=HA(a);return t(b)?(a.preventDefault(),G.c(b,bm)?(Ry(a.currentTarget),null):b):null}function KA(a){var b=IA(a);return t(b)?(a.preventDefault(),b):null} function LA(a,b,c,d){a=t(a)?['"',v.h(a),'"'].join(""):"untitled";return new R(null,4,5,T,[dl,t(d)?new R(null,2,5,T,[jo,new r(null,1,[zl,d],null)],null):null,a,t(b)?new R(null,3,5,T,[ro," by ",t(c)?new R(null,3,5,T,[lo,new r(null,1,[ho,c],null),b],null):b],null):null],null)} function MA(a){var b=Mx(1,ig.h(iy)),c=Kx(1);lx(function(c){return function(){var d=function(){return function(a){return function(){function b(b){for(;;){a:try{for(;;){var c=a(b);if(!N(c,Z)){var d=c;break a}}}catch(x){if(x instanceof Object)b[5]=x,Cx(b),d=Z;else throw x;}if(!N(d,Z))return d}}function c(){var a=[null,null,null,null,null,null,null,null,null,null,null,null];a[0]=d;a[1]=1;return a}var d=null;d=function(a){switch(arguments.length){case 0:return c.call(this);case 1:return b.call(this,a)}throw Error("Invalid arity: "+ (arguments.length-1));};d.B=c;d.h=b;return d}()}(function(){return function(c){var d=c[1];if(7===d)return c[7]=c[2],Ax(c,12,b,!1);if(1===d)return c[2]=null,c[1]=2,Z;if(4===d)return c[8]=c[2],Ax(c,5,b,!0);if(6===d)return d=Jx(3E3),Ux(c,8,new R(null,2,5,T,[a,d],null));if(3===d)return Bx(c,c[2]);if(12===d)return c[9]=c[2],c[2]=null,c[1]=2,Z;if(2===d)return zx(c,4,a);if(11===d)return c[2]=c[2],c[1]=7,Z;if(9===d)return c[2]=null,c[1]=6,Z;if(5===d)return c[10]=c[2],c[2]=null,c[1]=6,Z;if(10===d)return c[2]= null,c[1]=11,Z;if(8===d){var e=c[2];d=J(e,0,null);e=J(e,1,null);e=G.c(e,a);c[11]=d;c[1]=e?9:10;return Z}return null}}(c),c)}(),f=function(){var a=d.B?d.B():d.call(null);a[6]=c;return a}();return yx(f)}}(c));return b} function NA(a,b){var c=dg.h(b),d=Kx(1);lx(function(b,c){return function(){var d=function(){return function(a){return function(){function b(b){for(;;){a:try{for(;;){var c=a(b);if(!N(c,Z)){var d=c;break a}}}catch(F){if(F instanceof Object)b[5]=F,Cx(b),d=Z;else throw F;}if(!N(d,Z))return d}}function c(){var a=[null,null,null,null,null,null,null,null,null,null,null,null,null];a[0]=d;a[1]=1;return a}var d=null;d=function(a){switch(arguments.length){case 0:return c.call(this);case 1:return b.call(this, a)}throw Error("Invalid arity: "+(arguments.length-1));};d.B=c;d.h=b;return d}()}(function(b,c){return function(d){var e=d[1];if(7===e){var f=d[7],h=wb(null==f);d[8]=d[2];d[1]=h?8:9;return Z}if(20===e)return f=d[7],d[1]=t(q===f.Fe)?23:24,Z;if(27===e)return d[2]=!1,d[1]=28,Z;if(1===e)return d[2]=null,d[1]=2,Z;if(24===e)return f=d[7],d[1]=t(!f.Tc)?26:27,Z;if(4===e){f=d[7];var k=d[9];h=d[2];var l=J(h,0,null),m=J(h,1,null);d[10]=m;d[7]=l;d[9]=h;d[1]=t(null==l)?5:6;return Z}return 15===e?(d[2]=!1,d[1]= 16,Z):21===e?(f=d[7],h=Ab(Yx,f),d[2]=h,d[1]=22,Z):31===e?(d[11]=d[2],d[2]=null,d[1]=2,Z):13===e?(d[2]=d[2],d[1]=10,Z):22===e?(d[1]=t(d[2])?29:30,Z):29===e?(f=d[7],h=B(a),h=Zx(f,h),h=gg.l(c,wo,h),d[2]=h,d[1]=31,Z):6===e?(d[2]=null,d[1]=7,Z):28===e?(d[2]=d[2],d[1]=25,Z):25===e?(d[2]=d[2],d[1]=22,Z):17===e?(m=d[10],f=d[7],k=d[9],h=gg.c(a,function(){return function(a,b){return function(a){return Xx(b,a)}}(k,f,m,m,f,k,e,b,c)}()),d[2]=h,d[1]=19,Z):3===e?Bx(d,d[2]):12===e?(f=d[7],d[1]=t(!f.Tc)?14:15,Z): 2===e?(h=B(c),h=E(h),Ux(d,4,h)):23===e?(d[2]=!0,d[1]=25,Z):19===e?(f=d[7],h=wb(null==f),d[12]=d[2],d[1]=h?20:21,Z):11===e?(d[2]=!0,d[1]=13,Z):9===e?(f=d[7],h=Ab(Wx,f),d[2]=h,d[1]=10,Z):5===e?(m=d[10],h=gg.l(c,re,m),d[2]=h,d[1]=7,Z):14===e?(f=d[7],h=Ab(Wx,f),d[2]=h,d[1]=16,Z):26===e?(f=d[7],h=Ab(Yx,f),d[2]=h,d[1]=28,Z):16===e?(d[2]=d[2],d[1]=13,Z):30===e?(d[2]=null,d[1]=31,Z):10===e?(d[1]=t(d[2])?17:18,Z):18===e?(d[2]=null,d[1]=19,Z):8===e?(f=d[7],d[1]=t(q===f.sb)?11:12,Z):null}}(b,c),b,c)}(),e=function(){var a= d.B?d.B():d.call(null);a[6]=b;return a}();return yx(e)}}(d,c));return d} function OA(a,b,c){c=Ty(c,!0);var d=Sy(b,JA),e=Sy(b,KA),f=yp(function(){return function(){return Hm.h(B(a))}}(c,d,e)),h=yp(function(){return function(){return el.h(B(a))}}(c,d,e,f)),k=yp(function(a,b,c,d,e){return function(){var a=B(d);return t(a)?a:B(e)}}(c,d,e,f,h)),l=yp(function(b,c,d,e,f,h){return function(){var b=Gk.h(B(a));b=t(b)?b:wb(B(h));return t(b)?"hud":null}}(c,d,e,f,h,k)),p=yp(function(){return function(){return["asciinema-theme-",v.h(gm.h(B(a)))].join("")}}(c,d,e,f,h,k,l)),m=yp(function(){return function(){var b= fl.h(B(a));return t(b)?b:80}}(c,d,e,f,h,k,l,p)),u=yp(function(){return function(){var b=no.h(B(a));return t(b)?b:24}}(c,d,e,f,h,k,l,p,m)),w=yp(function(){return function(){return wk.h(B(a))}}(c,d,e,f,h,k,l,p,m,u)),x=yp(function(){return function(){return V.h(B(a))}}(c,d,e,f,h,k,l,p,m,u,w)),C=yp(function(){return function(){return ml.h(B(a))}}(c,d,e,f,h,k,l,p,m,u,w,x)),F=yp(function(){return function(){return jn.h(B(a))}}(c,d,e,f,h,k,l,p,m,u,w,x,C)),I=yp(function(){return function(){return Uj.h(B(a))}}(c, d,e,f,h,k,l,p,m,u,w,x,C,F)),M=yp(function(){return function(){return wl.h(B(a))}}(c,d,e,f,h,k,l,p,m,u,w,x,C,F,I)),S=B(a),X=null!=S&&(S.m&64||q===S.G)?P(U,S):S,Ga=D.c(X,ki),db=D.c(X,li),Q=D.c(X,mi),xb=D.c(X,ni);return function(a,c,d,e,f,h,k,l,m,p,u,w,x,C,F,I,M,S,Q,X,Ga,db){return function(){return new R(null,3,5,T,[Cn,new r(null,5,[Jj,-1,Zj,c,Rn,d,Vm,a,vn,B(k)],null),new R(null,7,5,T,[Sm,new r(null,1,[vn,B(l)],null),new R(null,6,5,T,[rA,m,p,u,w,x],null),new R(null,5,5,T,[DA,C,F,I,b],null),t(t(Q)?Q: X)?new R(null,5,5,T,[LA,Q,X,Ga,db],null):null,t(B(h))?null:new R(null,2,5,T,[EA,b],null),t(B(e))?new R(null,1,5,T,[FA],null):null],null)],null)}}(c,d,e,f,h,k,l,p,m,u,w,x,C,F,I,M,S,X,Ga,db,Q,xb)} function PA(a){var b=Kx(null),c=Kx(new dx(bx(1),1));return function(b,c){return function(){return Pp(new r(null,4,[ln,"asciinema-player",Dm,function(b,c){return function(){return OA(a,b,c)}}(b,c),$k,function(b,c){return function(){var d=ty(Gl.h(B(a))),e=MA(c);Tx(e,b);return NA(a,Je([b,d]))}}(b,c),Wm,function(){return function(){return uy(Gl.h(B(a)))}}(b,c)],null))}}(b,c)};function QA(a,b){var c=null!=b&&(b.m&64||q===b.G)?P(U,b):b,d=D.c(c,Ak),e=D.c(c,Gl);d=a.h?a.h(d):a.call(null,d);zy(e,d);return K.l(c,Ak,d)}$x.prototype.sb=q;$x.prototype.qb=function(a,b){var c=null!=b&&(b.m&64||q===b.G)?P(U,b):b,d=D.c(c,Uj),e=D.c(c,wl),f=D.c(c,Gl);t(e)&&yy(f,Nu(d+5,e));return c};ay.prototype.sb=q;ay.prototype.qb=function(a,b){var c=null!=b&&(b.m&64||q===b.G)?P(U,b):b,d=D.c(c,Uj),e=D.c(c,wl),f=D.c(c,Gl);t(e)&&yy(f,Nu(d+-5,e));return c};by.prototype.sb=q; by.prototype.qb=function(a,b){var c=null!=b&&(b.m&64||q===b.G)?P(U,b):b,d=D.c(c,wl),e=D.c(c,Gl);t(d)&&(d*=nn.h(this),yy(e,d));return c};dy.prototype.sb=q;dy.prototype.qb=function(a,b){return QA(function(){return function(a){return a/2}}(this),b)};ey.prototype.sb=q;ey.prototype.qb=function(a,b){return QA(function(){return function(a){return 2*a}}(this),b)};fy.prototype.sb=q;fy.prototype.qb=function(a,b){xy(Gl.h(b));return b};gy.prototype.sb=q;gy.prototype.qb=function(a,b){return K.l(b,ml,so.h(this))}; hy.prototype.sb=q;hy.prototype.qb=function(a,b){return K.l(b,Gk,so.h(this))};jy.prototype.sb=q;jy.prototype.qb=function(a,b){var c=null!=a&&(a.m&64||q===a.G)?P(U,a):a;D.c(c,fl);D.c(c,no);D.c(c,wl);c=null!=b&&(b.m&64||q===b.G)?P(U,b):b;var d=D.c(c,fl),e=D.c(c,no),f=null!=this&&(this.m&64||q===this.G)?P(U,this):this,h=D.c(f,fl),k=D.c(f,no);f=D.c(f,wl);return K.A(c,fl,t(d)?d:h,be([no,t(e)?e:k,wl,f]))};ky.prototype.sb=q;ky.prototype.qb=function(a,b){return K.l(b,Hm,Hm.h(this))};oy.prototype.sb=q; oy.prototype.qb=function(a,b){var c=null!=b&&(b.m&64||q===b.G)?P(U,b):b,d=D.c(c,oi);t(d)&&(ap(bp),d.B?d.B():d.call(null));return c};ry.prototype.sb=q;ry.prototype.qb=function(a,b){return K.l(b,Uj,Zk.h(this))};function RA(){return ig.l(function(a,b){return new R(null,2,5,T,[a,new gy(b,null,null,null)],null)},rg(function(a){return a+.5},.5),og(new R(null,2,5,T,[!1,!0],null)))}function SA(a){var b=Dy(RA());return K.l(K.l(a,ml,!0),Ol,b)} function TA(a){a=null!=a&&(a.m&64||q===a.G)?P(U,a):a;var b=D.c(a,Ol);Tw(b);return K.l(K.l(a,ml,!0),Ol,null)}function UA(a){a=null!=a&&(a.m&64||q===a.G)?P(U,a):a;a=D.c(a,Ol);return t(a)?Je([a]):vi}my.prototype.sb=q; my.prototype.qb=function(a,b){var c=null!=a&&(a.m&64||q===a.G)?P(U,a):a;D.c(c,jn);var d=null!=b&&(b.m&64||q===b.G)?P(U,b):b,e=D.c(d,jn);c=D.c(d,pi);var f=D.c(d,qi),h=null!=this&&(this.m&64||q===this.G)?P(U,this):this;h=D.c(h,jn);if(G.c(e,h))return d;d=K.A(d,jn,h,be([el,!0]));if(t(h))return t(c)&&(c.B?c.B():c.call(null)),SA(d);t(f)&&(f.B?f.B():f.call(null));return TA(d)};my.prototype.Fe=q;my.prototype.de=function(a,b){return UA(b)};py.prototype.sb=q; py.prototype.qb=function(a,b){var c=K.l(b,V,V.h(this));c=null!=c&&(c.m&64||q===c.G)?P(U,c):c;var d=D.c(c,Ol);return t(d)?SA(TA(c)):c};py.prototype.Fe=q;py.prototype.de=function(a,b){return UA(b)};function VA(a){return t(a)?(a=ig.c(parseFloat,Fo(""+v.h(a),/:/)),a=ig.l(Ye,cf(a),rg(function(){return function(a){return 60*a}}(a),1)),P(Xe,a)):null} function WA(a,b,c){t(a)?"string"===typeof a?t(0===a.indexOf("data:application/json;base64,"))?(b=a.substring(29).replace(RegExp("\\s","g"),""),b=JSON.parse(atob(b)),b=fj(b),b=new r(null,1,[V,new r(null,1,[il,b],null)],null)):t(0===a.indexOf("data:text/plain,"))?(a=a.substring(16),b=Ju(Ot(t(b)?b:80,t(c)?c:24),a),b=new r(null,1,[V,b],null)):b=t(0===a.indexOf("npt:"))?new r(null,1,[Zk,VA(a.substring(4))],null):null:b=new r(null,1,[V,new r(null,1,[il,a],null)],null):b=null;return b} var XA=new r(null,2,[pl,new r(null,1,[On,!1],null),il,he],null); function YA(a,b){var c=null!=b&&(b.m&64||q===b.G)?P(U,b):b,d=D.c(c,no),e=D.l(c,wk,"small"),f=D.l(c,Ak,1),h=D.c(c,Hk),k=D.c(c,fl),l=D.c(c,rl),p=D.l(c,cm,!1),m=D.l(c,gm,"asciinema"),u=D.c(c,qm),w=D.c(c,Bm),x=D.l(c,vm,!1),C=D.l(c,Em,!1),F=function(){var a=VA(h);return t(a)?a:0}();w=WA(w,k,d);var I=null!=w&&(w.m&64||q===w.G)?P(U,w):w;w=D.c(I,V);I=D.c(I,Zk);var M=t(I)?I:wb(w)&&0=19.3.0 in ./venv37/lib/python3.7/site-packages (from chalice) (19.3.0)\r\n"] [12.125123, "o", "Requirement already satisfied: botocore<2.0.0,>=1.12.86 in ./venv37/lib/python3.7/site-packages (from chalice) (1.17.12)\r\n"] [12.132778, "o", "Requirement already satisfied: mypy-extensions==0.4.3 in ./venv37/lib/python3.7/site-packages (from chalice) (0.4.3)\r\n"] [12.136117, "o", "Requirement already satisfied: pyyaml<6.0.0,>=5.3.1 in ./venv37/lib/python3.7/site-packages (from chalice) (5.3.1)\r\n"] [12.138016, "o", "Requirement already satisfied: wheel in ./venv37/lib/python3.7/site-packages (from chalice) (0.34.2)\r\n"] [12.144637, "o", "Requirement already satisfied: click<8.0,>=6.6 in ./venv37/lib/python3.7/site-packages (from chalice) (7.1.2)\r\n"] [12.147122, "o", "Requirement already satisfied: pip<20.2,>=9 in ./venv37/lib/python3.7/site-packages (from chalice) (19.0.3)\r\n"] [12.149632, "o", "Requirement already satisfied: jmespath<1.0.0,>=0.9.3 in ./venv37/lib/python3.7/site-packages (from chalice) (0.10.0)\r\n"] [12.152261, "o", "Requirement already satisfied: six<2.0.0,>=1.10.0 in ./venv37/lib/python3.7/site-packages (from chalice) (1.15.0)\r\n"] [12.154232, "o", "Requirement already satisfied: setuptools in ./venv37/lib/python3.7/site-packages (from chalice) (40.8.0)\r\n"] [12.159711, "o", "Requirement already satisfied: enum-compat>=0.0.2 in ./venv37/lib/python3.7/site-packages (from chalice) (0.0.3)\r\n"] [12.162856, "o", "Requirement already satisfied: docutils<0.16,>=0.10 in ./venv37/lib/python3.7/site-packages (from botocore<2.0.0,>=1.12.86->chalice) (0.15.2)\r\n"] [12.166178, "o", "Requirement already satisfied: urllib3<1.26,>=1.20; python_version != \"3.4\" in ./venv37/lib/python3.7/site-packages (from botocore<2.0.0,>=1.12.86->chalice) (1.25.9)\r\n"] [12.17935, "o", "Requirement already satisfied: python-dateutil<3.0.0,>=2.1 in ./venv37/lib/python3.7/site-packages (from botocore<2.0.0,>=1.12.86->chalice) (2.8.1)\r\n"] [12.202073, "o", "\u001b[33mYou are using pip version 19.0.3, however version 20.2b1 is available.\r\nYou should consider upgrading via the 'pip install --upgrade pip' command.\u001b[0m\r\n"] [12.265649, "o", "(venv37) /tmp $ "] [12.98204, "o", "c"] [13.066351, "o", "h"] [13.155867, "o", "a"] [13.203798, "o", "l"] [13.264152, "o", "i"] [13.335991, "o", "c"] [13.375697, "o", "e"] [13.554865, "o", " "] [13.67067, "o", "n"] [13.747029, "o", "e"] [13.802796, "o", "w"] [13.898973, "o", "-"] [14.591986, "o", "p"] [15.039938, "o", "r"] [15.136758, "o", "o"] [15.183997, "o", "j"] [15.307781, "o", "e"] [15.395801, "o", "c"] [15.651701, "o", "t"] [15.750829, "o", " "] [15.909655, "o", "q"] [15.957861, "o", "u"] [16.021247, "o", "i"] [16.197586, "o", "k"] [16.285391, "o", "s"] [16.377484, "o", "t"] [16.465843, "o", "a"] [16.565281, "o", "r"] [16.941683, "o", "\b\b\b\b\b\b\b\b\u001b[K"] [17.116486, "o", "q"] [17.192849, "o", "u"] [17.23197, "o", "i"] [17.287961, "o", "c"] [17.376716, "o", "k"] [17.431874, "o", "s"] [17.534897, "o", "t"] [17.574701, "o", "a"] [17.67868, "o", "r"] [17.815443, "o", "t"] [17.971564, "o", "\r\n"] [18.312339, "o", "(venv37) /tmp $ "] [19.017756, "o", "c"] [19.094431, "o", "d"] [19.146139, "o", " "] [19.301759, "o", "q"] [19.38554, "o", "u"] [19.410578, "o", "i"] [19.513519, "o", "ckstart/"] [20.029639, "o", "\r\n"] [20.066324, "o", "(venv37) /tmp/quickstart $ "] [21.855514, "o", "t"] [21.939564, "o", "r"] [21.954566, "o", "e"] [22.05956, "o", "e"] [22.487414, "o", "\r\n"] [22.494275, "o", ".\r\n├── app.py\r\n└── requirements.txt\r\n\r\n0 directories, 2 files\r\n"] [22.504646, "o", "(venv37) /tmp/quickstart $ "] [23.627249, "o", "c"] [23.713762, "o", "h"] [23.79159, "o", "a"] [23.851259, "o", "l"] [23.907601, "o", "i"] [23.967212, "o", "c"] [24.019798, "o", "e"] [24.07564, "o", " "] [24.183206, "o", "d"] [24.313388, "o", "e"] [24.352526, "o", "p"] [24.409609, "o", "l"] [24.566365, "o", "o"] [24.674363, "o", "y"] [24.877033, "o", "\r\n"] [25.197886, "o", "Creating deployment package.\r\n"] [26.046612, "o", "Creating IAM role: quickstart-dev\r\n"] [27.100193, "o", "Creating lambda function: quickstart-dev\r\n"] [38.261393, "o", "Creating Rest API\r\n"] [40.005804, "o", "Resources deployed:\r\n - Lambda ARN: arn:aws:lambda:us-west-2:675687397822:function:quickstart-dev\r\n - Rest API URL: https://b5fnh9kzs6.execute-api.us-west-2.amazonaws.com/api/\r\n"] [40.080238, "o", "(venv37) /tmp/quickstart $ "] [43.567108, "o", "h"] [43.67546, "o", "t"] [43.82861, "o", "t"] [43.884828, "o", "p"] [43.972725, "o", " "] [44.350426, "o", "htt"] [44.350598, "o", "ps://b5fnh9k"] [44.350745, "o", "zs6"] [44.351105, "o", ".execute-api.us-west-2"] [44.351233, "o", ".amaz"] [44.351265, "o", "ona"] [44.351301, "o", "w"] [44.351453, "o", "s.com/api"] [44.351566, "o", "/"] [45.053598, "o", "\r\n"] [46.149458, "o", "\u001b[34mHTTP\u001b[39;49;00m/\u001b[34m1.1\u001b[39;49;00m \u001b[34m200\u001b[39;49;00m \u001b[36mOK\u001b[39;49;00m\r\n\u001b[36mConnection\u001b[39;49;00m: keep-alive\r\n\u001b[36mContent-Length\u001b[39;49;00m: 17\r\n\u001b[36mContent-Type\u001b[39;49;00m: application/json\r\n\u001b[36mDate\u001b[39;49;00m: Mon, 29 Jun 2020 18:00:35 GMT\r\n\u001b[36mVia\u001b[39;49;00m: 1.1 c6b0d1d85b2590c57ac754bf9e61944f.cloudfront.net (CloudFront)\r\n\u001b[36mX-Amz-Cf-Id\u001b[39;49;00m: kJfXoAUfXWQ-87o10cQyqlvjCCbD5SKBMvwdViXWEKS7buwkwO27yg==\r\n\u001b[36mX-Amz-Cf-Pop\u001b[39;49;00m: IAD89-C1\r\n\u001b[36mX-Amzn-Trace-Id\u001b[39;49;00m: Root=1-5efa2c43-31c191ba91e097c5059cb023;Sampled=0\r\n\u001b[36mX-Cache\u001b[39;49;00m: Miss from cloudfront\r\n\u001b[36mx-amz-apigw-id\u001b[39;49;00m: O5vagEJGPHcFZ7w=\r\n\u001b[36mx-amzn-RequestId\u001b[39;49;00m: 33ee07ef-d19e-4987-b9e5-dfcecd3f1173\r\r\n\r\r\n"] [46.152089, "o", "{\r\n \u001b[34;01m\"hello\"\u001b[39;49;00m: \u001b[33m\"world\"\u001b[39;49;00m\r\n}\r\n\r\n"] [46.219896, "o", "(venv37) /tmp/quickstart $ "] [49.057166, "o", "v"] [49.137315, "o", "i"] [49.141611, "o", "m"] [49.198615, "o", " "] [49.390691, "o", "a"] [49.506651, "o", "p"] [49.658458, "o", "p.py "] [50.447641, "o", "\r\n"] [50.506808, "o", "\u001b[?1000h\u001b[?2004h\u001b[?1049h\u001b[?1h\u001b=\u001b[?2004h"] [50.512619, "o", "\u001b[1;40r\u001b[?12h\u001b[?12l\u001b[27m\u001b[29m\u001b[m\u001b[H\u001b[2J\u001b[?25l\u001b[40;1H\"app.py\""] [50.512899, "o", " 29L, 735C"] [50.558635, "o", "\u001b[1;1H\u001b[93m 1 \u001b[mfrom chalice import Chalice\r\n\u001b[93m 2 \r\n 3 \u001b[mapp = Chalice(app_name='quickstart')\r\n\u001b[93m 4 \r\n 5 \r\n 6 \u001b[m@app.route('/')\r\n\u001b[93m 7 \u001b[mdef index():\r\n\u001b[93m 8 \u001b[m return {'hello': 'world'}\r\n\u001b[93m 9 \r\n 10 \r\n 11 \u001b[m# The view function above will return {\"hello\": \"world\"}\r\n\u001b[93m 12 \u001b[m# whenever you make an HTTP GET request to '/'.\r\n\u001b[93m 13 \u001b[m#\r\n\u001b[93m 14 \u001b[m# Here are a few more examples:\r\n\u001b[93m 15 \u001b[m#\r\n\u001b[93m 16 \u001b[m# @app.route('/hello/{name}')\r\n\u001b[93m 17 \u001b[m# def hello_name(name):\r\n\u001b[93m 18 \u001b[m# # '/hello/james' -> {\"hello\": \"james\"}\r\n\u001b[93m 19 \u001b[m# return {'hello': name}\r\n\u001b[93m 20 \u001b[m#\r\n\u001b[93m 21 \u001b[m# @app.route('/users', methods=['POST'])\r\n\u001b[93m 22 \u001b[m# def create_user():\r\n\u001b[93m 23 \u001b[m# # This is the JSON body the user sent in their POST request.\r\n\u001b[93m 24 \u001b[m# user_as_json = app.current_request.json_body\r\n\u001b[93m 25 \u001b[m# # We'll echo the json body back to the user in a 'user' key.\r\n\u001b[93m 26 \u001b[m# return {'user': user_as_json}\r\n\u001b[93m 27 \u001b[m#\r\n\u001b[93m 28 \u001b[m# See the RE"] [50.559194, "o", "ADME documentation for more examples.\r\n\u001b[93m 29 \u001b[m#\r\n\u001b[94m~ \u001b[31;1H~ \u001b[32;1H~ \u001b[33;1H~ \u001b[34;1H~ \u001b[35;1H~ "] [50.559597, "o", " \u001b[36;1H~ \u001b[37;1H~ \u001b[38;1H~ \u001b[39;1H~ \u001b[m\u001b[40;1H\u001b[38;5;224m[Pymode] Activate virtualenv: /private/tmp/venv37"] [50.587979, "o", "\u001b[2;1H▽\u001b[6n\u001b[2;1H \u001b[1;1H"] [50.588141, "o", "\u001b[>c\u001b]10;?\u0007\u001b]11;?\u0007"] [50.605848, "o", "\u001b[m\u001b[1;5H\u001b[38;5;81mfrom\u001b[9Cimport\u001b[m\u001b[3;9H\u001b[93m=\u001b[17C=\u001b[m\u001b[95m'quickstart'\u001b[m\u001b[6;5H\u001b[38;5;81m@\u001b[m\u001b[1m\u001b[96mapp\u001b[m.\u001b[1m\u001b[96mroute\u001b[m(\u001b[95m'/'\u001b[m\u001b[7;5H\u001b[93mdef\u001b[m \u001b[1m\u001b[96mindex\u001b[m\u001b[8;9H\u001b[93mreturn\u001b[m {\u001b[95m'hello'\u001b[m: \u001b[95m'world'\u001b[m\u001b[11;5H\u001b[96m# The view function above will return {\"hello\": \"world\"}\u001b[12;5H# whenever you make an HTTP GET request to '/'.\u001b[13;5H#\u001b[14;5H# Here are a few more examples:\u001b[15;5H#\u001b[16;5H# @app.route('/hello/{name}')\u001b[17;5H# def hello_name(name):\u001b[18;5H# # '/hello/james' -> {\"hello\": \"james\"}\u001b[19;5H# return {'hello': name}\u001b[20;5H#\u001b[21;5H# @app.route('/users', methods=['POST'])\u001b[22;5H# def create_user():\u001b[23;5H# # This is the JSON body the user sent in their POST request.\u001b[24;5H# user_as_json = app.current_request.json_body\u001b[25;5H# # We'll echo the json body back to the user in a 'user' key.\u001b[26;5H# return {'user': user_as_json}\u001b[27;5H#\u001b[28;5H# See the README documentation for more examples.\u001b[29;5H#\u001b[m\u001b[40;141H1,1\u001b[11CAll\u001b[1;5H\u001b[?25h"] [50.606087, "o", "\u001b[?1000l\u001b[?1006h\u001b[?1002h\u001b[?1006l\u001b[?1002l\u001b[?1006h\u001b[?1002h\u001b[?12$p"] [51.146831, "o", "\u001b[?25l\u001b[40;141H2,0-1\u001b[2;5H\u001b[?25h"] [51.306094, "o", "\u001b[?25l\u001b[40;141H3,1 \u001b[3;5H\u001b[?25h"] [51.469845, "o", "\u001b[?25l\u001b[40;141H4,0-1\u001b[4;5H\u001b[?25h"] [51.631969, "o", "\u001b[?25l\u001b[40;141H5\u001b[5;5H\u001b[?25h"] [52.234099, "o", "\u001b[?25l\u001b[40;141H6,1 \u001b[6;5H\u001b[?25h"] [52.419097, "o", "\u001b[?25l\u001b[1C\u001b[1m\u001b[96m\u001b[48;5;242mapp\u001b[m\u001b[48;5;242m.\u001b[m\u001b[1m\u001b[96m\u001b[48;5;242mroute\u001b[m\u001b[48;5;242m(\u001b[m\u001b[95m\u001b[48;5;242m'/'\u001b[m\u001b[48;5;242m) "] [52.419253, "o", "\u001b[m\u001b[40;1H\u001b[1m-- VISUAL LINE --\u001b[m\u001b[40;18H\u001b[K\u001b[40;141H6,1\u001b[11CAll\u001b[6;5H\u001b[?25h"] [52.524012, "o", "\u001b[?25l\u001b[38;5;81m\u001b[48;5;242m@\u001b[m\u001b[7;6H\u001b[93m\u001b[48;5;242mef\u001b[m\u001b[48;5;242m \u001b[m\u001b[1m\u001b[96m\u001b[48;5;242mindex\u001b[m\u001b[48;5;242m(): \u001b[m\u001b[40;141H7\u001b[7;5H\u001b[?25h"] [52.658516, "o", "\u001b[?25l\u001b[93m\u001b[48;5;242md\u001b[m\u001b[8;6H\u001b[48;5;242m \u001b[m\u001b[93m\u001b[48;5;242mreturn\u001b[m\u001b[48;5;242m {\u001b[m\u001b[95m\u001b[48;5;242m'hello'\u001b[m\u001b[48;5;242m: \u001b[m\u001b[95m\u001b[48;5;242m'world'\u001b[m\u001b[48;5;242m} \u001b[m\u001b[40;141H8\u001b[8;5H\u001b[?25h"] [52.770352, "o", "\u001b[?25l\u001b[48;5;242m \u001b[m\u001b[40;141H9,0-1\u001b[9;5H\u001b[?25h"] [52.904444, "o", "\u001b[?25l\u001b[6;5H\u001b[38;5;81m@\u001b[m\u001b[1m\u001b[96mapp\u001b[m.\u001b[1m\u001b[96mroute\u001b[m(\u001b[95m'/'\u001b[m)\u001b[6;20H\u001b[K\u001b[7;5H\u001b[93mdef\u001b[m \u001b[1m\u001b[96mindex\u001b[m():\u001b[7;17H\u001b[K\u001b[8;5H \u001b[93mreturn\u001b[m {\u001b[95m'hello'\u001b[m: \u001b[95m'world'\u001b[m}\u001b[8;34H\u001b[K\u001b[40;1H\u001b[K\u001b[40;141H6,1\u001b[11CAll\r4 lines yanked\u001b[40;141H\u001b[K"] [52.904636, "o", "\u001b[40;141H6,1\u001b[11CAll\u001b[6;5H\u001b[?25h"] [53.068878, "o", "\u001b[?25l\u001b[40;141H7\u001b[7;5H\u001b[?25h"] [53.188894, "o", "\u001b[?25l\u001b[40;141H8\u001b[8;5H\u001b[?25h"] [53.31296, "o", "\u001b[?25l\u001b[40;141H9,0-1\u001b[9;5H\u001b[?25h"] [53.404885, "o", "\u001b[?25l\u001b[40;3Hmore lines\u001b[40;13H\u001b[K"] [53.42306, "o", "\u001b[10;5H\u001b[38;5;81m@\u001b[m\u001b[1m\u001b[96mapp\u001b[m.\u001b[1m\u001b[96mroute\u001b[m(\u001b[95m'/'\u001b[m)\u001b[11;5H\u001b[93mdef\u001b[m \u001b[1m\u001b[96mindex\u001b[m():\u001b[11;17H\u001b[K\u001b[12;5H \u001b[93mreturn\u001b[m {\u001b[95m'hello'\u001b[m: \u001b[95m'world'\u001b[m}\u001b[12;34H\u001b[K\u001b[13;5H\u001b[K\u001b[14;5H\u001b[K\u001b[15;6H\u001b[96m The view function above will return {\"hello\": \"world\"}\u001b[16;7Hwhenever you make an HTTP GET request to '/'.\u001b[m\u001b[17;6H\u001b[K\u001b[18;7H\u001b[96mHere are a few more examples:\u001b[m\u001b[18;36H\u001b[K\u001b[19;6H\u001b[K\u001b[20;6H\u001b[96m @app.route('/hello/{name}')\u001b[21;7Hdef hello_name(name):\u001b[m\u001b[21;28H\u001b[K\u001b[22;7H\u001b[96m # '/hello/james' -> {\"hello\": \"james\"}\u001b[23;10Hreturn {'hello': name}\u001b[m\u001b[23;32H\u001b[K\u001b[24;6H\u001b[K\u001b[25;7H\u001b[96m@app.route('/users', methods=['POST'])\u001b[m\u001b[25;45H\u001b[K\u001b[26;7H\u001b[96mdef create_user():\u001b[m\u001b[26;25H\u001b[K\u001b[27;6H\u001b[96m # This is the JSON body the user sent in their POST request.\u001b[28;7H user_as_json = app.current_request.json_body\u001b[29;6H # We'll echo the json body back to the user in a 'user' key.\u001b[m\r\n\u001b[93m 30 \u001b[m\u001b[96m# return {'user': user_as_json}\u001b[m\u001b[30;40H\u001b[K\u001b[31;1H\u001b[93m 31 \u001b[m\u001b[96m#\u001b[m\u001b[31;6H\u001b[K\u001b[32;1H\u001b["] [53.423236, "o", "93m 32 \u001b[m\u001b[96m# See the README documentation for more examples.\u001b[m\u001b[32;54H\u001b[K\u001b[33;1H\u001b[93m 33 \u001b[m\u001b[96m#\u001b[m\u001b[33;6H\u001b[K\u001b[40;141H10,1\u001b[10CAll\u001b[40;141H\u001b[K\u001b[40;141H10,1\u001b[10CAll\u001b[10;5H\u001b[?25h"] [54.356725, "o", "\u001b[?25l\u001b[10C\u001b[46m(\u001b[3C)\u001b[m\u001b[40;145H5\u001b[10;19H\u001b[?25h"] [54.514045, "o", "\u001b[?25l\b\b\b\b(\u001b[3C)\u001b[40;145H4\u001b[10;18H\u001b[?25h"] [54.664626, "o", "\u001b[?25l\u001b[40;1H\u001b[1m-- INSERT --\u001b[m\u001b[40;141H\u001b[K\u001b[40;141H10,14\u001b[9CAll"] [54.664783, "o", "\u001b[10;18H\u001b[?25h"] [54.853485, "o", "\u001b[?25l\u001b[95mh'\u001b[m)\u001b[40;145H5\u001b[10;19H\u001b[?25h"] [54.980214, "o", "\u001b[?25l\u001b[95me'\u001b[m)\u001b[40;145H6\u001b[10;20H\u001b[?25h"] [55.07667, "o", "\u001b[?25l\u001b[95ml'\u001b[m)\u001b[40;145H7\u001b[10;21H\u001b[?25h"] [55.1742, "o", "\u001b[?25l\u001b[95ml'\u001b[m)\u001b[40;145H8\u001b[10;22H\u001b[?25h"] [55.352723, "o", "\u001b[?25l\u001b[95mo'\u001b[m)\u001b[40;145H9\u001b[10;23H\u001b[?25h"] [55.688281, "o", "\u001b[?25l\u001b[95m/'\u001b[m)\u001b[40;144H20\u001b[10;24H\u001b[?25h"] [56.129681, "o", "\u001b[?25l\u001b[95m{'\u001b[m)\u001b[40;145H1\u001b[10;25H\u001b[?25h"] [56.32739, "o", "\u001b[?25l\u001b[95mn'\u001b[m)\u001b[40;145H2\u001b[10;26H\u001b[?25h"] [56.424794, "o", "\u001b[?25l\u001b[95ma'\u001b[m)\u001b[40;145H3\u001b[10;27H\u001b[?25h"] [56.482477, "o", "\u001b[?25l\u001b[95mm'\u001b[m)\u001b[40;145H4\u001b[10;28H\u001b[?25h"] [56.582245, "o", "\u001b[?25l\u001b[95me'\u001b[m)\u001b[40;145H5\u001b[10;29H\u001b[?25h"] [56.753992, "o", "\u001b[?25l\b\b\b\b\b\u001b[38;5;224m{name}\u001b[m\u001b[95m'\u001b[m)\u001b[40;145H6\u001b[10;30H\u001b[?25h"] [56.956073, "o", "\u001b[40;1H\u001b[K\u001b[10;29H"] [57.005342, "o", "\u001b[?25l"] [57.006671, "o", "\u001b[40;141H10,25\u001b[9CAll\u001b[10;29H\u001b[?25h\u001b[?25l\u001b[40;142H1,12\u001b[11;16H\u001b[?25h"] [57.196056, "o", "\u001b[?25l\b\b\u001b[46m()\u001b[m\u001b[40;145H1\u001b[11;15H\u001b[?25h"] [57.432569, "o", "\u001b[?25l\u001b[40;1H\u001b[1m-- INSERT --\u001b[m\u001b[40;141H\u001b[K\u001b[40;141H11,11\u001b[9CAll"] [57.432697, "o", "\u001b[11;15H\u001b[?25h"] [57.602247, "o", "\u001b[?25l\u001b[46mn\u001b[m):\b\b\bn\u001b[46m)\u001b[m\u001b[40;145H2\u001b[11;16H\u001b[?25h"] [57.683304, "o", "\u001b[?25l\u001b[46ma\u001b[m):\b\b\ba\u001b[46m)\u001b[m\u001b[40;145H3\u001b[11;17H\u001b[?25h"] [57.774657, "o", "\u001b[?25l\u001b[46mm\u001b[m):\b\b\bm\u001b[46m)\u001b[m\u001b[40;145H4\u001b[11;18H\u001b[?25h"] [57.8501, "o", "\u001b[?25l\u001b[46me\u001b[m):\b\b\be\u001b[46m)\u001b[m\u001b[40;145H5\u001b[11;19H\u001b[?25h"] [57.956926, "o", "\u001b[40;1H\u001b[K\u001b[11;18H"] [58.111393, "o", "\u001b[?25l"] [58.113038, "o", "\b\b\b\b(name)\u001b[40;141H11,14\u001b[9CAll\u001b[11;18H\u001b[?25h\u001b[?25l\u001b[40;145H \u001b[11;5H\u001b[?25h"] [58.279802, "o", "\u001b[?25l\u001b[40;144H5\u001b[11;9H\u001b[?25h"] [58.791478, "o", "\u001b[?25l\u001b[40;1H\u001b[1m-- INSERT --\u001b[m\u001b[40;141H\u001b[K\u001b[40;141H11,5\u001b[10CAll"] [58.796463, "o", "\u001b[11;9H(name):\u001b[11;16H\u001b[K\u001b[11;9H\u001b[46m(\u001b[mname\u001b[46m)\b\b\b\b\b\b\u001b[?25h"] [58.911903, "o", "\u001b[?25l\u001b[m\u001b[1m\u001b[96m\u001b[46mh\u001b[m(nam\u001b[46me\u001b[m):\u001b[11;9H\u001b[1m\u001b[96mh\u001b[m\u001b[46m(\u001b[mname\u001b[46m)\u001b[m\u001b[40;144H6\u001b[11;10H\u001b[?25h"] [59.002263, "o", "\u001b[?25l\u001b[1m\u001b[96m\u001b[46me\u001b[m(nam\u001b[46me\u001b[m):\u001b[11;10H\u001b[1m\u001b[96me\u001b[m\u001b[46m(\u001b[mname\u001b[46m)\u001b[m\u001b[40;144H7\u001b[11;11H\u001b[?25h"] [59.087106, "o", "\u001b[?25l\u001b[1m\u001b[96m\u001b[46ml\u001b[m(nam\u001b[46me\u001b[m):\u001b[11;11H\u001b[1m\u001b[96ml\u001b[m\u001b[46m(\u001b[mname\u001b[46m)\u001b[m\u001b[40;144H8\u001b[11;12H\u001b[?25h"] [59.197175, "o", "\u001b[?25l\u001b[1m\u001b[96m\u001b[46ml\u001b[m(nam\u001b[46me\u001b[m):\u001b[11;12H\u001b[1m\u001b[96ml\u001b[m\u001b[46m(\u001b[mname\u001b[46m)\u001b[m\u001b[40;144H9\u001b[11;13H\u001b[?25h"] [59.340986, "o", "\u001b[?25l\u001b[1m\u001b[96m\u001b[46mo\u001b[m(nam\u001b[46me\u001b[m):\u001b[11;13H\u001b[1m\u001b[96mo\u001b[m\u001b[46m(\u001b[mname\u001b[46m)\u001b[m\u001b[40;144H10\u001b[11;14H\u001b[?25h"] [59.478202, "o", "\u001b[40;1H\u001b[K\u001b[11;13H"] [59.59759, "o", "\u001b[?25l"] [59.599543, "o", "\u001b[1C(name)\u001b[40;141H11,9\u001b[10CAll\u001b[11;13H\u001b[?25h\u001b[?25l\u001b[40;142H2\u001b[12;13H\u001b[?25h"] [60.309843, "o", "\u001b[?25l\u001b[3C\u001b[46m{\u001b[16C}\u001b[m\u001b[40;144H12\u001b[12;16H\u001b[?25h"] [60.430347, "o", "\u001b[?25l{\u001b[16C}\u001b[40;145H4\u001b[12;18H\u001b[?25h"] [60.56452, "o", "\u001b[?25l\u001b[40;145H9\u001b[12;23H\u001b[?25h"] [60.705548, "o", "\u001b[?25l\u001b[40;144H22\u001b[12;26H\u001b[?25h"] [61.376546, "o", "\u001b[?25l\u001b[40;1H\u001b[1m-- INSERT --\u001b[m\u001b[40;141H\u001b[K\u001b[40;141H12,22\u001b[9CAll"] [61.381539, "o", "\u001b[12;26H}\u001b[12;27H\u001b[K\u001b[12;16H\u001b[46m{\u001b[9C}\b\u001b[?25h"] [61.933986, "o", "\u001b[?25ln\u001b[m}\b\bn\u001b[46m}\u001b[m\u001b[40;145H3\u001b[12;27H\u001b[?25h"] [62.020328, "o", "\u001b[?25l\u001b[46ma\u001b[m}\b\ba\u001b[46m}\u001b[m\u001b[40;145H4\u001b[12;28H\u001b[?25h"] [62.088359, "o", "\u001b[?25l\u001b[46mm\u001b[m}\b\bm\u001b[46m}\u001b[m\u001b[40;145H5\u001b[12;29H\u001b[?25h"] [62.173839, "o", "\u001b[?25l\u001b[46me\u001b[m}\b\be\u001b[46m}\u001b[m\u001b[40;145H6\u001b[12;30H\u001b[?25h"] [62.436729, "o", "\u001b[40;1H\u001b[K\u001b[12;29H"] [62.575634, "o", "\u001b[?25l"] [62.576755, "o", "\u001b[12;16H{\u001b[13C}\u001b[40;141H12,25\u001b[9CAll\u001b[12;29H\u001b[?25h\u001b[?25l\u001b[40;142H3,0-1\u001b[13;5H\u001b[?25h"] [62.867449, "o", "\u001b[?25l\u001b[40;1H21 fewer lines\u001b[40;141H\u001b[K"] [62.868158, "o", "\u001b[13;1H\u001b[94m~ \u001b[14;1H~ \u001b[15;1H~ \u001b[16;1H~ \u001b[17;1H~ \u001b[18;1H~ \u001b[19;1H~ "] [62.868378, "o", " \u001b[20;1H~ \u001b[21;1H~ \u001b[22;1H~ \u001b[23;1H~ \u001b[24;1H~ \u001b[25;1H~ "] [62.86863, "o", " \u001b[26;1H~ \u001b[27;1H~ \u001b[28;1H~ \u001b[29;1H~ \u001b[30;1H~ \u001b[31;1H~ "] [62.868824, "o", " \u001b[32;1H~ \u001b[33;1H~ \u001b[m\u001b[40;141H12,5\u001b[10CAll\u001b[40;141H\u001b[K\u001b[40;141H12,5\u001b[10CAll\u001b[12;9H\u001b[?25h"] [63.205557, "o", "\u0007\u001b[?25l\u001b[?25h\u001b[?25l\u001b[40;1H\u001b[K\u001b[40;1H:\u001b[?2004h"] [63.205812, "o", "\u001b[?25h"] [63.324591, "o", "w\u001b[?25l\u001b[?25h"] [63.348525, "o", "q"] [63.348727, "o", "\u001b[?25l\u001b[?25h"] [63.909529, "o", "\u001b[?25l\u001b[40;1H\u001b[K\u001b[40;141H12,5\u001b[10CAll\u001b[12;9H\u001b[?25h"] [64.012766, "o", "\u0007\u001b[?25l\u001b[40;142H1\u001b[11;9H\u001b[?25h"] [64.131252, "o", "\u001b[?25l\u001b[40;142H0\u001b[10;9H\u001b[?25h"] [64.267846, "o", "\u001b[?25l\u001b[40;141H9,0-1\u001b[9;5H\u001b[?25h"] [64.654867, "o", "\u001b[?25l\u001b[40;1H\u001b[1m-- INSERT --\u001b[m\u001b[40;141H\u001b[K\u001b[40;141H10,1\u001b[10CTop"] [64.656708, "o", "\u001b[10;5H\u001b[K\u001b[11;5H\u001b[38;5;81m@\u001b[m\u001b[1m\u001b[96mapp\u001b[m.\u001b[1m\u001b[96mroute\u001b[m(\u001b[95m'/hello/\u001b[m\u001b[38;5;224m{name}\u001b[m\u001b[95m'\u001b[m)\u001b[12;5H\u001b[93mdef\u001b[m \u001b[1m\u001b[96mhello\u001b[m(name):\u001b[12;21H\u001b[K\u001b[13;1H\u001b[93m 13 \u001b[m \u001b[93mreturn\u001b[m {\u001b[95m'hello'\u001b[m: name}\u001b[13;31H\u001b[K\u001b[10;5H\u001b[?25h"] [64.911661, "o", "\u001b[40;1H\u001b[K\u001b[10;5H"] [65.200368, "o", "\u001b[?25l"] [65.200588, "o", "\u001b[40;141H10,0-1\u001b[8CAll\u001b[10;5H\u001b[?25h\u001b[?25l\u001b[40;141H\u001b[K\u001b[40;1H:\u001b[?2004h\u001b[?25h"] [65.332362, "o", "w\u001b[?25l\u001b[?25h"] [65.367727, "o", "q\u001b[?25l\u001b[?25h"] [66.912049, "o", "\r"] [66.912257, "o", "\u001b[?25l\u001b[?1006l\u001b[?1002l\u001b[?2004l"] [66.91259, "o", "\"app.py\""] [66.914931, "o", " 13L, 201C written"] [67.026347, "o", "\r\r\r\n\u001b[?2004l\u001b[?1l\u001b>\u001b[?25h\u001b[?1049l"] [67.042676, "o", "(venv37) /tmp/quickstart $ "] [67.389317, "o", "c"] [67.465545, "o", "h"] [67.564323, "o", "a"] [67.607364, "o", "l"] [67.667366, "o", "i"] [67.743259, "o", "c"] [67.79956, "o", "e"] [67.883361, "o", " "] [67.99061, "o", "d"] [68.123009, "o", "e"] [68.164435, "o", "p"] [68.212231, "o", "l"] [68.355133, "o", "o"] [68.481266, "o", "y"] [68.756228, "o", "\r\n"] [69.130517, "o", "Creating deployment package.\r\n"] [70.214404, "o", "Updating policy for IAM role: quickstart-dev\r\n"] [70.279891, "o", "Updating lambda function: quickstart-dev\r\n"] [71.065479, "o", "Updating rest API\r\n"] [72.568024, "o", "Resources deployed:\r\n - Lambda ARN: arn:aws:lambda:us-west-2:675687397822:function:quickstart-dev\r\n - Rest API URL: https://b5fnh9kzs6.execute-api.us-west-2.amazonaws.com/api/\r\n"] [72.646763, "o", "(venv37) /tmp/quickstart $ "] [74.602372, "o", "chalice deploy\b\b\b\b\b\b\b\b\b\b\b\b\b\b"] [74.833353, "o", "\u001b[3Pvim app.py \b\b\b\b\b\b\b\b\b\b\b"] [75.247373, "o", "http https://b5fnh9kzs6.execute-api.us-west-2.amazonaws.com/api/\r\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C"] [76.011695, "o", "\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C"] [76.460939, "o", "h"] [76.593905, "o", "e"] [76.682525, "o", "l"] [76.8132, "o", "l"] [76.96527, "o", "o"] [77.257256, "o", "/"] [77.485964, "o", "j"] [77.628394, "o", "a"] [77.67508, "o", "m"] [77.810034, "o", "e"] [77.846264, "o", "s"] [79.500303, "o", "\r\n"] [80.556672, "o", "\u001b[34mHTTP\u001b[39;49;00m/\u001b[34m1.1\u001b[39;49;00m \u001b[34m200\u001b[39;49;00m \u001b[36mOK\u001b[39;49;00m\r\n\u001b[36mConnection\u001b[39;49;00m: keep-alive\r\n\u001b[36mContent-Length\u001b[39;49;00m: 17\r\n\u001b[36mContent-Type\u001b[39;49;00m: application/json\r\n\u001b[36mDate\u001b[39;49;00m: Mon, 29 Jun 2020 18:01:09 GMT\r\n\u001b[36mVia\u001b[39;49;00m: 1.1 fba666ceffdeb316c8edf476d8994bd5.cloudfront.net (CloudFront)\r\n\u001b[36mX-Amz-Cf-Id\u001b[39;49;00m: Q1-NF5uST8iVuyrnEnclDVa-Ji-SxHUqbaqJxJWhEyL9TMnincbIkQ==\r\n\u001b[36mX-Amz-Cf-Pop\u001b[39;49;00m: IAD89-C1\r\n\u001b[36mX-Amzn-Trace-Id\u001b[39;49;00m: Root=1-5efa2c65-84110420cb5f38eaa1c19888;Sampled=0\r\n\u001b[36mX-Cache\u001b[39;49;00m: Miss from cloudfront\r\n\u001b[36mx-amz-apigw-id\u001b[39;49;00m: O5vf3FeCvHcFX3Q=\r\n\u001b[36mx-amzn-RequestId\u001b[39;49;00m: 6a6ce2d8-74cb-4dcd-a5a0-9b55b911f7e2\r\r\n\r\r\n"] [80.558673, "o", "{\r\n \u001b[34;01m\"hello\"\u001b[39;49;00m: \u001b[33m\"james\"\u001b[39;49;00m\r\n}\r\n\r\n"] [80.603004, "o", "(venv37) /tmp/quickstart $ "] [85.496612, "o", "c"] [85.543118, "o", "h"] [85.62348, "o", "a"] [85.703524, "o", "l"] [85.75966, "o", "i"] [85.81519, "o", "c"] [85.89096, "o", "e"] [85.97549, "o", " "] [86.174666, "o", "d"] [86.303506, "o", "e"] [86.419472, "o", "l"] [86.474898, "o", "e"] [86.570301, "o", "t"] [86.647516, "o", "e"] [87.030095, "o", "\r\n"] [87.340116, "o", "Deleting Rest API: b5fnh9kzs6\r\n"] [88.075667, "o", "Deleting function: arn:aws:lambda:us-west-2:675687397822:function:quickstart-dev\r\n"] [88.601449, "o", "Deleting IAM role: quickstart-dev\r\n"] [88.906189, "o", "(venv37) /tmp/quickstart $ "] [90.826624, "o", "exit\r\n"] ================================================ FILE: docs/source/theme/smithy/static/casts/chalice-quickstart.cast ================================================ {"version": 2, "width": 87, "height": 23, "timestamp": 1593531270, "env": {"SHELL": "/bin/bash", "TERM": "xterm-256color"}} [1.67467, "o", "/tmp $ "] [2.82912, "o", "p"] [2.908222, "o", "y"] [3.054837, "o", "t"] [3.159015, "o", "h"] [3.236088, "o", "o"] [3.358366, "o", "n"] [3.506435, "o", "3"] [3.646629, "o", " "] [3.800527, "o", "-"] [3.874477, "o", "m"] [3.939706, "o", " "] [4.113357, "o", "v"] [4.206063, "o", "e"] [4.298333, "o", "n"] [4.394909, "o", "v"] [4.470036, "o", " "] [4.534614, "o", "/"] [4.664571, "o", "t"] [4.728648, "o", "m"] [4.792779, "o", "p"] [4.874453, "o", "/"] [5.10581, "o", "v"] [5.258582, "o", "e"] [5.35144, "o", "n"] [5.451347, "o", "v"] [5.742178, "o", "3"] [5.850467, "o", "7"] [6.310548, "o", "\r\n"] [7.626815, "o", "/tmp $ "] [8.061287, "o", "."] [8.154682, "o", " "] [8.23327, "o", "/"] [8.392318, "o", "t"] [8.472022, "o", "m"] [8.524285, "o", "p"] [8.604116, "o", "/"] [8.809077, "o", "v"] [8.963934, "o", "e"] [9.191374, "o", "nv37/"] [9.90222, "o", "b"] [9.954488, "o", "i"] [10.010006, "o", "n"] [10.08251, "o", "/"] [10.33724, "o", "a"] [10.425233, "o", "c"] [10.597212, "o", "tivate"] [10.986728, "o", "\r\n"] [10.99959, "o", "(venv37) /tmp $ "] [11.522134, "o", "p"] [11.653115, "o", "i"] [11.716907, "o", "p"] [11.809288, "o", " "] [11.853119, "o", "i"] [11.929131, "o", "n"] [11.996453, "o", "s"] [12.029432, "o", "t"] [12.129105, "o", "a"] [12.197512, "o", "l"] [12.297103, "o", "l"] [12.360499, "o", " "] [12.441458, "o", "c"] [12.51321, "o", "h"] [12.588779, "o", "a"] [12.681105, "o", "l"] [12.729368, "o", "i"] [12.785045, "o", "c"] [12.825263, "o", "e"] [13.08795, "o", "\r\n"] [13.559207, "o", "Requirement already satisfied: chalice in ./venv37/lib/python3.7/site-packages (1.15.1)\r\n"] [13.576265, "o", "Requirement already satisfied: botocore<2.0.0,>=1.12.86 in ./venv37/lib/python3.7/site-packages (from chalice) (1.17.12)\r\n"] [13.585476, "o", "Requirement already satisfied: pyyaml<6.0.0,>=5.3.1 in ./venv37/lib/python3.7/site-packages (from chalice) (5.3.1)\r\n"] [13.590093, "o", "Requirement already satisfied: wheel in ./venv37/lib/python3.7/site-packages (from chalice) (0.34.2)\r\n"] [13.594832, "o", "Requirement already satisfied: click<8.0,>=6.6 in ./venv37/lib/python3.7/site-packages (from chalice) (7.1.2)\r\n"] [13.597284, "o", "Requirement already satisfied: setuptools in ./venv37/lib/python3.7/site-packages (from chalice) (40.8.0)\r\n"] [13.602554, "o", "Requirement already satisfied: mypy-extensions==0.4.3 in ./venv37/lib/python3.7/site-packages (from chalice) (0.4.3)\r\n"] [13.608629, "o", "Requirement already satisfied: attrs<20.0.0,>=19.3.0 in ./venv37/lib/python3.7/site-packages (from chalice) (19.3.0)\r\n"] [13.642509, "o", "Requirement already satisfied: pip<20.2,>=9 in ./venv37/lib/python3.7/site-packages (from chalice) (19.0.3)\r\n"] [13.645222, "o", "Requirement already satisfied: enum-compat>=0.0.2 in ./venv37/lib/python3.7/site-packages (from chalice) (0.0.3)\r\n"] [13.648654, "o", "Requirement already satisfied: six<2.0.0,>=1.10.0 in ./venv37/lib/python3.7/site-packages (from chalice) (1.15.0)\r\n"] [13.651191, "o", "Requirement already satisfied: jmespath<1.0.0,>=0.9.3 in ./venv37/lib/python3.7/site-packages (from chalice) (0.10.0)\r\n"] [13.655427, "o", "Requirement already satisfied: docutils<0.16,>=0.10 in ./venv37/lib/python3.7/site-packages (from botocore<2.0.0,>=1.12.86->chalice) (0.15.2)\r\n"] [13.661959, "o", "Requirement already satisfied: python-dateutil<3.0.0,>=2.1 in ./venv37/lib/python3.7/site-packages (from botocore<2.0.0,>=1.12.86->chalice) (2.8.1)\r\n"] [13.666173, "o", "Requirement already satisfied: urllib3<1.26,>=1.20; python_version != \"3.4\" in ./venv37/lib/python3.7/site-packages (from botocore<2.0.0,>=1.12.86->chalice) (1.25.9)\r\n"] [13.704113, "o", "\u001b[33mYou are using pip version 19.0.3, however version 20.2b1 is available.\r\nYou should consider upgrading via the 'pip install --upgrade pip' command.\u001b[0m\r\n"] [13.765639, "o", "(venv37) /tmp $ "] [15.423004, "o", "c"] [15.494675, "o", "h"] [15.625856, "o", "a"] [15.924079, "o", "l"] [15.99449, "o", "i"] [16.055846, "o", "c"] [16.134495, "o", "e"] [16.258072, "o", " "] [16.333947, "o", "n"] [16.393892, "o", "e"] [16.453783, "o", "w"] [16.572702, "o", "-"] [16.713632, "o", "p"] [16.808701, "o", "r"] [16.869814, "o", "o"] [16.929625, "o", "j"] [17.013662, "o", "e"] [17.089494, "o", "c"] [17.310739, "o", "t"] [17.391292, "o", " "] [17.537597, "o", "q"] [17.605757, "o", "u"] [17.66049, "o", "i"] [17.774063, "o", "c"] [17.890681, "o", "k"] [18.226816, "o", "s"] [18.314648, "o", "t"] [18.44831, "o", "a"] [18.507571, "o", "r"] [18.671791, "o", "t"] [18.774308, "o", "\r\n"] [19.11508, "o", "(venv37) /tmp $ "] [19.594546, "o", "c"] [19.658678, "o", "d"] [19.73047, "o", " "] [19.90467, "o", "q"] [19.995339, "o", "u"] [20.043158, "o", "i"] [20.147159, "o", "ckstart/"] [20.563526, "o", "\r\n"] [20.598524, "o", "(venv37) /tmp/quickstart $ "] [21.372239, "o", "t"] [21.448471, "o", "r"] [21.484382, "o", "e"] [21.725807, "o", "e"] [21.943864, "o", "\r\n"] [21.954549, "o", ".\r\n├── app.py\r\n└── requirements.txt\r\n\r\n0 directories, 2 files\r\n"] [21.966425, "o", "(venv37) /tmp/quickstart $ "] [23.820448, "o", "c"] [23.901148, "o", "h"] [23.988039, "o", "a"] [24.067824, "o", "l"] [24.11203, "o", "i"] [24.187865, "o", "c"] [24.264505, "o", "e"] [24.295824, "o", " "] [24.390982, "o", "d"] [24.534279, "o", "e"] [24.573985, "o", "p"] [24.621019, "o", "l"] [24.783132, "o", "o"] [24.891243, "o", "y"] [25.192351, "o", "\r\n"] [25.535327, "o", "Creating deployment package.\r\n"] [26.122212, "o", "Creating IAM role: quickstart-dev\r\n"] [26.291715, "o", "Creating lambda function: quickstart-dev\r\n"] [37.103502, "o", "Creating Rest API\r\n"] [38.421184, "o", "Resources deployed:\r\n - Lambda ARN: arn:aws:lambda:us-west-2:675687397822:function:quickstart-dev\r\n - Rest API URL: https://vdpmshluug.execute-api.us-west-2.amazonaws.com/api/\r\n"] [38.49129, "o", "(venv37) /tmp/quickstart $ "] [43.017523, "o", "h"] [43.12237, "o", "t"] [43.250348, "o", "t"] [43.329082, "o", "p"] [43.377087, "o", " "] [43.636587, "o", "ht"] [43.636784, "o", "tps://vdpmshluug.e"] [43.636878, "o", "xecute-a"] [43.636913, "o", "pi."] [43.636942, "o", "us-"] [43.636967, "o", "we"] [43.636989, "o", "st"] [43.637241, "o", "-2"] [43.637339, "o", ".amazonaws.com/ \rapi/"] [44.53644, "o", "\r\n"] [45.509916, "o", "\u001b[34mHTTP\u001b[39;49;00m/\u001b[34m1.1\u001b[39;49;00m \u001b[34m200\u001b[39;49;00m \u001b[36mOK\u001b[39;49;00m\r\n\u001b[36mConnection\u001b[39;49;00m: keep-alive\r\n\u001b[36mContent-Length\u001b[39;49;00m: 17\r\n\u001b[36mContent-Type\u001b[39;49;00m: application/json\r\n\u001b[36mDate\u001b[39;49;00m: Tue, 30 Jun 2020 15:35:15 GMT\r\n\u001b[36mVia\u001b[39;49;00m: 1.1 2d922ab79d41a826404f05ff416bb98c.cloudfront.net (CloudFront)\r\n\u001b[36mX-Amz-Cf-Id\u001b[39;49;00m: 4A5pVKErCSnHdfawudfAY12sTIIe4c1yaZ3VhGReZkcsPBoq3DIJpw==\r\n\u001b[36mX-Amz-Cf-Pop\u001b[39;49;00m: EWR53-C1\r\n\u001b[36mX-Amzn-Trace-Id\u001b[39;49;00m: Root=1-5efb5bb3-ece83f6c31b38a9f1fefd9ac;Sampled=0\r\n\u001b[36mX-Cache\u001b[39;49;00m: Miss from cloudfront\r\n\u001b[36mx-amz-apigw-id\u001b[39;49;00m: O8tEBGN4vHcFr7g=\r\n\u001b[36mx-amzn-RequestId\u001b[39;49;00m: 6a63cf9d-de05-4d36-aba1-67806adda070\r\r\n\r\r\n"] [45.511603, "o", "{\r\n \u001b[34;01m\"hello\"\u001b[39;49;00m: \u001b[33m\"world\"\u001b[39;49;00m\r\n}\r\n\r\n"] [45.553016, "o", "(venv37) /tmp/quickstart $ "] [47.356873, "o", "v"] [47.457163, "o", "i"] [47.496395, "o", "m"] [47.539968, "o", " "] [47.698229, "o", "a"] [47.778021, "o", "p"] [47.943521, "o", "p.py "] [48.343912, "o", "\r\n"] [48.398523, "o", "\u001b[?1000h\u001b[?2004h\u001b[?1049h\u001b[?1h\u001b=\u001b[?2004h"] [48.402476, "o", "\u001b[1;23r\u001b[?12h\u001b[?12l\u001b[27m\u001b[29m\u001b[m\u001b[H\u001b[2J\u001b[?25l\u001b[23;1H\"app.py\""] [48.402636, "o", " 29L, 735C"] [48.46919, "o", "\u001b[1;1H\u001b[93m 1 \u001b[mfrom chalice import Chalice\r\n\u001b[93m 2 \r\n 3 \u001b[mapp = Chalice(app_name='quickstart')\r\n\u001b[93m 4 \r\n 5 \r\n 6 \u001b[m@app.route('/')\r\n\u001b[93m 7 \u001b[mdef index():\r\n\u001b[93m 8 \u001b[m return {'hello': 'world'}\r\n\u001b[93m 9 \r\n 10 \r\n 11 \u001b[m# The view function above will return {\"hello\": \"world\"}\r\n\u001b[93m 12 \u001b[m# whenever you make an HTTP GET request to '/'.\r\n\u001b[93m 13 \u001b[m#\r\n\u001b[93m 14 \u001b[m# Here are a few more examples:\r\n\u001b[93m 15 \u001b[m#\r\n\u001b[93m 16 \u001b[m# @app.route('/hello/{name}')\r\n\u001b[93m 17 \u001b[m# def hello_name(name):\r\n\u001b[93m 18 \u001b[m# # '/hello/james' -> {\"hello\": \"james\"}\r\n\u001b[93m 19 \u001b[m# return {'hello': name}\r\n\u001b[93m 20 \u001b[m#\r\n\u001b[93m 21 \u001b[m# @app.route('/users', methods=['POST'])\r\n\u001b[93m 22 \u001b[m# def create_user():\r\n\u001b[38;5;224m[Pymode] Activate virtualenv: /tmp/venv37"] [48.499132, "o", "\u001b[2;1H▽\u001b[6n\u001b[2;1H \u001b[1;1H\u001b[>c"] [48.499279, "o", "\u001b]10;?\u0007\u001b]11;?\u0007"] [48.510137, "o", "\u001b[m\u001b[1;5H\u001b[38;5;81mfrom\u001b[9Cimport\u001b[m\u001b[3;9H\u001b[93m=\u001b[17C=\u001b[m\u001b[95m'quickstart'\u001b[m\u001b[6;5H\u001b[38;5;81m@\u001b[m\u001b[1m\u001b[96mapp\u001b[m.\u001b[1m\u001b[96mroute\u001b[m(\u001b[95m'/'\u001b[m\u001b[7;5H\u001b[93mdef\u001b[m \u001b[1m\u001b[96mindex\u001b[m\u001b[8;9H\u001b[93mreturn\u001b[m {\u001b[95m'hello'\u001b[m: \u001b[95m'world'\u001b[m\u001b[11;5H\u001b[96m# The view function above will return {\"hello\": \"world\"}\u001b[12;5H# whenever you make an HTTP GET request to '/'.\u001b[13;5H#\u001b[14;5H# Here are a few more examples:\u001b[15;5H#\u001b[16;5H# @app.route('/hello/{name}')\u001b[17;5H# def hello_name(name):\u001b[18;5H# # '/hello/james' -> {\"hello\": \"james\"}\u001b[19;5H# return {'hello': name}\u001b[20;5H#\u001b[21;5H# @app.route('/users', methods=['POST'])\u001b[22;5H# def create_user():\u001b[m\u001b[23;70H1,1\u001b[11CTop\u001b[1;5H\u001b[?25h"] [48.510277, "o", "\u001b[?1000l\u001b[?1006h\u001b[?1002h\u001b[?1006l\u001b[?1002l\u001b[?1006h\u001b[?1002h\u001b[?12$p"] [49.693463, "o", "\u001b[?25l\u001b[23;70H2,0-1\u001b[2;5H\u001b[?25h"] [49.870311, "o", "\u001b[?25l\u001b[23;70H3,1 \u001b[3;5H\u001b[?25h"] [50.029084, "o", "\u001b[?25l\u001b[23;70H4,0-1\u001b[4;5H\u001b[?25h"] [50.167341, "o", "\u001b[?25l\u001b[23;70H5\u001b[5;5H\u001b[?25h"] [50.305287, "o", "\u001b[?25l\u001b[23;70H6,1 \u001b[6;5H\u001b[?25h"] [50.453483, "o", "\u001b[?25l\u001b[23;70H7\u001b[7;5H\u001b[?25h"] [50.77406, "o", "\u001b[?25l\u001b[23;70H6\u001b[6;5H\u001b[?25h"] [50.938923, "o", "\u001b[?25l\u001b[1C\u001b[1m\u001b[96m\u001b[48;5;242mapp\u001b[m\u001b[48;5;242m.\u001b[m\u001b[1m\u001b[96m\u001b[48;5;242mroute\u001b[m\u001b[48;5;242m(\u001b[m\u001b[95m\u001b[48;5;242m'/'\u001b[m\u001b[48;5;242m) \u001b[m\u001b[23;1H\u001b[1m-- VISUAL LINE --\u001b[m\u001b[23;18H\u001b[K\u001b[23;70H6,1\u001b[11CTop\u001b[6;5H\u001b[?25h"] [51.031843, "o", "\u001b[?25l\u001b[38;5;81m\u001b[48;5;242m@\u001b[m\u001b[7;6H\u001b[93m\u001b[48;5;242mef\u001b[m\u001b[48;5;242m \u001b[m\u001b[1m\u001b[96m\u001b[48;5;242mindex\u001b[m\u001b[48;5;242m(): \u001b[m\u001b[23;70H7\u001b[7;5H\u001b[?25h"] [51.152844, "o", "\u001b[?25l\u001b[93m\u001b[48;5;242md\u001b[m\u001b[8;6H\u001b[48;5;242m \u001b[m\u001b[93m\u001b[48;5;242mreturn\u001b[m\u001b[48;5;242m {\u001b[m\u001b[95m\u001b[48;5;242m'hello'\u001b[m\u001b[48;5;242m: \u001b[m\u001b[95m\u001b[48;5;242m'world'\u001b[m\u001b[48;5;242m} \u001b[m\u001b[23;70H8\u001b[8;5H\u001b[?25h"] [51.462757, "o", "\u001b[?25l\u001b[48;5;242m \u001b[m\u001b[23;70H9,0-1\u001b[9;5H\u001b[?25h"] [51.617787, "o", "\u001b[?25l\u001b[6;5H\u001b[38;5;81m@\u001b[m\u001b[1m\u001b[96mapp\u001b[m.\u001b[1m\u001b[96mroute\u001b[m(\u001b[95m'/'\u001b[m)\u001b[6;20H\u001b[K\u001b[7;5H\u001b[93mdef\u001b[m \u001b[1m\u001b[96mindex\u001b[m():\u001b[7;17H\u001b[K\u001b[8;5H \u001b[93mreturn\u001b[m {\u001b[95m'hello'\u001b[m: \u001b[95m'world'\u001b[m}\u001b[8;34H\u001b[K\u001b[23;1H\u001b[K\u001b[23;70H6,1\u001b[11CTop\r4 lines yanked\u001b[23;70H\u001b[K"] [51.617956, "o", "\u001b[23;70H6,1\u001b[11CTop\u001b[6;5H\u001b[?25h"] [51.7452, "o", "\u001b[?25l\u001b[23;70H7\u001b[7;5H\u001b[?25h"] [51.875628, "o", "\u001b[?25l\u001b[23;70H8\u001b[8;5H\u001b[?25h"] [52.020364, "o", "\u001b[?25l\u001b[23;70H9,0-1\u001b[9;5H\u001b[?25h"] [52.165269, "o", "\u001b[?25l\u001b[23;3Hmore lines\u001b[23;13H\u001b[K"] [52.172521, "o", "\u001b[10;5H\u001b[38;5;81m@\u001b[m\u001b[1m\u001b[96mapp\u001b[m.\u001b[1m\u001b[96mroute\u001b[m(\u001b[95m'/'\u001b[m)\u001b[11;5H\u001b[93mdef\u001b[m \u001b[1m\u001b[96mindex\u001b[m():\u001b[11;17H\u001b[K\u001b[12;5H \u001b[93mreturn\u001b[m {\u001b[95m'hello'\u001b[m: \u001b[95m'world'\u001b[m}\u001b[12;34H\u001b[K\u001b[13;5H\u001b[K\u001b[14;5H\u001b[K\u001b[15;6H\u001b[96m The view function above will return {\"hello\": \"world\"}\u001b[16;7Hwhenever you make an HTTP GET request to '/'.\u001b[m\u001b[17;6H\u001b[K\u001b[18;7H\u001b[96mHere are a few more examples:\u001b[m\u001b[18;36H\u001b[K\u001b[19;6H\u001b[K\u001b[20;6H\u001b[96m @app.route('/hello/{name}')\u001b[21;7Hdef hello_name(name):\u001b[m\u001b[21;28H\u001b[K\u001b[22;7H\u001b[96m # '/hello/james' -> {\"hello\": \"james\"}\u001b[m\u001b[23;70H10,1\u001b[10CTop\u001b[23;70H\u001b[K\u001b[23;70H10,1\u001b[10CTop\u001b[10;5H\u001b[?25h"] [52.884211, "o", "\u001b[?25l\u001b[23;73H2\u001b[10;6H\u001b[?25h"] [53.145411, "o", "\u001b[?25l\u001b[9C\u001b[46m(\u001b[3C)\u001b[m\u001b[23;73H15\u001b[10;19H\u001b[?25h"] [53.281847, "o", "\u001b[?25l\b\b\b\b(\u001b[3C)\u001b[23;74H4\u001b[10;18H\u001b[?25h"] [53.396746, "o", "\u001b[?25l\u001b[23;1H\u001b[1m-- INSERT --\u001b[m\u001b[23;70H\u001b[K\u001b[23;70H10,14\u001b[9CTop"] [53.396918, "o", "\u001b[10;18H\u001b[?25h"] [53.560679, "o", "\u001b[?25l\u001b[95mh'\u001b[m)\u001b[23;74H5\u001b[10;19H\u001b[?25h"] [53.658424, "o", "\u001b[?25l\u001b[95me'\u001b[m)\u001b[23;74H6\u001b[10;20H\u001b[?25h"] [53.754007, "o", "\u001b[?25l\u001b[95ml'\u001b[m)\u001b[23;74H7\u001b[10;21H\u001b[?25h"] [53.843242, "o", "\u001b[?25l\u001b[95ml'\u001b[m)\u001b[23;74H8\u001b[10;22H\u001b[?25h"] [54.264358, "o", "\u001b[?25l\u001b[95mo'\u001b[m)\u001b[23;74H9\u001b[10;23H\u001b[?25h"] [54.570344, "o", "\u001b[?25l\u001b[95m/'\u001b[m)\u001b[23;73H20\u001b[10;24H\u001b[?25h"] [54.885885, "o", "\u001b[?25l\u001b[95m{'\u001b[m)\u001b[23;74H1\u001b[10;25H\u001b[?25h"] [55.06869, "o", "\u001b[?25l\u001b[95mn'\u001b[m)\u001b[23;74H2\u001b[10;26H\u001b[?25h"] [55.147467, "o", "\u001b[?25l\u001b[95ma'\u001b[m)\u001b[23;74H3\u001b[10;27H\u001b[?25h"] [55.224402, "o", "\u001b[?25l\u001b[95mm'\u001b[m)\u001b[23;74H4\u001b[10;28H\u001b[?25h"] [55.310708, "o", "\u001b[?25l\u001b[95me'\u001b[m)\u001b[23;74H5\u001b[10;29H\u001b[?25h"] [55.47705, "o", "\u001b[?25l\b\b\b\b\b\u001b[38;5;224m{name}\u001b[m\u001b[95m'\u001b[m)\u001b[23;74H6\u001b[10;30H\u001b[?25h"] [55.681933, "o", "\u001b[23;1H\u001b[K\u001b[10;29H"] [55.74385, "o", "\u001b[?25l"] [55.74576, "o", "\u001b[23;70H10,25\u001b[9CTop\u001b[10;29H\u001b[?25h\u001b[?25l\u001b[23;71H1,12\u001b[11;16H\u001b[?25h"] [55.818802, "o", "\u001b[?25l\u001b[23;74H \u001b[11;5H\u001b[?25h"] [55.895759, "o", "\u001b[?25l\u001b[23;73H5\u001b[11;9H\u001b[?25h"] [56.207749, "o", "\u001b[?25l\u001b[23;1H\u001b[1m-- INSERT --\u001b[m\u001b[23;70H\u001b[K\u001b[23;70H11,5\u001b[10CTop"] [56.211248, "o", "\u001b[11;9H():\u001b[11;12H\u001b[K\u001b[11;9H\u001b[46m()\b\b\u001b[?25h"] [56.309676, "o", "\u001b[?25l\u001b[m\u001b[1m\u001b[96m\u001b[46mh\u001b[m\u001b[46m(\u001b[m):\b\b\b\b\u001b[1m\u001b[96mh\u001b[m\u001b[46m()\u001b[m\u001b[23;73H6\u001b[11;10H\u001b[?25h"] [56.422079, "o", "\u001b[?25l\u001b[1m\u001b[96m\u001b[46me\u001b[m\u001b[46m(\u001b[m):\b\b\b\b\u001b[1m\u001b[96me\u001b[m\u001b[46m()\u001b[m\u001b[23;73H7\u001b[11;11H\u001b[?25h"] [56.502157, "o", "\u001b[?25l\u001b[1m\u001b[96m\u001b[46ml\u001b[m\u001b[46m(\u001b[m):\b\b\b\b\u001b[1m\u001b[96ml\u001b[m\u001b[46m()\u001b[m\u001b[23;73H8\u001b[11;12H\u001b[?25h"] [56.599656, "o", "\u001b[?25l\u001b[1m\u001b[96m\u001b[46ml\u001b[m\u001b[46m(\u001b[m):\b\b\b\b\u001b[1m\u001b[96ml\u001b[m\u001b[46m()\u001b[m\u001b[23;73H9\u001b[11;13H\u001b[?25h"] [56.764724, "o", "\u001b[?25l\u001b[1m\u001b[96m\u001b[46mo\u001b[m\u001b[46m(\u001b[m):\b\b\b\b\u001b[1m\u001b[96mo\u001b[m\u001b[46m()\u001b[m\u001b[23;73H10\u001b[11;14H\u001b[?25h"] [56.941756, "o", "\u001b[23;1H\u001b[K\u001b[11;13H"] [57.01945, "o", "\u001b[?25l"] [57.022469, "o", "\u001b[1C()\u001b[23;70H11,9\u001b[10CTop\u001b[11;13H\u001b[?25h\u001b[?25l\u001b[1C\u001b[46m()\u001b[m\u001b[23;73H10\u001b[11;14H\u001b[?25h"] [57.136892, "o", "\u001b[?25l\u001b[23;74H1\u001b[11;15H\u001b[?25h"] [57.532494, "o", "\u001b[?25l\u001b[23;1H\u001b[1m-- INSERT --\u001b[m\u001b[23;70H\u001b[K\u001b[23;70H11,11\u001b[9CTop"] [57.532692, "o", "\u001b[11;15H\u001b[?25h"] [57.753309, "o", "\u001b[?25l\u001b[46mn\u001b[m):\b\b\bn\u001b[46m)\u001b[m\u001b[23;74H2\u001b[11;16H\u001b[?25h"] [57.857846, "o", "\u001b[?25l\u001b[46ma\u001b[m):\b\b\ba\u001b[46m)\u001b[m\u001b[23;74H3\u001b[11;17H\u001b[?25h"] [58.364461, "o", "\u001b[?25l\u001b[46mm\u001b[m):\b\b\bm\u001b[46m)\u001b[m\u001b[23;74H4\u001b[11;18H\u001b[?25h"] [58.474158, "o", "\u001b[?25l\u001b[46me\u001b[m):\b\b\be\u001b[46m)\u001b[m\u001b[23;74H5\u001b[11;19H\u001b[?25h"] [58.641462, "o", "\u001b[23;1H\u001b[K\u001b[11;18H"] [58.743445, "o", "\u001b[?25l"] [58.745529, "o", "\b\b\b\b(name)\u001b[23;70H11,14\u001b[9CTop\u001b[11;18H\u001b[?25h\u001b[?25l\u001b[23;71H2\u001b[12;18H\u001b[?25h"] [58.873794, "o", "\u001b[?25l\u001b[23;74H9\u001b[12;23H\u001b[?25h"] [59.019762, "o", "\u001b[?25l\u001b[23;73H22\u001b[12;26H\u001b[?25h"] [59.129529, "o", "\u001b[?25l\u001b[23;74H3\u001b[12;27H\u001b[?25h"] [59.612607, "o", "\u001b[?25l\u001b[23;74H2\u001b[12;26H\u001b[?25h"] [60.092337, "o", "\u001b[?25l\u001b[23;1H\u001b[1m-- INSERT --\u001b[m\u001b[23;70H\u001b[K\u001b[23;70H12,22\u001b[9CTop"] [60.096577, "o", "\u001b[12;26H}\u001b[12;27H\u001b[K\u001b[12;16H\u001b[46m{\u001b[9C}\b\u001b[?25h"] [60.363434, "o", "\u001b[?25ln\u001b[m}\b\bn\u001b[46m}\u001b[m\u001b[23;74H3\u001b[12;27H\u001b[?25h"] [60.449639, "o", "\u001b[?25l\u001b[46ma\u001b[m}\b\ba\u001b[46m}\u001b[m\u001b[23;74H4\u001b[12;28H\u001b[?25h"] [60.522062, "o", "\u001b[?25l\u001b[46mm\u001b[m}\b\bm\u001b[46m}\u001b[m\u001b[23;74H5\u001b[12;29H\u001b[?25h"] [60.617924, "o", "\u001b[?25l\u001b[46me\u001b[m}\b\be\u001b[46m}\u001b[m\u001b[23;74H6\u001b[12;30H\u001b[?25h"] [60.748263, "o", "\u001b[23;1H\u001b[K\u001b[12;29H"] [60.830157, "o", "\u001b[?25l"] [60.831801, "o", "\u001b[12;16H{\u001b[13C}\u001b[23;70H12,25\u001b[9CTop\u001b[12;29H\u001b[?25h\u001b[?25l\u001b[23;71H3,0-1\u001b[13;5H\u001b[?25h"] [61.137592, "o", "\u001b[?25l\u001b[23;1H21 fewer lines\u001b[23;70H\u001b[K"] [61.138012, "o", "\u001b[13;1H\u001b[94m~ \u001b[14;1H~ \u001b[15;1H~ \u001b[16;1H~ \u001b[17;1H~ \u001b[18;1H~ \u001b[19;1H~ \u001b[20;1H~ \u001b[21;1H~ \u001b[22;1H~ \u001b[m\u001b[23;70H12,5\u001b[10CAll\u001b[23;70H\u001b[K"] [61.138212, "o", "\u001b[23;70H12,5\u001b[10CAll\u001b[12;9H\u001b[?25h"] [61.620498, "o", "\u0007\u001b[?25l\u001b[?25h\u001b[?25l\u001b[23;71H1\u001b[11;9H\u001b[?25h"] [61.761612, "o", "\u001b[?25l\u001b[23;71H0\u001b[10;9H\u001b[?25h"] [61.889628, "o", "\u001b[?25l\u001b[23;70H9,0-1\u001b[9;5H\u001b[?25h"] [62.018677, "o", "\u001b[?25l\u001b[23;1H\u001b[1m-- INSERT --\u001b[m\u001b[23;13H\u001b[K\u001b[23;70H10,1\u001b[10CTop"] [62.021847, "o", "\u001b[10;5H\u001b[K\u001b[11;5H\u001b[38;5;81m@\u001b[m\u001b[1m\u001b[96mapp\u001b[m.\u001b[1m\u001b[96mroute\u001b[m(\u001b[95m'/hello/\u001b[m\u001b[38;5;224m{name}\u001b[m\u001b[95m'\u001b[m)\u001b[12;5H\u001b[93mdef\u001b[m \u001b[1m\u001b[96mhello\u001b[m(name):\u001b[12;21H\u001b[K\u001b[13;1H\u001b[93m 13 \u001b[m \u001b[93mreturn\u001b[m {\u001b[95m'hello'\u001b[m: name}\u001b[13;31H\u001b[K\u001b[10;5H\u001b[?25h"] [62.315932, "o", "\u001b[23;1H\u001b[K\u001b[10;5H"] [63.317825, "o", "\u001b[?25l"] [63.318144, "o", "\u001b[23;70H10,0-1\u001b[8CAll\u001b[10;5H\u001b[?25h"] [65.342036, "o", "\u001b[?25l\u001b[23;70H\u001b[K\u001b[23;1H:\u001b[?2004h"] [65.342284, "o", "\u001b[?25h"] [65.436725, "o", "w\u001b[?25l\u001b[?25h"] [65.498001, "o", "q\u001b[?25l\u001b[?25h"] [65.789977, "o", "\r"] [65.790186, "o", "\u001b[?25l\u001b[?1006l\u001b[?1002l\u001b[?2004l"] [65.790669, "o", "\"app.py\""] [65.793193, "o", " 13L, 201C written"] [65.902159, "o", "\r\r\r\n\u001b[?2004l\u001b[?1l\u001b>\u001b[?25h\u001b[?1049l"] [65.91921, "o", "(venv37) /tmp/quickstart $ "] [66.222652, "o", "c"] [66.318574, "o", "h"] [66.39079, "o", "a"] [66.463041, "o", "l"] [66.515746, "o", "i"] [66.578604, "o", "c"] [66.667005, "o", "e"] [66.702913, "o", " "] [66.818854, "o", "d"] [66.957969, "o", "e"] [67.025938, "o", "p"] [67.089679, "o", "l"] [67.242964, "o", "o"] [67.36255, "o", "y"] [67.764503, "o", "\r\n"] [68.096635, "o", "Creating deployment package.\r\n"] [69.001699, "o", "Updating policy for IAM role: quickstart-dev\r\n"] [69.076994, "o", "Updating lambda function: quickstart-dev\r\n"] [69.718098, "o", "Updating rest API\r\n"] [70.732661, "o", "Resources deployed:\r\n - Lambda ARN: arn:aws:lambda:us-west-2:675687397822:function:quickstart-dev\r\n - Rest API URL: https://vdpmshluug.execute-api.us-west-2.amazonaws.com/api/\r\n"] [70.801669, "o", "(venv37) /tmp/quickstart $ "] [75.836485, "o", "h"] [75.916746, "o", "t"] [76.050503, "o", "t"] [76.126675, "o", "p"] [76.19904, "o", " "] [76.388309, "o", "htt"] [76.388474, "o", "ps://vdpmshluug"] [76.388533, "o", ".execu"] [76.388571, "o", "te"] [76.388605, "o", "-api"] [76.388636, "o", ".us"] [76.388685, "o", "-wes"] [76.388934, "o", "t-2.amazonaws.com"] [76.389032, "o", "/ \rapi/"] [77.149797, "o", "h"] [77.302346, "o", "e"] [77.402471, "o", "l"] [77.520633, "o", "l"] [77.717694, "o", "o"] [78.514487, "o", "/"] [78.87048, "o", "j"] [79.002202, "o", "a"] [79.058052, "o", "m"] [79.158039, "o", "e"] [79.193908, "o", "s"] [79.899556, "o", "\r\n"] [80.371799, "o", "\u001b[34mHTTP\u001b[39;49;00m/\u001b[34m1.1\u001b[39;49;00m \u001b[34m200\u001b[39;49;00m \u001b[36mOK\u001b[39;49;00m\r\n\u001b[36mConnection\u001b[39;49;00m: keep-alive\r\n\u001b[36mContent-Length\u001b[39;49;00m: 17\r\n\u001b[36mContent-Type\u001b[39;49;00m: application/json\r\n\u001b[36mDate\u001b[39;49;00m: Tue, 30 Jun 2020 15:35:57 GMT\r\n\u001b[36mVia\u001b[39;49;00m: 1.1 f78e2a2d083c0945ee670c9d5d179e9e.cloudfront.net (CloudFront)\r\n\u001b[36mX-Amz-Cf-Id\u001b[39;49;00m: QBy4a8cMibLHz6UuJ3AGj3U68AhGC-nKETLPft_GOL1bko7N4rxeOg==\r\n\u001b[36mX-Amz-Cf-Pop\u001b[39;49;00m: EWR53-C1\r\n\u001b[36mX-Amzn-Trace-Id\u001b[39;49;00m: Root=1-5efb5bdd-2a649157a49e65e720b9f244;Sampled=0\r\n\u001b[36mX-Cache\u001b[39;49;00m: Miss from cloudfront\r\n\u001b[36mx-amz-apigw-id\u001b[39;49;00m: O8tKoH5QvHcF3ng=\r\n\u001b[36mx-amzn-RequestId\u001b[39;49;00m: 167d0760-64e3-4447-80de-01be0e8853e3\r\r\n\r\r\n"] [80.373638, "o", "{\r\n \u001b[34;01m\"hello\"\u001b[39;49;00m: \u001b[33m\"james\"\u001b[39;49;00m\r\n}\r\n\r\n"] [80.420141, "o", "(venv37) /tmp/quickstart $ "] [94.075245, "o", "c"] [94.142242, "o", "h"] [94.235231, "o", "a"] [94.303331, "o", "l"] [94.343169, "o", "i"] [94.430906, "o", "c"] [94.475222, "o", "e"] [94.547124, "o", " "] [94.682051, "o", "d"] [94.846182, "o", "e"] [94.986099, "o", "l"] [95.026072, "o", "e"] [95.162597, "o", "t"] [95.266324, "o", "e"] [95.628994, "o", "\r\n"] [96.026774, "o", "Deleting Rest API: vdpmshluug\r\n"] [96.802902, "o", "Deleting function: arn:aws:lambda:us-west-2:675687397822:function:quickstart-dev\r\n"] [97.219203, "o", "Deleting IAM role: quickstart-dev\r\n"] [98.090105, "o", "(venv37) /tmp/quickstart $ "] [101.249136, "o", "exit\r\n"] ================================================ FILE: docs/source/theme/smithy/static/custom-tabs.css ================================================ /** * Add overrides to how tabs are styled to make them less visually * obstrusive. Note, however, that this might need to be looked at in * the future if we ever want to use non-code-tabs. */ /* No need for margin below code samples when in a tab. */ .code-tab pre { margin-bottom: 0; } .sphinx-tabs { margin-top: 1rem; } /* Code tabs should encompass the entire tab. */ .code-tab.tab { padding: 0 !important; } .ui.tabular.menu { border: none; } .ui.tabular.menu .item { font-size: 0.8em; font-family: "SFMono-Regular",Consolas,"Liberation Mono",Menlo,Courier,monospace; text-transform: lowercase; border: none !important; } .ui.attached.segment.code-tab { border: none !important; } /* Remove code tab headings and use the same color as code backgrounds */ .ui.tabular.menu .active.item { border: none !important; background-color: inherit; text-decoration: underline; font-weight: normal; } ================================================ FILE: docs/source/theme/smithy/static/default.css_t ================================================ /* ----- Layout ------ */ html { font-family: {{ theme_regular_font }}; background-color: {{ theme_site_background }}; } /* Base scaffolding taken from Markswatch theme */ body { color: #24292e; font-family: {{ theme_regular_font }}; font-size: 16px; line-height: 1.5; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%; word-wrap: break-word; display: flex; min-height: 100vh; flex-direction: column; background-color: {{ theme_site_background }}; } /* Utility class used in things like hidden form fields */ .hidden { display: none; } #site-container { color: #24292e; } blockquote { padding: 0 0 0 1.5em; margin: 0 2rem 1rem 0; color: #777; border-left: 0.5rem solid #eee; font-family: {{ theme_code_font }}; font-size: 1em; } .width-wrapper { max-width: 1140px; margin: auto; position: relative; padding: 0 1em; } /* ----- Headings ------ */ h1, h2, h3, h4, h5, h6 { color: #000; line-height: 1.25; margin-bottom: 1em; font-weight: 600; } h1 a, h2 a, h3 a, h4 a, h5 a, h6 a, h7 a { color: #000; } h1, h2, h3, h4, h5, h6, h7 { border-bottom: 1px solid #ccc; padding-bottom: 0.3em; } h2, h3, h4, h5, h6, h7 { margin-top: 1.25em; } h1 { margin-top: 0; font-size: 2.4em; } h2 { font-size: 2em; } h3 { font-size: 1.8em; } h4 { font-size: 1.6em; font-weight: normal; } h5 { font-size: 1.3em; font-weight: normal; } h6 { font-size: 1em; font-weight: bold; } h7 { font-size: 0.85em; font-weight: bold; } /* ----- Landing page ------ */ #splash { padding: 3em 0 2em 0; color: #fff; margin-bottom: 1em; background-color: {{ theme_dark_primary }}; /* * Generated from https://www.heropatterns.com/, by Steve Schoger * https://creativecommons.org/licenses/by/4.0/ */ background-color: #19222e; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'%3E%3Cg fill-rule='evenodd'%3E%3Cg fill='%23535060' fill-opacity='0.25'%3E%3Cpath opacity='.5' d='M96 95h4v1h-4v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9zm-1 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-9-10h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm9-10v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-9-10h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm9-10v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-9-10h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm9-10v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-9-10h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9z'/%3E%3Cpath d='M6 5V0H5v5H0v1h5v94h1V6h94V5H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E"); } #splash h1 { color: #fff; padding: 0; margin: 0 0 1rem; font-size: 1.7em; border: none; font-weight: bold; } .splash-highlight { color: #FFF896; } #splash .highlight-chalice { color: #232f3E; background-color: #232f3e; } #splash .highlight-chalice .highlight { color: #232f3E; background-color: #232f3e; } #splash .highlight-chalice pre { font-size: 0.9em; background-color: #232f3e; border-radius: 3px; font-size: 1em; color: #A9B7C6; margin-bottom: 4px; margin-top: 0; padding-top: 0; } #splash .splash-column:first-child { padding-right: 3%; text-align: center; } #splash-logo { max-width: 100%; } .lp-image { width: 8%; height: 8%; padding-top: 32px; } .headline { font-weight: 400; color: #cfcedf; font-size: 2.3em; text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.3); } @media (max-width: 600px) { #splash h1 { font-size: 1.5em; } #splash .headline { font-size: 18px; } } .splash-link { display: inline-block; border-radius: 6px; padding: 12px; text-align: center; margin: 2em auto 1em auto; font-weight: 500; width: 160px; color: #fff; background: {{ theme_medium_accent }}; text-transform: uppercase; } .splash-link:hover { text-decoration: none; color: {{ theme_medium_accent }}; background: #fff; } .splash-link-small { display: block; text-decoration: underline; color: #fff; margin-bottom: 2em; } .splash-link-small:hover { color: #fff; } .splash-row { display: flex; } .splash-column { display: flex; flex-direction: column; justify-content: top; width: 50%; padding: 0 20px; margin-top: 2em; } .splash-box { border: 1px solid #859191; padding: 20px; margin: 25px; } .splash-clickable { text-decoration: none; } #splash .splash-column { margin-top: 0; } .splash-column-one-third { display: flex; flex-direction: column; justify-content: top; width: 40%; padding: 0 20px; margin-top: 2em; } .splash-column-two-third { display: flex; flex-direction: column; justify-content: top; width: 60%; padding: 0 20px; margin-top: 2em; } .splash-column h2, .faq h2 { color: {{ theme_medium_primary }}; padding: 0; font-size: 1.25em; text-align: left; border: none; } .quickstart-vid { background: #2BBA9C1A; padding-bottom: 64px; } #quickstart-img { width: 15%; padding-top: 20px; padding-right: 24px; float: left; } #quickstart-title { position: relative; padding-top: 24px; } .faq .faq-heading { text-align: center; color: {{ theme_medium_primary }}; margin: 2rem 0; font-size: 2em; } .splash-column h2 { margin: 0 0 1rem 0; } .splash-column-one-third h2 { margin: 0 0 1rem 0; font-size: 1.50em; } #splash pre { position: relative; } .see-full-example { display: block; position: absolute; bottom: 0.5em; right: 0.5em; font-size: 12px; color: #999; } .feature-desc { font-size: 14px; } /* ----- Visual separations ------ */ #page-container { padding: 3rem 2rem 2rem 2em; background: {{ theme_site_background }}; flex: 1; } /* Make document and right nav left and right aligned columns. */ #page-container > .width-wrapper { display: flex; flex-flow: row; } #landing-container { min-height: 500px; background: {{ theme_site_background }}; } #page-container li > p.first:last-child, #landing-container li > p.first:last-child { margin-bottom: 0; } dt { margin-bottom: 0.5em; } dd { margin-left: 2em; } dd > ul { padding-left: 0; list-style-position: inside; } hr { height: 0.25em; padding: 0; margin: 24px 0; background-color: #e1e4e8; border: 0; } /* ----- Anchors ------ */ a { color: {{ theme_link_color }}; text-decoration: none; } a:hover, .reference.external:hover { text-decoration: underline; color: {{ theme_link_color }}; } .reference.external { text-decoration: underline dotted #ccc; } .headerlink { visibility: none; margin-left: 1em; font-size: 18px; vertical-align: super; line-height: 0; opacity: 0; transition: opacity .25s; } *:hover > .headerlink { opacity: 1; } /* ----- Header and footer ------ */ header { display: block; width: 100%; flex: none; box-shadow: 0px 1px 2px 0px rgba(0, 0, 0, 0.2); z-index: 1; } .header-flex { display: flex; justify-content: space-between; } .site-logo { position: relative; margin-top: 6px; } .site-logo a { border-radius: 6px; padding: 3px 6px; color: {{ theme_medium_primary }}; font-weight: 500; font-size: 22px; display: block; } .site-logo a:hover { text-decoration: none; } .site-logo .logo-text { font-weight: 700; padding-left: 6px; } .site-logo .version-text { font-weight: 300; font-size: 12px; position: absolute; right: -14px; top: 4px; color: {{ theme_medium_primary }}; } .site-logo .logo-icon { color: transparent; text-shadow: 0 0 0 #fff; font-size: 18px; vertical-align: text-bottom; padding: 2px 3px 2px 4px; border-radius: 6px; } .logo-icon img { height: 50%; width: 50%; } #page-navigation { padding: 10px 1em; display: flex; list-style: none; margin-bottom: 0; } #page-navigation li { cursor: pointer; } #page-navigation .site-page { line-height: 1.8; margin-right: 0 18px 0 0; } #page-navigation .site-page a { display: block; padding: 0 1em; color: #444; } #page-navigation .site-page a:hover { color: {{ theme_link_color }}; text-decoration: none; } #page-navigation .site-search { margin-top: 3px; margin-left: 2em; } #page-navigation .site-search .search-input { padding: 0 1em; color: #111; border: solid 1px #999; border-radius: 3px; width: 180px; font-size: 0.9em; border-radius: 10px; } #page-navigation .site-search button { border: none; padding: 5px 10px; background-color: {{ theme_site_background }}; color: #777; } footer { padding: 2em; background-color: {{ theme_dark_primary }}; color: #fefefe; } .copyright { text-align: center; width: 100%; } .faq { background: #ebe9e7; padding: 2em 0 4em; margin: 4em 0 0 0; } .faq h2 { color: {{ theme_medium_primary }}; border-bottom: none; } img.align-center, .figure.align-center, object.align-center { display: block; margin-left: auto; margin-right: auto; } /* ----- Tables ------ */ table { border-spacing: 0; border-collapse: collapse; border-width: 1px; border-color: #ccc; border-style: solid; width: 100%; margin: 0 0 1rem 0; } tr { border-bottom: 1px solid #ccc; } tr.row-even { background-color: #f7f7f7; } th { text-align: left; padding: 1.2rem 1.6em; vertical-align: top; } td, th { padding: 8px 16px; border: none; border-right: 1px solid #ccc; } /* Field list tables */ table.field-list { margin: 2em 0; } table.field-list th { background-color: {{ theme_site_background }}; color: #24292e; border-right: 1px solid #e8e8e8; min-width: 0; } /* hlist tables */ table.hlist { border: none; } .hlist > tbody > tr { border-bottom: none; } .hlist > tbody > tr > td { border-right: 1px solid #ccc; vertical-align: top; padding-right: 12px; } .hlist > tbody > tr > td > ul { list-style-type: none; padding: 0; margin: 0; } .hlist > tbody > tr > td:last-child { border-right: none; } /* ----- Code and pre ------ */ code { padding: 0.2em 0.4em; margin: 0; font-size: 85%; background-color: rgba(27, 31, 35, 0.05); border-radius: 3px; } pre { padding: 1em; overflow: auto; line-height: 1.45; background-color: #f1f1f1; border-radius: 3px; font-size: 1em; color: #6e6b5e; overflow-x: auto white-space: pre; word-break: normal; word-wrap: normal; /* box-shadow: 0 1px 1px rgba(0, 0, 0, 0.4); */ } code, pre { font-family: {{ theme_code_font }}; } pre a { color: #A5C25C; text-decoration: underline; } pre a:hover { color: #A5C25C; } /* Hacky but fixes links inside of grammars */ pre a code.xref { font-size: 100%; } /* ----- Tables with line numbers (..code directives) ------ */ table.highlighttable { margin: 0.5em 0 2em 0; width: 100%; overflow: auto; display: block; border: none; background-color: #f1f1f1; } .highlighttable pre { box-shadow: none; } table.highlighttable td { border: none; padding: 0; } table.highlighttable td.linenos { border-right: 1px solid #ccc; width: 20px; /* This will enlarge if needed. */ text-align: right; } table.highlighttable tr { border-bottom: none; } table.highlighttable td.linenos pre { padding-left: 0.8em; color: #999; } table.highlighttable pre { margin: 0; } .code-block-caption .caption-text { font-style: italic; color: #666; } .code-block-caption:hover .headerlink { visibility: visible; opacity: 100; } /* ------- Misc. -------- */ /* RFC directive styling */ .rfc strong { font-weight: normal; } /* Table of contents "title" */ .contents { padding: 1em 2em; background-color: #eee; } .contents ul { list-style-type: none; margin: 0; padding: 0; } .contents li { text-indent: -1.5em; padding-left: 1.5em; } .contents li::before { content: "•"; color: rgba(0, 0, 0, 0.5); padding-right: 0.5em; } .topic-title { font-weight: 300; color: rgba(0, 0, 0, 0.6); } /* -------- Document ---------- */ /* TODO: These columns styles are convoluted. */ .side-column { min-height: 100px; position: static; flex: 0 0 300px; top: 0; } .column-body { position: sticky; top: 30px; overflow: auto; } #document-body { padding: 0 3.5rem 0 0; min-height: 300px; flex: auto; overflow: hidden; } #right-column { font-size: 14px; padding-top: 1em; border-left: 1px solid rgba(0, 0, 0, 0.15); } #right-column > .column-body > .sidebar { padding-left: 1em; } #right-column > .column-body > .sidebar > ul > li > a { font-weight: bold; color: {{ theme_primary }}; } #right-column > .column-body > .sidebar > ul > li > ul { margin: 8px 0 0 1em; } #sidebar-navigation { font-size: 15px; padding-right: 1em; } #sidebar-navigation ul { list-style: none; padding: 0; margin: 0; } #sidebar-navigation li { margin-bottom: 8px; } #sidebar-navigation ul ul { margin: 8px 0 0 1em; } #sidebar-navigation a { color: rgba(0, 0, 0, 0.87); display: block; } #right-column a { color: rgba(0, 0, 0, 0.87); display: block; } #right-column a:hover { color: {{ theme_link_color }}; } #right-column a code { color: rgba(0, 0, 0, 0.87); } #right-column h3 { font-size: 1em; line-height: 1.2em; font-weight: 200; margin: 0 0 1em 0; } #right-column ul { list-style: none; padding: 0; margin: 0 0 0 0.5em; } #right-column ul ul { margin: 0 0 0 1.5em; } #right-column li { margin-bottom: 5px; } .side-column a { padding-left: 6px; border-left: 2px solid rgba(0, 0, 0, 0); } .side-column a.current { border-left: 2px solid {{ theme_primary }}; font-weight: bold; } /* -- admonitions ----------------------------------------------------------- */ .rubric { margin: 2em 0 1em 0; font-weight: bold; } .admonition { margin: 20px 0; padding: 1em 0.8em; border-bottom: 1px solid #ddd; } .admonition dt { font-weight: bold; } .admonition dl { margin-bottom: 0; } .admonition-title { margin: 0px 0 1em; padding: 0; font-size: 1em; line-height: 1.1; font-weight: bold; color: rgba(0, 0, 0, 0.6) } .admonition.danger, .admonition.error { background-color: #f8d7da; } .admonition.important, .admonition.warning, .admonition.attention, .admonition.caution { background-color: #f6eab7; } .admonition.note, .admonition.hint { background-color: #ddecfc; } .admonition.tip { background-color: #dff6da; } div.seealso { background-color: #eee; } div.admonition tt.xref, div.admonition a tt { border-bottom: 1px solid {{ theme_site_background }}; } div.admonition p.last { margin-bottom: 0; } /* -- search page ----------------------- */ ul.search { margin: 10px 0 0 20px; padding: 0; list-style: none; } ul.search li { padding: 5px 0 5px 20px; background: url(file.png) no-repeat 0 7px; } ul.search li a { font-weight: bold; } ul.search li div.context { color: #888; margin: 2px 0 0 30px; text-align: left; } ul.keywordmatches li.goodmatch a { font-weight: bold; } dt:target, .highlighted { background-color: #fbe54e; } /* -------- Page relations (bottom next/previous links) ------- */ .relations { margin-top: 3em; padding-top: 2em; display: flex; border-top: 1px solid rgba(0, 0, 0, 0.15); } .relations .previous-page { margin-right: auto; } .relations .next-page { margin-left: auto; } .relations a { display: block; color: {{ theme_medium_primary }}; border-radius: 2px; border: 1px solid {{ theme_medium_primary }}; padding: 0.8rem 1em; transition: background .25s, color 0.25s; } .relations a:hover { color: #fff; background: {{ theme_medium_primary }}; text-decoration: none; } .next-previous { font-size: 0.8em; text-align: right; margin-bottom: 1em; } #right-column .next-previous a { display: inline; padding-right: 2em; text-transform: uppercase; font-weight: bold; color: {{ theme_link_color }}; } /* -------- toctree --------- */ .large-toctree > ul { padding-left: 0; } .large-toctree .toctree-l1 { margin-bottom: 2em; list-style-type: none; } .large-toctree .toctree-l1:not(:last-child) { border-bottom: 1px solid #ccc; padding-bottom: 2em; } .large-toctree .toctree-l1 > a { font-weight: bold; padding-bottom: 1em; display: block; } .caption-text { font-weight: 300; color: #222; } /* -------- Parent links --------- */ .rel-parents { list-style-type: none; padding: 0; margin: 0 0 0.5rem 0; opacity: 0.9; font-size: 90%; } .rel-parents li { padding: 0; margin: 0; display: inline; } .rel-parents li:after { content: " / "; } .rel-parents li:last-child:after { content: ""; } /* -------- media query helpers ------- */ @media (max-width: 1100px) { #right-column { display: none; } #page-container { padding: 1.5em; } } @media (max-width: 991px) { #left-column { font-size: 0.8em; } .hidden-sm, tr.hidden-sm, th.hidden-sm, td.hidden-sm { display: none !important } } @media (max-width: 767px) { #document-body { padding: 0; } .hidden-xs, tr.hidden-xs, th.hidden-xs, td.hidden-xs { display: none !important } } @media (max-width: 600px) { /* Make the header navigation usable on a small screen. */ header { display: block; padding: 1em 1em 1em 0; border-bottom: 1px solid rgba(1, 1, 1, 0.2); } .header-flex, .site-logo, #page-navigation { display: block; margin-left: 0.5em; margin: 0.5em 0 0 0.5em; padding: 0; } header .logo-icon { display: none; } .site-logo .logo-text { padding-left: 16px; } .splash-row { display: block; } .splash-column { display: block; width: 100%; padding: 0; } body, pre { font-size: 0.9em; } } /* ------- Make tables scroll on smaller screens. ------ */ @media (max-width: 992px) { table { display: block; width: 100%; overflow-x: auto; -ms-overflow-style: -ms-autohiding-scrollbar; } } /* ------- Printer friendly styling ------ */ @media print{ /* Hide the header, footer, relations, and table of contents */ header, footer, .relations, .contents { display: none; } /* Make text and padding a lot tighter */ body { font-size: 12px; line-height: 1.1; } h1, h2, h3, h4, h5, h6 { margin-top: 1.5em; } .sphinx-tabs { margin: 0.5em 0 !important; } /* Hide code-tab tabs */ .tabular { display: none !important; } #document-body { padding: 0; height: 100%; } .hidden-xs, tr.hidden-xs, th.hidden-xs, td.hidden-xs { display: none !important } } ================================================ FILE: docs/source/theme/smithy/theme.conf ================================================ [theme] inherit = basic stylesheet = default.css [options] primary = #232f3E dark_primary = #232f3E medium_primary = #232f3E light_primary = #2BBA9C medium_accent = #2BBA9C dark_accent = #283D3B site_background = #fff link_color = #00818e; regular_font = -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol' code_font = "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace ga_id = ================================================ FILE: docs/source/topics/authorizers.rst ================================================ Authorization ============= Chalice supports multiple mechanisms for authorization. This topic covers how you can integrate authorization into your Chalice applications. In Chalice, all the authorizers are configured per-route and specified using the ``authorizer`` kwarg to an ``@app.route()`` call. You control which type of authorizer to use based on what's passed as the ``authorizer`` kwarg. You can use the same authorizer instance for multiple routes. The first set of authorizers chalice supports cover the scenario where you have some existing authorization mechanism that you just want your Chalice app to use. Chalice also supports built-in authorizers, which allows Chalice to manage your custom authorizers as part of ``chalice deploy``. This is covered in the Built-in Authorizers section. AWS IAM Authorizer ------------------ The IAM Authorizer allows you to control access to API Gateway with `IAM permissions`_ To associate an IAM authorizer with a route in chalice, you use the :class:`IAMAUthorizer` class: .. code-block:: python from chalice import IAMAuthorizer authorizer = IAMAuthorizer() @app.route('/iam-auth', methods=['GET'], authorizer=authorizer) def authenticated(): return {"success": True} See the `API Gateway documentation `__ for more information on controlling access to API Gateway with IAM permissions. Amazon Cognito User Pools ------------------------- In addition to using IAM roles and policies with the :class:`IAMAuthorizer` you can also use a `Cognito user pools`_ to control who can access your Chalice app. A cognito user pool serves as your own identity provider to maintain a user directory. To integrate Cognito user pools with Chalice, you'll need to have an existing cognito user pool configured. .. code-block:: python from chalice import CognitoUserPoolAuthorizer authorizer = CognitoUserPoolAuthorizer( 'MyPool', provider_arns=['arn:aws:cognito:...:userpool/name']) @app.route('/user-pools', methods=['GET'], authorizer=authorizer) def authenticated(): return {"success": True} For more information about using Cognito user pools with API Gateway, see the `Use Amazon Cognito User Pools documentation `__. Custom Authorizers ------------------ API Gateway also lets you write custom authorizers using a Lambda function. You can configure a Chalice route to use a pre-existing Lambda function as a custom authorizer. If you also want to write and manage your Lambda authorizer using Chalice, see the next section, Built-in Authorizers. To connect an existing Lambda function as a custom authorizer in chalice, you use the ``CustomAuthorizer`` class: .. code-block:: python from chalice import CustomAuthorizer authorizer = CustomAuthorizer( 'MyCustomAuth', header='Authorization', authorizer_uri=('arn:aws:apigateway:region:lambda:path/2015-03-31' '/functions/arn:aws:lambda:region:account-id:' 'function:FunctionName/invocations')) @app.route('/custom-auth', methods=['GET'], authorizer=authorizer) def authenticated(): return {"success": True} .. _builtin-authorizers: Built-in Authorizers -------------------- The ``IAMAuthorizer``, ``CognitoUserPoolAuthorizer``, and the ``CustomAuthorizer`` classes are all for cases where you have existing resources for managing authorization and you want to wire them together with your Chalice app. A Built-in authorizer is used when you'd like to write your custom authorizer in Chalice, and have the additional Lambda functions managed when you run ``chalice deploy/delete``. This section will cover how to use the built-in authorizers in chalice. Creating an authorizer in chalice requires you use the ``@app.authorizer`` decorator to a function. The function must accept a single arg, which will be an instance of :class:`AuthRequest`. The function must return a :class:`AuthResponse`. As an example, we'll port the example from the `API Gateway documentation`_. First, we'll show the code and then walk through it: .. code-block:: python from chalice import Chalice, AuthResponse app = Chalice(app_name='demoauth1') @app.authorizer() def demo_auth(auth_request): token = auth_request.token # This is just for demo purposes as shown in the API Gateway docs. # Normally you'd call an oauth provider, validate the # jwt token, etc. # In this example, the token is treated as the status for demo # purposes. if token == 'allow': return AuthResponse(routes=['/'], principal_id='user') else: # By specifying an empty list of routes, # we're saying this user is not authorized # for any URLs, which will result in an # Unauthorized response. return AuthResponse(routes=[], principal_id='user') @app.route('/', authorizer=demo_auth) def index(): return {'context': app.current_request.context} In the example above we define a built-in authorizer by decorating the ``demo_auth`` function with the ``@app.authorizer()`` decorator. Note you must use ``@app.authorizer()`` and not ``@app.authorizer``. A built-in authorizer function has this type signature:: def auth_handler(auth_request: AuthRequest) -> AuthResponse: ... Within the auth handler you must determine if the request is authorized or not. The ``AuthResponse`` contains the allowed URLs as well as the principal id of the user. You can optionally return a dictionary of key value pairs (as the ``context`` kwarg). This dictionary will be passed through on subsequent requests. In our example above we're not using the context dictionary. API Gateway will convert all the values in the ``context`` dictionary to string values. Now let's deploy our app. As usual, we just need to run ``chalice deploy`` and chalice will automatically deploy all the necessary Lambda functions for us. Now when we try to make a request, we'll get an Unauthorized error:: $ http https://api.us-west-2.amazonaws.com/api/ HTTP/1.1 401 Unauthorized { "message": "Unauthorized" } If we add the appropriate authorization header, we'll see the call succeed:: $ http https://api.us-west-2.amazonaws.com/api/ 'Authorization: allow' HTTP/1.1 200 OK { "context": { "accountId": "12345", "apiId": "api", "authorizer": { "principalId": "user" }, "httpMethod": "GET", "identity": { "accessKey": null, "accountId": null, "apiKey": "", "caller": null, "cognitoAuthenticationProvider": null, "cognitoAuthenticationType": null, "cognitoIdentityId": null, "cognitoIdentityPoolId": null, "sourceIp": "1.1.1.1", "user": null, "userAgent": "HTTPie/0.9.9", "userArn": null }, "path": "/api/", "requestId": "d35d2063-56be-11e7-9ce1-dd61c24a3668", "resourceId": "id", "resourcePath": "/", "stage": "dev" } } The low level API for API Gateway's custom authorizer feature requires that an IAM policy must be returned. The :class:`AuthResponse` class we're using is a wrapper over building the IAM policy ourselves. If you want low level control and would prefer to construct the IAM policy yourself you can return a dictionary of the IAM policy instead of an instance of :class:`AuthResponse`. If you do that, the dictionary is returned without modification back to API Gateway. For more information on custom authorizers, see the `Use API Gateway Custom Authorizers `__ page in the API Gateway user guide. Scopes ------------------------- OAuth 2.0 and OpenID Connect (OIDC) scopes can be used to implement access controls in your Chalice app. Scopes are supported when using the Cognito Authorizer, Custom Authorizers, and Built-In Authorizers. To integrate Scopes with a Cognito Authorizer in Chalice, you'll need to have an existing `Cognito user pools`_ and `Cognito resource server`_ configured. Scopes for Cognito Authorizers need to include the full identifier which is ``resourceServerIdentifier/scopeName``. Scopes can be configured per-authorizer using the ``scopes`` attribute. .. code-block:: python from chalice import CognitoUserPoolAuthorizer authorizer = CognitoUserPoolAuthorizer( 'MyPool', provider_arns=['arn:aws:cognito:...:userpool/name'], scopes=["https://mychaliceapp.example.com/todos.read"]) @app.route('/user-pools', methods=['GET'], authorizer=authorizer) def authenticated(): return {"success": True} Scopes can be configured per-route for an Authorizer using ``with_scopes``. .. code-block:: python from chalice import CognitoUserPoolAuthorizer authorizer = CognitoUserPoolAuthorizer( 'MyPool', provider_arns=['arn:aws:cognito:...:userpool/name']) @app.route( '/user-pools', methods=['GET'], authorizer=authorizer.with_scopes(["https://mychaliceapp.example.com/todos.read"])) def authenticated(): return {"success": True} Scopes can also be used with custom authorizers and built-in authorizers. These authorizers will need to inspect the access token to determine if access should be granted based on the scopes configured for the authorizer and route. .. _IAM permissions: https://docs.aws.amazon.com/IAM/latest/UserGuide/access_controlling.html .. _Cognito User Pools: https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools.html .. _Cognito Resource Server: https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-define-resource-servers.html .. _API Gateway documentation: https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html ================================================ FILE: docs/source/topics/blueprints.rst ================================================ Blueprints ========== Chalice blueprints are used to organize your application into logical components. Using a blueprint, you define your resources and decorators in modules outside of your ``app.py``. You then register a blueprint in your main ``app.py`` file. Blueprints support any decorator available on an application object. .. note:: The Chalice blueprints are conceptually similar to `Blueprints `__ in Flask. Flask blueprints allow you to define a set of URL routes separately from the main ``Flask`` object. This concept is extended to all resources in Chalice. A Chalice blueprint can have Lambda functions, event handlers, built-in authorizers, etc. in addition to a collection of routes. Example ------- In this example, we'll create a blueprint with part of our routes defined in a separate file. First, let's create an application:: $ chalice new-project blueprint-demo $ cd blueprint-demo $ mkdir chalicelib $ touch chalicelib/__init__.py $ touch chalicelib/blueprints.py Next, we'll open the ``chalicelib/blueprints.py`` file: .. code-block:: python from chalice import Blueprint extra_routes = Blueprint(__name__) @extra_routes.route('/foo') def foo(): return {'foo': 'bar'} The ``__name__`` is used to denote the import path of the blueprint. This name must match the import name of the module so the function can be properly imported when running in Lambda. We'll now import this module in our ``app.py`` and register this blueprint. We'll also add a route in our ``app.py`` directly: .. code-block:: python from chalice import Chalice from chalicelib.blueprints import extra_routes app = Chalice(app_name='blueprint-demo') app.register_blueprint(extra_routes) @app.route('/') def index(): return {'hello': 'world'} At this point, we've defined two routes. One route, ``/``, is directly defined in our ``app.py`` file. The other route, ``/foo`` is defined in ``chalicelib/blueprints.py``. It was added to our Chalice app when we registered it via ``app.register_blueprint(extra_routes)``. We can deploy our application to verify this works as expected:: $ chalice deploy Creating deployment package. Creating IAM role: blueprint-demo-dev Creating lambda function: blueprint-demo-dev Creating Rest API Resources deployed: - Lambda ARN: arn:aws:lambda:us-west-2:1234:function:blueprint-demo-dev - Rest API URL: https://rest-api.execute-api.us-west-2.amazonaws.com/api/ We should now be able to request the ``/`` and ``/foo`` routes:: $ http https://rest-api.execute-api.us-west-2.amazonaws.com/api/ HTTP/1.1 200 OK Connection: keep-alive Content-Length: 17 Content-Type: application/json Date: Sat, 22 Dec 2018 01:05:48 GMT Via: 1.1 5ab5dc09da67e3ea794ec8a82992cc89.cloudfront.net (CloudFront) X-Amz-Cf-Id: Cdsow9--fnTH5EdjkjWBMWINCCMD4nGmi4S_3iMYMK0rpc8Mpiymgw== X-Amzn-Trace-Id: Root=1-5c1d8dec-f1ef3ee83c7c654ca7fb3a70;Sampled=0 X-Cache: Miss from cloudfront x-amz-apigw-id: SSMc6H_yvHcFcEw= x-amzn-RequestId: b7bd0c87-0585-11e9-90cf-59b71c1a1de1 { "hello": "world" } $ http https://rest-api.execute-api.us-west-2.amazonaws.com/api/foo HTTP/1.1 200 OK Connection: keep-alive Content-Length: 13 Content-Type: application/json Date: Sat, 22 Dec 2018 01:05:51 GMT Via: 1.1 95b0ac620fa3a80ee590ecf1cda1c698.cloudfront.net (CloudFront) X-Amz-Cf-Id: HX4l1BNdWvYDRXan17PFZya1vaomoJel4rP7d8_stdw2qT50v7Iybg== X-Amzn-Trace-Id: Root=1-5c1d8def-214e7f681ff82c00fd81f37a;Sampled=0 X-Cache: Miss from cloudfront x-amz-apigw-id: SSMdXF40vHcF-mg= x-amzn-RequestId: b96f77bf-0585-11e9-b229-01305cd40040 { "foo": "bar" } Blueprint Registration ---------------------- The ``app.register_blueprint`` function accepts two optional arguments, ``name_prefix`` and ``url_prefix``. This allows you to register the resources in your blueprint at a certain url and name prefix. If you specify ``url_prefix``, any routes defined in your blueprint will have the ``url_prefix`` prepended to it. If you specify the ``name_prefix``, any Lambda functions created will have the ``name_prefix`` prepended to the resource name. .. note:: The ``name_prefix`` parameter does not apply to the Lambda function associated with API Gateway, which is anything decorated with ``@app.route()``. Advanced Example ---------------- Let's create a more advanced example. If this application, let's say we want to organize our application into separate modules for our API and our event sources. We can create an app with these files:: $ ls -la chalicelib/ __init__.py api.py events.py The contents of ``api.py`` are: .. code-block:: python from chalice import Blueprint myapi = Blueprint(__name__) @myapi.route('/') def index(): return {'hello': 'world'} @myapi.route('/foo') def index(): return {'foo': 'bar'} The contents of ``events.py`` are: .. code-block:: python from chalice import Blueprint myevents = Blueprint(__name__) @myevents.schedule('rate(5 minutes)') def cron(event): pass @myevents.on_sns_message('MyTopic') def handle_sns_message(event): pass In our ``app.py`` we'll register these blueprints: .. code-block:: python from chalice import Chalice from chalicelib.events import myevents from chalicelib.api import myapi app = Chalice(app_name='blueprint-demo') app.register_blueprint(myevents) app.register_blueprint(myapi) Now our ``app.py`` only registers the necessary blueprints, and all our resources are defined in blueprints. ================================================ FILE: docs/source/topics/cd.rst ================================================ =========================== Continuous Deployment (CD) =========================== Chalice can be used to set up a basic Continuous Deployment pipeline. The ``chalice deploy`` command is good for getting up and running quickly with Chalice, but in a team environment properly managing permissions and sharing and updating the ``deployed.json`` file will get messy. One way to scale up your chalice app is to create a continuous deployment pipeline. The pipeline can run tests on code changes and, if they pass, promote the new build to a testing stage. More checks can be put in place to manually promote a build to production, or you can do so automatically. This model greatly simplifies managing what resources belong to your Chalice app as they are all stored in the Continuous Deployment pipeline. Chalice can generate a CloudFormation template that will create a starter CD pipeline. By default it contains an AWS CodeCommit repo, an AWS CodeBuild stage for packaging your chalice app, and an AWS CodePipeline stage to deploy your application using CloudFormation. You can also configure a source repository hosted on GitHub instead of a CodeCommit repository. Pipeline Template Versions ========================== This starter pipeline template can be generated using the ``generate-pipeline`` command. There are two versions of this pipeline. The older ``v1`` template is the default (for backwards compatibility reasons), but the newer template version, ``v2``, is recommended. The version can be specified using the ``--pipeline-version`` option. These are the differences between ``v1`` and ``v2`` templates: * The ``v1`` templates use version ``0.1`` of the CodeBuild buildspec, whereas ``v2`` uses ``0.2`` of the CodeBuild buildspec. Buildspec ``0.2`` is the recommended version to use with CodeBuild. See their `documentation `__ for more information. * The ``v2`` template uses `AWS Secrets Manager `__ to configure access to a GitHub repository. * The ``v2`` buildspec uses `runtime-versions `__ to configure which version of Python to use instead of a Python version specific CodeBuild image. For ``v2`` templates the ``aws/codebuild/amazonlinux2-x86_64-standard`` image. **The v2 pipeline template requires Python 3.7 or higher.** If you're using Python versions less than 3.7 you must use the ``v1`` pipeline template. Usage example ============= Setting up the deployment pipeline is a two step process. First use the ``chalice generate-pipeline`` command to generate a base CloudFormation template. Second use the AWS CLI to deploy the CloudFormation template using the ``aws cloudformation deploy`` command. Below is an example. :: $ chalice generate-pipeline --pipeline-version v2 pipeline.json $ aws cloudformation deploy --stack-name mystack --template-file pipeline.json --capabilities CAPABILITY_IAM Waiting for changeset to be created.. Waiting for stack create/update to complete Successfully created/updated stack - mystack .. note:: To configure your Chalice app to use a GitHub repository instead of CodeCommit see the :ref:`cicd-github-repo` section below. Once the CloudFormation template has finished creating the stack, you will have several new AWS resources that make up a bare bones CD pipeline. * **CodeCommit Repository** - The `CodeCommit `_ repository is the entrypoint into the pipeline. Any code you want to deploy should be pushed to this remote. * **CodePipeline Pipeline** - The `CodePipeline `_ is what coordinates the build process, and pushes the released code out. * **CodeBuild Project** - The `CodeBuild `_ project is where the code bundle is built that will be pushed to Lambda. The default CloudFormation template will create a CodeBuild stage that builds a package using ``chalice package`` and then uploads those artifacts for CodePipeline to deploy. * **S3 Buckets** - Two S3 buckets are created on your behalf. * **artifactbucketstore** - This bucket stores artifacts that are built by the CodeBuild project. The only artifact by default is the ``transformed.yaml`` created by the ``aws cloudformation package`` command. * **applicationbucket** - Stores the application bundle after the Chalice application has been packaged in the CodeBuild stage. * Each resource is created with all the required IAM roles and policies. CodeCommit repository --------------------- The CodeCommit repository can be added as a git remote for deployment. This makes it easy to kick off deployments. The developer doing the deployment only needs to push the release code up to the CodeCommit repository master branch. All the developer needs is keys that allow for push access to the CodeCommit repository. This is a lot easier than managing a set of ``deployed.json`` resources across a repsoitory and manually doing ``chalice deploy`` whenever a change needs to be deployed. The default CodeCommit repository that is created is empty, you will have to populate it with the Chalice application code. Permissions will also need to be set up, you can find the documentation on how to do that `here `_ . You can retrieve the CodeCommit clone URL by searching for the ``SourceRepoURL`` in the CloudFormation stack output:: $ aws cloudformation describe-stacks --stack-name mystack \ --query "Stacks[0].Outputs[?OutputKey=='SourceRepoURL'] | [0].OutputValue" CodePipeline ------------ CodePipeline is the main coordinator between all the other resources. It watches for changes on the CodeCommit repository, and triggers builds in the CodeBuild project. If the build succeeds then it will start a CloudFormation deployment of the built artifacts to a beta stage. This should be treated as a starting point, not a fully featured CD system. CodeBuild build script ---------------------- By default Chalice will create the CodeBuild project with a default buildspec that does the following. .. code-block:: yaml version: 0.1 phases: install: commands: - sudo pip install --upgrade awscli - aws --version - sudo pip install chalice - sudo pip install -r requirements.txt - chalice package /tmp/packaged - aws cloudformation package --template-file /tmp/packaged/sam.json --s3-bucket ${APP_S3_BUCKET} --output-template-file transformed.yaml artifacts: type: zip files: - transformed.yaml The CodeBuild stage installs both the AWS CLI and Chalice, then creates a package out of your chalice project, pushing the package to the application S3 bucket that was created for you. The transformed CloudFormation template is the only artifact, and can be run by CodePipeline after the build has succeeded. Deploying to beta stage ----------------------- Once the CodeBuild stage has finished building the Chalice package and creating the ``transformed.yaml``, CodePipeline will take these artifacts and use them to create or update the beta stage. The ``transformed.yaml`` is a CloudFormation template that CodePipeline will execute, all the code it references has been uploaded to the application bucket by the AWS CLI in the CodeBuild stage, so this is the only artifact we need. Once the CodePipeline beta build stage is finished, the beta version of the app is deployed and ready for testing. Extending --------- It is recommended to use this pipeline as a starting point. The default template does not run any tests on the Chalice app before deploying to beta. There is also no mechanism provided by Chalice for a production stage. Ideally the CodeBuild stage would be used to run unit and functional tests before deploying to beta. After the beta stage is up, integration tests can be run against that endpoint, and if they all pass the beta stage could be promoted to a production stage using the CodePipeline manual approval feature. .. _cicd-github-repo: Configuring a GitHub Repository =============================== You can configure a GitHub repository instead of a CodeCommit repo when setting up your deployment pipeline by specifying the ``--source github`` option. When generating a CloudFormation template for a GitHub repository, there are several parameters that are added to your template that allow you to configure how to connect your GitHub repository with your CodePipeline. You must store your OAuth token that enables access to a GitHub repository in AWS Secrets Manager. You then specify the secret name/id and the JSON key name as CloudFormation parameters. These values default to a secret name of ``GithubRepoAccess`` and a JSON key name of ``OAuthToken``. Below is an example of how to configure a GitHub repository as the source for your deployment pipeline. First create a `GitHub token `__ that can be used in this template. Next create a secret in AWS Secrets Manager. You can either follow the documentation `here `__ or use the AWS CLI or any AWS SDK. For this example, we'll use the AWS CLI to create our secret. Create a file named ``/tmp/secrets.json`` with these contents:: {"OAuthToken": "abcdefghhijklmnop"} Be sure to replace the value of ``OAuthToken`` with the value of your GitHub token you created. Next we can create the secret using this command:: $ aws secretsmanager create-secret --name GithubRepoAccess \ --description "Token for Github Repo Access" \ --secret-string file:///tmp/secrets.json Now we can generate our deployment pipeline:: $ chalice generate-pipeline --pipeline-version v2 \ --source github --buildspec-file buildspec.yml pipeline.json This will create two files, a ``pipeline.json`` file containing our deployment pipeline and a ``buildspec.yml`` file. This buildspec file lets us update what commands should be run as part of our build process without having to redeploy our CloudFormation template. We now add and commit our changes to our repository. :: $ git add buildspec.yml pipeline.json $ git commit -m "Add deployment pipeline template" $ git push Now we're ready to deploy our CloudFormation template using the AWS CLI. Be sure to replace the ``GithubOwner`` and ``GithubRepoName`` with your own values for your GitHub repository. You'll also need to specify the ``GithubRepoSecretId`` and ``GithubRepoSecretJSONKey`` if you used values other than the default vaues of ``GithubRepoAccess`` and ``OAuthToken`` when creating your secret in Secrets Manager. :: $ aws cloudformation deploy --template-file pipeline.json \ --stack-name MyChaliceApp --parameter-overrides \ GithubOwner=repo-owner-name \ GithubRepoName=repo-name \ --capabilities CAPABILITY_IAM We've now created a deployment pipeline that will automatically deploy our Chalice app whenever we push to our GitHub repository. ================================================ FILE: docs/source/topics/cfn.rst ================================================ AWS CloudFormation Support ========================== When you run ``chalice deploy``, chalice will deploy your application using the `AWS SDK for Python `__). Chalice also provides functionality that allows you to manage deployments yourself using cloudformation. This is provided via the ``chalice package`` command. When you run this command, chalice will generate the AWS Lambda deployment package that contains your application as well as a `Serverless Application Model (SAM) `__ template. You can then use a tool like the AWS CLI, or any cloudformation deployment tools you use, to deploy your chalice application. Considerations -------------- Using the ``chalice package`` command is useful when you don't want to use ``chalice deploy`` to manage your deployments. There's several reasons why you might want to do this: * You have pre-existing infrastructure and tooling set up to manage cloudformation stacks. * You want to integrate with other cloudformation stacks to manage all your AWS resources, including resources outside of your chalice app. * You'd like to integrate with `AWS CodePipeline `__ to automatically deploy changes when you push to a git repo. Keep in mind that you can't switch between ``chalice deploy`` and ``chalice package`` + CloudFormation for deploying your app. If you choose to use ``chalice package`` and CloudFormation to deploy your app, you won't be able to switch back to ``chalice deploy``. Running ``chalice deploy`` would create an entirely new set of AWS resources (API Gateway Rest API, AWS Lambda function, etc). Template Merge -------------- It's a common use case to need to modify a Chalice generated template before deployment. Often to inject extra resources, values, or configurations that are not supported directly by Chalice. It will always be the case that something on AWS is not supported by Chalice directly that a consumer may want to interact with. The package command can now be invoked with the ``--merge-template`` argument:: $ chalice package --merge-template extras.json out This extras.json file should be a JSON formatted file which will be deep-merged on top of the sam.json that is generated by Chalice. For a simple example lets assume that we have the default new Chalice project and that extras.json has the following content:: { "Resources" : { "MusicTable" : { "Type" : "AWS::DynamoDB::Table", "Properties" : { "TableName" : "MusicData", "AttributeDefinitions" : [ { "AttributeName" : "Album", "AttributeType" : "S" }, { "AttributeName" : "Artist", "AttributeType" : "S" } ], "KeySchema" : [ { "AttributeName" : "Album", "KeyType" : "HASH" }, { "AttributeName" : "Artist", "KeyType" : "RANGE" } ], "ProvisionedThroughput" : { "ReadCapacityUnits" : "5", "WriteCapacityUnits" : "5" } } }, "APIHandler": { "Properties": { "Environment": { "Variables": { "MUSIC_TABLE": {"Ref": "MusicTable"} } } } } } } The generated template located at out/sam.json will have the DynamoDB table injected into the resource section, as well as the ``MUSIC_TABLE`` environment variable added to the ``APIHandler`` Lambda function:: ... "APIHandler": { "Type": "AWS::Serverless::Function", "Properties": { "Runtime": "python3.6", "Handler": "app.app", "CodeUri": "./deployment.zip", "Tags": { "aws-chalice": "version=1.10-:stage=dev:app=test" }, "Timeout": 60, "MemorySize": 128, "Role": { "Fn::GetAtt": [ "DefaultRole", "Arn" ] }, "Environment": { "Variables": { "MUSIC_TABLE": "MusicData" } } } }, ... This gives us the ability to inject arbitrary resources into our Chalice applications, and reference them from our Chalice-deployed functions. We can now rely on Chalice's policy auto-generation to generate a policy that allows DynamoDB access, inject our own policy modifications through the same extras.json file, or specify a custom policy using the config file. Example ------- In this example, we'll create a chalice app and deploy it using the AWS CLI. First install the necessary packages:: $ virtualenv /tmp/venv $ . /tmp/venv/bin/activate $ pip install chalice awscli $ chalice new-project test-cfn-deploy $ cd test-cfn-deploy At this point we've installed chalice and the AWS CLI and we have a basic app created locally. Next we'll run the ``package`` command and look at its contents:: $ $ chalice package /tmp/packaged-app/ Creating deployment package. $ ls -la /tmp/packaged-app/ -rw-r--r-- 1 j wheel 3355270 May 25 14:20 deployment.zip -rw-r--r-- 1 j wheel 3068 May 25 14:20 sam.json $ unzip -l /tmp/packaged-app/deployment.zip | tail -n 5 17292 05-25-17 14:19 chalice/app.py 283 05-25-17 14:19 chalice/__init__.py 796 05-25-17 14:20 app.py -------- ------- 9826899 723 files $ head < /tmp/packaged-app/sam.json { "AWSTemplateFormatVersion": "2010-09-09", "Outputs": { "RestAPIId": { "Value": { "Ref": "RestAPI" } }, "APIHandlerName": { "Value": { As you can see in the above example, the ``package`` command created a directory that contained two files, a ``deployment.zip`` file, which is the Lambda deployment package, and a ``sam.json`` file, which is the SAM template that can be deployed using CloudFormation. Next we're going to use the AWS CLI to deploy our app. To this, we'll first run the ``aws cloudformation package`` command, which will take our deployment.zip file and upload to an S3 bucket we specify:: $ aws cloudformation package \ --template-file /tmp/packaged-app/sam.json \ --s3-bucket myapp-bucket \ --output-template-file /tmp/packaged-app/packaged.yaml Now we can deploy our app using the ``aws cloudformation deploy`` command:: $ aws cloudformation deploy \ --template-file /tmp/packaged-app/packaged.yaml \ --stack-name test-cfn-stack \ --capabilities CAPABILITY_IAM Waiting for changeset to be created.. Waiting for stack create/update to complete Successfully created/updated stack - test-cfn-stack This will take a few minutes to complete, but once it's done, the endpoint url will be available as an output:: $ aws cloudformation describe-stacks --stack-name test-cfn-stack \ --query "Stacks[].Outputs[?OutputKey=='EndpointURL'][] | [0].OutputValue" "https://abc29hkq0i.execute-api.us-west-2.amazonaws.com/api/" $ http "https://abc29hkq0i.execute-api.us-west-2.amazonaws.com/api/" HTTP/1.1 200 OK Connection: keep-alive Content-Length: 18 Content-Type: application/json ... { "hello": "world" } ================================================ FILE: docs/source/topics/configfile.rst ================================================ Configuration File ================== Whenever you create a new project using ``chalice new-project``, a ``.chalice`` directory is created for you. In this directory is a ``config.json`` file that you can use to control what happens when you ``chalice deploy``:: $ tree -a . ├── .chalice │ └── config.json ├── app.py └── requirements.txt 1 directory, 3 files .. _stage-config: Stage Specific Configuration ---------------------------- As of version 0.7.0 of chalice, you can specify configuration that is specific to a chalice stage as well as configuration that should be shared across all stages. See the :doc:`stages` doc for more information about chalice stages. * ``stages`` - This value of this key is a mapping of chalice stage name to stage configuration. Chalice assumes a default stage name of ``dev``. If you run the ``chalice new-project`` command on chalice 0.7.0 or higher, this key along with the default ``dev`` key will automatically be created for you. See the examples section below for some stage specific configurations. The following config values can either be specified per stage config or as a top level key which is not tied to a specific stage. Whenever a stage specific configuration value is needed, the ``stages`` mapping is checked first. If no value is found then the top level keys will be checked. ``api_gateway_endpoint_type`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The endpoint configuration of the deployed API Gateway which determines how the API will be accessed, can be EDGE, REGIONAL, PRIVATE. Note this value can only be set as a top level key and defaults to EDGE. For more information see https://amzn.to/2LofApt ``api_gateway_endpoint_vpce`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ When configuring a Private API a VPC Endpoint id must be specified to configure a default resource policy on the API if an explicit policy is not specified. This value can be a list or a string of endpoint ids. ``api_gateway_policy_file`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~ A file pointing to an IAM resource policy for the REST API. If not specified chalice will autogenerate this policy when endpoint_type is PRIVATE. This filename is relative to the ``.chalice`` directory. ``api_gateway_stage`` ~~~~~~~~~~~~~~~~~~~~~ The name of the API gateway stage. This will also be the URL prefix for your API (``https://endpoint/prefix/your-api``). ``autogen_policy`` ~~~~~~~~~~~~~~~~~~ A boolean value that indicates if chalice should try to automatically generate an IAM policy based on analyzing your application source code. The default value is ``true``. If this value is ``false`` then chalice will try to load an IAM policy from disk at ``.chalice/policy-.json`` instead of auto-generating a policy from source code analysis. You can change the filename by providing the ``iam_policy_file`` config option. See :ref:`iam-role-pol-examples` for examples of how to configure IAM roles and policies. ``environment_variables`` ~~~~~~~~~~~~~~~~~~~~~~~~~ A mapping of key value pairs. These key value pairs will be set as environment variables in your application. All environment variables must be strings. If this key is specified in both a stage specific config option as well as a top level key, the stage specific environment variables will be merged into the top level keys. See the :ref:`env-var-examples` section below for a concrete example. ``iam_policy_file`` ~~~~~~~~~~~~~~~~~~~ When ``autogen_policy`` is ``false``, Chalice will try to load an IAM policy from disk instead of auto-generating one based on source code analysis. The default location of this file is ``.chalice/policy-.json``, e.g ``.chalice/policy-dev.json``, ``.chalice/policy-prod.json``, etc. You can change the filename by providing this ``iam_policy_file`` config option. This filename is relative to the ``.chalice`` directory. For example, this config will create an IAM role using the file in ``.chalice/my-policy.json``:: { "version": "2.0", "app_name": "app", "stages": { "dev": { "autogen_policy": false, "iam_policy_file": "my-policy.json" } } } See :ref:`iam-role-pol-examples` for more examples of how to configure IAM roles and policies. ``iam_role_arn`` ~~~~~~~~~~~~~~~~ If ``manage_iam_role`` is ``false``, you must specify this value that indicates which IAM role arn to use when configuration your application. This value is only used if ``manage_iam_role`` is ``false``. See :ref:`iam-role-pol-examples` for examples of how to configure IAM roles and policies. ``lambda_memory_size`` ~~~~~~~~~~~~~~~~~~~~~~ An integer representing the amount of memory, in MB, your Lambda function is given. AWS Lambda uses this memory size to infer the amount of CPU allocated to your function. The default ``lambda_memory_size`` value is ``128``. The value must be a multiple of 64 MB. ``lambda_timeout`` ~~~~~~~~~~~~~~~~~~ An integer representing the function execution time, in seconds, at which AWS Lambda should terminate the function. The default ``lambda_timeout`` is ``60`` seconds. ``layers`` ~~~~~~~~~~ A list of Lambda Layers arns. This value can be provided per stage as well as per Lambda function. See `AWS Lambda Layers Configuration`_. .. _automatic-layer-option: ``automatic_layer`` ~~~~~~~~~~~~~~~~~~~~ A boolean value that indicates whether chalice will automatically construct a single stage layer for all Lambda functions with requirements.txt libraries and vendored libraries. Boolean value defaults to ``false`` if not specified. See :ref:`package-3rd-party` for more information. .. _custom-domain-config-options: ``api_gateway_custom_domain`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ A mapping of key value pairs. The following are required keys when specifying an ``api_gateway_custom_domain``: - ``domain_name``: The custom domain name to associated with the REST API (api.example.com) - ``certificate_arn``: the ARN of ACM certificate for the current domain name. If you're using a ``REGIONAL`` endpoint type for your API, the ACM certificate **must** be in the same region as your API. If you're using an ``EDGE`` endpoint type, the certificate must be in ``us-east-1``. You can also provide the following optional configuration: - ``tls_version`` - The Transport Layer Security (TLS) version of the security policy for this domain name. Defaults to ``TLS_1_2``, you can also provide ``TLS_1_0`` for REST APIs. - ``url_prefix`` - A custom domain name plus a url_prefix (BasePathMapping) specification identifies a deployed REST API in a given stage. With custom domain names, you can set up your API's hostname, and choose a base path (for example, `myservice`) to map the alternative URL to your API (for example ``https://api.example.com/myservice``). If you don't set any ``url_prefix``, the resulting API's base URL is the same as the custom domain (for example ``https://api.example.com/``). - tags - A dictionary of tags with the keys being the tag key, and the values being the value for the tag. See the :doc:`domainname` documentation for more information on configuring your Chalice application with a custom domain name. See `AWS Custom Domain names setup`_ for the API Gateway documentation on configuring a custom domain name. .. _custom-domain-ws-config-options: ``websocket_api_custom_domain`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ A mapping of key value pairs. The following are required keys when specifying a ``websocket_api_custom_domain``: - ``domain_name``: The custom domain name to associated with the REST API (api.example.com) - ``certificate_arn``: the ARN of ACM certificate for the current domain name. If you're using a ``REGIONAL`` endpoint type for your API, the ACM certificate **must** be in the same region as your API. If you're using an ``EDGE`` endpoint type, the certificate must be in ``us-east-1``. You can also provide the following optional configuration: - ``tls_version`` - The Transport Layer Security (TLS) version of the security policy for this domain name. Defaults to ``TLS_1_2``, you can also provide ``TLS_1_0`` for REST APIs. - ``url_prefix`` - A custom domain name plus a url_prefix (BasePathMapping) specification identifies a deployed REST API in a given stage. With custom domain names, you can set up your API's hostname, and choose a base path (for example, `myservice`) to map the alternative URL to your API (for example ``https://api.example.com/myservice``). If you don't set any ``url_prefix``, the resulting API's base URL is the same as the custom domain (for example ``https://api.example.com/``). - tags - A dictionary of tags with the keys being the tag key, and the values being the value for the tag. See the :doc:`domainname` documentation for more information on configuring your Chalice application with a custom domain name. See `AWS Custom Domain names setup`_ for the API Gateway documentation on configuring a custom domain name. ``manage_iam_role`` ~~~~~~~~~~~~~~~~~~~ ``true``/``false``. Indicates if you want chalice to create and update the IAM role used for your application. By default, this value is ``true``. However, if you have a pre-existing role you've created, you can set this value to ``false`` and a role will not be created or updated. ``"manage_iam_role": false`` means that you are responsible for managing the role and any associated policies associated with that role. If this value is ``false`` you must specify an ``iam_role_arn``, otherwise an error is raised when you try to run ``chalice deploy``. ``minimum_compression_size`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ An integer value that indicates the minimum compression size to apply to the API gateway. If this key is specified in both a stage specific config option as well as a top level key, the stage specific key will override the top level key for the given stage. For more information check out the `Service Docs `__ ``reserved_concurrency`` ~~~~~~~~~~~~~~~~~~~~~~~~ An integer representing each function's reserved concurrency. This value can be provided per stage as well as per Lambda function. AWS Lambda reserves this value of concurrency to each lambda deployed in this stage. If the value is set to 0, invocations to this function are blocked. If the value is unset, there will be no reserved concurrency allocations. For more information, see `AWS Documentation on managing concurrency`_. ``subnet_ids`` ~~~~~~~~~~~~~~ A list of subnet ids for VPC configuration. This value can be provided per stage as well as per Lambda function. In order for this value to take effect, you must also provide the ``security_group_ids`` value. When both values are provided and ``autogen_policy`` is True, chalice will automatically update your IAM role with the necessary permissions to create, describe, and delete ENIs. If you are managing the IAM role policy yourself, make sure to update your permissions accordingly, as described in the `AWS Lambda VPC documentation`_. ``security_group_ids`` ~~~~~~~~~~~~~~~~~~~~~~ A list of security groups for VPC configuration. This value can be provided per stage as well as per Lambda function. In order for this value to take effect, you must also provide the ``subnet_ids`` value. ``tags`` ~~~~~~~~ A mapping of key value pairs. These key value pairs will be set as the tags on the resources running your deployed application. All tag keys and values must be strings. Similar to ``environment_variables``, if a key is specified in both a stage specific config option as well as a top level key, the stage specific tags will be merged into the top level keys. By default, all chalice deployed resources are tagged with the key ``'aws-chalice'`` whose value is ``'version={chalice-version}:stage={stage-name}:app={app-name}'``. Currently only the following chalice deployed resources are tagged: Lambda functions. ``xray`` ~~~~~~~~ A boolean that turns on AWS XRay's Active tracing configuration. This will turn on XRay for both Lambda functions and API Gateway stages. ``log_retention_in_days`` ~~~~~~~~~~~~~~~~~~~~~~~~~ An integer value that indicates the retention time to be applied to lambda function log groups. Only certain values are valid, see the `AWS CloudWatch Logs docs `__ .. warning:: If you using the `chalice package` command to generate a CloudFormation template, a Log Group resource will be added to your template with the configured ``log_retention_in_days``. This will cause your deployment to fail if this Log Group resource already exists (i.e. if the associated Lambda function has previously been invoked which results in Lambda automatically created a Log Group for the function). In order to use this configuration option, it should be part of the initial deployment of the Lambda function. .. _lambda-config: Lambda Specific Configuration ----------------------------- In addition to a chalice stage, there are also some configuration values that can be specified per Lambda function. A chalice app can have many stages, and a stage can have many Lambda functions. You have the option to specify configuration for a lambda function across all your stages, or for a lambda function in a specific stage. To configure per lambda configuration for a specific stage, you add a ``lambda_functions`` key in your stage configuration:: { "version": "2.0", "app_name": "app", "stages": { "dev": { "lambda_functions": { "foo": { "lambda_timeout": 120 } } } } } To specify per lambda configuration across all stages, you add a top level ``lambda_functions`` key:: { "version": "2.0", "app_name": "app", "lambda_functions": { "foo": { "lambda_timeout": 120 } } } Each key in the ``lambda_functions`` dictionary is the name of a Lambda function in your app. The value is a dictionary of configuration that will be applied to that function. These are the configuration options that can be applied per function: * ``autogen_policy`` * ``environment_variables`` * ``iam_policy_file`` * ``iam_role_arn`` * ``lambda_memory_size`` * ``lambda_timeout`` * ``layers`` * ``manage_iam_role`` * ``reserved_concurrency`` * ``security_group_ids`` * ``subnet_ids`` * ``tags`` * ``log_retention_in_days`` See the :ref:`stage-config` section above for a description of these config options. In general, the name of your lambda function will correspond to the name of the function in your app. For example: .. code-block:: python @app.lambda_function() def foo(event, context): pass To specify configuration for this function, you would use the key of ``foo`` in the ``lambda_functions`` configuration. There is one exception to this, which is any python function decorated with the ``@app.route()`` decorator. Chalice uses a single Lambda function for all requests from API gateway, and this name is ``api_handler``. So if you have an app like this: .. code-block:: python @app.route('/') def index(): pass @app.route('/foo/bar') def other_handler(): pass Then to specify configuration values for the underlying lambda function, which ``index()`` and ``other_handler()`` share, you would specify: .. code-block:: json { "lambda_functions": { "api_handler": { "subnet_ids": ["sn-1", "sn-2"], "security_group_ids": ["sg-10", "sg-11"], "layers": ["layer-arn-1", "layer-arn-2"], } } } Examples -------- Below are examples that show how you can configure your chalice app. Custom Domain Name ~~~~~~~~~~~~~~~~~~ Here's an example for configuring Custom domain name for dev stage for REST API:: { "version": "2.0", "app_name": "app", "stages": { "dev": { "autogen_policy": true, "api_gateway_stage": "dev" "api_gateway_custom_domain": { "domain_name": "api.example.com", "security_policy": "TLS 1.2|TLS 1.0", "certificate_arn": "arn:aws:acm:example.com", "url_prefixes": ["foo", "bar"], "tags": { "key": "tag1", "key1": "tag2" } }, }, } } In this config file we're specifying ``dev`` stage for ApiGateway. In the ``dev`` stage, chalice will automatically create ``custom domain name`` with specified ``url_prefixes`` that should contain information about `AWS Api Mapping key`_. If there is Websocket API ``websocket_api_custom_domain`` should be used instead of ``api_gateway_custom_domain``. .. _iam-role-pol-examples: IAM Roles and Policies ~~~~~~~~~~~~~~~~~~~~~~ Here's an example for configuring IAM policies across stages:: { "version": "2.0", "app_name": "app", "stages": { "dev": { "autogen_policy": true, "api_gateway_stage": "dev" }, "beta": { "autogen_policy": false, "iam_policy_file": "beta-app-policy.json" }, "prod": { "manage_iam_role": false, "iam_role_arn": "arn:aws:iam::...:role/prod-role" } } } In this config file we're specifying three stages, ``dev``, ``beta``, and ``prod``. In the ``dev`` stage, chalice will automatically generate an IAM policy based on analyzing the application source code. For the ``beta`` stage, chalice will load the ``.chalice/beta-app-policy.json`` file and use it as the policy to associate with the IAM role for that stage. In the ``prod`` stage, chalice won't modify any IAM roles. It will just set the IAM role for the Lambda function to be ``arn:aws:iam::...:role/prod-role``. Here's an example that shows config precedence:: { "version": "2.0", "app_name": "app", "api_gateway_stage": "api", "stages": { "dev": { }, "beta": { }, "prod": { "api_gateway_stage": "prod", "manage_iam_role": false, "iam_role_arn": "arn:aws:iam::...:role/prod-role" } } } In this config file, both the ``dev`` and ``beta`` stage will have an API gateway stage name of ``api`` because they will default to the top level ``api_gateway_stage`` key. However, the ``prod`` stage will have an API gateway stage name of ``prod`` because the ``api_gateway_stage`` is specified in ``{"stages": {"prod": ...}}`` mapping. .. _env-var-examples: Environment Variables ~~~~~~~~~~~~~~~~~~~~~ In the following example, environment variables are specified both as top level keys as well as per stage. This allows us to provide environment variables that all stages should have as well as stage specific environment variables:: { "version": "2.0", "app_name": "app", "environment_variables": { "SHARED_CONFIG": "foo", "OTHER_CONFIG": "from-top" }, "stages": { "dev": { "environment_variables": { "TABLE_NAME": "dev-table", "OTHER_CONFIG": "dev-value" } }, "prod": { "environment_variables": { "TABLE_NAME": "prod-table", "OTHER_CONFIG": "prod-value" } } } } For the above config, the ``dev`` stage will have the following environment variables set:: { "SHARED_CONFIG": "foo", "TABLE_NAME": "dev-table", "OTHER_CONFIG": "dev-value", } The ``prod`` stage will have these environment variables set:: { "SHARED_CONFIG": "foo", "TABLE_NAME": "prod-table", "OTHER_CONFIG": "prod-value", } Per Lambda Examples ~~~~~~~~~~~~~~~~~~~ Suppose we had the following chalice app: .. code-block:: python from chalice import Chalice app = Chalice(app_name='demo') @app.lambda_function() def foo(event, context): pass @app.lambda_function() def bar(event, context): pass Given these two functions, we'd like to configure the functions as follows: * Both functions should have an environment variable ``OWNER`` with value ``dev-team``. * The ``foo`` function should have an autogenerated IAM policy managed by chalice. * The ``foo`` function should be run in a VPC with subnet ids ``sn-1`` and ``sn-2``, with security groups ``sg-10`` and ``sg-11``. Chalice should also automatically configure the IAM policy with permissions to modify EC2 network interfaces. * The ``foo`` function should have two connected layers as ``layer-arn-1`` and ``layer-arn-2``. Chalice should automatically configure the IAM policy. * The ``bar`` function should use a pre-existing IAM role that was created outside of chalice. Chalice should not perform an IAM role management for the ``bar`` function. * The ``bar`` function should have an environment variable ``TABLE_NAME`` with value ``mytable``. We can accomplish all this with this config file:: { "stages": { "dev": { "environment_variables": { "OWNER": "dev-team" } "api_gateway_stage": "api", "lambda_functions": { "foo": { "subnet_ids": ["sn-1", "sn-2"], "security_group_ids": ["sg-10", "sg-11"], "layers": ["layer-arn-1", "layer-arn-2"], }, "bar": { "manage_iam_role": false, "iam_role_arn": "arn:aws:iam::my-role-name", "environment_variables": {"TABLE_NAME": "mytable"} } } } }, "version": "2.0", "app_name": "demo" } .. _AWS Lambda VPC documentation: https://docs.aws.amazon.com/lambda/latest/dg/vpc.html#vpc-configuring .. _AWS Documentation on managing concurrency: https://docs.aws.amazon.com/lambda/latest/dg/concurrent-executions.html .. _AWS Lambda Layers Configuration: https://docs.aws.amazon.com/lambda/latest/dg/configuration-layers.html .. _AWS Custom Domain names setup: https://docs.aws.amazon.com/apigateway/latest/developerguide/how-to-custom-domains.html .. _AWS Api Mapping key: https://docs.aws.amazon.com/apigatewayv2/latest/api-reference/domainnames-domainname-apimappings.html ================================================ FILE: docs/source/topics/domainname.rst ================================================ Custom Domain Names =================== Custom domain names are simpler and more intuitive URLs that you can provide to your API users. With custom domain names, you can set up your API's hostname, and choose a base path to map the alternative URL to your API. You must have an AWS managed certificate created or imported through AWS Certificate Manager (ACM) in order to configure a custom domain name for REST and WebSocket APIs. See `Get certificate in AWS Certificate Manager `__ for more information. Custom domain name can be configured per Chalice stage. .. note:: This document describes the configuration option and process needed to configure a custom domain with your Chalice application. If you'd like a step-by-step example that walks you through configuring a custom domain for a Chalice app using Amazon Route53 and ACM, see the :doc:`../tutorials/customdomain` tutorial. There are two steps to configuring a custom domain name. First you must configure your Chalice app such that it creates the necessary resources and configuration when provisioning your REST or WebSocket APIs. This is explained in the next two sections below. Then you must configure your DNS configuration to point your custom domain name to the domain name created by API Gateway. This is explained in the :ref:`dns-last-config` section below. Configure custom domain name for REST API ----------------------------------------- To create custom domain name for REST API, add the ``api_gateway_custom_domain`` configuration option to your ``.chalice/config.json`` file. You must specify the ``certificate_arn``, which is the ARN of your ACM certificate associated with your domain as well as your ``domain_name``. The remaining fields are optional and may be omitted. By default TLS 1.2 is used for your endpoint unless otherwise specified. Below is an example of all the configuration options you can specify when configuring a custom domain. They are explained in the :ref:`custom-domain-config-options` section of the :doc:`configfile` documentation. .. code-block:: json { "stages": { "dev": { "api_gateway_stage": "api", "api_gateway_custom_domain": { "domain_name": "api.example.com", "tls_version": "TLS_1_2|TLS_1_0", "certificate_arn": "arn:aws:acm:example", "url_prefix": "foo", "tags": { "key": "tag1", "key1": "tag2" } } } } } Configure custom domain name for WebSocket ------------------------------------------ To create custom domain name for WebSocket API, add the ``websocket_api_custom_domain`` configuration option to your ``.chalice/config.json`` file. Below is an example of all the configuration options you can specify when configuring a custom domain for a WebSocket API. They are explained in the :ref:`custom-domain-ws-config-options` section of the :doc:`configfile` documentation. .. code-block:: json { "stages": { "dev": { "api_gateway_stage": "api", "websocket_api_custom_domain": { "domain_name": "api.example.com", "tls_version": "TLS_1_2|TLS_1_0", "certificate_arn": "arn:aws:acm:example", "url_prefix": "foo", "tags": { "key": "tag1", "key1": "tag2" } } } } } .. _dns-last-config: DNS Configuration ----------------- Chalice only configures your API Gateway API with the necessary resources and configuration so a custom domain can be used. It does not alter any existing DNS records you have associated with your domain name. After you've deployed your Chalice app with the configuration options described above, you'll need to modify your DNS records to point to your API Gateway API using the web interface or API of your domain registrar associated with your domain name. When you run ``chalice deploy`` with a custom domain configured, there will be two new fields in the output:: $ chalice deploy Creating deployment package. Updating policy for IAM role: customdomain-dev Updating lambda function: customdomain-dev Updating rest API Creating custom domain name: api.chalice-demo-app.com Creating api mapping: / Resources deployed: - Lambda ARN: arn:aws:lambda:us-west-2:0123456789:function:customdomain-dev - Rest API URL: https://qxea58abcd.execute-api.us-west-2.amazonaws.com/api/ - Custom domain name: HostedZoneId: Z1UJRXOUMOOFQ8 AliasDomainName: d-6vj4cynstd.execute-api.us-west-2.amazonaws.com If you're using Route53 to manage your hosted zone, you'll need to create an Alias record using the ``HostedZoneId`` and ``AliasDomainName`` specified in the output of ``chalice deploy``. If you're using a third party domain registrar, you'll need to create a CNAME record to the ``AliasDomainName``. If you'd like a step-by-step example of how to do this with Route53, see the :doc:`../tutorials/customdomain` tutorial. ================================================ FILE: docs/source/topics/events.rst ================================================ ==================== Lambda Event Sources ==================== .. _scheduled-events: Scheduled Events ================ Chalice has support for `scheduled events`_. This feature allows you to periodically invoke a lambda function based on some regular schedule. You can specify a fixed rate or a cron expression. To create a scheduled event in chalice, you use the ``@app.schedule()`` decorator. Let's look at an example. .. code-block:: python app = chalice.Chalice(app_name='foo') @app.schedule('rate(1 hour)') def every_hour(event): print(event.to_dict()) In this example, we have a single lambda function that we want automatically invoked every hour. When you run ``chalice deploy`` Chalice will create a lambda function as well as the necessary CloudWatch events/rules such that the ``every_hour`` function is invoked every hour. The :meth:`Chalice.schedule` method accepts either a string or an instance of :class:`Rate` or :class:`Cron`. For example: .. code-block:: python app = chalice.Chalice(app_name='foo') @app.schedule(Rate(1, unit=Rate.HOURS)) def every_hour(event): print(event.to_dict()) The function you decorate must accept a single argument, which will be of type :class:`CloudWatchEvent`. You can use the ``schedule()`` decorator multiple times in your chalice app. Each ``schedule()`` decorator will result in a new lambda function and associated CloudWatch event rule. For example: .. code-block:: python app = chalice.Chalice(app_name='foo') @app.schedule(Rate(1, unit=Rate.HOURS)) def every_hour(event): print(event.to_dict()) @app.schedule(Rate(2, unit=Rate.HOURS)) def every_two_hours(event): print(event.to_dict()) In the app above, chalice will create two lambda functions, and configure ``every_hour`` to be invoked once an hour, and ``every_two_hours`` to be invoked once every two hours. .. _cwe-events: CloudWatch Events ================== You can configure a lambda function to subscribe to any `CloudWatch Event `__. To subscribe to a CloudWatch Event in chalice, you use the ``@app.on_cw_event()`` decorator. Let's look at an example. .. code-block:: python app = chalice.Chalice(app_name='foo') @app.on_cw_event({"source": ["aws.codecommit"]}) def on_code_commit_changes(event): print(event.to_dict()) In this example, we have a single lambda function that we subscribe to all events from the AWS Code Commit service. The first parameter to the decorator is the event pattern that will be used to filter the events sent to the function. See the `CloudWatch Event pattern docs `__ for additional syntax and examples. The function you decorate must accept a single argument, which will be of type :class:`CloudWatchEvent`. .. _s3-events: S3 Events ========= You can configure a lambda function to be invoked whenever certain events happen in an S3 bucket. This uses the `event notifications`_ feature provided by Amazon S3. To configure this, you just tell Chalice the name of an existing S3 bucket, along with what events should trigger the lambda function. This is done with the :meth:`Chalice.on_s3_event` decorator. Here's an example: .. code-block:: python from chalice import Chalice app = chalice.Chalice(app_name='s3eventdemo') app.debug = True @app.on_s3_event(bucket='mybucket-name', events=['s3:ObjectCreated:*']) def handle_s3_event(event): app.log.debug("Received event for bucket: %s, key: %s", event.bucket, event.key) In this example above, Chalice connects the S3 bucket to the ``handle_s3_event`` Lambda function such that whenever an object is uploaded to the ``mybucket-name`` bucket, the Lambda function will be invoked. This example also uses the ``.bucket`` and ``.key`` attributes from the ``event`` parameter, which is of type :class:`S3Event`. It will automatically create the appropriate S3 notification configuration as needed. Chalice will also leave any existing notification configuration on the ``mybucket-name`` untouched. It will only merge in the additional configuration needed for the ``handle_s3_event`` Lambda function. .. warning:: This feature only works when using `chalice deploy`. Because you configure the lambda function with the name of an existing S3 bucket, it is not possible to describe this using a CloudFormation/SAM template. The ``chalice package`` command will fail. You will eventually be able to request that chalice create a bucket for you, which will support the ``chalice package`` command. The function you decorate must accept a single argument, which will be of type :class:`S3Event`. .. _sns-events: SNS Events ========== You can configure a lambda function to be automatically invoked whenever something publishes to an SNS topic. Chalice will automatically handle creating the lambda function, subscribing the lambda function to the SNS topic, and modifying the lambda function policy to allow SNS to invoke the function. To configure this, you just need the name of an existing SNS topic you'd like to subscribe to. The SNS topic must already exist. Below is an example of how to set this up. The example uses boto3 to create the SNS topic. If you don't have boto3 installed in your virtual environment, be sure to install it with:: $ pip install boto3 First, we'll create an SNS topic using boto3. :: $ python >>> import boto3 >>> sns = boto3.client('sns') >>> sns.create_topic(Name='my-demo-topic') {'TopicArn': 'arn:aws:sns:us-west-2:12345:my-demo-topic', 'ResponseMetadata': {}} Next, we'll create our chalice app:: $ chalice new-project chalice-demo-sns $ cd chalice-demo-sns/ We'll update the ``app.py`` file to use the ``on_sns_message`` decorator: .. code-block:: python from chalice import Chalice app = Chalice(app_name='chalice-sns-demo') app.debug = True @app.on_sns_message(topic='my-demo-topic') def handle_sns_message(event): app.log.debug("Received message with subject: %s, message: %s", event.subject, event.message) We can now deploy our chalice app:: $ chalice deploy Creating deployment package. Creating IAM role: chalice-demo-sns-dev Creating lambda function: chalice-demo-sns-dev-handle_sns_message Subscribing chalice-demo-sns-dev-handle_sns_message to SNS topic my-demo-topic Resources deployed: - Lambda ARN: arn:aws:lambda:us-west-2:123:function:... And now we can test our app by publishing a few SNS messages to our topic. We'll do this using boto3. In the example below, we're using ``list_topics()`` to find the ARN associated with our topic name before calling the ``publish()`` method. :: $ python >>> import boto3 >>> sns = boto3.client('sns') >>> topic_arn = [t['TopicArn'] for t in sns.list_topics()['Topics'] ... if t['TopicArn'].endswith(':my-demo-topic')][0] >>> sns.publish(Message='TestMessage1', Subject='TestSubject1', ... TopicArn=topic_arn) {'MessageId': '12345', 'ResponseMetadata': {}} >>> sns.publish(Message='TestMessage2', Subject='TestSubject2', ... TopicArn=topic_arn) {'MessageId': '54321', 'ResponseMetadata': {}} To verify our function was called correctly, we can use the ``chalice logs`` command:: $ chalice logs -n handle_sns_message 2018-06-28 17:49:30.513000 547e0f chalice-demo-sns - DEBUG - Received message with subject: TestSubject1, message: TestMessage1 2018-06-28 17:49:40.391000 547e0f chalice-demo-sns - DEBUG - Received message with subject: TestSubject2, message: TestMessage2 In this example we used the SNS topic name to register our handler, but you can also use the topic arn. This can be useful if your topic is in another region or account. .. _sqs-events: SQS Events ========== You can configure a lambda function to be invoked whenever messages are available on an SQS queue. To configure this, use the :meth:`Chalice.on_sqs_message` decorator and provide the name of the SQS queue and an optional batch size. The message visibility timeout of your SQS queue must be greater than or equal to the lambda timeout. The default message visibility timeout when you create an SQS queue is 30 seconds, and the default timeout for a Lambda function is 60 seconds, so you'll need to modify one of these values in order to successfully connect an SQS queue to a Lambda function. You can check the visibility timeout of your queue using the ``GetQueueAttributes`` API call. Using the `AWS CLI `__, you can run this command to check the value:: $ aws sqs get-queue-attributes \ --queue-url https://us-west-2.queue.amazonaws.com/1/testq \ --attribute-names VisibilityTimeout { "Attributes": { "VisibilityTimeout": "30" } } You can set the visibility timeout of your SQS queue using the ``SetQueueAttributes`` API call. Again using the AWS CLI you can run this command:: $ aws sqs set-queue-attributes \ --queue-url https://us-west-2.queue.amazonaws.com/1/testq \ --attributes VisibilityTimeout=60 If you would prefer to change the timeout of your lambda function instead, you can specify this timeout value using the ``lambda_timeout`` config key if your ``.chalice/config.json`` file. See :ref:`lambda-config` for a list of all supported lambda configuration values in chalice. In this example below, we're setting the timeout of our ``handle_sqs_message`` lambda function to 30 seconds:: $ cat .chalice/config.json { "stages": { "dev": { "lambda_functions": { "handle_sqs_message": { "lambda_timeout": 30 } } } }, "version": "2.0", "app_name": "chalice-sqs-demo" } In this example below, we're connecting the ``handle_sqs_message`` lambda function to the ``my-queue`` SQS queue. Note that we are specifying the queue name, not the queue URL or queue ARN. If you are connecting your lambda function to a FIFO queue, make sure you specify the ``.fifo`` suffix, e.g. ``my-queue.fifo``. .. code-block:: python from chalice import Chalice app = chalice.Chalice(app_name='chalice-sqs-demo') app.debug = True @app.on_sqs_message(queue='my-queue', batch_size=1) def handle_sqs_message(event): for record in event: app.log.debug("Received message with contents: %s", record.body) Whenever a message is sent to the SQS queue our function will be automatically invoked. The function argument is an :class:`SQSEvent` object, and each ``record`` in the example above is of type :class:`SQSRecord`. Lambda takes care of automatically scaling your function as needed. See `Understanding Scaling Behavior`_ for more information on how Lambda scaling works. If your lambda function completes without raising an exception, then Lambda will automatically delete all the messages associated with the :class:`SQSEvent`. You don't need to manually call ``sqs.delete_message()`` in your lambda function. If your lambda function raises an exception, then Lambda won't delete any messages, and once the visibility timeout has been reached, the messages will be available again in the SQS queue. Note that if you are using a batch size of more than one, the entire batch succeeds or fails. This means that it is possible for your lambda function to see a message multiple times, even if it's successfully processed the message previously. There are a few options available to mitigate this: * Use a batch size of 1 (the default value). * Use a separate data store to check if you've already processed an SQS message. You can use services such as Amazon DynamoDB or Amazon ElastiCache. * Manually call ``sqs.delete_message()`` in your Lambda function once you've successfully processed a message. For more information on Lambda and SQS, see the `AWS documentation`_. .. _kinesis-events: Kinesis Events ============== You can configure a Lambda function to be invoked whenever messages are published to an Amazon Kinesis data stream. To configure this, use the :meth:`Chalice.on_kinesis_record` decorator and provide the name of the Kinesis stream. The :class:`KinesisEvent` that is passed in as the ``event`` argument to the event handler is also iterable. This allows you to iterate over all the records in the event. Additionally, each record has a ``.data`` attribute that is automatically base64 decoded for you. Here's an example: .. code-block:: python from chalice import Chalice app = chalice.Chalice(app_name='kinesiseventdemo') app.debug = True @app.on_kinesis_record(stream='mystream') def handle_kinesis_message(event): for record in event: # The .data attribute is automatically base64 decoded for you. app.log.debug("Received message with contents: %s", record.data) For more information on using Kinesis and Lambda, see `Using AWS Lambda with Amazon Kinesis `__. .. _dynamodb-events: DynamoDB Events =============== You can configure a Lambda function to be invoked whenever messages are published to an Amazon DynamoDB stream. To configure this, use the :meth:`Chalice.on_dynamodb_record` decorator and provide the name of the DynamoDB stream ARN. .. note:: Other event handlers such as :meth:`Chalice.on_kinesis_record`, :meth:`Chalice.on_sqs_message`, and :meth:`Chalice.on_sns_message` only require the resource name and not the full ARN. In the case of DynamoDB streams, there are auto-generated portions of the stream ARN that cannot be computed based on the resource name. This is why Chalice requires that full stream ARN when configuring a DynamoDB stream handler. The :class:`DynamoDBEvent` that is passed in as the ``event`` argument to the event handler is also iterable. This allows you to iterate over all the records in the event. Here's an example: .. code-block:: python from chalice import Chalice app = chalice.Chalice(app_name='ddb-event-demo') app.debug = True @app.on_dynamodb_record(stream_arn='arn:aws:dynamodb:.../stream/2020') def handle_ddb_message(event): for record in event: app.log.debug("New: %s", record.new_image) For more information on using Lambda and DynamoDB, see `Using AWS Lambda with Amazon DynamoDB `__. .. _event notifications: https://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html .. _AWS documentation: https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html .. _Understanding Scaling Behavior: https://docs.aws.amazon.com/lambda/latest/dg/scaling.html ================================================ FILE: docs/source/topics/experimental.rst ================================================ Experimental APIs ================= Chalice maintains backwards compatibility for all features that appear in this documentation. Any Chalice application using version 1.x will continue to work for all future versions of 1.x. We also believe that Chalice has a lot of potential for new ideas and APIs, many of which will take several iterations to get right. We may implement a new idea and need to make changes based on customer usage and feedback. This may include backwards incompatible changes all the way up to the removal of a feature. To accommodate these new features, Chalice has support for experimental APIs, which are features that are added to Chalice on a provisional basis. Because these features may include backwards incompatible changes, you must explicitly opt-in to using these features. This makes it clear that you are using an experimental feature that may change. Opting-in to Experimental APIs ------------------------------ Each experimental feature in chalice has a name associated with it. To opt-in to an experimental API, you must have the feature name to the ``experimental_feature_flags`` attribute on your ``app`` object. This attribute's type is a set of strings. .. code-block:: python from chalice import Chalice app = Chalice('myapp') app.experimental_feature_flags.update([ 'MYFEATURE1', 'MYFEATURE2', ]) If you use an experimental API without opting-in, you will receive a message whenever you run a Chalice CLI command. The error message tells you which feature flags you need to add:: $ chalice deploy You are using experimental features without explicitly opting in. Experimental features do not guarantee backwards compatibility and may be removed in the future. If you still like to use these experimental features, you can opt-in by adding this to your app.py file: app.experimental_feature_flags.update([ 'FEATURE_FLAG_NAME' ]) See https://aws.github.io/chalice/topics/experimental.rst for more details. The feature flag only happens when running CLI commands. There are no runtime checks for experimental features once your application is deployed. List of Experimental APIs ------------------------- In the table below, the "Feature Flag Name" column is the value you must add to the ``app.experimental_feature_flags`` attribute. The status of an experimental API can be: * ``Trial`` - You must explicitly opt-in to use this feature. * ``Accepted`` - This feature has graduated from an experimental feature to a fully supported, backwards compatible feature in Chalice. Accepted features still appear in the table for auditing purposes. * ``Rejected`` - This feature has been removed. .. list-table:: Experimental APIs :header-rows: 1 * - Feature - Feature Flag Name - Version Added - Version Finalized - Status - GitHub Issue(s) * - :doc:`blueprints` - ``BLUEPRINTS`` - 1.7.0 - 1.15.0 - Accepted - `#1023 `__, `#651 `__ * - :doc:`websockets` - ``WEBSOCKETS`` - 1.10.0 - n/a - Trial - `#1041 `__, `#1017 `__ See the `original discussion `__ for more background information and alternative proposals. ================================================ FILE: docs/source/topics/index.rst ================================================ Topics ====== .. toctree:: :maxdepth: 2 routing views configfile multifile logging sdks stages packaging pyversion cfn tf authorizers events purelambda blueprints websockets cd domainname experimental testing middleware ================================================ FILE: docs/source/topics/logging.rst ================================================ Logging ======= You have several options for logging in your application. You can use any of the options available to lambda functions as outlined in the `AWS Lambda Docs `_. The simplest option is to just use print statements. Anything you print will be accessible in cloudwatch logs as well as in the output of the ``chalice logs`` command. In addition to using the stdlib ``logging`` module directly, the framework offers a preconfigured logger designed to work nicely with Lambda. This is offered purely as a convenience, you can use ``print`` or the ``logging`` module directly if you prefer. You can access this logger via the ``app.log`` attribute, which is a logger specifically for your application. This attribute is an instance of ``logging.getLogger(your_app_name_)`` that's been preconfigured with reasonable defaults: * StreamHandler associated with ``sys.stdout``. * Log level set to ``logging.ERROR`` by default. You can also manually set the logging level by setting ``app.log.setLevel(logging.DEBUG)``. * A logging formatter that displays the app name, level name, and message. Examples -------- In the following application, we're using the application logger to emit two log messages, one at ``DEBUG`` and one at the ``ERROR`` level: .. code-block:: python from chalice import Chalice app = Chalice(app_name='demolog') @app.route('/') def index(): app.log.debug("This is a debug statement") app.log.error("This is an error statement") return {'hello': 'world'} If we make a request to this endpoint, and then look at ``chalice logs`` we'll see the following log message:: 2016-11-06 20:24:25.490000 9d2a92 demolog - ERROR - This is an error statement As you can see, only the ``ERROR`` level log is emitted because the default log level is ``ERROR``. Also note the log message formatting. This is the default format that's been automatically configured. We can make a change to set our log level to debug: .. code-block:: python import logging from chalice import Chalice app = Chalice(app_name='demolog') # Enable DEBUG logs. app.log.setLevel(logging.DEBUG) @app.route('/') def index(): app.log.debug("This is a debug statement") app.log.error("This is an error statement") return {'hello': 'world'} Now if we make a request to the ``/`` URL and look at the output of ``chalice logs``, we'll see the following log message:: 2016-11-07 12:29:15.714 431786 demolog - DEBUG - This is a debug statement 2016-11-07 12:29:15.714 431786 demolog - ERROR - This is an error statement As you can see here, both the debug and error log message are shown. You can use the ``-n/--name`` option to view the logs for a specific lambda function. By default, the logs for the API handler lambda function are shown. This corresponds to any log statements made within an ``@app.route()`` call. The name option is the logical name of the lambda function. This is the name of the python function by default, or whatever name you provided as the ``name`` kwarg to the ``@app.lambda_function()`` call. For example, given this app: .. code-block:: python from chalice import Chalice app = Chalice(app_name='multilog') @app.lambda_function() def foo(event, context): app.log.debug("Invoking from function foo") return {'hello': 'world'} @app.lambda_function(name='MyFunction) def bar(event, context): incr_counter() app.log.debug("Invoking from function bar") return {'hello': 'world'} You can retrieve logs for the above function by running:: $ chalice logs --name foo $ chalice logs --name MyFunction ================================================ FILE: docs/source/topics/middleware.rst ================================================ ========== Middleware ========== Chalice provides numerous features and capabilities right out of the box, but there are often times where you'll want to customize the behavior of Chalice for your specific needs. You can accomplish this by using middleware, which lets you alter the request and response lifecycle. Chalice middleware is a function that you register as part of your application that will automatically be invoked by Chalice whenever your Lambda functions are called. Below is an example of Chalice middleware: .. code-block:: python from chalice import Chalice app = Chalice(app_name='demo-middleware') @app.middleware('all') def my_middleware(event, get_response): app.log.info("Before calling my main Lambda function.") response = get_response(event) app.log.info("After calling my main Lambda function.") return response @app.route('/') def index(): return {'hello': 'world'} @app.on_sns_message('mytopic') def sns_handler(event): pass In this example, our middleware is emitting a log message before and after our Lambda function has been invoked. Because we specified an event type of ``all``, the ``my_middleware`` function will be called when either our REST API's ``index()`` or our ``sns_handler()`` Lambda function is invoked. Writing Middleware ================== Middleware must adhere to these requirements: * Must be a callable object that accepts two parameters, an ``event``, and a ``get_response`` function. The ``event`` type will depend on what type of handlers the middleware has been registered for (see "Registering Middleware" below). * Must return a response. This will be the response that gets returned back to the caller. * In order to invoke the next middleware in the chain and eventually call the actual Lambda handler, it must invoke ``get_response(event)``. * Middleware can short-circuit the request by returning its own response. It does not have to invoke ``get_response(event)`` if not needed. The response type should match the response type of the underlying Lambda handler. Below is the simplest middleware in Chalice that does nothing: .. code-block:: python @app.middleware('all') def noop_middleware(event, get_response): # The `event` type will depend on what type of # Lambda handler is being invoked. return get_response(event) Error Handling -------------- With the exception of middleware for REST APIs, all middleware follow the same error handling strategy. Any exceptions from a Lambda handler are propagated back to each middleware. You can then catch these exceptions in your middleware and process them as needed. For example: .. code-block:: python @app.middleware('all') def handle_errors(event, get_response): try: return get_response(event) except MyCustomError as e: # We don't want MyCustomError to propagate, instead # we'll convert this to an error response dictionary. return {"Error": e.__class__.__name__, "Message": str(e)} @app.lambda_function() def noop_middleware(event, context): raise MyCustomError("Raising an error.") If an exception is raised in a Lambda handler and no middleware catches the exception, the exception will be returned back to the client that invoked the Lambda function. Rest APIs ~~~~~~~~~ Rest APIs have special error processing for backwards compatibility purposes. If a chalice view function (decorated via ``@app.route``) raises an exception Chalice will automatically catch this exception and convert to a ``Response`` object with an appropriately set status code (see :ref:`view-error-handling`). As a result, middleware for Rest APIs won't see exceptions propagate, they will instead see a `Response` object as a result of calling ``get_response(event)``. In the case where you want to allow an exception to propagate out of a view function, you can raise a ``chalice.ChaliceUnhandledError`` exception. For example: .. code-block:: python from chalice import ChaliceUnhandledError @app.middleware('all') def handle_errors(event, get_response): try: return get_response(event) except ChaliceUnhandledError as e: return Response(status_code=500, body=str(e), headers={'Content-Type': 'text/plain'}) @app.route('/') def index(): # The handle_errors middleware will never see this exception. # This will automatically be converted to a ``Response`` object # with a status code of ``500``. raise MyCustomError("Raising an error.") @app.route('/error') def unhandled_error(): # The handle_errors middleware will see this exception because it's # of type ChaliceUnhandledError. raise ChaliceUnhandledError("Raising an error.") This is useful if you want to have middleware that applies to all event types that has consistent error handling behavior. If a ``chalice.ChaliceUnhandledError`` error is raised and no middleware catches and processes this error, then the standard error processing behavior will apply (a 500 response is returned back to the user, and if debug mode is enabled, the traceback is sent as the response body). Registering Middleware ---------------------- In order to register middleware, you use the ``@app.middleware()`` decorator. This function accepts a single arg that specifies what type of Lambda function it wants to be registered for. This allows you to apply middleware to only specific type of event handlers, e.g. only for REST APIs, or Websockets, or S3 event handlers. To register middleware for all Lambda functions, you can specify ``all``. Below are the supported event types along with the corresponding type of event that will be provided to the middleware: * ``all`` - ``Any`` * ``s3`` - :class:`S3Event` * ``sns`` - :class:`SNSEvent` * ``sqs`` - :class:`SQSEvent` * ``cloudwatch`` - :class:`CloudWatchEvent` * ``scheduled`` - :class:`CloudWatchEvent` * ``websocket`` - :class:`WebsocketEvent` * ``http`` - :class:`Request` * ``pure_lambda`` - :class:`LambdaFunctionEvent` .. note:: The ``chalice.LambdaFunctionEvent`` is the only case where the event type for the middleware does not match the event type of the corresponding Lambda handler. For backwards compatibility reasons, the existing signature of the ``@app.lambda_function()`` decorator is preserved (it accepts an ``event`` and ``context``) whereas for middleware, a consistent signature is needed, which is why the ``chalice.LambdaFunctionEvent`` is used. You can also use the :meth:`Chalice.register_middleware` method, which has the same behavior as :meth:`Chalice.middleware` except you provide the middleware function as an argument instead of decorating a function. This is useful when you want to import third party functions and use them as middleware. .. code-block:: python import thirdparty app.register_middleware(thirdparty.func, 'all') You can also use the :class:`ConvertToMiddleware` class to convert an existing Lambda wrapper to middleware. For example, if you had the following logging decorator: .. code-block:: python def log_invocation(func): def wrapper(event, context): logger.debug("Before lambda function.") response = func(event, context) logger.debug("After lambda function.") return wrapper @app.lambda_function() @log_invocation def myfunction(event, context): logger.debug("In myfunction().") Rather than decorate every Lambda function with the ``@log_invocation`` decorator, you can instead use ``ConvertToMiddleware`` to automatically apply this wrapper to every Lambda function in your app. .. code-block:: python from chalice import ConvertToMiddleware app.register_middleware(ConvertToMiddleware(log_invoation)) This is also useful to integrate with existing libraries that provide Lambda wrappers. See :ref:`powertools-example` for a more complete example. Examples ======== Below are some examples of common middleware patterns. Short Circuiting a Request -------------------------- In this example, we want to return a 400 bad response if a specific header is missing from a request. Because this is HTTP specific, we only want to register this handler for our ``http`` event type. .. code-block:: python from chalice import Response @app.middleware('http') def require_header(event, get_response): # From the list above, because this is an ``http`` event # type, we know that event will be of type ``chalice.Request``. if 'X-Custom-Header' not in event.headers: return Response( status_code=400, body={"Error": "Missing required 'X-Custom-Header'"}) # If the header exists then we'll defer to our normal request flow. return get_response(event) Modifying a Response -------------------- In this example, we want to measure the processing time and inject it as a key in our Lambda response. .. code-block:: python import time @app.middleware('pure_lambda') def inject_time(event, get_response): start = time.time() response = get_response(event) total = time.time() - start response.setdefault('metadata', {})['duration'] = total return response .. _powertools-example: Integrating with AWS Lambda Powertools -------------------------------------- `AWS Lambda Powertools `__ is a suite of utilities for AWS Lambda functions that makes tracing with AWS X-Ray, structured logging and creating custom metrics asynchronously easier. You can use Chalice middleware to easily integrate Lambda Powertools with your Chalice apps. In this example, we'll use the `Logger `__ and `Tracer `__ and convert them to Chalice middleware so they will be automatically applied to all Lambda functions in our application. .. code-block:: python from chalice import Chalice from chalice.app import ConvertToMiddleware # First, instead of using Chalice's built in logger, we'll instead use # the structured logger from powertools. In addition to automatically # injecting lambda context, let's say we also want to inject which # route is being invoked. from aws_lambda_powertools import Logger from aws_lambda_powertools import Tracer app = Chalice(app_name='chalice-powertools') logger = Logger(service=app.app_name) tracer = Tracer(service=app.app_name) # This will automatically convert any decorator on a lambda function # into middleware that will be connected to every lambda function # in our app. This lets us avoid decoratoring every lambda function # with this behavior, but it also works in cases where we don't control # the code (e.g. registering blueprints). app.register_middleware(ConvertToMiddleware(logger.inject_lambda_context)) app.register_middleware( ConvertToMiddleware( tracer.capture_lambda_handler(capture_response=False)) ) # Here we're writing Chalice specific middleware where for any HTTP # APIs, we want to add the request path to our structured log message. # This shows how we can combine both Chalice-style middleware with # other existing tools. @app.middleware('http') def inject_route_info(event, get_response): logger.structure_logs(append=True, request_path=event.path) return get_response(event) @app.route('/') def index(): logger.info("In index() function, this will have a 'path' key.") return {'hello': 'world'} @app.route('/foo/bar') def foobar(): logger.info("In foobar() function") return {'foo': 'bar'} @app.lambda_function() def myfunction(event, context): logger.info("In myfunction().") tracer.put_annotation(key="Status", value="SUCCESS") return {} For a more detailed walkthrough of configuring Chalice with Lambda Powertools, see `Following serverless best practices with AWS Chalice and Lambda Powertools `__. ================================================ FILE: docs/source/topics/multifile.rst ================================================ Multifile Support ================= The ``app.py`` file contains all of your view functions and route information, but you don't have to keep all of your application code in your ``app.py`` file. As your application grows, you may reach out a point where you'd prefer to structure your application in multiple files. You can create a ``chalicelib/`` directory, and anything in that directory is recursively included in the deployment package. This means that you can have files besides just ``.py`` files in ``chalicelib/``, including ``.json`` files for config, or any kind of binary assets. Let's take a look at a few examples. Consider the following app directory structure layout:: . ├── app.py ├── chalicelib │   └── __init__.py └── requirements.txt Where ``chalicelib/__init__.py`` contains: .. code-block:: python MESSAGE = 'world' and the ``app.py`` file contains: .. code-block:: python :linenos: :emphasize-lines: 2 from chalice import Chalice from chalicelib import MESSAGE app = Chalice(app_name="multifile") @app.route("/") def index(): return {"hello": MESSAGE} Note in line 2 we're importing the ``MESSAGE`` variable from the ``chalicelib`` package, which is a top level directory in our project. We've created a ``chalicelib/__init__.py`` file which turns the ``chalicelib`` directory into a python package. We can also use this directory to store config data. Consider this app structure layout:: . ├── app.py ├── chalicelib │   └── config.json └── requirements.txt With ``chalicelib/config.json`` containing:: {"message": "world"} In our ``app.py`` code, we can load and use our config file: .. code-block:: python :linenos: import os import json from chalice import Chalice app = Chalice(app_name="multifile") filename = os.path.join( os.path.dirname(__file__), 'chalicelib', 'config.json') with open(filename) as f: config = json.load(f) @app.route("/") def index(): # We can access ``config`` here if we want. return {"hello": config['message']} ================================================ FILE: docs/source/topics/packaging.rst ================================================ App Packaging ============= In order to deploy your Chalice app, a zip file is created that contains your application and all third party packages your application requires. This file is used by AWS Lambda and is referred to as a deployment package. Chalice will automatically create this deployment package for you, and offers several features to make this easier to manage. Chalice allows you to clearly separate application specific modules and packages you are writing from 3rd party package dependencies. By default, Chalice will create a single zip file containing everything necessary to deploy your application to Lambda. Chalice also has the ability to split your code into multiple files to leverage `AWS Lambda layers `__. This is discussed in the :ref:`package-3rd-party` section below. App Directories --------------- You have two options to structure application specific code/config: * **app.py** - This file includes all your route information and is always included in the deployment package. * **chalicelib/** - This directory (if it exists) is included in the deployment package. This is where you can add config files and additional application modules if you prefer not to have all your app code in the ``app.py`` file. See :doc:`multifile` for more info on the ``chalicelib/`` directory. Both the ``app.py`` and the ``chalicelib/`` directory are intended for code that you write yourself. .. _package-3rd-party: 3rd Party Packages ------------------ When handling third party packages, you can have Chalice manage these files as part of the deployment package of your Lambda function, or as a separate Lambda layer that's shared by all your Lambda functions. See the :ref:`package-examples` section for examples of how this works. There are two options for handling python package dependencies: * **requirements.txt** - During the packaging process, Chalice will install any packages it finds or can build compatible wheels for. Specifically all pure python packages as well as all packages that upload wheel files for the ``manylinux1_x86_64`` platform will be automatically installable. * **vendor/** - The *contents* of this directory are automatically added to your deployment package and its location in your Lambda functions will depend on whether you are using automatic Lambda Layers, described in the :ref:`package-auto-layers` section. Chalice will also check for an optional ``vendor/`` directory in the project root directory. The contents of this directory are automatically included in the top level of the deployment package (see :ref:`package-examples` for specific examples). The ``vendor/`` directory is helpful in these scenarios: * You need to include custom packages or binary content that is not accessible via ``pip``. These may be internal packages that aren't public. * Wheel files are not available for a package you need from pip. * A package is installable with ``requirements.txt`` but has optional c extensions. Chalice can build the dependency without the c extensions, but if you want better performance you can vendor a version that is compiled. As a general rule of thumb, code that you write goes in either ``app.py`` or ``chalicelib/``, and dependencies are either specified in ``requirements.txt`` or placed in the ``vendor/`` directory. .. _package-auto-layers: Automatic Lambda Layers ~~~~~~~~~~~~~~~~~~~~~~~ By default, Chalice will create a single zip file that contains all the code needed to run your application. You can set the :ref:`automatic-layer-option` in your ``.chalice/config.json`` file which will instruct Chalice to create your 3rd party packages as a separate Lambda layer. There are several benefits to this approach: * The layer is created once and then shared across all Lambda functions in your application. * When creating or updating a Lambda function, you send the entire contents of the zip file that contains your app. This is repeated for each Lambda function. As a result, there is unnecessary time and network bandwidth used to send the same 3rd party dependencies for each Lambda function. When using layers, Chalice will specify the layer ARN when creating or updating your Lambda function, which cuts down on the time it takes to package and deploy your application. * Saves storage space in Lambda. There is a 75GB maximum size for all your Lambda functions. If you're not using layers, each Lambda function stores its own copies of your 3rd party dependencies. We recommend setting ``"automatic_layer": true`` in your ``.chalice/config.json`` due to these benefits. Migrating to ``"automatic_layer": true`` is mostly backwards compatible with one notable exception: the location of the vendor files is different. When not using automatic layers, any files placed in ``vendor/`` will be available in your CWD of your application. However, when using layers, these files will be unzipped to ``/opt/python/lib/pythonX.Y/site-packages``. If you are using the ``vendor/`` directory to include custom built python packages then this change is transparent as that directory is automatically added to the python path. However, if you are trying to read a file from ``vendor/`` directly, then this will no longer work. For example, if you have:: . ├── app.py └── vendor └── myimage.png And your ``app.py`` attempts to read this file: .. code-block:: python @app.lambda_function() def handler(event, context): with open('myimage.png') as f: do_something(f) This code will no longer work. You have two options. You can either place static assets in ``chalicelib/`` or you'll have to check both directories for your file:: '/opt/python/lib/python%s.%s/site-packages' % sys.version_info[:2] .. code-block:: python @app.lambda_function() def handler(event, context): with open_vendor_file('myimage.png') as f: do_something(f) def open_vendor_file(filename): directories = [ '.', '/opt/python/lib/python%s.%s/site-packages' % sys.version_info[:2] ] for dirname in directories: full_path = os.path.join(dirname, filename) if os.path.isfile(full_path): return open(full_path) Environment Variables --------------------- As part of the packaging and deployment process, Chalice will import your ``app.py`` file. This will result in any top level module code being executed. This can sometimes have undesireable behavior. When running any Chalice CLI commands, a ``AWS_CHALICE_CLI_MODE`` environment variable is set. You can check if this env var is set in your ``app.py`` if you have code that you don't want to run whenever your app is packaged and deployed. .. code-block:: python import os app = Chalice(app_name='testimport') expensive_connection = None if 'AWS_CHALICE_CLI_MODE' not in os.environ: # We're running in Lambda, we want to start up # our connection to our DB. expensive_connection = ConnectToDB() Chalice will also set any environment variables specified in your global or stage specific configuration whenever your app is packaged and deployed. Per-Lambda function environment variables are not set when importing your app (this would require importing your application for each Lambda function). For example, given the config below you would be able to access the ``STAGE_VAR`` environment variable but not the ``PER_FUNCTION`` variable during the building/packaging process when Chalice imports your application. This can be useful if you want to move configuration or resource names out of your app.py file. :: { "stages": { "dev": { "environment_variables": { "STAGE_VAR": "stage-var" } "api_gateway_stage": "api", "lambda_functions": { "foo": { "environment_variables": {"PER_FUNCTION": "per-function"} } } } }, "version": "2.0", "app_name": "demo" } This only applies to the packaging stage. When the ``foo`` function is invoked on Lambda, the ``PER_FUNCTION`` environment variable will be set as expected. .. _package-examples: Examples -------- Suppose I have the following app structure:: . ├── app.py ├── chalicelib │   ├── __init__.py │   └── utils.py ├── requirements.txt └── vendor   ├── myimage.png └── internalpackage └── __init__.py And the ``requirements.txt`` file had one requirement:: $ cat requirements.txt sortedcontainers==1.5.4 With the default behavior of not using layers (``"automatic_layer": false``), the final deployment package directory structure would look like this:: deployment.zip . ├── app.py ├── chalicelib │   ├── __init__.py │   └── utils.py ├── myimage.png ├── internalpackage │   └── __init__.py └── sortedcontainers └── __init__.py This directory structure is then zipped up and sent to AWS Lambda during the deployment process. Suppose our application had two Lambda functions. Each Lambda function has its own copy of the application deployment package, as shown in the architecture diagram below. .. image:: ../img/no-auto-layer.png :width: 50% :align: center :alt: Default behavior with no layers. If you are using ``"automatic_layer": true``, then two zip files will be created. The deployment package used for the Lambda function will be:: deployment.zip . ├── app.py └── chalicelib    ├── __init__.py    └── utils.py And the zip file for the shared lambda layer will look like this:: layer-deployment.zip . └── python └── lib └── python3.7 └── site-packages ├── myimage.png ├── internalpackage │   └── __init__.py └── sortedcontainers └── __init__.py Below is an updated diagram of the same Chalice application using automatic layers. Both functions now share the same Lambda layer that contains the third party packages used by the application. .. image:: ../img/auto-layer.png :width: 80% :align: center :alt: Shared layer for 3rd party code. Cryptography Example -------------------- .. note:: Since the original version of this example was written, cryptography has released version 2.0 which does have manylinux1 wheels available. This means if you want to use cryptography in a Chalice app all you need to do is add ``cryptography`` or ``cryptography>=2.0`` in your requirements.txt file. This example will use version 1.9 of Cryptography because it is a good example of a library with C extensions and no wheel files. Below shows an example of how to use the `cryptography 1.9 `__ package in a Chalice app for the ``python3.6`` lambda environment. Suppose you are on a Mac or Windows and want to deploy a Chalice app that depends on the ``cryptography==1.9`` package. If you simply add it to your ``requirements.txt`` file and try to deploy it with ``chalice deploy`` you will get the following warning during deployment:: $ cat requirements.txt cryptography==1.9 $ chalice deploy Updating IAM policy. Updating lambda function... Creating deployment package. Could not install dependencies: cryptography==1.9 You will have to build these yourself and vendor them in the chalice vendor folder. Your deployment will continue but may not work correctly if missing dependencies are not present. For more information: http://aws.github.io/chalice/topics/packaging.html This happened because the ``cryptography`` version 1.9 does not have wheel files available on PyPi, and has C extensions. Since we are not on the same platform as AWS Lambda, the compiled C extensions Chalice built were not compatible. To get around this we are going to leverage the ``vendor/`` directory, and build the ``cryptography`` package on a compatible linux system. You can do this yourself by building ``cryptography`` on an Amazon Linux instance running in EC2. All of the following commands were run inside a ``python 3.6`` virtual environment. * Download the source first:: $ pip download cryptography==1.9 This will download all the requirements into the current working directory. The directory should have the following contents: * ``asn1crypto-0.22.0-py2.py3-none-any.whl`` * ``cffi-1.10.0-cp36-cp36m-manylinux1_x86_64.whl`` * ``cryptography-1.9.tar.gz`` * ``idna-2.5-py2.py3-none-any.whl`` * ``pycparser-2.17.tar.gz`` * ``six-1.10.0-py2.py3-none-any.whl`` This is a complete set of dependencies required for the cryptography package. Most of these packages have wheels that were downloaded, which means they can simply be put in the ``requirements.txt`` and Chalice will take care of downloading them. That leaves ``cryptography`` itself and ``pycparser`` as the only two that did not have a wheel file available for download. * Next build the ``cryptography`` source package into a wheel file:: $ pip wheel cryptography-1.9.tar.gz This will take a few seconds and build a wheel file for both ``cryptography`` and ``pycparser``. The directory should now have two additional wheel files: * ``cryptography-1.9-cp36-cp36m-linux_x86_64.whl`` * ``pycparser-2.17-py2.py3-none-any.whl`` The ``cryptography`` wheel file has been built with a compatible architecture for Lambda (``linux_x86_64``) and the ``pycparser`` has been built for ``any`` architecture which means it can also be automatically packaged by Chalice if it is listed in the ``requirements.txt`` file. * Download the ``cryptography`` wheel file from the Amazon Linux instance and unzip it into the ``vendor/`` directory in the root directory of your Chalice app. You should now have a project directory that looks like this:: $ tree . ├── app.py ├── requirements.txt └── vendor ├── cryptography │   ├── ... Lots of files │ └── cryptography-1.9.dist-info ├── DESCRIPTION.rst ├── METADATA ├── RECORD ├── WHEEL ├── entry_points.txt ├── metadata.json └── top_level.txt The ``requirements.txt`` file should look like this:: $ cat requirements.txt cffi==1.10.0 six==1.10.0 asn1crypto==0.22.0 idna==2.5 pycparser==2.17 In your ``app.py`` file you can now import ``cryptography``, and these dependencies will all get included when the ``chalice deploy`` command is run. ================================================ FILE: docs/source/topics/purelambda.rst ================================================ ===================== Pure Lambda Functions ===================== Chalice provides abstractions over AWS Lambda functions, including: * An API handler that coordinates with API Gateway for creating rest APIs. * A custom authorizer that allows you to integrate custom auth logic in your rest API. * A scheduled event that includes managing the CloudWatch Event rules, targets, and permissions. However, chalice also supports managing pure Lambda functions that don't have any abstractions built on top. This is useful if you want to create a Lambda function for something that's not supported by chalice or if you just want to create Lambda functions but don't want to manage handling dependencies and deployments yourself. In order to do this, you can use the :meth:`Chalice.lambda_function` decorator to denote that this python function is a pure lambda function that should be invoked as is, without any input or output mapping. When you use this function, you must provide a function that maps to the same function signature expected by AWS Lambda as `defined here`_. Let's look at an example. .. code-block:: python app = chalice.Chalice(app_name='foo') @app.route('/') def index(): return {'hello': 'world'} @app.lambda_function() def custom_lambda_function(event, context): # Anything you want here. return {} @app.lambda_function(name='MyFunction') def other_lambda_function(event, context): # Anything you want here. return {} In this example, we've updated the starter hello world app with two extra Lambda functions. When you run ``chalice deploy`` Chalice will create three Lambda functions. The first lambda function is for the API handler used by API gateway. The second and third lambda functions will be pure lambda functions. These two additional lambda functions won't be hooked up to anything. You'll need to manage connecting them to any additional AWS Resources on your own. .. _defined here: https://docs.aws.amazon.com/lambda/latest/dg/python-programming-model-handler-types.html ================================================ FILE: docs/source/topics/pyversion.rst ================================================ Python Version Support ====================== Chalice supports all versions of python supported by AWS Lambda, which is currently Python 3.6 and greater. You can see the list of supported python versions for Lambda in their `docs `__. Chalice will automatically pick which version of python to use for Lambda based on the major version of python you are using. You don't have to explicitly configure which version of python you want to use. For example:: $ python --version Python 3.6.1 $ chalice new-project test-versions $ cd test-versions $ chalice package test-package $ grep -C 3 python test-package/sam.json "APIHandler": { "Type": "AWS::Serverless::Function", "Properties": { "Runtime": "python3.6", "Handler": "app.app", "CodeUri": "./deployment.zip", "Events": { # Similarly, if we were to run "chalice deploy" we'd # use python3.6 for the runtime. $ chalice --debug deploy Initiating first time deployment... Deploying to: dev ... "Runtime":"python3.6" ... https://rest-api-id.execute-api.us-west-2.amazonaws.com/api/ In the example above, we're using python 3.6.1 so chalice automatically selects the ``python3.6`` runtime for lambda. If we were using python 3.9.6, chalice would automatically select ``python3.9`` as the runtime. Chalice will emit a warning if the minor version does not match a python version supported by Lambda. Chalice will select the closest Lambda version in this scenario, as shown in the table below. We strongly encourage you to develop your application using the same major/minor version of python you plan on using on AWS Lambda. Changing Python Runtime Versions ================================ The version of the python runtime to use in AWS Lambda can be reconfigured whenever you deploy your chalice app. This allows you to migrate to newer Python versions in AWS Lambda by creating a new virtual environment that uses python3. For example, suppose you have an existing chalice app that uses Python 3.6 :: $ python --version Python 3.6.1 $ chalice deploy ... https://endpoint/api To upgrade the application to use Python 3.9, create a python3 virtual environment and redeploy. :: $ deactivate $ python3 -m venv /tmp/venv3 $ source /tmp/venv3/bin/activate $ python --version Python 3.9.6 $ chalice deploy ... ================================================ FILE: docs/source/topics/routing.rst ================================================ Routing ======= The :meth:`Chalice.route` method is used to construct which routes you want to create for your API. The concept is the same mechanism used by `Flask `__ and `bottle `__. You decorate a function with ``@app.route(...)``, and whenever a user requests that URL, the function you've decorated is called. For example, suppose you deployed this app: .. code-block:: python from chalice import Chalice app = Chalice(app_name='helloworld') @app.route('/') def index(): return {'view': 'index'} @app.route('/a') def a(): return {'view': 'a'} @app.route('/b') def b(): return {'view': 'b'} If you go to ``https://endpoint/``, the ``index()`` function would be called. If you went to ``https://endpoint/a`` and ``https://endpoint/b``, then the ``a()`` and ``b()`` function would be called, respectively. .. note:: Do not end your route paths with a trailing slash. If you do this, the ``chalice deploy`` command will raise a validation error. You can also create a route that captures part of the URL. This captured value will then be passed in as arguments to your view function: .. code-block:: python from chalice import Chalice app = Chalice(app_name='helloworld') @app.route('/users/{name}') def users(name): return {'name': name} If you then go to ``https://endpoint/users/james``, then the view function will be called as: ``users('james')``. The parameters are passed as keyword parameters based on the name as they appear in the URL. The argument names for the view function must match the name of the captured argument: .. code-block:: python from chalice import Chalice app = Chalice(app_name='helloworld') @app.route('/a/{first}/b/{second}') def users(first, second): return {'first': first, 'second': second} Other Request Metadata ---------------------- The route path can only contain ``[a-zA-Z0-9._-]`` chars and curly braces for parts of the URL you want to capture. You do not need to model other parts of the request you want to capture, including headers and query strings. Within a view function, you can introspect the current request using the :attr:`app.current_request ` attribute. This also means you cannot control the routing based on query strings or headers. Here's an example for accessing query string data in a view function: .. code-block:: python from chalice import Chalice app = Chalice(app_name='helloworld') @app.route('/users/{name}') def users(name): result = {'name': name} if app.current_request.query_params.get('include-greeting') == 'true': result['greeting'] = 'Hello, %s' % name return result In the function above, if the user provides a ``?include-greeting=true`` in the HTTP request, then an additional ``greeting`` key will be returned:: $ http https://endpoint/api/users/bob { "name": "bob" } $ http https://endpoint/api/users/bob?include-greeting=true { "greeting": "Hello, bob", "name": "bob" } ================================================ FILE: docs/source/topics/sdks.rst ================================================ SDK Generation ============== The ``@app.route(...)`` information you provide chalice allows it to create corresponding routes in API Gateway. One of the benefits of this approach is that we can leverage API Gateway's SDK generation process. Chalice offers a ``chalice generate-sdk`` command that will automatically generate an SDK based on your declared routes. .. note:: The only supported language at this time is javascript. Keep in mind that chalice itself does not have any logic for generating SDKs. The SDK generation happens service side in `API Gateway`_, the ``chalice generate-sdk`` is just a high level wrapper around that functionality. To generate an SDK for a chalice app, run this command from the project directory:: $ chalice generate-sdk /tmp/sdk You should now have a generated javascript sdk in ``/tmp/sdk``. API Gateway includes a ``README.md`` as part of its SDK generation which contains details on how to use the javascript SDK. Example ------- Suppose we have the following chalice app: .. code-block:: python from chalice import Chalice app = Chalice(app_name='sdktest') @app.route('/', cors=True) def index(): return {'hello': 'world'} @app.route('/foo', cors=True) def foo(): return {'foo': True} @app.route('/hello/{name}', cors=True) def hello_name(name): return {'hello': name} @app.route('/users/{user_id}', methods=['PUT'], cors=True) def update_user(user_id): return {"msg": "fake updated user", "userId": user_id} Let's generate a javascript SDK and test it out in the browser. Run the following command from the project dir:: $ chalice generate-sdk /tmp/sdkdemo $ cd /tmp/sdkdemo $ ls -la -rw-r--r-- 1 jamessar r 3227 Nov 21 17:06 README.md -rw-r--r-- 1 jamessar r 9243 Nov 21 17:06 apigClient.js drwxr-xr-x 6 jamessar r 204 Nov 21 17:06 lib You should now be able to follow the instructions from API Gateway in the ``README.md`` file. Below is a snippet that shows how the generated javascript SDK methods correspond to the ``@app.route()`` calls in chalice. .. code-block:: html Example HTML File ~~~~~~~~~~~~~~~~~ If you want to try out the example above, you can use the following index.html page to test: .. code-block:: html SDK Test
result of rootGet()
result of fooGet()
result of helloNameGet({name: 'jimmy'})
result of usersUserIdPut({user_id: '123'})
.. _API Gateway: https://docs.aws.amazon.com/apigateway/latest/developerguide/how-to-generate-sdk.html ================================================ FILE: docs/source/topics/stages.rst ================================================ Chalice Stages ============== Chalice has the concept of stages, which are completely separate sets of AWS resources. When you first create a chalice project and run commands such as ``chalice deploy`` and ``chalice url``, you don't have to specify any stage values or stage configuration. This is because chalice will use a stage named ``dev`` by default. You may eventually want to have multiple stages of your application. A common configuration would be to have a ``dev``, ``beta`` and ``prod`` stage. A ``dev`` stage would be used by developers to test out new features. Completed features would be deployed to ``beta``, and the ``prod`` stage would be used for serving production traffic. Chalice can help you manage this. To create a new chalice stage, specify the ``--stage`` argument. If the stage does not exist yet, it will be created for you:: $ chalice deploy --stage prod By creating a new chalice stage, a new API Gateway rest API, Lambda function, and potentially (depending on config settings) a new IAM role will be created for you. Example ------- Let's say we have a new app:: $ chalice new-project myapp $ cd myapp $ chalice deploy ... https://mmnkdi.execute-api.us-west-2.amazonaws.com/api/ We've just created our first stage, ``dev``. We can iterate on our application and continue to run ``chalice deploy`` to deploy our code to the ``dev`` stage. Let's say we want to now create a ``prod`` stage. To do this, we can run:: $ chalice deploy --stage prod ... https://wk9fhx.execute-api.us-west-2.amazonaws.com/api/ We now have two completely separate rest APIs:: $ chalice url --stage dev https://mmnkdi.execute-api.us-west-2.amazonaws.com/api/ $ chalice url --stage prod https://wk9fhx.execute-api.us-west-2.amazonaws.com/api/ Additionally, we can see all our deployed values by looking at the ``.chalice/deployed/dev.json`` or ``.chalice/deployed/prod.json`` files:: $ cat .chalice/deployed/dev.json { "resources": [ { "name": "api_handler", "resource_type": "lambda_function", "lambda_arn": "arn:aws:lambda:...:function:myapp-dev" }, { "name": "rest_api", "resource_type": "rest_api", "rest_api_id": "wk9fhx", "rest_api_url": "https://wk9fhx.execute-api.us-west-2.amazonaws.com/api/" } ], "schema_version": "2.0", "backend": "api" } $ cat .chalice/deployed/prod.json { "resources": [ { "name": "api_handler", "resource_type": "lambda_function", "lambda_arn": "arn:aws:lambda:...:function:myapp-prod" }, { "name": "rest_api", "resource_type": "rest_api", "rest_api_id": "mmnkdi", "rest_api_url": "https://mmnkdi.execute-api.us-west-2.amazonaws.com/api/" } ], "schema_version": "2.0", "backend": "api" } ================================================ FILE: docs/source/topics/testing.rst ================================================ Testing ======= Chalice provides a :ref:`test client ` in ``chalice.test`` that you can use to test your Chalice applications. This client lets you invoke Lambda function and event handlers directly, as well as test your REST APIs. .. _testing-lambda-functions: Lambda Functions ---------------- To test lambda functions, use the :meth:`Client.lambda_.invoke ` method. The test client is intended to be used as a context manager. For example, given this sample app: .. code-block:: python from chalice import Chalice app = Chalice(app_name="testclient") @app.lambda_function() def foo(event, context): return {'hello': 'world'} @app.lambda_function() def bar(event, context): return {'event': event} Here's how you can test these functions with the test client. In our example, we'll be using `pytest `__, but the Chalice test client will work with any testing framework. We'll create a new ``tests/`` directory and create a ``tests/__init__.py`` and a ``tests/test_app.py`` file. :: $ mkdir tests $ touch tests/{__init__.py,test_app.py} The ``tests/test_app.py`` file should have the following contents: .. code-block:: python from chalice.test import Client from app import app def test_foo_function(): with Client(app) as client: result = client.lambda_.invoke('foo') assert result.payload == {'hello': 'world'} def test_bar_function(): with Client(app) as client: result = client.lambda_.invoke( 'bar', {'my': 'event'}) assert result.payload == {'event': {'my': 'event'}} Now we can run our tests with ``pytest``:: $ pip install pytest $ py.test tests/test_app.py ========================= test session starts ========================== platform darwin -- Python 3.7.3, pytest-5.3.1, py-1.5.3, pluggy-0.12.0 rootdir: /tmp/testclient plugins: hypothesis-4.43.1, cov-2.8.1 collected 2 items test_app.py .. [100%] ========================= 2 passed in 0.32s ============================ .. note:: See the :ref:`testing-pytest-fixtures` section for how to use pytest fixtures with the Chalice test client. For testing Lambda functions that are connected to specific events, you can use the :attr:`Client.events` attribute to generate sample events. For example: .. code-block:: python from chalice import Chalice @app.on_sns_message(topic='mytopic') def foo(event): return {'message': event.message} # Test code from chalice.test import Client def test_sns_handler(): with Client(app) as client: response = client.lambda_.invoke( "foo", client.events.generate_sns_event(message="hello world") ) assert response.payload == {'message': 'hello world'} Environment Variables ~~~~~~~~~~~~~~~~~~~~~ The Chalice test client will also configure any environment variables you have configured with your Lambda functions in your ``.chalice/config.json`` file. For example, suppose you had these config file: .. code-block:: json { "version": "2.0", "app_name": "testenv", "stages": { "prod": { "api_gateway_stage": "api", "environment_variables": { "MY_ENV_VAR": "TOP LEVEL" }, "lambda_functions": { "bar": { "environment_variables": { "MY_ENV_VAR": "OVERRIDE" } } } } } } These sets a ``MY_ENV_VAR`` environment variable for the ``prod`` stage. The ``bar`` function overrides this environment variable with its own custom value. To test this, we need to specify the ``prod`` stage when we create our test client: .. code-block:: python from chalice import Chalice app = Chalice(app_name="testclient") @app.lambda_function() def foo(event, context): return {'value': os.environ.get('MY_ENV_VAR')} @app.lambda_function() def bar(event, context): return {'value': os.environ.get('MY_ENV_VAR')} # Test code from chalice.test import Client def test_foo_function(): with Client(app, stage_name='prod') as client: result = client.lambda_.invoke('foo') assert result.payload == {'value': 'TOP LEVEL'} def test_bar_function(): with Client(app) as client: result = client.lambda_.invoke('bar') assert result.payload == {'value': 'OVERRIDE'} REST APIs --------- You can test your REST API with the Chalice test client using the :attr:`Client.http` attribute. For example, given this REST API: .. code-block:: python from chalice import Chalice app = Chalice(app_name="testclient") @app.route('/') def index(): return {'hello': 'world'} You can test this route with: .. code-block:: python from chalice.test import Client from app import app def test_index(): with Client(app) as client: response = client.http.get('/') assert response.json_body == {'hello': 'world'} If you want to access the response body's raw bytes, you can use the ``body`` attribute: .. code-block:: python from chalice.test import Client from app import app def test_index(): with Client(app) as client: response = client.http.get('/') assert response.body == b'{"hello":"world"}' You can also test other HTTP methods by using the corresponding ``post()``, ``put()``, ``delete()``, etc. method calls. .. code-block:: python import json from chalice import Chalice app = Chalice(app_name="testclient") @app.route('/', methods=['POST']) def index() return app.current_request.json_body def test_index(): with Client(app) as client: response = client.http.post( '/myview', headers={'Content-Type':'application/json'}, body=json.dumps({'example':'json'}) ) assert response.json_body == {'example': 'json'} You can also test builtin authorizers with the test client: .. code-block:: python from chalice import Chalice app = Chalice(app_name="testclient") @app.authorizer() def myauth(event) if event.token == 'allow': return AuthResponse(['*'], principal_id='id') return AuthResponse([], principal_id='noone') @app.route('/needs-auth', authorizer=myauth) def needs_auth() return {'success': True} # Test code: from chalice.test import Client def test_needs_auth(): with Client(app) as client: response = client.http.get( '/needs-auth', headers={'Authorization': 'allow'}) assert response.json_body == {'success': True} assert client.http.get( '/needs-auth', headers={'Authorization': 'deny'}).status_code == 403 .. _testing-boto3-client-calls: Testing Boto3 Client Calls -------------------------- If your event handlers are making AWS API calls using boto3 or botocore, you can use the `botocore stubber `__ to test your API calls. For example, suppose we have an app that makes an API call to Amazon Rekognition whenever an object is uploaded to S3: .. code-block:: python import boto3 from chalice import Chalice app = Chalice(app_name='testclient') _REKOGNITION_CLIENT = None def get_rekognition_client(): global _REKOGNITION_CLIENT if _REKOGNITION_CLIENT is None: _REKOGNITION_CLIENT = boto3.client('rekognition') return _REKOGNITION_CLIENT @app.on_s3_event(bucket='mybucket', events=['s3:ObjectCreated:*']) def handle_object_created(event): client = get_rekognition_client() response = client.detect_labels( Image={ 'S3Object': { 'Bucket': event.bucket, 'Name': event.key, }, }, MinConfidence=50.0 ) labels = [label['Name'] for label in response['Labels']] # In the real app we'd now do something with these labels # (e.g. store than in a database so we can query them later). return labels To test this, we'll combine the botocore stubber and the Chalice test client: .. code-block:: python from chalice.test import Client import app from botocore.stub import Stubber def test_calls_rekognition(): client = app.get_rekognition_client() stub = Stubber(client) stub.add_response( 'detect_labels', expected_params={ 'Image': { 'S3Object': { 'Bucket': 'mybucket', 'Name': 'mykey', } }, 'MinConfidence': 50.0, }, service_response={ 'Labels': [ {'Name': 'Dog', 'Confidence': 75.0}, {'Name': 'Mountain', 'Confidence': 80.0}, {'Name': 'Snow', 'Confidence': 85.0}, ] }, ) with stub: with Client(app.app) as client: event = client.events.generate_s3_event( bucket='mybucket', key='mykey') response = client.lambda_.invoke('handle_object_created', event) assert response.payload == ['Dog', 'Mountain', 'Snow'] stub.assert_no_pending_responses() In the testcase above, we first tell the stubber what API call we're expecting, along with the parameters we'll send and the response we expect back from the Rekognition service. Next we use the ``with stub:`` line to activate our stubs. This also ensures that when our test exits that we'll deactive the stubs for this client. Now we the ``client.lambda_.invoke`` method is called, our stubbed client will return the preconfigured response data instead of making an actual API call to the Rekognition service. .. _testing-pytest-fixtures: Pytest Fixtures --------------- Both the Botocore stubber and the Chalice test client are used within a context manager. In our previous example, this resulted in multiple levels of nesting, which is required for every test we write. If you're using pytest as your test framework, you can create `test fixtures `__ to reduce the boiler plate code. Let's rewrite several of these tests to use pytest fixtures. First we'll create a test fixture for the Chalice test client: .. code-block:: python import app from pytest import fixture from chalice.test import Client @fixture def test_client(): with Client(app.app) as client: yield client Now our original tests for the ``foo`` and ``bar`` Lambda functions from the :ref:`testing-lambda-functions` section can be rewritten to use this fixture: .. code-block:: python def test_foo_function(test_client): result = test_client.lambda_.invoke('foo') assert result.payload == {'hello': 'world'} def test_bar_function(test_client): result = test_client.lambda_.invoke( 'bar', {'my': 'event'}) assert result.payload == {'event': {'my': 'event'}} We can also create a fixture for the botocore stubber. This allows us to rewrite the ``test_calls_rekognition()`` test from the :ref:`previous section ` in a more simplified manner. Below is the entire test file using both the botocore and Chalice test client fixtures: .. code-block:: python import app from pytest import fixture from chalice.test import Client @fixture def test_client(): with Client(app.app) as client: yield client @fixture def rekognition_stub(): client = app.get_rekognition_client() stub = Stubber(client) with stub: yield stub def test_calls_rekognition(test_client, rekognition_stub): rekognition_stub.add_response( 'detect_labels', expected_params={ 'Image': { 'S3Object': { 'Bucket': 'mybucket', 'Name': 'mykey', } }, 'MinConfidence': 50.0, }, service_response={ 'Labels': [ {'Name': 'Dog', 'Confidence': 75.0}, {'Name': 'Mountain', 'Confidence': 80.0}, {'Name': 'Snow', 'Confidence': 85.0}, ] }, ) event = test_client.events.generate_s3_event( bucket='mybucket', key='mykey') response = test_client.lambda_.invoke('handle_object_created', event) assert response.payload == ['Dog', 'Mountain', 'Snow'] stub.assert_no_pending_responses() Next Steps ---------- For reference documentation on the methods and attributes of the Chalice test client, see the :ref:`test client ` section in the API documentation. ================================================ FILE: docs/source/topics/tf.rst ================================================ Terraform Support ================= When you run ``chalice deploy``, chalice will deploy your application using the `AWS SDK for Python `__. Chalice also provides functionality that allows you to manage deployments yourself using terraform. This is provided via the ``chalice package --pkg-format terraform`` command. When you run this command, chalice will generate the AWS Lambda deployment package that contains your application and a `Terraform `__ configuration file. You can then use the terraform cli to deploy your chalice application. Considerations -------------- Using the ``chalice package`` command is useful when you don't want to use ``chalice deploy`` to manage your deployments. There's several reasons why you might want to do this: * You have pre-existing infrastructure and tooling set up to manage Terraform deployments. * You want to integrate with other Terraform resources to manage all your application resources, including resources outside of your chalice app. * You'd like to integrate with `AWS CodePipeline `__ to automatically deploy changes when you push to a git repo. Keep in mind that you can't switch between ``chalice deploy`` and ``chalice package`` + Terraform for deploying your app. If you choose to use ``chalice package`` and Terraform to deploy your app, you won't be able to switch back to ``chalice deploy``. Running ``chalice deploy`` would create an entirely new set of AWS resources (API Gateway Rest API, AWS Lambda function, etc). Example ------- In this example, we'll create a chalice app and deploy it using the AWS CLI. First install the necessary packages:: $ virtualenv /tmp/venv $ . /tmp/venv/bin/activate $ pip install chalice awscli $ chalice new-project test-tf-deploy $ cd test-tf-deploy At this point we've installed chalice and the AWS CLI and we have a basic app created locally. Next we'll run the ``package`` command:: $ chalice package --pkg-format terraform /tmp/packaged-app/ Creating deployment package. $ ls -la /tmp/packaged-app/ -rw-r--r-- 1 j wheel 3355270 May 25 14:20 deployment.zip -rw-r--r-- 1 j wheel 3068 May 25 14:20 chalice.tf.json $ unzip -l /tmp/packaged-app/deployment.zip | tail -n 5 17292 05-25-17 14:19 chalice/app.py 283 05-25-17 14:19 chalice/__init__.py 796 05-25-17 14:20 app.py -------- ------- 9826899 723 files As you can see in the above example, the ``package --pkg-format`` command created a directory that contained two files, a ``deployment.zip`` file, which is the Lambda deployment package, and a ``chalice.tf.json`` file, which is the Terraform template that can be deployed using Terraform. Next we're going to use the Terraform CLI to deploy our app. Note terraform will deploy run against all terraform files in this directory, so we can add additional resources for our application by adding terraform additional files here. The Chalice terraform template includes two static data values (`app` and `stage` names) that we can optionally use when constructing these additional resources, ie. `${data.null_data_source.chalice.outputs.app}` First let's run Terraform init to install the AWS Terraform Provider:: $ cd /tmp/packaged-app $ terraform init Now we can deploy our app using the ``terraform apply`` command:: $ terraform apply data.aws_region.chalice: Refreshing state... data.aws_caller_identity.chalice: Refreshing state... An execution plan has been generated and is shown below. Resource actions are indicated with the following symbols: + create ... (omit plan output) Plan: 14 to add, 0 to change, 0 to destroy. Do you want to perform these actions? Terraform will perform the actions described above. Only 'yes' will be accepted to approve. Enter a value: yes ... (omit apply output) Apply complete! Resources: 14 added, 0 changed, 0 destroyed. Outputs: EndpointURL = https://7bnxriulj5.execute-api.us-east-1.amazonaws.com/dev RestApiId = 7bnxriulj5 This will take a minute to complete, but once it's done, the endpoint url will be available as an output which we can then curl:: $ http "$(terraform output EndpointURL)" HTTP/1.1 200 OK Connection: keep-alive Content-Length: 18 Content-Type: application/json ... { "hello": "world" } ================================================ FILE: docs/source/topics/views.rst ================================================ Views ===== A view function in chalice is the function attached to an ``@app.route()`` decorator. In the example below, ``index`` is the view function: .. code-block:: python from chalice import Chalice app = Chalice(app_name='helloworld') @app.route('/') def index(): return {'view': 'index'} View Function Parameters ------------------------ A view function's parameters correspond to the number of captured URL parameters specified in the ``@app.route`` call. In the example above, the route ``/`` specifies no captured parameters so the ``index`` view function accepts no parameters. However, in the view function below, a single URL parameter, ``{city}`` is specified, so the view function must accept a single parameter: .. code-block:: python from chalice import Chalice app = Chalice(app_name='helloworld') @app.route('/cities/{city}') def index(city): return {'city': city} This indicates that the value of ``{city}`` is variable, and whatever value is provided in the URL is passed to the ``index`` view function. For example:: GET /cities/seattle --> index('seattle') GET /cities/portland --> index('portland') If you want to access any other metadata of the incoming HTTP request, you can use the ``app.current_request`` property, which is an instance of the the :class:`Request` class. View Function Return Values --------------------------- The response returned back to the client depends on the behavior of the view function. There are several options available: * Returning an instance of :class:`Response`. This gives you complete control over what gets returned back to the customer. * A ``bytes`` type response body must have a ``Content-Type`` header value that is present in the ``app.api.binary_types`` list in order to be handled properly. * Any other return value will be serialized as JSON and sent back as the response body with content type ``application/json``. * Any subclass of ``ChaliceViewError`` will result in an HTTP response being returned with the status code associated with that response, and a JSON response body containing a ``Code`` and a ``Message``. This is discussed in more detail below. * Any other exception raised will result in a 500 HTTP response. The body of that response depends on whether debug mode is enabled. .. _view-error-handling: Error Handling -------------- Chalice provides a built in set of exception classes that map to common HTTP errors including: * ``BadRequestError``- returns a status code of 400 * ``UnauthorizedError``- returns a status code of 401 * ``ForbiddenError``- returns a status code of 403 * ``NotFoundError``- returns a status code of 404 * ``ConflictError``- returns a status code of 409 * ``TooManyRequestsError``- returns a status code of 429 * ``ChaliceViewError``- returns a status code of 500 You can raise these anywhere in your view functions and chalice will convert these to the appropriate HTTP response. The default chalice error responses will send the error back as ``application/json`` with the response body containing a ``Code`` corresponding to the exception class name and a ``Message`` key corresponding to the string provided when the exception was instantiated. For example: .. code-block:: python from chalice import Chalice from chalice import BadRequestError app = Chalice(app_name="badrequest") @app.route('/badrequest') def badrequest(): raise BadRequestError("This is a bad request") This view function will generate the following HTTP response:: $ http https://endpoint/api/badrequest HTTP/1.1 400 Bad Request { "Code": "BadRequestError", "Message": "This is a bad request" } In addition to the built in chalice exceptions, you can use the :class:`Response` class to customize the HTTP errors if you prefer to either not have JSON error responses or customize the JSON response body for errors. For example: .. code-block:: python from chalice import Chalice, Response app = Chalice(app_name="badrequest") @app.route('/badrequest') def badrequest(): return Response(body='Plain text error message', headers={'Content-Type': 'text/plain'}, status_code=400) Specifying HTTP Methods ----------------------- So far, our examples have only allowed GET requests. It's actually possible to support additional HTTP methods. Here's an example of a view function that supports PUT: .. code-block:: python @app.route('/resource/{value}', methods=['PUT']) def put_test(value): return {"value": value} We can test this method using the ``http`` command:: $ http PUT https://endpoint/api/resource/foo HTTP/1.1 200 OK { "value": "foo" } Note that the ``methods`` kwarg accepts a list of methods. Your view function will be called when any of the HTTP methods you specify are used for the specified resource. For example: .. code-block:: python @app.route('/myview', methods=['POST', 'PUT']) def myview(): pass The above view function will be called when either an HTTP POST or PUT is sent to ``/myview`` as shown below:: POST /myview --> myview() PUT /myview --> myview() Alternatively if you do not want to share the same view function across multiple HTTP methods for the same route url, you may define separate view functions to the same route url but have the view functions differ by HTTP method. For example: .. code-block:: python @app.route('/myview', methods=['POST']) def myview_post(): pass @app.route('/myview', methods=['PUT']) def myview_put(): pass This setup will route all HTTP POST's to ``/myview`` to the ``myview_post()`` view function and route all HTTP PUT's to ``/myview`` to the ``myview_put()`` view function as shown below:: POST /myview --> myview_post() PUT /myview --> myview_put() If you do chose to use separate view functions for the same route path, it is important to know: * View functions that share the same route cannot have the same names. For example, two view functions that both share the same route path cannot both be named ``view()``. * View functions that share the same route cannot overlap in supported HTTP methods. For example if two view functions both share the same route path, they both cannot contain ``'PUT'`` in their route ``methods`` list. * View functions that share the same route path and have CORS configured cannot have differing CORS configuration. For example, if two view functions that both share the same route path, the route configuration for one of the view functions cannot set ``cors=True`` while having the route configuration of the other view function be set to ``cors=app.CORSConfig(allow_origin='https://foo.example.com')``. Binary Content -------------- Chalice supports binary payloads through its ``app.api.binary_types`` list. Any type in this list is considered a binary ``Content-Type``. Whenever a request with a ``Content-Type`` header is encountered that matches an entry in the ``binary_types`` list, its body will be available as a ``bytes`` type on the property ``app.current_request.raw_body``. Similarly, in order to send binary data back in a response, simply set your ``Content-Type`` header to something present in the ``binary_types`` list. Note that you can override the default types by modifying the ``app.api.binary_types`` list at the module level. Here is an example app which simply echoes back binary content: .. code-block:: python from chalice import Chalice, Response app = Chalice(app_name="binary-response") @app.route('/bin-echo', methods=['POST'], content_types=['application/octet-stream']) def bin_echo(): raw_request_body = app.current_request.raw_body return Response(body=raw_request_body, status_code=200, headers={'Content-Type': 'application/octet-stream'}) You can see this app echo back binary data sent to it:: $ echo -n -e "\xFE\xED" | http POST $(chalice url)bin-echo \ Accept:application/octet-stream Content-Type:application/octet-stream | xxd 0000000: feed .. Note that both the ``Accept`` and ``Content-Type`` headers are required. If you fail to set the ``Content-Type`` header on the request will result in a ``415 UnsupportedMediaType`` error. Care must be taken when configuring what ``content_types`` a route accepts, they must all be valid binary types, or they must all be non-binary types. The ``Accept`` header must also be set if the data returned is to be the raw binary, if is omitted the call return a ``400`` Bad Request response. For example, here is the same call as above without the ``Accept`` header:: $ echo -n -e "\xFE\xED" | http POST $(chalice url)bin-echo \ Content-Type:application/octet-stream HTTP/1.1 400 Bad Request Connection: keep-alive Content-Length: 270 Content-Type: application/json Date: Sat, 27 May 2017 07:09:51 GMT { "Code": "BadRequest", "Message": "Request did not specify an Accept header with application/octet-stream, The response has a Content-Type of application/octet-stream. If a response has a binary Content-Type then the request must specify an Accept header that matches." } Usage Recommendations --------------------- If you want to return a JSON response body, just return the corresponding python types directly. You don't need to use the :class:`Response` class. Chalice will automatically convert this to a JSON HTTP response as a convenience for you. Use the :class:`Response` class when you want to return non-JSON content, or when you want to inject custom HTTP headers to your response. For errors, raise the built in ``ChaliceViewError`` subclasses (e.g ``BadRequestError``, ``NotFoundError``, ``ConflictError`` etc) when you want to return a HTTP error response with a preconfigured JSON body containing a ``Code`` and ``Message``. Use the :class:`Response` class when you want to customize the error responses to either return a different JSON error response body, or to return an HTTP response that's not ``application/json``. ================================================ FILE: docs/source/topics/websockets.rst ================================================ Websockets ========== .. warning:: Websockets are considered an experimental API. You'll need to opt-in to this feature using the ``WEBSOCKETS`` feature flag: .. code-block:: python app = Chalice('myapp') app.experimental_feature_flags.update([ 'WEBSOCKETS' ]) See :doc:`experimental` for more information. Chalice supports websockets through integration with an API Gateway Websocket API. If any of the decorators are present in a Chalice app, then an API Gateway Websocket API will be deployed and wired to Lambda Functions. Responding to websocket events ------------------------------ In a Chalice app the websocket API is accessed through the three decorators ``on_ws_connect``, ``on_ws_message``, ``on_ws_disconnect``. These handle a new websocket connection, an incoming message on an existing connection, and a connection being cleaned up respectively. A decorated websocket handler function takes one argument ``event`` with the type :ref:`WebsocketEvent `. This class allows easy access to information about the API Gateway Websocket API, and information about the particular socket the handler is being invoked to serve. Below is a simple working example application that prints to CloudWatch Logs for each of the events. .. code-block:: python from boto3.session import Session from chalice import Chalice app = Chalice(app_name='test-websockets') app.experimental_feature_flags.update([ 'WEBSOCKETS', ]) app.websocket_api.session = Session() @app.on_ws_connect() def connect(event): print('New connection: %s' % event.connection_id) @app.on_ws_message() def message(event): print('%s: %s' % (event.connection_id, event.body)) @app.on_ws_disconnect() def disconnect(event): print('%s disconnected' % event.connection_id) Setting the websocket protocol on new connections ------------------------------------------------- You can return a dictionary or an instance of :class:`Response` in the ``on_ws_connect`` handler, similar to what you'd do in a Rest API. Note that API Gateway does not forward arbitrary headers or a response body back to the client, so this is primarily used to set a ``Sec-WebSocket-Protocol`` header value. .. code-block:: python from chalice import Chalice app = Chalice(app_name='test-websockets') app.experimental_feature_flags.update([ 'WEBSOCKETS', ]) @app.on_ws_connect() def connect(event): print('New connection: %s' % event.connection_id) # We don't need to explicitly set a statusCode. return { 'headers': {'Sec-WebSocket-Protocol': 'My-Protocol'}, } You don't need to explicitly set a ``statusCode`` if you return a dictionary from the ``on_ws_connect`` header, but if want to return one you should **not** set the status code to ``101``. API Gateway will automatically do this for you. For successful connection handling you should return a ``200`` status code if you want to explicitly set a ``statusCode``. Sending a message over a websocket ---------------------------------- To send a message to a websocket client Chalice, use the :ref:`app.websocket_api.send() ` method. This method will work in any of the decorated functions outlined in the above section. Two pieces of information are needed to send a message. The identifier of the websocket, and the contents for the message. Below is a simple example that when it receives a message, it sends back the message ``"I got your message!"`` over the same socket. .. code-block:: python from boto3.session import Session from chalice import Chalice app = Chalice(app_name='test-websockets') app.experimental_feature_flags.update([ 'WEBSOCKETS', ]) app.websocket_api.session = Session() @app.on_ws_message() def message(event): app.websocket_api.send(event.connection_id, 'I got your message!') See :ref:`websocket-tutorial` for completely worked example applications. ================================================ FILE: docs/source/tutorials/basicrestapi.rst ================================================ REST API Tutorial ================= In this tutorial, we create a REST API and explore Chalice features that help us write REST APIs. Installation and Configuration ------------------------------ If you haven't already setup and configured Chalice, see the :doc:`../quickstart` for a step by step guide. In a nutshell, you can get a basic Chalice app created with:: $ python3 --version Python 3.7.3 $ python3 -m venv venv37 $ . venv37/bin/activate $ python3 -m pip install chalice $ chalice new-project helloworld $ cd helloworld URL Parameters -------------- The default template when you run the ``new-project`` generates a sample REST API for you: .. code-block:: python from chalice import Chalice app = Chalice(app_name='helloworld') @app.route('/') def index(): return {'hello': 'world'} We're going to make a few changes to our ``app.py`` file that demonstrate the capabilities provided by Chalice. Our application so far has a single view that allows you to make an HTTP GET request to ``/``. Now let's suppose we want to capture parts of the URI: .. code-block:: python from chalice import Chalice app = Chalice(app_name='helloworld') CITIES_TO_STATE = { 'seattle': 'WA', 'portland': 'OR', } @app.route('/') def index(): return {'hello': 'world'} @app.route('/cities/{city}') def state_of_city(city): return {'state': CITIES_TO_STATE[city]} In the example above, we've now added a ``state_of_city`` view that allows a user to specify a city name. The view function takes the city name and returns the name of the state the city is in. Notice that the ``@app.route`` decorator has a URL pattern of ``/cities/{city}``. This means that the value of ``{city}`` is captured and passed to the view function. You can also see that the ``state_of_city`` takes a single argument. This argument is the name of the city provided by the user. For example:: GET /cities/seattle --> state_of_city('seattle') GET /cities/portland --> state_of_city('portland') Now that we've updated our ``app.py`` file with this new view function, let's redeploy our application. You can run ``chalice deploy`` from the ``helloworld`` directory and it will deploy your application:: $ chalice deploy Let's try it out. Note the examples below use the ``http`` command from the ``httpie`` package. You can install this using ``pip install httpie``:: $ http https://endpoint/api/cities/seattle HTTP/1.1 200 OK { "state": "WA" } $ http https://endpoint/api/cities/portland HTTP/1.1 200 OK { "state": "OR" } Notice what happens if we try to request a city that's not in our ``CITIES_TO_STATE`` map:: $ http https://endpoint/api/cities/vancouver HTTP/1.1 500 Internal Server Error Content-Type: application/json X-Cache: Error from cloudfront { "Code": "ChaliceViewError", "Message": "An internal server error occurred." } In the next section, we'll see how to fix this and provide better error messages. Error Messages -------------- In the example above, you'll notice that when our app raised an uncaught exception, a 500 internal server error was returned. In this section, we're going to show how you can debug and improve these error messages. The first thing we're going to look at is how we can debug this issue. By default, debugging is turned off, but you can enable debugging to get more information: .. code-block:: python from chalice import Chalice app = Chalice(app_name='helloworld') app.debug = True The ``app.debug = True`` enables debugging for your app. Save this file and redeploy your changes:: $ chalice deploy ... https://endpoint/api/ Now, when you request the same URL that returned an internal server error, you'll get back the original stack trace:: $ http https://endpoint/api/cities/vancouver Traceback (most recent call last): File "/var/task/chalice/app.py", line 304, in _get_view_function_response response = view_function(*function_args) File "/var/task/app.py", line 18, in state_of_city return {'state': CITIES_TO_STATE[city]} KeyError: u'vancouver' We can see that the error is caused from an uncaught ``KeyError`` resulting from trying to access the ``vancouver`` key. Now that we know the error, we can fix our code. What we'd like to do is catch this exception and instead return a more helpful error message to the user. Here's the updated code: .. code-block:: python from chalice import BadRequestError @app.route('/cities/{city}') def state_of_city(city): try: return {'state': CITIES_TO_STATE[city]} except KeyError: raise BadRequestError("Unknown city '%s', valid choices are: %s" % ( city, ', '.join(CITIES_TO_STATE.keys()))) Save and deploy these changes:: $ chalice deploy $ http https://endpoint/api/cities/vancouver HTTP/1.1 400 Bad Request { "Code": "BadRequestError", "Message": "Unknown city 'vancouver', valid choices are: portland, seattle" } We can see now that we have received a ``Code`` and ``Message`` key, with the message being the value we passed to ``BadRequestError``. Whenever you raise a ``BadRequestError`` from your view function, the framework will return an HTTP status code of 400 along with a JSON body with a ``Code`` and ``Message``. There are a few additional exceptions you can raise from your python code:: * BadRequestError - return a status code of 400 * UnauthorizedError - return a status code of 401 * ForbiddenError - return a status code of 403 * NotFoundError - return a status code of 404 * ConflictError - return a status code of 409 * UnprocessableEntityError - return a status code of 422 * TooManyRequestsError - return a status code of 429 * ChaliceViewError - return a status code of 500 You can import these directly from the ``chalice`` package: .. code-block:: python from chalice import UnauthorizedError Additional Routing ------------------ So far, our examples have only allowed GET requests. It's actually possible to support additional HTTP methods. Here's an example of a view function that supports PUT: .. code-block:: python @app.route('/resource/{value}', methods=['PUT']) def put_test(value): return {"value": value} We can test this method using the ``http`` command:: $ http PUT https://endpoint/api/resource/foo HTTP/1.1 200 OK { "value": "foo" } Note that the ``methods`` kwarg accepts a list of methods. Your view function will be called when any of the HTTP methods you specify are used for the specified resource. For example: .. code-block:: python @app.route('/myview', methods=['POST', 'PUT']) def myview(): pass The above view function will be called when either an HTTP POST or PUT is sent to ``/myview``. Alternatively if you do not want to share the same view function across multiple HTTP methods for the same route url, you may define separate view functions to the same route url but have the view functions differ by HTTP method. For example: .. code-block:: python @app.route('/myview', methods=['POST']) def myview_post(): pass @app.route('/myview', methods=['PUT']) def myview_put(): pass This setup routes HTTP POST requests to ``/myview`` to the ``myview_post()`` view function and routes HTTP PUT requests to ``/myview`` to the ``myview_put()`` view function. It is also important to note that the view functions **must** have unique names. For example, both view functions cannot be named ``myview()``. In the next section we'll go over how you can introspect the given request in order to differentiate between various HTTP methods. Request Metadata ---------------- In the examples above, you saw how to create a view function that supports an HTTP PUT request as well as a view function that supports both POST and PUT via the same view function. However, there's more information we might need about a given request: * In a PUT/POST, you frequently send a request body. We need some way of accessing the contents of the request body. * For view functions that support multiple HTTP methods, we'd like to detect which HTTP method was used so we can have different code paths for PUTs vs. POSTs. All of this and more is handled by the current request object that the ``chalice`` library makes available to each view function when it's called. Let's see an example of this. Suppose we want to create a view function that allowed you to PUT data to an object and retrieve that data via a corresponding GET. We could accomplish that with the following view function: .. code-block:: python from chalice import NotFoundError OBJECTS = { } @app.route('/objects/{key}', methods=['GET', 'PUT']) def myobject(key): request = app.current_request if request.method == 'PUT': OBJECTS[key] = request.json_body elif request.method == 'GET': try: return {key: OBJECTS[key]} except KeyError: raise NotFoundError(key) Save this in your ``app.py`` file and rerun ``chalice deploy``. Now, you can make a PUT request to ``/objects/your-key`` with a request body, and retrieve the value of that body by making a subsequent ``GET`` request to the same resource. Here's an example of its usage:: # First, trying to retrieve the key will return a 404. $ http GET https://endpoint/api/objects/mykey HTTP/1.1 404 Not Found { "Code": "NotFoundError", "Message": "mykey" } # Next, we'll create that key by sending a PUT request. $ echo '{"foo": "bar"}' | http PUT https://endpoint/api/objects/mykey HTTP/1.1 200 OK null # And now we no longer get a 404, we instead get the value we previously # put. $ http GET https://endpoint/api/objects/mykey HTTP/1.1 200 OK { "mykey": { "foo": "bar" } } You might see a problem with storing the objects in a module level ``OBJECTS`` variable. We address this in the next section. The ``app.current_request`` object is an instance of the :class:`Request` class, which also has the following properties. * ``current_request.query_params`` - A dict of the query params. * ``current_request.headers`` - A dict of the request headers. * ``current_request.uri_params`` - A dict of the captured URI params. * ``current_request.method`` - The HTTP method (as a string). * ``current_request.json_body`` - The parsed JSON body. * ``current_request.raw_body`` - The raw HTTP body as bytes. * ``current_request.context`` - A dict of additional context information * ``current_request.stage_vars`` - Configuration for the API Gateway stage The ``current_request`` object also has a ``to_dict`` method, which returns all the information about the current request as a dictionary. Let's use this method to write a view function that returns everything it knows about the request: .. code-block:: python @app.route('/introspect') def introspect(): return app.current_request.to_dict() Save this to your ``app.py`` file and redeploy with ``chalice deploy``. Here's an example of hitting the ``/introspect`` URL. Note how we're sending a query string as well as a custom ``X-TestHeader`` header:: $ http 'https://endpoint/api/introspect?query1=value1&query2=value2' 'X-TestHeader: Foo' HTTP/1.1 200 OK { "context": { "apiId": "apiId", "httpMethod": "GET", "identity": { "accessKey": null, "accountId": null, "apiKey": null, "caller": null, "cognitoAuthenticationProvider": null, "cognitoAuthenticationType": null, "cognitoIdentityId": null, "cognitoIdentityPoolId": null, "sourceIp": "1.1.1.1", "userAgent": "HTTPie/0.9.3", "userArn": null }, "requestId": "request-id", "resourceId": "resourceId", "resourcePath": "/introspect", "stage": "dev" }, "headers": { "accept": "*/*", ... "x-testheader": "Foo" }, "method": "GET", "query_params": { "query1": "value1", "query2": "value2" }, "raw_body": null, "stage_vars": null, "uri_params": null } Request Content Types --------------------- The default behavior of a view function supports a request body of ``application/json``. When a request is made with a ``Content-Type`` of ``application/json``, the ``app.current_request.json_body`` attribute is automatically set for you. This value is the parsed JSON body. You can also configure a view function to support other content types. You can do this by specifying the ``content_types`` parameter value to your ``app.route`` function. This parameter is a list of acceptable content types. Here's an example of this feature: .. code-block:: python import sys from chalice import Chalice from urllib.parse import urlparse, parse_qs app = Chalice(app_name='helloworld') @app.route('/', methods=['POST'], content_types=['application/x-www-form-urlencoded']) def index(): parsed = parse_qs(app.current_request.raw_body.decode()) return { 'states': parsed.get('states', []) } There's a few things worth noting in this view function. First, we've specified that we only accept the ``application/x-www-form-urlencoded`` content type. If we try to send a request with ``application/json``, we'll now get a ``415 Unsupported Media Type`` response:: $ http POST https://endpoint/api/ states=WA states=CA --debug ... >>> requests.request(**{'allow_redirects': False, 'headers': {'Accept': 'application/json', 'Content-Type': 'application/json', ... HTTP/1.1 415 Unsupported Media Type { "message": "Unsupported Media Type" } If we use the ``--form`` argument, we can see the expected behavior of this view function because ``httpie`` sets the ``Content-Type`` header to ``application/x-www-form-urlencoded``:: $ http --form POST https://endpoint/api/formtest states=WA states=CA --debug ... >>> requests.request(**{'allow_redirects': False, 'headers': {'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', ... HTTP/1.1 200 OK { "states": [ "WA", "CA" ] } The second thing worth noting is that ``app.current_request.json_body`` **is only available for the application/json content type.** In our example above, we used ``app.current_request.raw_body`` to access the raw body bytes: .. code-block:: python parsed = parse_qs(app.current_request.raw_body) ``app.current_request.json_body`` is set to ``None`` whenever the ``Content-Type`` is not ``application/json``. This means that you will need to use ``app.current_request.raw_body`` and parse the request body as needed. Customizing the HTTP Response ----------------------------- The return value from a chalice view function is serialized as JSON as the response body returned back to the caller. This makes it easy to create rest APIs that return JSON response bodies. Chalice allows you to control this behavior by returning an instance of a chalice specific ``Response`` class. This behavior allows you to: * Specify the status code to return * Specify custom headers to add to the response * Specify response bodies that are not ``application/json`` Here's an example of this: .. code-block:: python from chalice import Chalice, Response app = Chalice(app_name='custom-response') @app.route('/') def index(): return Response(body='hello world!', status_code=200, headers={'Content-Type': 'text/plain'}) This will result in a plain text response body:: $ http https://endpoint/api/ HTTP/1.1 200 OK Content-Length: 12 Content-Type: text/plain hello world! GZIP compression for JSON ------------------------- The return value from a chalice view function is serialized as JSON as the response body returned back to the caller. This makes it easy to create rest APIs that return JSON response bodies. Chalice allows you to control this behavior by returning an instance of a chalice specific ``Response`` class. This behavior allows you to: * Add ``application/json`` to binary_types * Specify the status code to return * Specify custom header ``Content-Type: application/json`` * Specify custom header ``Content-Encoding: gzip`` Here's an example of this: .. code-block:: python import json import gzip from chalice import Chalice, Response app = Chalice(app_name='compress-response') app.api.binary_types.append('application/json') @app.route('/') def index(): blob = json.dumps({'hello': 'world'}).encode('utf-8') payload = gzip.compress(blob) custom_headers = { 'Content-Type': 'application/json', 'Content-Encoding': 'gzip' } return Response(body=payload, status_code=200, headers=custom_headers) CORS Support ------------ You can specify whether a view supports CORS by adding the ``cors=True`` parameter to your ``@app.route()`` call. By default this value is ``False``. Global CORS can be set by setting ``app.api.cors = True``. .. code-block:: python @app.route('/supports-cors', methods=['PUT'], cors=True) def supports_cors(): return {} Setting ``cors=True`` has similar behavior to enabling CORS using the AWS Console. This includes: * Injecting the ``Access-Control-Allow-Origin: *`` header to your responses, including all error responses you can return. * Automatically adding an ``OPTIONS`` method to support preflighting requests. The preflight request will return a response that includes: * ``Access-Control-Allow-Origin: *`` * The ``Access-Control-Allow-Methods`` header will return a list of all HTTP methods you've called out in your view function. In the example above, this will be ``PUT,OPTIONS``. * ``Access-Control-Allow-Headers: Content-Type,X-Amz-Date,Authorization, X-Api-Key,X-Amz-Security-Token``. If more fine grained control of the CORS headers is desired, set the ``cors`` parameter to an instance of ``CORSConfig`` instead of ``True``. The ``CORSConfig`` object can be imported from from the ``chalice`` package it's constructor takes the following keyword arguments that map to CORS headers: ================= ==== ================================ Argument Type Header ================= ==== ================================ allow_origin str Access-Control-Allow-Origin allow_headers list Access-Control-Allow-Headers expose_headers list Access-Control-Expose-Headers max_age int Access-Control-Max-Age allow_credentials bool Access-Control-Allow-Credentials ================= ==== ================================ Code sample defining more CORS headers: .. code-block:: python from chalice import CORSConfig cors_config = CORSConfig( allow_origin='https://foo.example.com', allow_headers=['X-Special-Header'], max_age=600, expose_headers=['X-Special-Header'], allow_credentials=True ) @app.route('/custom-cors', methods=['GET'], cors=cors_config) def supports_custom_cors(): return {'cors': True} There's a couple of things to keep in mind when enabling cors for a view: * An ``OPTIONS`` method for preflighting is always injected. Ensure that you don't have ``OPTIONS`` in the ``methods=[...]`` list of your view function. * Even though the ``Access-Control-Allow-Origin`` header can be set to a string that is a space separated list of origins, this behavior does not work on all clients that implement CORS. You should only supply a single origin to the ``CORSConfig`` object. If you need to supply multiple origins you will need to define a custom handler for it that accepts ``OPTIONS`` requests and matches the ``Origin`` header against a whitelist of origins. If the match is successful then return just their ``Origin`` back to them in the ``Access-Control-Allow-Origin`` header. Example: .. code-block:: python from chalice import Chalice, Response app = Chalice(app_name='multipleorigincors') _ALLOWED_ORIGINS = set([ 'http://allowed1.example.com', 'http://allowed2.example.com', ]) @app.route('/cors_multiple_origins', methods=['GET', 'OPTIONS']) def supports_cors_multiple_origins(): method = app.current_request.method if method == 'OPTIONS': headers = { 'Access-Control-Allow-Method': 'GET,OPTIONS', 'Access-Control-Allow-Origin': ','.join(_ALLOWED_ORIGINS), 'Access-Control-Allow-Headers': 'X-Some-Header', } origin = app.current_request.headers.get('origin', '') if origin in _ALLOWED_ORIGINS: headers.update({'Access-Control-Allow-Origin': origin}) return Response( body=None, headers=headers, ) elif method == 'GET': return 'Foo' ================================================ FILE: docs/source/tutorials/cdk.rst ================================================ Deploying with the AWS CDK ========================== In this tutorial, we're going to create a REST API with an Amazon DynamoDB table as our data store. We'll be using the `AWS Cloud Development Kit (CDK) `__ to deploy our application, and we'll show how to use the integration between Chalice and the CDK in order to build and deploy our application. By combining Chalice and the CDK together, you can use Chalice to write your application code using its familiar, decorator-based APIs, and use the CDK and the full breadth of its construct libraries to create the service infrastructure and resources needed for your application. We'll also see how we can use the Chalice construct to manipulate our Chalice application using the CDK APIs as well as take resources from CDK constructs and map them into our Chalice application. Installation and Configuration ------------------------------ This tutorial requires that both Chalice and the AWS CDK is installed. The CDK is written in Typescript and requires node and npm to be installed. See the `Getting started with the AWS CDK `__ for more details on install the CDK. First, we'll install the CDK. :: $ npm install -g aws-cdk You should now have a ``cdk`` executable you can run. :: $ cdk --version 1.83.0 (build 827c5f4) Next we'll create a Python virtual environment and install Chalice. Be sure to use Python 3.6 or greater. :: $ python3 -m venv demo $ . demo/bin/activate $ python3 -m pip install chalice $ chalice --version chalice 1.22.0, python 3.7.8, darwin 19.6.0 CDK integration with Chalice is available as an optional package installation. To install the necessary dependencies run the following command: :: $ python3 -m pip install "chalice[cdkv2]" **Note:** Please use CDK version 2, support for CDK version 1 ended on June 1, 2023. See `Working with the AWS CDK in Python Doc `__ for more information. You're now ready to create your first Chalice and CDK application. Project Creation ---------------- To create a new project we'll use the ``chalice new-project`` command with no arguments. Enter a name for your project and select ``[CDK] REST API with DynamoDB backend`` for the project type. :: $ chalice new-project ___ _ _ _ _ ___ ___ ___ / __|| || | /_\ | | |_ _|/ __|| __| | (__ | __ | / _ \ | |__ | || (__ | _| \___||_||_|/_/ \_\|____||___|\___||___| The python serverless microframework for AWS allows you to quickly create and deploy applications using Amazon API Gateway and AWS Lambda. Please enter the project name [?] Enter the project name: cdkdemo [?] Select your project type: [CDK] REST API with DynamoDB backend REST API S3 Event Handler Lambda Functions only Legacy REST API Template [CDK] REST API with DynamoDB backend Your project has been generated in ./cdkdemo Next, we'll ``cd`` into the ``cdkdemo`` directory and see what Chalice has generated. :: $ cd cdkdemo $ tree . ├── README.rst ├── infrastructure # CDK Application │   ├── app.py │   ├── cdk.json │   ├── requirements.txt │   └── stacks │   ├── __init__.py │   └── chaliceapp.py ├── requirements.txt └── runtime # Chalice Application ├── app.py └── requirements.txt There's two top level directories, ``infrastructure`` and ``runtime``, which correspond to the CDK application and the Chalice application. The ``infrastructure`` directory is where we can add additional AWS resources needed by our application, and the ``runtime`` directory is where we write our application code for our Lambda functions. We'll look at these in more detail, but first we'll deploy our application. In order to build and deploy our application, we need to install the dependencies used by our application. We can do this by installing the requirements file in the top level directory of our project. :: $ python3 -m pip install -r requirements.txt If this is your first time using the CDK, you'll need to bootstrap your account, which will deploy an AWS CloudFormation stack that contains resources needed to store our application. You can do this by running the ``cdk bootstrap`` command from the ``infrastructure`` directory. :: $ cd infrastructure $ cdk bootstrap Packaging Chalice app for cdkdemo Creating deployment package. The stack cdkdemo already includes a CDKMetadata resource ⏳ Bootstrapping environment aws://12345/us-west-2... CDKToolkit: creating CloudFormation changeset... [██████████████████████████████████████████████████████████] (3/3) ✅ Environment aws://12345/us-west-2 bootstrapped. We can now deploy our application using the ``cdk deploy`` command. Make sure you're still in the ``infrastructure`` directory. :: $ cdk deploy Packaging Chalice app for cdkdemo Creating deployment package. Reusing existing deployment package. The stack cdkdemo already includes a CDKMetadata resource This deployment will make potentially sensitive changes according to your current security approval level (--require-approval broadening). Please confirm you intend to make the following modifications: ... Do you wish to deploy these changes (y/n)? y cdkdemo: deploying... [0%] start: Publishing abcd:current [100%] success: Published abcd:current cdkdemo: creating CloudFormation changeset... [██████████████████████████████████████████████████████████] (10/10) ✅ cdkdemo Outputs: cdkdemo.APIHandlerArn = arn:aws:lambda:us-west-2:12345:function:cdkdemo-APIHandler-C8OLGQT9YIDO cdkdemo.APIHandlerName = cdkdemo-APIHandler-C8OLGQT9YIDO cdkdemo.AppTableName = cdkdemo-AppTable815C50BC-1OPGOPFYODZOJ cdkdemo.EndpointURL = https://abcd.execute-api.us-west-2.amazonaws.com/api/ cdkdemo.RestAPIId = abcd Stack ARN: arn:aws:cloudformation:us-west-2:12345:stack/cdkdemo/574c4850-1d23-11eb-8cae-0aea264da24f We've now deployed a Chalice application powered by the CDK. We can now test our REST API. .. note:: If you've Chalice before, you may be familiar with the ``chalice deploy`` command. When we use the AWS CDK to deploy our application we no longer use ``chalice deploy`` and instead we run ``cdk deploy`` from the ``infrastructure/`` directory. You should not use ``chalice deploy`` to deploy your application when using Chalice's CDK integration. Testing ------- To test our application, we make HTTP requests to our ``EndpointUrl``, which is shown as the value for ``cdkdemo.EndpointUrl`` in the output section above. We're using `httpie `__ to make our HTTP requests from the command line. :: $ python3 -m pip install httpie $ http POST https://abcd.execute-api.us-west-2.amazonaws.com/api/users/ username=jamesls name=James HTTP/1.1 200 OK ... {} $ http https://abcd.execute-api.us-west-2.amazonaws.com/api/users/jamesls HTTP/1.1 200 OK Content-Type: application/json ... { "name": "James", "username": "jamesls" } Now that we have our sample application up and running, let's walk through the project code so we can better understand what's happening. Code Walkthrough ---------------- The ``runtime/`` directory contains code where you define your Lambda event handlers (e.g. ``@app.route()``, ``@app.on_s3_event()``, etc.). When you create a Chalice application without the CDK, this is normally the root directory for your application. You should also see your Chalice config file in ``.chalice/config.json``. The ``infrastructure/`` directory contains the definitions for the AWS resources used by your application. This is the directory structure that would be generated if you were only using the CDK and not Chalice. This is why the combined Chalice/CDK application template has a new top level directory with separate sub directories for the CDK app and the Chalice app. To better understand how the two applications communicate with each other, we'll examine how the DynamoDB table was added to the application. First, let’s look at the code for our REST API in ``runtime/app.py``. .. code-block:: python import os import boto3 from chalice import Chalice app = Chalice(app_name='cdkdemo') dynamodb = boto3.resource('dynamodb') dynamodb_table = dynamodb.Table(os.environ.get('APP_TABLE_NAME', '')) @app.route('/users', methods=['POST']) def create_user(): ... @app.route('/users/{username}', methods=['GET']) def get_user(username): ... The name of the DynamoDB table is passed through an environment variable, ``APP_TABLE_NAME``. We then create a ``dynamodb.Table`` resource given this name. This environment variable is generated and mapped in the CDK stack that Chalice generated for us. This is located in ``../infrastructure/stacks/chaliceapp.py``. Let's look at the contents of the ``../infrastructure/stacks/chaliceapp.py`` file now. .. code-block:: python import os from aws_cdk import ( aws_dynamodb as dynamodb, core as cdk ) from chalice.cdk import Chalice RUNTIME_SOURCE_DIR = os.path.join( os.path.dirname(os.path.dirname(__file__)), os.pardir, 'runtime') class ChaliceApp(cdk.Stack): def __init__(self, scope: cdk.Construct, id: str, **kwargs) -> None: super().__init__(scope, id, **kwargs) self.dynamodb_table = self._create_ddb_table() self.chalice = Chalice( self, 'ChaliceApp', source_dir=RUNTIME_SOURCE_DIR, stage_config={ 'environment_variables': { 'APP_TABLE_NAME': self.dynamodb_table.table_name } } ) self.dynamodb_table.grant_read_write_data( self.chalice.get_role('DefaultRole') ) def _create_ddb_table(self): dynamodb_table = dynamodb.Table( self, 'AppTable', partition_key=dynamodb.Attribute( name='PK', type=dynamodb.AttributeType.STRING), sort_key=dynamodb.Attribute( name='SK', type=dynamodb.AttributeType.STRING ), removal_policy=cdk.RemovalPolicy.DESTROY) cdk.CfnOutput(self, 'AppTableName', value=dynamodb_table.table_name) return dynamodb_table Our CDK stack is using the Chalice construct from the ``chalice.cdk`` package. This provides us two benefits. First, we can generate CDK resources and pass them into our Chalice application by mapping environment variables. Second, we can take resources generated in our Chalice application and reference them with the CDK API. For example, we’re generating a DynamoDB table in the ``self._create_ddb_table()`` method, and then mapping it into our Chalice application by providing a ``stage_config`` override. This dictionary is merged with the existing Chalice configuration located in ./runtime/.chalice/config.json. If we want to pass additional values into our Chalice application we can update the environment_variables dictionary in our stage_config. We’re also able to retrieve references to our resources in our Chalice application and reference them in our CDK stack. For example, once we’ve created our DynamoDB table we also need to grant the IAM role associated with your Lambda function access to this table. We do this by using the ``grant_read_write_data`` method on our table resource, and we provide a reference to the default role that Chalice creates for us by using the ``self.chalice.get_role()`` method. Next Steps ---------- Feel free to experiment with this sample app. Add new resources to your application by updating the ``infrastructure/stacks/chaliceapp.py`` file, map CDK resources into your Chalice app through environment variables, and redeploy your application by running ``cdk deploy`` from the ``infrastructure/`` directory. ================================================ FILE: docs/source/tutorials/customdomain.rst ================================================ Custom Domain Names =================== In this tutorial, we're going to create a REST API and associate our own custom domain with this REST API. This allows us to not use the auto-generated domain name that API Gateway automatically creates when we deploy our REST API. Installation and Configuration ------------------------------ If you haven't already setup and configured Chalice, see the :doc:`../quickstart` for a step by step guide. You can run these commands to create a basic Chalice app:: $ python3 --version Python 3.9.22 $ python3 -m venv venv39 $ . venv39/bin/activate $ python3 -m pip install chalice $ chalice new-project customdomain $ cd customdomain Configure and Deploy a REGIONAL Endpoint ---------------------------------------- Before we configure a custom domain for our REST API, we'll deploy our REST API so we can see the auto-generated domain name that API Gateway creates for us. First, we'll change our endpoint type from EDGE (the default) to REGIONAL. Update your ``.chalice/config.json`` file so it looks like this:: $ cat .chalice/config.json { "version": "2.0", "app_name": "customdomain", "api_gateway_endpoint_type": "REGIONAL", "stages": { "dev": { "api_gateway_stage": "api" } } } Now we'll deploy our application. Note the URL that's printed when your application is deployed. :: $ chalice deploy Creating deployment package. Creating IAM role: customdomain-dev Creating lambda function: customdomain-dev Creating Rest API Resources deployed: - Lambda ARN: arn:aws:lambda:us-west-2:12345:function:customdomain-dev - Rest API URL: https://qxea58abcd.execute-api.us-west-2.amazonaws.com/api/ You now have an API up and running using API Gateway and Lambda:: $ curl https://qxea58abcd.execute-api.us-west-2.amazonaws.com/api/ {"hello": "world"} The ``qxea58abcd.execute-api.us-west-2.amazonaws.com`` domain name was auto-generated by API Gateway. Replacing this domain name with our own custom domain name allows us to use simpler and more intuitive URLs that we can provide to our API users. Configuring a Custom Domain --------------------------- In this tutorial, we're using Amazon Route53 to manage our DNS configuration. If you're using a third-party domain registrar, the steps will be similar, but you will have to create your DNS records using your provider's web interface or API. For this tutorial, we'll configure the domain ``chalice-demo-app.com``. Be sure to replace this value with your own domain name. Creating a Hosted Zone ~~~~~~~~~~~~~~~~~~~~~~ First, we'll need to create a hosted zone in Route 53. If you already have a hosted zone created for your domain, you can skip this step. We'll be using the AWS CLI V2 to configure our domain. You can follow the `installation instructions `__ if you don't have the AWS CLI installed. :: $ aws route53 create-hosted-zone --name chalice-demo-app.com --caller-reference 12345 { "Location": "https://route53.amazonaws.com/2013-04-01/hostedzone/ZABCDEFGABCDEFGLDO822", "HostedZone": { "Id": "/hostedzone/ZABCDEFGABCDEFGLDO822", "Name": "chalice-demo-app.com.", "CallerReference": "12345", "Config": { "PrivateZone": false }, "ResourceRecordSetCount": 2 }, "ChangeInfo": { "Id": "/change/C07395431VDLB0CY65VP", "Status": "PENDING", "SubmittedAt": "2020-07-21T17:13:54.709000+00:00" }, "DelegationSet": { "NameServers": [ "ns-123.awsdns-31.net", "ns-123.awsdns-05.com", "ns-123.awsdns-09.org", "ns-123.awsdns-40.co.uk" ] } } You'll need to save the value of the hosted zone id for later. From the output above the line ``"Id": "/hostedzone/ZABCDEFGABCDEFGLDO822",`` contains our hosted zone id of ``ZABCDEFGABCDEFGLDO822``. We'll refer to this value as ``$OUR_HOSTED_ZONE_ID`` later. You'll now need to register the ``"NameServers"`` shown in the output above with your domain registrar. Creating an ACM Certificate ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Now that we have our hosted zone, we'll need to create an ACM certificate associated with this domain. This is the SSL/TLS certificate that will be used when requests are made to our custom domain. In this example, we'll create a wildcard certificate for ``*.chalice-demo-app.com``. Note that we're creating a ``REGIONAL`` endpoint type for our REST API, which means that our ACM certificate **must** be in the same region as our REST API. In this example, we're using ``us-west-2``. If you're using the default ``EDGE`` endpoint type, the ACM cert must be in ``us-east-1``. You can explicitly specify the region using the ``--region`` CLI parameter if needed. :: $ aws acm request-certificate --domain-name "*.chalice-demo-app.com" \ --validation-method DNS --idempotency-token 12345 \ --options CertificateTransparencyLoggingPreference=DISABLED { "CertificateArn": "arn:aws:acm:us-west-2:0123456789:certificate/578efbda-6bc7-4ae2-9964-6e6c3f58008b" } Save the value of ``CertificateArn`` shown in the output above. We'll need this value when we configure our app to use this custom domain. Before we can use this certificate, we need to validate this certificate. This process demonstrates that we own or control the domain name associated with the certificate. In the command above, we used the ``--validation-method DNS``, which requires us to add CNAME records to validate we control our domain name. ACM supports both `DNS validation `__ as well as `email validation `__. To validate our domain, we'll now create the necessary CNAME records in our hosted zone using the Route53 API. First, we need to retrieve the values for our CNAME record. Be sure to replace the value of ``--certificate-arn`` with your own certificate ARN in the command below:: $ aws acm describe-certificate --certificate-arn arn:aws:acm:us-west-2:0123456789:certificate/578efbda-6bc7-4ae2-9964-6e6c3f58008b \ --query Certificate.DomainValidationOptions[0] { "DomainName": "*.chalice-demo-app.com", "ValidationDomain": "*.chalice-demo-app.com", "ValidationStatus": "PENDING_VALIDATION", "ResourceRecord": { "Name": "_1234567891234567897eb5512d9fb554.chalice-demo-app.com.", "Type": "CNAME", "Value": "_123456789123456789e7495341c27cd1.jfrzftwwjs.acm-validations.aws." }, "ValidationMethod": "DNS" } Next we'll create a CNAME record for ``_1234567891234567897eb5512d9fb554.chalice-demo-app.com.`` with a value of ``_123456789123456789e7495341c27cd1.jfrzftwwjs.acm-validations.aws.``:: $ aws route53 change-resource-record-sets \ --hosted-zone-id $OUR_HOSTED_ZONE_ID --change-batch \ '{ "Changes": [ { "Action": "CREATE", "ResourceRecordSet": { "Name": "_0073e080112eb8de8c7eb5512d9fb554.chalice-demo-app.com.", "Type": "CNAME", "TTL": 300, "ResourceRecords": [{"Value": "_6e560a5a9831aad210e7495341c27cd1.jfrzftwwjs.acm-validations.aws."}] } } ] }' # Command output: { "ChangeInfo": { "Id": "/change/C0339874QPDDRA8TKT7U", "Status": "PENDING", "SubmittedAt": "2020-07-21T17:36:39.902000+00:00" } } It will take a few minutes before ACM validates your domain. You can move on to the next steps, or if you'd like to wait until the domain is validated you can use the CLI's ``certificate-validated`` waiter, which will block until the ACM certificate is validated:: $ aws acm wait certificate-validated \ --certificate-arn arn:aws:acm:us-west-2:0123456789:certificate/578efbda-6bc7-4ae2-9964-6e6c3f58008b Chalice App Configuration ~~~~~~~~~~~~~~~~~~~~~~~~~ Now that we have our hosted zone and ACM certificate created, we can configure our Chalice application with our custom domain. To do so we need to add `api_gateway_custom_domain `__ configuration option and specify our ACM certificate ARN as well as the our custom domain name. You're ``.chalice/config.json`` file should look like this: .. code-block:: json { "version": "2.0", "app_name": "customdomain", "api_gateway_endpoint_type": "REGIONAL", "stages": { "dev": { "api_gateway_custom_domain": { "domain_name": "api.chalice-demo-app.com", "certificate_arn": "arn:aws:acm:us-west-2:0123456789:certificate/578efbda-6bc7-4ae2-9964-6e6c3f58008b" }, "api_gateway_stage": "api" } } } We we rerun the ``chalice deploy`` command you'll notice there's a new ``Custom domain name:`` line in the output:: $ chalice deploy Creating deployment package. Updating policy for IAM role: customdomain-dev Updating lambda function: customdomain-dev Updating rest API Creating custom domain name: api.chalice-demo-app.com Creating api mapping: / Resources deployed: - Lambda ARN: arn:aws:lambda:us-west-2:0123456789:function:customdomain-dev - Rest API URL: https://qxea58abcd.execute-api.us-west-2.amazonaws.com/api/ - Custom domain name: HostedZoneId: Z1UJRXOUMOOFQ8 AliasDomainName: d-6vj4cynstd.execute-api.us-west-2.amazonaws.com Now that we've configured our Chalice app with our custom domain, there's one step left. We need to update our DNS configuration to point to our REST API. To do this, we'll use the values of ``HostedZoneId`` and ``AliasDomainName`` in the output above to create an alias record in our hosted zone. Alias Record Configuration ~~~~~~~~~~~~~~~~~~~~~~~~~~ You can run the following command to create an alias record to your REST API. Note that there are two different hosted zone id values here. The value specified as the ``--hosted-zone-id`` value is the ID of our hosted zone ID (``$OUR_HOSTED_ZONE_ID``) that we created earlier in this example. The value of the ``HostedZoneId`` in the ``AliasTarget`` section is the value of the ``HostedZoneId`` generated by API Gateway shown in the output of ``chalice deploy`` above. :: $ aws route53 change-resource-record-sets --hosted-zone-id ZABCDEFGABCDEFGLDO822 --change-batch \ '{ "Changes": [ { "Action": "CREATE", "ResourceRecordSet": { "Name": "api.chalice-demo-app.com", "Type": "A", "AliasTarget": { "DNSName": "d-6vj4cynstd.execute-api.us-west-2.amazonaws.com", "HostedZoneId": "Z1UJRXOUMOOFQ8", "EvaluateTargetHealth": false } } } ] }' # Command output: { "ChangeInfo": { "Id": "/change/C0539657Y0HMX8XBC5EH", "Status": "PENDING", "SubmittedAt": "2020-07-21T17:52:34.935000+00:00" } } Verification ------------ Our Chalice application is now configured to use our custom domain. We can verify this by making a request to our custom domain. In this example, this is ``api.chalice-demo-app.com``:: $ curl -i https://api.chalice-demo-app.com/ HTTP/1.1 200 OK Date: Tue, 21 Jul 2020 17:56:00 GMT Content-Type: application/json Content-Length: 17 Connection: keep-alive x-amzn-RequestId: 9f33fbb9-6b10-469e-827f-f287199c9bc5 x-amz-apigw-id: QCPXoEwPIAMFi8Q= X-Amzn-Trace-Id: Root=1-5f172c30-dccc232932a16a539dfc01b9;Sampled=0 {"hello":"world"} Next Steps ---------- For more information on configuring custom domains, check out our :doc:`topic guide <../topics/domainname>` on custom domains as well as the config file reference for the :ref:`custom-domain-config-options` and the :ref:`custom-domain-ws-config-options` options. ================================================ FILE: docs/source/tutorials/events.rst ================================================ Event Sources Tutorial ====================== In the :doc:`../quickstart` guide, we looked at how to create a REST API using the ``@app.route()`` decorator. Chalice also has additional decorators that connects your code to specific event sources. This results in your code being invoked when a specific event occurs. In this tutorial we'll look at a few examples. Installation and Configuration ------------------------------ If you haven't already setup and configured Chalice, see the :doc:`../quickstart` for a step by step guide. In a nutshell, you can get a basic Chalice app created with:: $ python3 --version Python 3.9.22 $ python3 -m venv venv39 $ . venv39/bin/activate $ python3 -m pip install chalice $ chalice new-project chalice-sns-demo $ cd chalice-sns-demo We'll also be using the AWS CLI in this tutorial. You can follow `these instructions `__ for installing the AWS CLI v2. Amazon SNS Topics ----------------- In this first example, we'll create a Chalice application that will call our Lambda function whenever a message is published to an `SNS Topic `__. First, we'll create an SNS topic. This is what we'll connect to our Lambda function:: $ aws sns create-topic --name MyDemoTopic { "TopicArn": "arn:aws:sns:us-west-2:12345:MyDemoTopic" } Be sure to save the ``TopicArn`` value for later. In this example that would be ``arn:aws:sns:us-west-2:12345:MyDemoTopic``. Next, we'll update the ``app.py`` to create a lambda function that connects to an SNS topic: .. code-block:: python from chalice import Chalice app = Chalice(app_name='chalice-sns-demo', debug=True) @app.on_sns_message(topic='MyDemoTopic') def handle_sns_message(event): app.log.debug("Received message with subject: %s, message: %s", event.subject, event.message) In the code above, we're using the ``@app.on_sns_message()`` decorator to connect the SNS topic named ``MyDemoTopic`` with the ``handle_sns_message`` function. Note that we're using the name of the topic and not the ``TopicArn``. Now we can deploy our chalice app:: $ chalice deploy Creating deployment package. Creating IAM role: chalice-demo-sns-dev Creating lambda function: chalice-demo-sns-dev-handle_sns_message Subscribing chalice-demo-sns-dev-handle_sns_message to SNS topic my-demo-topic Resources deployed: - Lambda ARN: arn:aws:lambda:us-west-2:123:function:... Now we can test our app by publishing a few SNS messages to our topic. :: $ aws sns publish --topic-arn arn:aws:sns:us-west-2:12345:MyDemoTopic \ --subject TestSubject --message TestMessage { "MessageId": "abcdefgh-3e56-54bd-a471-72477b5388af" } $ aws sns publish --topic-arn arn:aws:sns:us-west-2:12345:MyDemoTopic \ --subject TestSubject2 --message TestMessage2 { "MessageId": "abcdefgh-3e56-54bd-a471-72477b5388ag" } We should now see log messages showing that our Lambda function was invoked. We can wait for the messages using the ``chalice logs`` command. :: $ chalice logs --follow -n handle_sns_message ... 217378 chalice-sns-demo - DEBUG - Received message with subject: TestSubject, message: TestMessage ... 217378 chalice-sns-demo - DEBUG - Received message with subject: TestSubject2, message: TestMessage2 Next Steps ---------- In addition to SNS, chalice supports other event sources including Amazon S3, Amazon SQS, as well as scheduled events. You can check out the topic guide on :doc:`../topics/events` for more details. Cleaning Up ----------- Once you're done experimenting you can clean up by deleting the Chalice app and deleting the SNS topic:: $ chalice delete Deleting function: arn:aws:lambda:us-west-2:21345:function:chalice-sns-demo... Deleting IAM role: chalice-sns-demo-dev $ aws sns delete-topic --topic-arn arn:aws:sns:us-west-2:12345:MyDemoTopic ================================================ FILE: docs/source/tutorials/index.rst ================================================ Tutorials ========= These step-by-step tutorials show you how to use various features of Chalice. These are perfect if you're new to Chalice and want to learn what Chalice can do. If you want more complete, real-world examples, you can check out our :doc:`../samples/index`. Rest API Tutorials ------------------ :doc:`basicrestapi` This tutorial walks you through creating a REST API in Chalice. It covers features such as routing, URL parameters, error handling, etc. :doc:`customdomain` In this tutorial, we show you how to configure a REST API with your own custom domain name. .. _websocket-tutorial: Websocket Tutorials ------------------- :doc:`wsecho` Learn the basics of creating websocket APIs in Chalice. This tutorial creates an echo server that echoes back any message that the client sends to the websocket server. :doc:`wschat` In this more complete example, learn how to create a basic chat application based on websockets. Event Source Tutorials ---------------------- :doc:`events` This tutorial shows you how to create an event handler that's triggered whenever a message is published to an SNS topic. .. toctree:: :hidden: :glob: * AWS CDK Tutorials ----------------- :doc:`cdk` This tutorial walks you through creating a REST API with a DynamoDB data store that's deployed using the AWS CDK. It shows you how you can combine the APIs of Chalice with CDK construct APIs ================================================ FILE: docs/source/tutorials/wschat.rst ================================================ Chat Server Example =================== .. note:: This example is for illustration purposes and does not represent best practices. A simple chat server example application. This example will walk through deploying a chat application with separate chat rooms and nicknames. It uses a DynamoDB table to store state like connection IDs between websocket messages. First install a copy of Chalice in a fresh environment, create a new project and cd into the directory:: $ pip install -U chalice $ chalice new-project chalice-chat-example $ cd chalice-chat-example Our Chalice application will need boto3 as a dependency for both DynamoDB access and in order to communicate back with API Gateway to send websocket messages. Let's add a boto3 to the ``requirements.txt`` file:: $ echo "boto3>=1.9.91" > requirements.txt Now that the requirement has been added. Let's install it locally since our next script will need it as well:: $ pip install -r requirements.txt Unlike our previous example where we used ``chalice deploy``, we will use ``chalice package`` to create a CloudFormation template. The AWS CLI will be used to deploy the template. To install the AWS CLI run the command:: $ pip install -U awscli Starting in Chalice 1.10, the package command has a ``--merge-template`` argument that allows us to merge in a custom JSON file to the generated CloudFormation template. Since Chalice does not have any built-in support for DynamoDB currently, we will make a ``resources.json`` file with the DynamoDB definition. The template file will set the environment variable TABLE in all our Lambda functions as a CloudFormatiion reference to the DynamoDB table. Finally, the template will also override our IAM policy with a custom one to allow all the DynamoDB operations our application will need. Below is the JSON file that contains all of our custom Cloudformation. .. code-block:: json :caption: resources.json { "Resources": { "ChaliceChatTable": { "Type": "AWS::DynamoDB::Table", "Properties": { "AttributeDefinitions": [ { "AttributeName": "PK", "AttributeType": "S" }, { "AttributeName": "SK", "AttributeType": "S" } ], "KeySchema": [ { "AttributeName": "PK", "KeyType": "HASH" }, { "AttributeName": "SK", "KeyType": "RANGE" } ], "GlobalSecondaryIndexes": [ { "IndexName": "ReverseLookup", "KeySchema": [ { "AttributeName": "SK", "KeyType": "HASH" }, { "AttributeName": "PK", "KeyType": "RANGE" } ], "Projection": { "ProjectionType": "ALL" }, "ProvisionedThroughput": { "ReadCapacityUnits": 1, "WriteCapacityUnits": 1 } } ], "ProvisionedThroughput": { "ReadCapacityUnits": 1, "WriteCapacityUnits": 1 }, "TableName": "ChaliceChat" } }, "WebsocketConnect": { "Properties": { "Environment": { "Variables": { "TABLE": { "Ref": "ChaliceChatTable" } } } } }, "WebsocketMessage": { "Properties": { "Environment": { "Variables": { "TABLE": { "Ref": "ChaliceChatTable" } } } } }, "WebsocketDisconnect": { "Properties": { "Environment": { "Variables": { "TABLE": { "Ref": "ChaliceChatTable" } } } } }, "DefaultRole": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { "Version": "2012-10-17", "Statement": [ { "Sid": "", "Effect": "Allow", "Principal": { "Service": "lambda.amazonaws.com" }, "Action": "sts:AssumeRole" } ] }, "Policies": [ { "PolicyName": "DefaultRolePolicy", "PolicyDocument": { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents" ], "Resource": "arn:aws:logs:*:*:*" }, { "Effect": "Allow", "Action": [ "execute-api:ManageConnections" ], "Resource": "arn:aws:execute-api:*:*:*/@connections/*" }, { "Effect": "Allow", "Action": [ "dynamodb:DeleteItem", "dynamodb:PutItem", "dynamodb:GetItem", "dynamodb:UpdateItem", "dynamodb:Query", "dynamodb:Scan" ], "Resource": [ { "Fn::Sub": "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${ChaliceChatTable}" }, { "Fn::Sub": "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${ChaliceChatTable}/index/ReverseLookup" } ] } ] } } ] } } } } The current directory layout should now look like this:: $ tree -a . . ├── .chalice │   └── config.json ├── .gitignore ├── app.py ├── resources.json └── requirements.txt 1 directory, 5 files Next let's fill out the ``app.py`` file since it is pretty simple. Most of this example code is contained in the ``chalicelib/`` directory. .. code-block:: python :caption: chalice-chat-example/app.py from boto3.session import Session from chalice import Chalice from chalicelib import Storage from chalicelib import Sender from chalicelib import Handler app = Chalice(app_name="chalice-chat-example") app.websocket_api.session = Session() app.experimental_feature_flags.update([ 'WEBSOCKETS' ]) STORAGE = Storage.from_env() SENDER = Sender(app, STORAGE) HANDLER = Handler(STORAGE, SENDER) @app.on_ws_connect() def connect(event): STORAGE.create_connection(event.connection_id) @app.on_ws_disconnect() def disconnect(event): STORAGE.delete_connection(event.connection_id) @app.on_ws_message() def message(event): HANDLER.handle(event.connection_id, event.body) Similar to the previous example. We need to use ``boto3`` to construct a Session and pass it to ``app.websocket_api.session``. We opt into the usage of the ``WEBSOCKET`` experimental feature. Most of the actual work is done in some classes that we import from ``chalicelib/``. These classes are detailed below, and the various parts are explained in comments and doc strings. In addition to the previous example, we register a handler for ``on_ws_connect`` and ``on_ws_disconnect`` to handle events from API gateway when a new socket is trying to connect, or an existing socket is disconnected. Finally before being able to deploy and test the app out, we need to fill out the chalicelib directory. This is the bulk of the app and it is explained inline in comments. Create a new directory called ``chalicelib`` and inside that directory create an ``__init__.py`` file and fill it out with the following file. .. code-block:: python :caption: chalice-chat-example/chalicelib/__init__.py import os import boto3 from boto3.dynamodb.conditions import Key from chalice import WebsocketDisconnectedError class Storage(object): """An abstraction to interact with the DynamoDB Table.""" def __init__(self, table): """Initialize Storage object :param table: A boto3 dynamodb Table resource object. """ self._table = table @classmethod def from_env(cls): """Create table from the environment. The environment variable TABLE is present for a deployed application since it is set in all of the Lambda functions by a CloudFormation reference. We default to '', which will happen when we run ``chalice package`` since it loads the application, and no environment variable has been set. For local testing, a value should be manually set in the environment if '' will not suffice. """ table_name = os.environ.get('TABLE', '') table = boto3.resource('dynamodb').Table(table_name) return cls(table) def create_connection(self, connection_id): """Create a new connection object in the dtabase. When a new connection is created, we create a stub for it in the table. The stub uses a primary key of the connection_id and a sort key of username_. This translates to a connection with an unset username. The first message sent over the wire from the connection is to be used as the username, and this entry will be re-written. :param connection_id: The connection id to write to the table. """ self._table.put_item( Item={ 'PK': connection_id, 'SK': 'username_', }, ) def set_username(self, connection_id, old_name, username): """Set the username. The SK entry that goes with this connection id that starts with username_ is taken to be the username. The previous entry needs to be deleted, and a new entry needs to be written. :param connection_id: Connection id of the user trying to change their name. :param old_name: The original username. Since this is part of the key, it needs to be deleted and re-created rather than updated. :param username: The new username the user wants. """ self._table.delete_item( Key={ 'PK': connection_id, 'SK': 'username_%s' % old_name, }, ) self._table.put_item( Item={ 'PK': connection_id, 'SK': 'username_%s' % username, }, ) def list_rooms(self): """Get a list of all rooms that exist. Scan through the table looking for SKs that start with room_ which indicates a room that a user is in. Collect a unique set of those and return them. """ r = self._table.scan() rooms = set([item['SK'].split('_', 1)[1] for item in r['Items'] if item['SK'].startswith('room_')]) return rooms def set_room(self, connection_id, room): """Set the room a user is currently in. The room a user is in is in the form of an SK that starts with room_ prefix. :param connection_id: The connection id to move to a room. :param room: The room name to join. """ self._table.put_item( Item={ 'PK': connection_id, 'SK': 'room_%s' % room, }, ) def remove_room(self, connection_id, room): """Remove a user from a room. The room a user is in is in the form of an SK that starts with room_ prefix. To leave a room we need to delete this entry. :param connection_id: The connection id to move to a room. :param room: The room name to join. """ self._table.delete_item( Key={ 'PK': connection_id, 'SK': 'room_%s' % room, }, ) def get_connection_ids_by_room(self, room): """Find all connection ids that go to a room. This is needed whenever we broadcast to a room. We collect all their connection ids so we can send messages to them. We use a ReverseLookup table here which inverts the PK, SK relationship creating a partition called room_{room}. Everything in that partition is a connection in the room. :param room: Room name to get all connection ids from. """ r = self._table.query( IndexName='ReverseLookup', KeyConditionExpression=( Key('SK').eq('room_%s' % room) ), Select='ALL_ATTRIBUTES', ) return [item['PK'] for item in r['Items']] def delete_connection(self, connection_id): """Delete a connection. Called when a connection is disconnected and all its entries need to be deleted. :param connection_id: The connection partition to delete from the table. """ try: r = self._table.query( KeyConditionExpression=( Key('PK').eq(connection_id) ), Select='ALL_ATTRIBUTES', ) for item in r['Items']: self._table.delete_item( Key={ 'PK': connection_id, 'SK': item['SK'], }, ) except Exception as e: print(e) def get_record_by_connection(self, connection_id): """Get all the properties associated with a connection. Each connection_id creates a partition in the table with multiple SK entries. Each SK entry is in the format {property}_{value}. This method reads all those records from the database and puts them all into dictionary and returns it. :param connection_id: The connection to get properties for. """ r = self._table.query( KeyConditionExpression=( Key('PK').eq(connection_id) ), Select='ALL_ATTRIBUTES', ) r = { entry['SK'].split('_', 1)[0]: entry['SK'].split('_', 1)[1] for entry in r['Items'] } return r class Sender(object): """Class to send messages over websockets.""" def __init__(self, app, storage): """Initialize a sender object. :param app: A Chalice application object. :param storage: A Storage object. """ self._app = app self._storage = storage def send(self, connection_id, message): """Send a message over a websocket. :param connection_id: API Gateway Connection ID to send a message to. :param message: The message to send to the connection. """ try: # Call the chalice websocket api send method self._app.websocket_api.send(connection_id, message) except WebsocketDisconnectedError as e: # If the websocket has been closed, we delete the connection # from our database. self._storage.delete_connection(e.connection_id) def broadcast(self, connection_ids, message): """"Send a message to multiple connections. :param connection_id: A list of API Gateway Connection IDs to send the message to. :param message: The message to send to the connections. """ for cid in connection_ids: self.send(cid, message) class Handler(object): """Handler object that handles messages received from a websocket. This class implements the bulk of our app behavior. """ def __init__(self, storage, sender): """Initialize a Handler object. :param storage: Storage object to interact with database. :param sender: Sender object to send messages to websockets. """ self._storage = storage self._sender = sender # Command table to translate a string command name into a # method to call. self._command_table = { 'help': self._help, 'nick': self._nick, 'join': self._join, 'room': self._room, 'quit': self._quit, 'ls': self._list, } def handle(self, connection_id, message): """Entry point for our application. :param connection_id: Connection id that the message came from. :param message: Message we got from the connection. """ # First look the user up in the database and get a record for it. record = self._storage.get_record_by_connection(connection_id) if record['username'] == '': # If the user does not have a username, we assume that the message # is the username they want and we call _handle_login_message. self._handle_login_message(connection_id, message) else: # Otherwise we assume the user is logged in. So we call # a method to handle the message. We pass along the # record we loaded from the database so we don't need to # again. self._handle_message(connection_id, message, record) def _handle_login_message(self, connection_id, message): """Handle a login message. The message is the username to give the user. Re-write the database entry for this user to reset their username from '' to {message}. Once that is done send a message back to the user to confirm the name choice. Also send a /help prompt. """ self._storage.set_username(connection_id, '', message) self._sender.send( connection_id, 'Using nickname: %s\nType /help for list of commands.' % message ) def _handle_message(self, connection_id, message, record): """"Handle a message from a connected and logged in user. If the message starts with a / it's a command. Otherwise its a text message to send to all rooms in the room. :param connection_id: Connection id that the message came from. :param message: Message we got from the connection. :param record: A data record about the sender. """ if message.startswith('/'): self._handle_command(connection_id, message[1:], record) else: self._handle_text(connection_id, message, record) def _handle_command(self, connection_id, message, record): """Handle a command message. Check the command name and look it up in our command table. If there is an entry, we call that method and pass along the connection_id, arguments, and the loaded record. :param connection_id: Connection id that the message came from. :param message: Message we got from the connection. :param record: A data record about the sender. """ args = message.split(' ') command_name = args.pop(0).lower() command = self._command_table.get(command_name) if command: command(connection_id, args, record) else: # If no command method is found, send an error message # back to the user. self._sender( connection_id, 'Unknown command: %s' % command_name) def _handle_text(self, connection_id, message, record): """Handle a raw text message. :param connection_id: Connection id that the message came from. :param message: Message we got from the connection. :param record: A data record about the sender. """ if 'room' not in record: # If the user is not in a room send them an error message # and return early. self._sender.send( connection_id, 'Cannot send message if not in chatroom.') return # Collect a list of connection_ids in the same room as the message # sender. connection_ids = self._storage.get_connection_ids_by_room( record['room']) # Prefix the message with the sender's name. message = '%s: %s' % (record['username'], message) # Broadcast the new message to everyone in the room. self._sender.broadcast(connection_ids, message) def _help(self, connection_id, _message, _record): """Send the help message. Build a help message and send back to the same connection. :param connection_id: Connection id that the message came from. """ self._sender.send( connection_id, '\n'.join([ 'Commands available:', ' /help', ' Display this message.', ' /join {chat_room_name}', ' Join a chatroom named {chat_room_name}.', ' /nick {nickname}', ' Change your name to {nickname}. If no {nickname}', ' is provided then your current name will be printed', ' /room', ' Print out the name of the room you are currently ', ' in.', ' /ls', ' If you are in a room, list all users also in the', ' room. Otherwise, list all rooms.', ' /quit', ' Leave current room.', '', 'If you are in a room, raw text messages that do not start ', 'with a / will be sent to everyone else in the room.', ]), ) def _nick(self, connection_id, args, record): """Change or check nickname (username). :param connection_id: Connection id that the message came from. :param args: Argument list that came after the command. :param record: A data record about the sender. """ if not args: # If a nickname argument was not provided, we just want to # report the current nickname to the user. self._sender.send( connection_id, 'Current nickname: %s' % record['username']) return # The first argument is assumed to be the new desired nickname. nick = args[0] # Change the username from record['username'] to nick in the storage # layer. self._storage.set_username(connection_id, record['username'], nick) # Send a message to the requestor to confirm the nickname change. self._sender.send(connection_id, 'Nickname is: %s' % nick) # Get the room the user is in. room = record.get('room') if room: # If the user was in a room, announce to the room they have # changed their name. Don't send this me sage to the user since # they already got a name change message. room_connections = self._storage.get_connection_ids_by_room(room) room_connections.remove(connection_id) self._sender.broadcast( room_connections, '%s is now known as %s.' % (record['username'], nick)) def _join(self, connection_id, args, record): """Join a chat room. :param connection_id: Connection id that the message came from. :param args: Argument list. The first argument should be the name of the room to join. :param record: A data record about the sender. """ # Get the room name to join. room = args[0] # Call quit to leave the current room we are in if there is any. self._quit(connection_id, '', record) # Get a list of connections in the target chat room. room_connections = self._storage.get_connection_ids_by_room(room) # Join the target chat room. self._storage.set_room(connection_id, room) # Send a message to the requestor that they have joined the room. # At the same time send an announcement to everyone who was already # in the room to alert them of the new user. self._sender.send( connection_id, 'Joined chat room "%s"' % room) message = '%s joined room.' % record['username'] self._sender.broadcast(room_connections, message) def _room(self, connection_id, _args, record): """Report the name of the current room. :param connection_id: Connection id that the message came from. :param record: A data record about the sender. """ if 'room' in record: # If the user is in a room send them the name back. self._sender.send(connection_id, record['room']) else: # If the user is not in a room. Tell them so, and how to # join a room. self._sender.send( connection_id, 'Not currently in a room. Type /join {room_name} to do so.' ) def _quit(self, connection_id, _args, record): """Quit from a room. :param connection_id: Connection id that the message came from. :param record: A data record about the sender. """ if 'room' not in record: # If the user is not in a room there is nothing to do. return # Find the current room name, and delete that entry from # the database. room_name = record['room'] self._storage.remove_room(connection_id, room_name) # Send a message to the user to inform them they left the room. self._sender.send( connection_id, 'Left chat room "%s"' % room_name) # Tell everyone in the room that the user has left. self._sender.broadcast( self._storage.get_connection_ids_by_room(room_name), '%s left room.' % record['username'], ) def _list(self, connection_id, _args, record): """Show a context dependent listing. :param connection_id: Connection id that the message came from. :param record: A data record about the sender. """ room = record.get('room') if room: # If the user is in a room, get a listing of everyone # in the room. result = [ self._storage.get_record_by_connection(c_id)['username'] for c_id in self._storage.get_connection_ids_by_room(room) ] else: # If they are not in a room. Get a listing of all rooms # currently open. result = self._storage.list_rooms() # Send the result list back to the requestor. self._sender.send(connection_id, '\n'.join(result)) The final directory layout should be :: $ tree -a . . ├── .chalice │   ├── config.json ├── .gitignore ├── app.py ├── chalicelib │   └── __init__.py ├── resources.json └── requirements.txt 2 directories, 6 files Deploying our app with CloudFormation requires 3 steps. First we use Chalice to package our app into a JSON CloudFormation template:: $ chalice package --merge-template resources.json out This will result in a new directory called ``out`` being created, inside which there is a ``sam.json`` file. This template contains our Chalice app as a CloudFormation template, merged with our ``resources.json`` template. Next we use the AWS CLI to package this template, and prepare it for deployment. In order for this to work you will need to replace ``$BUCKET`` with the name of a bucket you control:: $ aws cloudformation package --template-file out/sam.json \ --s3-bucket $BUCKET --output-template-file out/template.yml Once this is complete, a new template should be located at ``out/template.yml`` this is the final CloudFormation template which is ready for deployment. Deploying it with the AWS CLI can be done with the following command:: $ aws cloudformation deploy --template-file out/template.yml \ --stack-name ChaliceChat --capabilities CAPABILITY_IAM This command should wait awhile, and once it exits the app should be ready. To get the websocket connection URL, we can use the AWS CLI again to check the stack output ``WebsocketConnectEndpointURL``:: $ aws cloudformation describe-stacks --stack-name ChaliceChat \ --query "Stacks[0].Outputs[?OutputKey=='WebsocketConnectEndpointURL'].OutputValue" \ --output text wss://{id}.execute-api.{region}.amazonaws.com/api/ Once deployed we can take the result of the previous command and connect to it using ``wsdump.py``. Below is a sample of two running clients, the first message sent to the server is used as the client's username. .. code-block:: bash :caption: client-1 $ wsdump.py wss://{id}.execute-api.{region}.amazonaws.com/api/ Press Ctrl+C to quit > John < Using nickname: John Type /help for list of commands. > /help < Commands available: /help Display this message. /join {chat_room_name} Join a chatroom named {chat_room_name}. /nick {nickname} Change your name to {nickname}. If no {nickname} is provided then your current name will be printed /room Print out the name of the room you are currently in. /ls If you are in a room, list all users also in the room. Otherwise, list all rooms. /quit Leave current room. If you are in a room, raw text messages that do not start with a / will be sent to everyone else in the room. > /join chalice < Joined chat room "chalice" < Jenny joined room. > Hi < John: Hi < Jenny is now known as JennyJones. > /quit < Left chat room "chalice" > /ls < chalice > Ctrl-D .. code-block:: bash :caption: client-2 $ wsdump.py wss://{id}.execute-api.{region}.amazonaws.com/api/ Press Ctrl+C to quit > Jenny < Using nickname: Jenny Type /help for list of commands. > /help < Commands available: /help Display this message. /join {chat_room_name} Join a chatroom named {chat_room_name}. /nick {nickname} Change your name to {nickname}. If no {nickname} is provided then your current name will be printed /room Print out the name of the room you are currently in. /ls If you are in a room, list all users also in the room. Otherwise, list all rooms. /quit Leave current room. If you are in a room, raw text messages that do not start with a / will be sent to everyone else in the room. > /join chalice < Joined chat room "chalice" > /ls < John Jenny < John: Hi > /nick JennyJones < Nickname is: JennyJones < John left room. > /ls < JennyJones > /room < chalice > /nick < Current nickname: JennyJones > Ctrl-D To delete the resources you can run use the AWS CLI to delete the stack:: $ aws cloudformation delete-stack --stack-name ChaliceChat ================================================ FILE: docs/source/tutorials/wsecho.rst ================================================ Echo Server Example =================== An echo server is a simple server that echos any message it receives back to the client that sent it. First install a copy of Chalice in a fresh environment, create a new project and cd into the directory:: $ pip install -U chalice $ chalice new-project echo-server $ cd echo-server Our Chalice application will need boto3 as a dependency for both API Gateway to send websocket messages. Let's add a boto3 to the ``requirements.txt`` file:: $ echo "boto3>=1.9.91" > requirements.txt Now that the requirement has been added. Let's install it locally since our next script will need it as well:: $ pip install -r requirements.txt Next replace the contents of the ``app.py`` file with the code below. .. code-block:: python :caption: app.py :linenos: from boto3.session import Session from chalice import Chalice from chalice import WebsocketDisconnectedError app = Chalice(app_name="echo-server") app.websocket_api.session = Session() app.experimental_feature_flags.update([ 'WEBSOCKETS' ]) @app.on_ws_message() def message(event): try: app.websocket_api.send( connection_id=event.connection_id, message=event.body, ) except WebsocketDisconnectedError as e: pass # Disconnected so we can't send the message back. Stepping through this app line by line, the first thing to note is that we need to import and instantiate a boto3 session. This session is manually assigned to ``app.websocket_api.session``. This is needed because in order to send websocket responses to API Gateway we need to construct a boto3 client. Chalice does not take a direct dependency on boto3 or botocore, so we need to provide the Session ourselves. .. code-block:: python from boto3.session import Session app.websocket_api.session = Session() Next we enable the experimental feature ``WEBSOCKETS``. Websockets are an experimental feature and are subject to API changes. This includes all aspects of the Websocket API exposted in Chalice. Including any public members of ``app.websocket_api``, and the three decorators ``on_ws_connect``, ``on_ws_message``, and ``on_ws_disconnect``. .. code-block:: python app.experimental_feature_flags.update([ 'WEBSOCKETS' ]) To register a websocket handler, and cause Chalice to deploy an API Gateway Websocket API we use the ``app.on_ws_message()`` decorator. The event parameter here is a wrapper object with some convenience parameters attached. The most useful are ``event.connection_id`` and ``event.body``. The ``connection_id`` is an API Gateway specific identifier that allows you to refer to the connection that sent the message. The ``body`` is the content of the message. .. code-block:: python @app.on_ws_message() def message(event): Since this is an echo server, the message handler simply reads the content it received on the socket, and rewrites it back to the same socket. To send a message to a socket we call ``app.websocket_api.send(connection_id, message)``. In this case, we just use the same ``connection_id`` we got the message from, and use the ``body`` we got from the event as the ``message`` to send. .. code-block:: python app.websocket_api.send( connection_id=event.connection_id, message=event.body, ) Finally, we catch the exception ``WebsocketDisconnectedError`` which is raised by ``app.websocket_api.send`` if the provided ``connection_id`` is not connected anymore. In our case this doesn't really matter since we don't have anything tracking our connections. The error has a ``connection_id`` property that contains the offending connection id. .. code-block:: python except WebsocketDisconnectedError as e: pass # Disconnected so we can't send the message back. Now that we understand the code, lets deploy it with ``chalice deploy``:: $ chalice deploy Creating deployment package. Creating IAM role: echo-server-dev Creating lambda function: echo-server-dev-websocket_message Creating websocket api: echo-server-dev-websocket-api Resources deployed: - Lambda ARN: arn:aws:lambda:region:0123456789:function:echo-server-dev-websocket_message - Websocket API URL: wss://{websocket_api_id}.execute-api.region.amazonaws.com/api/ To test out the echo server we will use the ``websocket-client`` package. You install it from PyPI:: $ pip install websocket-client After deploying the Chalice app the output will contain a URL for connecting to the websocket API labeled: ``- Websocket API URL:``. The ``websocket-client`` package installs a command line tool called ``wsdump.py`` which can be used to test websocket echo server:: $ wsdump wss://{websocket_api_id}.execute-api.region.amazonaws.com/api/ Press Ctrl+C to quit > foo < foo > bar < bar > foo bar baz < foo bar baz > Every message sent to the server (lines that start with ``>``) result in a message sent to us (lines that start with ``<``) with the same content. If something goes wrong, you can check the chalice error logs using the following command:: $ chalice logs -n websocket_message .. note:: If you encounter an Internal Server Error here it is likely that you forgot to include ``boto3>=1.9.91`` in the ``requirements.txt`` file. To tear down the example. Just run:: $ chalice delete Deleting Websocket API: {websocket_api_id} Deleting function: arn:aws:lambda:us-west-2:0123456789:function:echo-server-dev-websocket_message Deleting IAM role: echo-server-dev Next Steps ---------- In this tutorial, we created an echo server with websockets. If you'd like to try something more ambitious, you can follow our tutorial for creating a sample :doc:`Chat application with websocket `. ================================================ FILE: docs/source/upgrading.rst ================================================ Upgrade Notes ============= This document provides additional documentation on upgrading your version of chalice. If you're just interested in the high level changes, see the `CHANGELOG.md `__) file. .. _v1-2-0: 1.2.0 ----- This release features a rewrite of the Chalice deployer (`#604 `__). This is a backwards compatible change, and should not have any noticeable changes with deployments with the exception of fixing deployer bugs (e.g. https://github.com/aws/chalice/issues/604). This code path affects the ``chalice deploy``, ``chalice delete``, and ``chalice package`` commands. While this release is backwards compatible, you will notice several changes when you upgrade to version 1.2.0. The output of ``chalice deploy`` has changed in order to give more details about the resources it creates along with a more detailed summary at the end:: $ chalice deploy Creating deployment package. Creating IAM role: myapp-dev Creating lambda function: myapp-dev-foo Creating lambda function: myapp-dev Creating Rest API Resources deployed: - Lambda ARN: arn:aws:lambda:us-west-2:12345:function:myapp-dev-foo - Lambda ARN: arn:aws:lambda:us-west-2:12345:function:myapp-dev - Rest API URL: https://abcd.execute-api.us-west-2.amazonaws.com/api/ Also, the files used to store deployed values has changed. These files are used internally by the ``chalice deploy/delete`` commands and you typically do not interact with these files directly. It's mentioned here in case you notice new files in your ``.chalice`` directory. Note that these files are *not* part of the public interface of Chalice and are documented here for completeness and to help with debugging issues. In versions < 1.2.0, the value of deployed resources was stored in ``.chalice/deployed.json`` and looked like this:: { "dev": { "region": "us-west-2", "api_handler_name": "demoauth4-dev", "api_handler_arn": "arn:aws:lambda:us-west-2:123:function:myapp-dev", "rest_api_id": "abcd", "lambda_functions": { "myapp-dev-foo": { "type": "pure_lambda", "arn": "arn:aws:lambda:us-west-2:123:function:myapp-dev-foo" } }, "chalice_version": "1.1.1", "api_gateway_stage": "api", "backend": "api" }, "prod": {...} } In version 1.2.0, the deployed resources are split into multiple files, one file per chalice stage. These files are in the ``.chalice/deployed/``, so if you had a dev and a prod chalice stage you'd have ``.chalice/deployed/dev.json`` and ``.chalice/deployed/prod.json``. The schema has also changed and looks like this:: $ cat .chalice/deployed/dev.json { "schema_version": "2.0", "resources": [ { "role_name": "myapp-dev", "role_arn": "arn:aws:iam::123:role/myapp-dev", "name": "default-role", "resource_type": "iam_role" }, { "lambda_arn": "arn:aws:lambda:us-west-2:123:function:myapp-dev-foo", "name": "foo", "resource_type": "lambda_function" }, { "lambda_arn": "arn:aws:lambda:us-west-2:123:function:myapp-dev", "name": "api_handler", "resource_type": "lambda_function" }, { "name": "rest_api", "rest_api_id": "abcd", "rest_api_url": "https://abcd.execute-api.us-west-2.amazonaws.com/api", "resource_type": "rest_api" } ], "backend": "api" } When you run ``chalice deploy`` for the first time after upgrading to version 1.2.0, chalice will automatically converted ``.chalice/deployed.json`` over to the format as you deploy a given stage. .. warning:: Once you upgrade to 1.2.0, chalice will only update the new ``.chalice/deployed/.json``. This means you cannot downgrade to earlier versions of chalice unless you manually update ``.chalice/deployed.json`` as well. The ``chalice package`` command has also been updated to use the deployer. This results in several changes compared to the previous version: * Pure lambdas are supported * Scheduled events are supported * Parity between the behavior of ``chalice deploy`` and ``chalice package`` As part of this change, the CFN resource names have been updated to use ``CamelCase`` names. Previously, chalice converted your python function names to CFN resource names by removing all non alphanumeric characters and appending an md5 checksum, e.g ``my_function -> myfunction3bfc``. With this new packager update, the resource name would be converted as ``my_function -> MyFunction``. Note, the ``Outputs`` section renames unchanged in order to preserve backwards compatibility. In order to fix parity issues with ``chalice deploy`` and ``chalice package``, we now explicitly create an IAM role resource as part of the default configuration. .. _v1-0-0b2: 1.0.0b2 ------- The url parameter names and the function argument names must match. Previously, the routing code would use positional args ``handler(*args)`` to invoke a view function. In this version, kwargs are now used instead: ``handler(**view_args)``. For example, this code will no longer work: .. code-block:: python @app.route('/{a}/{b}') def myview(first, second) return {} The example above must be updated to: .. code-block:: python @app.route('/{a}/{b}') def myview(a, b) return {} Now that functions are invoked with kwargs, the order doesn't matter. You may also write the above view function as: .. code-block:: python @app.route('/{a}/{b}') def myview(b, a) return {} This was done to have consistent behavior with other web frameworks such as Flask. .. _v1-0-0b1: 1.0.0b1 ------- The ``Chalice.define_authorizer`` method has been removed. This has been deprecated since v0.8.1. See :doc:`topics/authorizers` for updated information on configuring authorizers in Chalice as well as the original deprecation notice in the :ref:`v0-8-1` upgrade notes. The optional deprecated positional parameter in the ``chalice deploy`` command for specifying the API Gateway stage has been removed. If you want to specify the API Gateway stage, you can use the ``--api-gateway-stage`` option in the ``chalice deploy`` command:: # Deprecated and removed in 1.0.0b1 $ chalice deploy prod # Equivalent and updated way to specify an API Gateway stage: $ chalice deploy --api-gateway-stage prod .. _v0-9-0: 0.9.0 ----- The 0.9.0 release changed the type of ``app.current_request.raw_body`` to always be of type ``bytes()``. This only affects users that were using python3. Previously you would get a type ``str()``, but with the introduction of `binary content type support `__, the ``raw_body`` attribute was made to consistently be of type ``bytes()``. .. _v0-8-1: 0.8.1 ----- The 0.8.1 changed the preferred way of specifying authorizers for view functions. You now specify either an instance of ``chalice.CognitoUserPoolAuthorizer`` or ``chalice.CustomAuthorizer`` to an ``@app.route()`` function using the ``authorizer`` argument. Deprecated: .. code-block:: python @app.route('/user-pools', methods=['GET'], authorizer_name='MyPool') def authenticated(): return {"secure": True} app.define_authorizer( name='MyPool', header='Authorization', auth_type='cognito_user_pools', provider_arns=['arn:aws:cognito:...:userpool/name'] ) Equivalent, and preferred way .. code-block:: python from chalice import CognitoUserPoolAuthorizer authorizer = CognitoUserPoolAuthorizer( 'MyPool', header='Authorization', provider_arns=['arn:aws:cognito:...:userpool/name']) @app.route('/user-pools', methods=['GET'], authorizer=authorizer) def authenticated(): return {"secure": True} The ``define_authorizer`` is still available, but is now deprecated and will be removed in future versions of chalice. You can also use the new ``authorizer`` argument to provider a ``CustomAuthorizer``: .. code-block:: python from chalice import CustomAuthorizer authorizer = CustomAuthorizer( 'MyCustomAuth', header='Authorization', authorizer_uri=('arn:aws:apigateway:region:lambda:path/2015-03-01' '/functions/arn:aws:lambda:region:account-id:' 'function:FunctionName/invocations')) @app.route('/custom-auth', methods=['GET'], authorizer=authorizer) def authenticated(): return {"secure": True} .. _v0-7-0: 0.7.0 ----- The 0.7.0 release adds several major features to chalice. While the majority of these features are introduced in a backwards compatible way, there are a few backwards incompatible changes that were made in order to support these new major features. Separate Stages ~~~~~~~~~~~~~~~ Prior to this version, chalice had a notion of a "stage" that corresponded to an API gateway stage. You can create and deploy a new API gateway stage by running ``chalice deploy ``. In 0.7.0, stage support was been reworked such that a chalice stage is a completely separate set of AWS resources. This means that if you have two chalice stages, say ``dev`` and ``prod``, then you will have two separate sets of AWS resources, one set per stage: * Two API Gateway Rest APIs * Two separate Lambda functions * Two separate IAM roles The :doc:`topics/stages` doc has more details on the new chalice stages feature. This section highlights the key differences between the old stage behavior and the new chalice stage functionality in 0.7.0. In order to ease transition to this new model, the following changes were made: * A new ``--stage`` argument was added to the ``deploy``, ``logs``, ``url``, ``generate-sdk``, and ``package`` commands. If this value is specified and the stage does not exist, a new chalice stage with that name will be created for you. * The existing form ``chalice deploy `` has been deprecated. The command will still work in version 0.7.0, but a deprecation warning will be printed to stderr. * If you want the pre-existing behavior of creating a new API gateway stage (while using the same Lambda function), you can use the ``--api-gateway-stage`` argument. This is the replacement for the deprecated form ``chalice deploy ``. * The default stage if no ``--stage`` option is provided is ``dev``. By defaulting to a ``dev`` stage, the pre-existing behavior of not specifying a stage name, e.g ``chalice deploy``, ``chalice url``, etc. will still work exactly the same. * A new ``stages`` key is supported in the ``.chalice/config.json``. This allows you to specify configuration specific to a chalice stage. See the :doc:`topics/configfile` doc for more information about stage specific configuration. * Setting ``autogen_policy`` to false will result in chalice looking for a IAM policy file named ``.chalice/policy-.json``. Previously it would look for a file named ``.chalice/policy.json``. You can also explicitly set this value to In order to ease transition, chalice will check for a ``.chalice/policy.json`` file when depoying to the ``dev`` stage. Support for ``.chalice/policy.json`` will be removed in future versions of chalice and users are encouraged to switch to the stage specific ``.chalice/policy-.json`` files. See the :doc:`topics/stages` doc for more details on the new chalice stages feature. **Note, the AWS resource names it creates now have the form ``-``, e.g. ``myapp-dev``, ``myapp-prod``.** We recommend using the new stage specific resource names. However, If you would like to use the existing resource names for a specific stage, you can create a ``.chalice/deployed.json`` file that specifies the existing values:: { "dev": { "backend": "api", "api_handler_arn": "lambda-function-arn", "api_handler_name": "lambda-function-name", "rest_api_id": "your-rest-api-id", "api_gateway_stage": "dev", "region": "your region (e.g us-west-2)", "chalice_version": "0.7.0", } } This file is discussed in the next section. Deployed Values ~~~~~~~~~~~~~~~ In version 0.7.0, the way deployed values are stored and retrieved has changed. In prior versions, only the ``lambda_arn`` was saved, and its value was written to the ``.chalice/config.json`` file. Any of other deployed values that were needed (for example the API Gateway rest API id) was dynamically queried by assuming the resource names matches the app name. In this version of chalice, a separate ``.chalice/deployed.json`` file is written on every deployement which contains all the resources that have been created. While this should be a transparent change, you may noticed issues if you run commands such as ``chalice url`` and ``chalice logs`` without first deploying. To fix this issue, run ``chalice deploy`` and version 0.7.0 of chalice so a ``.chalice/deployed.json`` will be created for you. Authorizer Changes ~~~~~~~~~~~~~~~~~~ **The ``authorizer_id`` and ``authorization_type`` args are no longer supported in ``@app.route(...)`` calls.** They have been replaced with an ``authorizer_name`` parameter and an ``app.define_authorizer`` method. This version changed the internals of how an API gateway REST API is created. Prior to 0.7.0, the AWS SDK for Python was used to make the appropriate service API calls to API gateway include ``create_rest_api`` and ``put_method / put_method_response`` for each route. In version 0.7.0, this internal mechanism was changed to instead generate a swagger document. The rest api is then created or updated by calling ``import_rest_api`` or ``put_rest_api`` and providing the swagger document. This simplifies the internals and also unifies the code base for the newly added ``chalice package`` command (which uses a swagger document internally). One consequence of this change is that the entire REST API must be defined in the swagger document. With the previous ``authorizer_id`` parameter, you would create/deploy a rest api, create your authorizer, and then provide that ``authorizer_id`` in your ``@app.route`` calls. Now they must be defined all at once in the ``app.py`` file: .. code-block:: python app = chalice.Chalice(app_name='demo') @app.route('/auth-required', authorizer_name='MyUserPool') def foo(): return {} app.define_authorizer( name='MyUserPool', header='Authorization', auth_type='cognito_user_pools', provider_arns=['arn:aws:cognito:...:userpool/name'] ) .. _v0-6-0: 0.6.0 ----- This version changed how the internals of how API gateway resources are created by chalice. The integration type changed from ``AWS`` to ``AWS_PROXY``. This was to enable additional functionality, notable to allows users to provide non-JSON HTTP responses and inject arbitrary headers to the HTTP responses. While this change to the internals is primarily internal, there are several user-visible changes. * Uncaught exceptions with ``app.debug = False`` (the default value) will result in a more generic ``InternalServerError`` error. The previous behavior was to return a ``ChaliceViewError``. * When you enabled debug mode via ``app.debug = True``, the HTTP response will contain the python stack trace as the entire request body. This is to improve the readability of stack traces. For example:: $ http https://endpoint/dev/ HTTP/1.1 500 Internal Server Error Content-Length: 358 Content-Type: text/plain Traceback (most recent call last): File "/var/task/chalice/app.py", line 286, in __call__ response = view_function(*function_args) File "/var/task/app.py", line 12, in index return a() File "/var/task/app.py", line 16, in a return b() File "/var/task/app.py", line 19, in b raise ValueError("Hello, error!") ValueError: Hello, error! * Content type validation now has error responses that match the same error response format used for other chalice built in responses. Chalice was previously relying on API gateway to perform the content type validation. As a result of the ``AWS_PROXY`` work, this logic has moved into the chalice handler and now has a consistent error response:: $ http https://endpoint/dev/ 'Content-Type: text/plain' HTTP/1.1 415 Unsupported Media Type Content-Type: application/json { "Code": "UnsupportedMediaType", "Message": "Unsupported media type: text/plain" } * The keys in the ``app.current_request.to_dict()`` now match the casing used by the ``AWS_PPROXY`` lambda integration, which are ``lowerCamelCased``. This method is primarily intended for introspection purposes. ================================================ FILE: requirements-dev.in ================================================ -r requirements-test.in pylint<4.0.0 doc8<1.0.0 pydocstyle flake8 Sphinx==4.3.2 docutils mypy wheel pygments types-six types-python-dateutil types-PyYAML standard-imghdr ================================================ FILE: requirements-dev.txt ================================================ # # This file is autogenerated by pip-compile with Python 3.9 # by the following command: # # pip-compile --output-file=requirements-dev.txt requirements-dev.in # alabaster==0.7.13 # via sphinx astroid==3.3.10 # via pylint attrs==24.2.0 # via hypothesis babel==2.16.0 # via sphinx boto3==1.35.66 # via -r requirements-test.in botocore==1.35.66 # via # boto3 # s3transfer certifi==2024.8.30 # via requests charset-normalizer==3.4.0 # via requests coverage[toml]==7.6.1 # via # -r requirements-test.in # pytest-cov dill==0.3.9 # via pylint doc8==0.11.2 # via -r requirements-dev.in docutils==0.17.1 # via # -r requirements-dev.in # doc8 # restructuredtext-lint # sphinx exceptiongroup==1.2.2 # via # hypothesis # pytest flake8==7.1.1 # via -r requirements-dev.in hypothesis==6.113.0 # via -r requirements-test.in idna==3.10 # via requests imagesize==1.4.1 # via sphinx iniconfig==2.0.0 # via pytest isort==5.13.2 # via pylint jinja2==3.1.4 # via sphinx jmespath==1.0.1 # via # boto3 # botocore markupsafe==2.1.5 # via jinja2 mccabe==0.7.0 # via # flake8 # pylint mypy==1.13.0 # via -r requirements-dev.in mypy-extensions==1.0.0 # via mypy packaging==24.2 # via # pytest # sphinx pbr==6.1.0 # via stevedore platformdirs==4.3.6 # via pylint pluggy==1.5.0 # via pytest pycodestyle==2.12.1 # via flake8 pydocstyle==6.3.0 # via -r requirements-dev.in pyflakes==3.2.0 # via flake8 pygments==2.18.0 # via # -r requirements-dev.in # doc8 # sphinx pylint==3.3.7 # via -r requirements-dev.in pytest==8.3.3 # via # -r requirements-test.in # pytest-cov pytest-cov==5.0.0 # via -r requirements-test.in python-dateutil==2.9.0.post0 # via botocore requests==2.32.3 # via # -r requirements-test.in # sphinx restructuredtext-lint==1.4.0 # via doc8 s3transfer==0.10.4 # via boto3 six==1.16.0 # via python-dateutil snowballstemmer==2.2.0 # via # pydocstyle # sphinx sortedcontainers==2.4.0 # via hypothesis sphinx==4.3.2 # via -r requirements-dev.in sphinxcontrib-applehelp==1.0.4 # via sphinx sphinxcontrib-devhelp==1.0.2 # via sphinx sphinxcontrib-htmlhelp==2.0.1 # via sphinx sphinxcontrib-jsmath==1.0.1 # via sphinx sphinxcontrib-qthelp==1.0.3 # via sphinx sphinxcontrib-serializinghtml==1.1.5 # via sphinx standard-imghdr==3.13.0 # via -r requirements-dev.in stevedore==5.3.0 # via doc8 tomli==2.1.0 # via # coverage # mypy # pylint # pytest tomlkit==0.13.2 # via pylint types-python-dateutil==2.9.0.20241003 # via -r requirements-dev.in types-pyyaml==6.0.12.20240917 # via -r requirements-dev.in types-six==1.16.21.20241105 # via -r requirements-dev.in typing-extensions==4.12.2 # via # astroid # mypy # pylint urllib3==1.26.20 # via # botocore # requests websocket-client==1.8.0 # via -r requirements-test.in wheel==0.45.0 # via -r requirements-dev.in # The following packages are considered to be unsafe in a requirements file: # setuptools ================================================ FILE: requirements-test.in ================================================ pytest boto3<2.0.0 hypothesis coverage websocket-client<2.0.0 pytest-cov requests ================================================ FILE: requirements-test.txt ================================================ # # This file is autogenerated by pip-compile with Python 3.9 # by the following command: # # pip-compile --output-file=requirements-test.txt requirements-test.in # attrs==23.1.0 # via hypothesis boto3==1.33.13 # via -r requirements-test.in botocore==1.33.13 # via # boto3 # s3transfer certifi==2023.11.17 # via requests charset-normalizer==3.3.2 # via requests coverage[toml]==7.2.7 # via # -r requirements-test.in # pytest-cov exceptiongroup==1.2.0 # via # hypothesis # pytest hypothesis==6.79.4 # via -r requirements-test.in idna==3.6 # via requests iniconfig==2.0.0 # via pytest jmespath==1.0.1 # via # boto3 # botocore packaging==23.2 # via pytest pluggy==1.2.0 # via pytest pytest==7.4.3 # via # -r requirements-test.in # pytest-cov pytest-cov==4.1.0 # via -r requirements-test.in python-dateutil==2.8.2 # via botocore requests==2.31.0 # via -r requirements-test.in s3transfer==0.8.2 # via boto3 six==1.16.0 # via python-dateutil sortedcontainers==2.4.0 # via hypothesis tomli==2.0.1 # via # coverage # pytest urllib3==1.26.18 # via # botocore # requests websocket-client==1.6.1 # via -r requirements-test.in ================================================ FILE: scripts/gh-page-docs ================================================ #!/bin/bash # Run this from the rootdir of the repository: # # ./scripts/gh-page-docs # # This script will check the docs for errors, render them # to html, then copy them over to a local (separate) checkout # of this repo's gh-pages branch. # Do not run this with a virtualenv activated. This is intended # to be run in CI systems where you start with system python. set -e CHECKOUT_DIR="/tmp/chalice-gh-doc-build" VENV_DIR="/tmp/chalice-gh-doc-build-venv37" echo "Setting up environment" python3 -m venv $VENV_DIR source "${VENV_DIR}/bin/activate" echo echo which python3 python3 -c "import sys; print(sys.executable)" which pip3 echo echo python3 -m pip install -e . python3 -m pip install -r requirements-dev.txt # Don't allow docs to be deployed if there's any errors. echo "Linting docs and checking for errors" make doccheck echo "Building docs" cd docs make clean && make html echo echo "Copy docs to local checkout" rm -rf "${CHECKOUT_DIR}" git clone https://github.com/aws/chalice.git --branch gh-pages \ --single-branch ${CHECKOUT_DIR} rsync -av --delete --exclude '.git' build/html/ ${CHECKOUT_DIR}/ # Add a .nojekyll file so make sure we don't ignore _static # paths. touch ${CHECKOUT_DIR}/.nojekyll echo "Commiting docs." cd ${CHECKOUT_DIR} git add -A . git commit -am "Updating generated documentation" git remote add upstream git@github.com:aws/chalice.git echo "Docs are available at ${CHECKOUT_DIR}" # This step is usually handled by the CI system that has access # to the creds needed to push back to github. echo "Run 'git push upstream gh-pages' to deploy the docs to github pages" ================================================ FILE: scripts/release ================================================ #!/usr/bin/env python3 import os import re import subprocess import click ROOT_DIR = os.path.dirname( os.path.dirname(os.path.abspath(__file__)), ) @click.group() def cli(): """Command line tool for managing releases. To do a chalice release, run these commands:: \b $ NEXT_VERSION=$(jmeslog query next-version) $ scripts/release bump-version --version-number ${NEXT_VERSION} $ git add -A . $ git commit -m "Bumping version to $NEXT_VERSION" $ scripts/release tag-release $ scripts/release build-release $ git push upstream master --tags $ twine upload dist/chalice-* """ pass @cli.command('bump-version') @click.option('--version-number') def bump_version(version_number): """Update necessary files with next version number.""" print(f"Bumping version to: {version_number}") _create_new_changelog_release() for filename, replacer in get_files_to_change().items(): print("Bumping version in %s" % filename) with open(filename, 'r') as f: contents = f.read() if callable(replacer): new_contents = replacer(version_number, contents) else: new_contents = _regex_based_version_bump( version_number, replacer, contents) with open(filename, 'w') as f: f.write(new_contents) def _create_new_changelog_release(): # This takes everything from .changes/next-release/ and creates # a new release entry for them. subprocess.check_call(['jmeslog', 'new-release']) @cli.command('build-release') def build_release(): """Build sdist/whl files.""" original = os.getcwd() os.chdir(ROOT_DIR) try: subprocess.check_call( ['python', 'setup.py', 'sdist', 'bdist_wheel'] ) finally: os.chdir(original) @cli.command('tag-release') def tag_release(): """Create a git tag based on the current version number.""" # We're assuming that setup.py has already been updated # manually or using scripts/release/bump-version so the # current version in setup.py is the version number we should tag. version_number = get_current_version_number() click.echo("Tagging %s release" % version_number) subprocess.check_call( ['git', 'tag', '-a', version_number, '-m', 'Tagging %s release' % version_number], ) @cli.command('get-version') def get_version(): """Print the current version number in setup.py.""" click.echo(get_current_version_number()) def get_files_to_change(): # A mapping of all files that require version bumps. # You can either specify: # * Tuple[str, str] - regex to search, replacement string # * Callable[[str, str], str] - function to handle custom logic files_with_version_numbers = { 'chalice/app.py': ( "__version__: str = '.*'", "__version__: str = '{version}'"), 'CHANGELOG.md': update_changelog, 'docs/source/conf.py': update_doc_conf, 'setup.py': ("version='(.*)'", "version='{version}'"), } return files_with_version_numbers def _regex_based_version_bump(next_version_number, replacer, contents): regex = replacer[0] replacement = replacer[1].format(version=next_version_number) new_contents = re.sub(regex, replacement, contents) return new_contents def update_changelog(next_version_number, contents): output = subprocess.check_output(['jmeslog', 'render', '-t', 'changelog']) return output.decode('utf-8') def update_doc_conf(next_version_number, contents): # For the docs the 'version' is only X.Y # and the release is X.Y.Z version = '.'.join(next_version_number.split('.')[:2]) release = next_version_number new_contents = [] for line in contents.splitlines(): if line.startswith('version ='): new_contents.append("version = u'%s'" % version) elif line.startswith('release = '): new_contents.append("release = u'%s'" % release) else: new_contents.append(line) # Ensure the file ends with a newline. new_contents.append('') return '\n'.join(new_contents) def get_next_version_number(release_type): # Returns a string like '1.0.0'. current = get_current_version_number() # Convert to a list of ints: [1, 0, 0]. version_parts = list(int(i) for i in current.split('.')) # We've already validated that release_type is from a fixed # list of choices so we know it's going to be one of these. # We only support integer version parts, which shouldn't be # a problem now that we're post 1.0. if release_type == 'patch': version_parts[-1] += 1 elif release_type == 'minor': version_parts[1] += 1 version_parts[-1] = 0 return '.'.join(str(i) for i in version_parts) def get_current_version_number(): # We can avoid executing setup.py because we know # specifically how the version is hardcoded in the setup.py file. # This won't work for the general case. regex = re.compile("version='(.*)',") with open(os.path.join(ROOT_DIR, 'setup.py')) as f: for line in f: match = regex.search(line) if match is not None: return match.groups()[0] raise RuntimeError("Could not find version number from setup.py") def main(): return cli() if __name__ == '__main__': main() ================================================ FILE: setup.cfg ================================================ [mypy] [mypy-chalice.vendored.*] ignore_errors = true [mypy-chalice.templates.*] ignore_errors = true ================================================ FILE: setup.py ================================================ #!/usr/bin/env python import os from setuptools import setup, find_packages with open('README.rst') as readme_file: README = readme_file.read() def recursive_include(relative_dir): all_paths = [] root_prefix = os.path.join( os.path.dirname(os.path.abspath(__file__)), 'chalice') full_path = os.path.join(root_prefix, relative_dir) for rootdir, _, filenames in os.walk(full_path): for filename in filenames: abs_filename = os.path.join(rootdir, filename) all_paths.append(abs_filename[len(root_prefix) + 1:]) return all_paths install_requires = [ 'click>=7,<9.0', 'botocore>=1.14.0,<2.0.0', 'six>=1.10.0,<2.0.0', 'pip>=9,<25.1', 'jmespath>=0.9.3,<2.0.0', 'pyyaml>=5.3.1,<7.0.0', 'inquirer>=3.0.0,<4.0.0', 'wheel', 'setuptools' ] setup( name='chalice', version='1.32.0', description="Microframework", long_description=README, author="James Saryerwinnie", author_email='js@jamesls.com', url='https://github.com/aws/chalice', packages=find_packages(exclude=['tests', 'tests.*']), install_requires=install_requires, extras_require={ 'event-file-poller': ['watchdog==2.3.1'], 'cdk': [ 'aws_cdk.aws_iam>=1.85.0,<2.0', 'aws_cdk.aws-s3-assets>=1.85.0,<2.0', 'aws_cdk.cloudformation-include>=1.85.0,<2.0', 'aws_cdk.core>=1.85.0,<2.0', ], 'cdkv2': ["aws-cdk-lib>2.0,<3.0"] }, license="Apache License 2.0", package_data={'chalice': [ '*.json', '*.pyi', 'py.typed'] + recursive_include('templates')}, include_package_data=True, zip_safe=False, keywords='chalice', entry_points={ 'console_scripts': [ 'chalice = chalice.cli:main', ] }, classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'License :: OSI Approved :: Apache Software License', 'Natural Language :: English', "Programming Language :: Python :: 3", 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: 3.13', ], ) ================================================ FILE: tests/__init__.py ================================================ ================================================ FILE: tests/aws/__init__.py ================================================ ================================================ FILE: tests/aws/conftest.py ================================================ DEPLOY_TEST_BASENAME = 'test_features.py' def pytest_collection_modifyitems(session, config, items): # Ensure that all tests with require a redeploy are run after # tests that don't need a redeploy. start, end = _get_start_end_index(DEPLOY_TEST_BASENAME, items) marked = [] unmarked = [] for item in items[start:end]: if item.get_closest_marker('on_redeploy') is not None: marked.append(item) else: unmarked.append(item) items[start:end] = unmarked + marked def _get_start_end_index(basename, items): # precondition: all the tests for test_features.py are # in a contiguous range. This is the case because pytest # will group all tests in a module together. matched = [item.fspath.basename == basename for item in items] if not any(matched): return 0, len(items) return ( matched.index(True), len(matched) - list(reversed(matched)).index(True) ) ================================================ FILE: tests/aws/test_features.py ================================================ import json import os import time import shutil import uuid from unittest import mock import botocore.session import pytest import requests import websocket from chalice.cli.factory import CLIFactory from chalice.utils import OSUtils, UI from chalice.deploy.deployer import ChaliceDeploymentError from chalice.config import DeployedResources CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) PROJECT_DIR = os.path.join(CURRENT_DIR, 'testapp') APP_FILE = os.path.join(PROJECT_DIR, 'app.py') RANDOM_APP_NAME = 'smoketest-%s' % str(uuid.uuid4())[:13] def retry(max_attempts, delay): def _create_wrapped_retry_function(function): def _wrapped_with_retry(*args, **kwargs): for _ in range(max_attempts): result = function(*args, **kwargs) if result is not None: return result time.sleep(delay) raise RuntimeError("Exhausted max retries of %s for function: %s" % (max_attempts, function)) return _wrapped_with_retry return _create_wrapped_retry_function class InternalServerError(Exception): pass class SmokeTestApplication(object): # Number of seconds to wait after redeploy before starting # to poll for successful 200. _REDEPLOY_SLEEP = 30 # Seconds to wait between poll attempts after redeploy. _POLLING_DELAY = 5 # Number of successful wait attempts before we consider the app # stabilized. _NUM_SUCCESS = 3 def __init__(self, deployed_values, stage_name, app_name, app_dir, region): self._deployed_resources = DeployedResources(deployed_values) self.stage_name = stage_name self.app_name = app_name # The name of the tmpdir where the app is copied. self.app_dir = app_dir self._has_redeployed = False self._region = region @property def url(self): return ( "https://{rest_api_id}.execute-api.{region}.amazonaws.com/" "{api_gateway_stage}".format(rest_api_id=self.rest_api_id, region=self._region, api_gateway_stage='api') ) @property def rest_api_id(self): return self._deployed_resources.resource_values( 'rest_api')['rest_api_id'] @property def websocket_api_id(self): return self._deployed_resources.resource_values( 'websocket_api')['websocket_api_id'] @property def websocket_connect_url(self): return ( "wss://{websocket_api_id}.execute-api.{region}.amazonaws.com/" "{api_gateway_stage}".format( websocket_api_id=self.websocket_api_id, region=self._region, api_gateway_stage='api', ) ) @retry(max_attempts=10, delay=5) def get_json(self, url): try: return self._get_json(url) except requests.exceptions.HTTPError: pass def _get_json(self, url): if not url.startswith('/'): url = '/' + url response = requests.get(self.url + url) response.raise_for_status() return response.json() @retry(max_attempts=10, delay=5) def get_response(self, url, headers=None): try: return self._send_request('GET', url, headers=headers) except InternalServerError: pass def _send_request(self, http_method, url, headers=None, data=None): kwargs = {} if headers is not None: kwargs['headers'] = headers if data is not None: kwargs['data'] = data response = requests.request(http_method, self.url + url, **kwargs) if response.status_code >= 500: raise InternalServerError() return response @retry(max_attempts=10, delay=5) def post_response(self, url, headers=None, data=None): try: return self._send_request('POST', url, headers=headers, data=data) except InternalServerError: pass @retry(max_attempts=10, delay=5) def put_response(self, url): try: return self._send_request('PUT', url) except InternalServerError: pass @retry(max_attempts=10, delay=5) def options_response(self, url): try: return self._send_request('OPTIONS', url) except InternalServerError: pass def redeploy_once(self): # Redeploy the application once. If a redeploy # has already happened, this function is a noop. if self._has_redeployed: return new_file = os.path.join(self.app_dir, 'app-redeploy.py') original_app_py = os.path.join(self.app_dir, 'app.py') shutil.move(original_app_py, original_app_py + '.bak') shutil.copy(new_file, original_app_py) _deploy_app(self.app_dir) self._has_redeployed = True # Give it settling time before running more tests. time.sleep(self._REDEPLOY_SLEEP) for _ in range(self._NUM_SUCCESS): self._wait_for_stablize() time.sleep(self._POLLING_DELAY) def _wait_for_stablize(self): # After a deployment we sometimes need to wait for # API Gateway to propagate all of its changes. # We're going to give it num_attempts to give us a # 200 response before failing. return self.get_json('/') @pytest.fixture def apig_client(): s = botocore.session.get_session() return s.create_client('apigateway') @pytest.fixture(scope='module') def smoke_test_app(tmpdir_factory): # We can't use the monkeypatch fixture here because this is a module scope # fixture and monkeypatch is a function scoped fixture. os.environ['APP_NAME'] = RANDOM_APP_NAME tmpdir = str(tmpdir_factory.mktemp(RANDOM_APP_NAME)) OSUtils().copytree(PROJECT_DIR, tmpdir) _inject_app_name(tmpdir) application = _deploy_app(tmpdir) yield application _delete_app(application, tmpdir) os.environ.pop('APP_NAME') def _inject_app_name(dirname): config_filename = os.path.join(dirname, '.chalice', 'config.json') with open(config_filename) as f: data = json.load(f) data['app_name'] = RANDOM_APP_NAME data['stages']['dev']['environment_variables']['APP_NAME'] = \ RANDOM_APP_NAME with open(config_filename, 'w') as f: f.write(json.dumps(data, indent=2)) def _deploy_app(temp_dirname): factory = CLIFactory(temp_dirname) config = factory.create_config_obj( chalice_stage_name='dev', autogen_policy=True ) session = factory.create_botocore_session() d = factory.create_default_deployer(session, config, UI()) region = session.get_config_variable('region') deployed = _deploy_with_retries(d, config) application = SmokeTestApplication( region=region, deployed_values=deployed, stage_name='dev', app_name=RANDOM_APP_NAME, app_dir=temp_dirname, ) return application @retry(max_attempts=10, delay=20) def _deploy_with_retries(deployer, config): try: deployed_stages = deployer.deploy(config, 'dev') return deployed_stages except ChaliceDeploymentError as e: # API Gateway aggressively throttles deployments. # If we run into this case, we just wait and try # again. error_code = _get_error_code_from_exception(e) if error_code != 'TooManyRequestsException': raise def _get_error_code_from_exception(exception): error_response = getattr(exception.original_error, 'response', None) if error_response is None: return None return error_response.get('Error', {}).get('Code') def _delete_app(application, temp_dirname): factory = CLIFactory(temp_dirname) config = factory.create_config_obj( chalice_stage_name='dev', autogen_policy=True ) session = factory.create_botocore_session() d = factory.create_deletion_deployer(session, UI()) _deploy_with_retries(d, config) def test_returns_simple_response(smoke_test_app): assert smoke_test_app.get_json('/') == {'hello': 'world'} def test_can_have_nested_routes(smoke_test_app): assert smoke_test_app.get_json('/a/b/c/d/e/f/g') == {'nested': True} def test_supports_path_params(smoke_test_app): assert smoke_test_app.get_json('/path/foo') == {'path': 'foo'} assert smoke_test_app.get_json('/path/bar') == {'path': 'bar'} def test_path_params_mapped_in_api(smoke_test_app, apig_client): # Use the API Gateway API to ensure that path parameters # are modeled as such. Otherwise this will break # SDK generation and any future features that depend # on params. We could try to verify the generated # javascript SDK looks ok. Instead we're going to # query the resources we've created in API gateway # and make sure requestParameters are present. rest_api_id = smoke_test_app.rest_api_id response = apig_client.get_export(restApiId=rest_api_id, stageName='api', exportType='swagger') swagger_doc = json.loads(response['body'].read()) route_config = swagger_doc['paths']['/path/{name}']['get'] assert route_config.get('parameters', {}) == [ {'name': 'name', 'in': 'path', 'required': True, 'type': 'string'}, ] def test_single_doc_mapped_in_api(smoke_test_app, apig_client): # We'll use the same API Gateway technique as in # test_path_params_mapped_in_api() rest_api_id = smoke_test_app.rest_api_id doc_parts = apig_client.get_documentation_parts( restApiId=rest_api_id, type='METHOD', path='/singledoc' ) doc_props = json.loads(doc_parts['items'][0]['properties']) assert 'summary' in doc_props assert 'description' not in doc_props assert doc_props['summary'] == 'Single line docstring.' def test_multi_doc_mapped_in_api(smoke_test_app, apig_client): # We'll use the same API Gateway technique as in # test_path_params_mapped_in_api() rest_api_id = smoke_test_app.rest_api_id doc_parts = apig_client.get_documentation_parts( restApiId=rest_api_id, type='METHOD', path='/multidoc' ) doc_props = json.loads(doc_parts['items'][0]['properties']) assert 'summary' in doc_props assert 'description' in doc_props assert doc_props['summary'] == 'Multi-line docstring.' assert doc_props['description'] == 'And here is another line.' @retry(max_attempts=18, delay=10) def _get_resource_id(apig_client, rest_api_id, path): # This is the resource id for the '/path/{name}' # route. As far as I know this is the best way to get # this id. matches = [ resource for resource in apig_client.get_resources(restApiId=rest_api_id)['items'] if resource['path'] == path ] if matches: return matches[0]['id'] def test_supports_post(smoke_test_app): response = smoke_test_app.post_response('/post') response.raise_for_status() assert response.json() == {'success': True} with pytest.raises(requests.HTTPError): # Only POST is supported. response = smoke_test_app.get_response('/post') response.raise_for_status() def test_supports_put(smoke_test_app): response = smoke_test_app.put_response('/put') response.raise_for_status() assert response.json() == {'success': True} with pytest.raises(requests.HTTPError): # Only PUT is supported. response = smoke_test_app.get_response('/put') response.raise_for_status() def test_supports_shared_routes(smoke_test_app): response = smoke_test_app.get_json('/shared') assert response == {'method': 'GET'} response = smoke_test_app.post_response('/shared') assert response.json() == {'method': 'POST'} def test_can_read_json_body_on_post(smoke_test_app): response = smoke_test_app.post_response( '/jsonpost', data=json.dumps({'hello': 'world'}), headers={'Content-Type': 'application/json'}) response.raise_for_status() assert response.json() == {'json_body': {'hello': 'world'}} def test_can_raise_bad_request(smoke_test_app): response = smoke_test_app.get_response('/badrequest') assert response.status_code == 400 assert response.json()['Code'] == 'BadRequestError' assert response.json()['Message'] == 'Bad request.' def test_can_raise_not_found(smoke_test_app): response = smoke_test_app.get_response('/notfound') assert response.status_code == 404 assert response.json()['Code'] == 'NotFoundError' def test_unexpected_error_raises_500_in_prod_mode(smoke_test_app): # Can't use smoke_test_app.get_response() because we're explicitly # testing for a 500. response = requests.get(smoke_test_app.url + '/arbitrary-error') assert response.status_code == 500 assert response.json()['Code'] == 'InternalServerError' assert 'internal server error' in response.json()['Message'] def test_can_route_multiple_methods_in_one_view(smoke_test_app): response = smoke_test_app.get_response('/multimethod') response.raise_for_status() assert response.json()['method'] == 'GET' response = smoke_test_app.post_response('/multimethod') response.raise_for_status() assert response.json()['method'] == 'POST' def test_form_encoded_content_type(smoke_test_app): response = smoke_test_app.post_response('/formencoded', data={'foo': 'bar'}) response.raise_for_status() assert response.json() == {'parsed': {'foo': ['bar']}} def test_can_round_trip_binary(smoke_test_app): # xde xed xbe xef will fail unicode decoding because xbe is an invalid # start byte in utf-8. bin_data = b'\xDE\xAD\xBE\xEF' response = smoke_test_app.post_response( '/binary', headers={'Content-Type': 'application/octet-stream', 'Accept': 'application/octet-stream'}, data=bin_data) response.raise_for_status() assert response.content == bin_data def test_can_round_trip_binary_custom_content_type(smoke_test_app): bin_data = b'\xDE\xAD\xBE\xEF' response = smoke_test_app.post_response( '/custom-binary', headers={'Content-Type': 'application/binary', 'Accept': 'application/binary'}, data=bin_data ) assert response.content == bin_data def test_can_return_default_binary_data_to_a_browser(smoke_test_app): base64encoded_response = b'3q2+7w==' accept = 'text/html,application/xhtml+xml;q=0.9,image/webp,*/*;q=0.8' response = smoke_test_app.get_response( '/get-binary', headers={'Accept': accept}) response.raise_for_status() assert response.content == base64encoded_response def _assert_contains_access_control_allow_methods(headers, methods): actual_methods = headers['Access-Control-Allow-Methods'].split(',') assert sorted(methods) == sorted(actual_methods), ( 'The expected allowed methods does not match the actual allowed ' 'methods for CORS.') def test_can_support_cors(smoke_test_app): response = smoke_test_app.get_response('/cors') response.raise_for_status() assert response.headers['Access-Control-Allow-Origin'] == '*' # Should also have injected an OPTIONs request. response = smoke_test_app.options_response('/cors') response.raise_for_status() headers = response.headers assert headers['Access-Control-Allow-Origin'] == '*' assert headers['Access-Control-Allow-Headers'] == ( 'Authorization,Content-Type,X-Amz-Date,X-Amz-Security-Token,' 'X-Api-Key') _assert_contains_access_control_allow_methods( headers, ['GET', 'POST', 'PUT', 'OPTIONS']) def test_can_support_custom_cors(smoke_test_app): response = smoke_test_app.get_response('/custom_cors') response.raise_for_status() expected_allow_origin = 'https://foo.example.com' assert response.headers[ 'Access-Control-Allow-Origin'] == expected_allow_origin # Should also have injected an OPTIONs request. response = smoke_test_app.options_response('/custom_cors') response.raise_for_status() headers = response.headers assert headers['Access-Control-Allow-Origin'] == expected_allow_origin assert headers['Access-Control-Allow-Headers'] == ( 'Authorization,Content-Type,X-Amz-Date,X-Amz-Security-Token,' 'X-Api-Key,X-Special-Header') _assert_contains_access_control_allow_methods( headers, ['GET', 'POST', 'PUT', 'OPTIONS']) assert headers['Access-Control-Max-Age'] == '600' assert headers['Access-Control-Expose-Headers'] == 'X-Special-Header' assert headers['Access-Control-Allow-Credentials'] == 'true' def test_to_dict_is_also_json_serializable(smoke_test_app): assert 'headers' in smoke_test_app.get_json('/todict') def test_multifile_support(smoke_test_app): response = smoke_test_app.get_json('/multifile') assert response == {'message': 'success'} def test_custom_response(smoke_test_app): response = smoke_test_app.get_response('/custom-response') response.raise_for_status() # Custom header assert response.headers['Content-Type'] == 'text/plain' # Multi headers assert response.headers['Set-Cookie'] == 'key=value, foo=bar' # Custom status code assert response.status_code == 204 def test_api_key_required_fails_with_no_key(smoke_test_app): response = smoke_test_app.get_response('/api-key-required') # Request should fail because we're not providing # an API key. assert response.status_code == 403 def test_can_handle_charset(smoke_test_app): # Should pass content type validation even with charset specified. response = smoke_test_app.get_response( '/json-only', headers={'Content-Type': 'application/json; charset=utf-8'} ) assert response.status_code == 200 def test_can_use_builtin_custom_auth(smoke_test_app): url = '/builtin-auth' # First time without an Auth header, we should fail. response = smoke_test_app.get_response(url) assert response.status_code == 401 # Now with the proper auth header, things should work. response = smoke_test_app.get_response( url, headers={'Authorization': 'yes'} ) assert response.status_code == 200 context = response.json()['context'] assert 'authorizer' in context # The keyval context we added shuld also be in the authorizer # dict. assert context['authorizer']['foo'] == 'bar' def test_can_use_shared_auth(smoke_test_app): response = smoke_test_app.get_response('/fake-profile') # GETs are allowed assert response.status_code == 200 # However, POSTs require auth. # This has the same auth config as /builtin-auth, # so we're testing the auth handler can be shared. assert smoke_test_app.post_response('/fake-profile').status_code == 401 response = smoke_test_app.post_response('/fake-profile', headers={'Authorization': 'yes'}) assert response.status_code == 200 context = response.json()['context'] assert 'authorizer' in context assert context['authorizer']['foo'] == 'bar' def test_empty_raw_body(smoke_test_app): response = smoke_test_app.post_response('/repr-raw-body') response.raise_for_status() assert response.json() == {'repr-raw-body': ''} def test_websocket_lifecycle(smoke_test_app): ws = websocket.create_connection(smoke_test_app.websocket_connect_url) ws.send("Hello, World 1") ws.recv() ws.close() ws = websocket.create_connection(smoke_test_app.websocket_connect_url) ws.send("Hello, World 2") second_response = json.loads(ws.recv()) ws.close() expected_second_response = [ [mock.ANY, 'Hello, World 1'], [mock.ANY, 'Hello, World 2'] ] assert expected_second_response == second_response assert second_response[0][0] != second_response[1][0] @pytest.mark.on_redeploy def test_redeploy_no_change_view(smoke_test_app): smoke_test_app.redeploy_once() assert smoke_test_app.get_json('/') == {'hello': 'world'} @pytest.mark.on_redeploy def test_redeploy_changed_function(smoke_test_app): smoke_test_app.redeploy_once() assert smoke_test_app.get_json('/a/b/c/d/e/f/g') == { 'redeployed': True} @pytest.mark.on_redeploy def test_redeploy_new_function(smoke_test_app): smoke_test_app.redeploy_once() assert smoke_test_app.get_json('/redeploy') == {'success': True} @pytest.mark.on_redeploy def test_redeploy_change_route_info(smoke_test_app): smoke_test_app.redeploy_once() # POST is no longer allowed: assert smoke_test_app.post_response('/multimethod').status_code == 403 # But PUT is now allowed in the redeployed app.py assert smoke_test_app.put_response('/multimethod').status_code == 200 @pytest.mark.on_redeploy def test_redeploy_view_deleted(smoke_test_app): smoke_test_app.redeploy_once() response = smoke_test_app.get_response('/path/foo') # Request should fail because it's not in the redeployed # app.py assert response.status_code == 403 ================================================ FILE: tests/aws/test_websockets.py ================================================ import os import json import uuid import threading import shutil import time import pytest import websocket from chalice.cli.factory import CLIFactory from chalice.utils import OSUtils, UI from chalice.deploy.deployer import ChaliceDeploymentError from chalice.config import DeployedResources CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) PROJECT_DIR = os.path.join(CURRENT_DIR, 'testwebsocketapp') APP_FILE = os.path.join(PROJECT_DIR, 'app.py') RANDOM_APP_NAME = 'smoketest-%s' % str(uuid.uuid4())[:13] def retry(max_attempts, delay): def _create_wrapped_retry_function(function): def _wrapped_with_retry(*args, **kwargs): for _ in range(max_attempts): result = function(*args, **kwargs) if result is not None: return result time.sleep(delay) raise RuntimeError("Exhausted max retries of %s for function: %s" % (max_attempts, function)) return _wrapped_with_retry return _create_wrapped_retry_function def _create_ws_connection(url, attempts=5, delay=5): for _ in range(attempts): try: ws = websocket.create_connection(url) return ws except websocket.WebSocketBadStatusException: time.sleep(delay) def _inject_app_name(dirname): config_filename = os.path.join(dirname, '.chalice', 'config.json') with open(config_filename) as f: data = json.load(f) data['app_name'] = RANDOM_APP_NAME data['stages']['dev']['environment_variables']['APP_NAME'] = \ RANDOM_APP_NAME with open(config_filename, 'w') as f: f.write(json.dumps(data, indent=2)) def _deploy_app(temp_dirname): factory = CLIFactory(temp_dirname) config = factory.create_config_obj( chalice_stage_name='dev', autogen_policy=True ) session = factory.create_botocore_session() d = factory.create_default_deployer(session, config, UI()) region = session.get_config_variable('region') deployed = _deploy_with_retries(d, config) application = SmokeTestApplication( region=region, deployed_values=deployed, stage_name='dev', app_name=RANDOM_APP_NAME, app_dir=temp_dirname, ) return application @retry(max_attempts=10, delay=20) def _deploy_with_retries(deployer, config): try: deployed_stages = deployer.deploy(config, 'dev') return deployed_stages except ChaliceDeploymentError as e: # API Gateway aggressively throttles deployments. # If we run into this case, we just wait and try # again. error_code = _get_error_code_from_exception(e) if error_code != 'TooManyRequestsException': raise def _get_error_code_from_exception(exception): error_response = getattr(exception.original_error, 'response', None) if error_response is None: return None return error_response.get('Error', {}).get('Code') def _delete_app(application, temp_dirname): factory = CLIFactory(temp_dirname) config = factory.create_config_obj( chalice_stage_name='dev', autogen_policy=True ) session = factory.create_botocore_session() d = factory.create_deletion_deployer(session, UI()) _deploy_with_retries(d, config) class SmokeTestApplication(object): # Number of seconds to wait after redeploy before starting # to poll for successful 200. _REDEPLOY_SLEEP = 20 # Seconds to wait between poll attempts after redeploy. _POLLING_DELAY = 5 def __init__(self, deployed_values, stage_name, app_name, app_dir, region): self._deployed_resources = DeployedResources(deployed_values) self.stage_name = stage_name self.app_name = app_name # The name of the tmpdir where the app is copied. self.app_dir = app_dir self._has_redeployed = False self._region = region @property def websocket_api_id(self): return self._deployed_resources.resource_values( 'websocket_api')['websocket_api_id'] @property def websocket_connect_url(self): return ( "wss://{websocket_api_id}.execute-api.{region}.amazonaws.com/" "{api_gateway_stage}".format( websocket_api_id=self.websocket_api_id, region=self._region, api_gateway_stage='api', ) ) @property def websocket_message_handler_arn(self): return self._deployed_resources.resource_values( 'websocket_message')['lambda_arn'] @property def region(self): return self._region def redeploy_once(self): # Redeploy the application once. If a redeploy # has already happened, this function is a noop. if self._has_redeployed: return new_file = os.path.join(self.app_dir, 'app-redeploy.py') original_app_py = os.path.join(self.app_dir, 'app.py') shutil.move(original_app_py, original_app_py + '.bak') shutil.copy(new_file, original_app_py) _deploy_app(self.app_dir) self._has_redeployed = True # Give it settling time before running more tests. time.sleep(self._REDEPLOY_SLEEP) @pytest.fixture def smoke_test_app_ws(tmpdir_factory): # We can't use the monkeypatch fixture here because this is a module scope # fixture and monkeypatch is a function scoped fixture. os.environ['APP_NAME'] = RANDOM_APP_NAME tmpdir = str(tmpdir_factory.mktemp(RANDOM_APP_NAME)) _create_dynamodb_table(RANDOM_APP_NAME, tmpdir) OSUtils().copytree(PROJECT_DIR, tmpdir) _inject_app_name(tmpdir) application = _deploy_app(tmpdir) yield application _delete_app(application, tmpdir) _delete_dynamodb_table(RANDOM_APP_NAME, tmpdir) os.environ.pop('APP_NAME') def _create_dynamodb_table(table_name, temp_dirname): factory = CLIFactory(temp_dirname) session = factory.create_botocore_session() ddb = session.create_client('dynamodb') ddb.create_table( TableName=table_name, AttributeDefinitions=[ { 'AttributeName': 'entry', 'AttributeType': 'N', }, ], KeySchema=[ { 'AttributeName': 'entry', 'KeyType': 'HASH', }, ], ProvisionedThroughput={ 'ReadCapacityUnits': 50, 'WriteCapacityUnits': 50, }, ) def _delete_dynamodb_table(table_name, temp_dirname): factory = CLIFactory(temp_dirname) session = factory.create_botocore_session() ddb = session.create_client('dynamodb') ddb.delete_table( TableName=table_name, ) class Task(threading.Thread): def __init__(self, action, delay=0.05): threading.Thread.__init__(self) self._action = action self._done = threading.Event() self._delay = delay def run(self): while not self._done.is_set(): self._action() time.sleep(self._delay) def stop(self): self._done.set() def counter(): """Generator of sequential increasing numbers""" count = 1 while True: yield count count += 1 class CountingMessageSender(object): """Class to send values from a counter over a websocket.""" def __init__(self, ws, counter): self._ws = ws self._counter = counter self._last_sent = None def send(self): value = next(self._counter) self._ws.send('%s' % value) self._last_sent = value @property def last_sent(self): return self._last_sent def get_numbers_from_dynamodb(temp_dirname): """Get numbers from DynamoDB in the format written by testwebsocketapp. """ factory = CLIFactory(temp_dirname) session = factory.create_botocore_session() ddb = session.create_client('dynamodb') paginator = ddb.get_paginator('scan') numbers = sorted([ int(item['entry']['N']) for page in paginator.paginate( TableName=RANDOM_APP_NAME, ConsistentRead=True, ) for item in page['Items'] ]) return numbers def get_errors_from_dynamodb(temp_dirname): factory = CLIFactory(temp_dirname) session = factory.create_botocore_session() ddb = session.create_client('dynamodb') item = ddb.get_item(TableName=RANDOM_APP_NAME, Key={'entry': {'N': '-9999'}}) if 'Item' not in item: return None return item['Item']['errormsg']['S'] def find_skips_in_seq(numbers): """Find non-sequential gaps in a sequence of numbers :type numbers: Iterable of ints :param numbers: Iterable to check for gaps :returns: List of tuples with the gaps in the format [(start_of_gap, end_of_gap, ...)]. If the list is empty then there are no gaps. """ last = numbers[0] - 1 skips = [] for elem in numbers: if elem != last + 1: skips.append((last, elem)) last = elem return skips def test_websocket_redployment_does_not_lose_messages(smoke_test_app_ws): # This test is to check if one persistant connection is affected by an app # redeployment. A connetion is made to the app, and a sequence of numbers # is sent over the socket and written to a DynamoDB table. The app is # redeployed in a seprate thread. After the redeployment we wait a # second to ensure more numbers have been sent. Finally we inspect the # DynamoDB table to ensure there are no gaps in the numbers we saw on the # server side, and that the first and last number we sent is also present. ws = _create_ws_connection(smoke_test_app_ws.websocket_connect_url) counter_generator = counter() sender = CountingMessageSender(ws, counter_generator) ping_endpoint = Task(sender.send) ping_endpoint.start() smoke_test_app_ws.redeploy_once() time.sleep(1) ping_endpoint.stop() errors = get_errors_from_dynamodb(smoke_test_app_ws.app_dir) assert errors is None numbers = get_numbers_from_dynamodb(smoke_test_app_ws.app_dir) assert 1 in numbers assert sender.last_sent in numbers skips = find_skips_in_seq(numbers) assert skips == [] ================================================ FILE: tests/aws/testapp/.chalice/config.json ================================================ { "stages": { "dev": { "api_gateway_stage": "api", "environment_variables": { "APP_NAME": "replaceme" } } }, "version": "2.0", "app_name": "replaceme" } ================================================ FILE: tests/aws/testapp/app-redeploy.py ================================================ """Test app redeploy. This file is copied over to app.py during the integration tests to test behavior on redeploys. """ import os from chalice import Chalice app = Chalice(app_name=os.environ['APP_NAME']) # Test an unchanged view, this is the exact # version from app.py @app.route('/') def index(): return {'hello': 'world'} # Test same route info but changed view code. @app.route('/a/b/c/d/e/f/g') def nested_route(): return {'redeployed': True} # Test route deletion. This view is in the original # app.py but is now deleted. # @app.route('/path/{name}') # def supports_path_params(name): # return {'path': name} # Test route modification with the same view code. # The original version had methods=['GET', 'POST'] @app.route('/multimethod', methods=['GET', 'PUT']) def multiple_methods(): return {'method': app.current_request.method} # Test new view function added that wasn't in the original # app.py file. @app.route('/redeploy') def redeploy(): return {'success': True} ================================================ FILE: tests/aws/testapp/app.py ================================================ import os import json try: from urllib.parse import parse_qs except ImportError: from urlparse import parse_qs import boto3.session from chalice import ( Chalice, BadRequestError, NotFoundError, Response, CORSConfig, UnauthorizedError, AuthResponse, AuthRoute, ) # This is a test app that is used by integration tests. # This app exercises all the major features of chalice # and helps prevent regressions. app = Chalice(app_name=os.environ['APP_NAME']) app.websocket_api.session = boto3.session.Session() app.experimental_feature_flags.update([ 'WEBSOCKETS' ]) app.api.binary_types.append('application/binary') @app.authorizer(ttl_seconds=300) def dummy_auth(auth_request): if auth_request.token == 'yes': return AuthResponse( routes=['/builtin-auth', AuthRoute('/fake-profile', methods=['POST'])], context={'foo': 'bar'}, principal_id='foo' ) else: raise UnauthorizedError('Authorization failed') @app.route('/') def index(): return {'hello': 'world'} @app.route('/a/b/c/d/e/f/g') def nested_route(): return {'nested': True} @app.route('/path/{name}') def supports_path_params(name): return {'path': name} @app.route('/singledoc') def single_doc(): """Single line docstring.""" return {'docstring': 'single'} @app.route('/multidoc') def multi_doc(): """Multi-line docstring. And here is another line. """ return {'docstring': 'multi'} @app.route('/post', methods=['POST']) def supports_only_post(): return {'success': True} @app.route('/put', methods=['PUT']) def supports_only_put(): return {'success': True} @app.route('/jsonpost', methods=['POST']) def supports_post_body_as_json(): json_body = app.current_request.json_body return {'json_body': json_body} @app.route('/multimethod', methods=['GET', 'POST']) def multiple_methods(): return {'method': app.current_request.method} @app.route('/badrequest') def bad_request_error(): raise BadRequestError("Bad request.") @app.route('/notfound') def not_found_error(): raise NotFoundError("Not found") @app.route('/arbitrary-error') def raise_arbitrary_error(): raise TypeError("Uncaught exception") @app.route('/formencoded', methods=['POST'], content_types=['application/x-www-form-urlencoded']) def form_encoded(): parsed = parse_qs(app.current_request.raw_body.decode('utf-8')) return { 'parsed': parsed } @app.route('/json-only', content_types=['application/json']) def json_only(): return {'success': True} @app.route('/cors', methods=['GET', 'POST', 'PUT'], cors=True) def supports_cors(): # It doesn't really matter what we return here because # we'll be checking the response headers to verify CORS support. return {'cors': True} @app.route('/custom_cors', methods=['GET', 'POST', 'PUT'], cors=CORSConfig( allow_origin='https://foo.example.com', allow_headers=['X-Special-Header'], max_age=600, expose_headers=['X-Special-Header'], allow_credentials=True)) def supports_custom_cors(): return {'cors': True} @app.route('/todict', methods=['GET']) def todict(): return app.current_request.to_dict() @app.route('/multifile') def multifile(): from chalicelib import MESSAGE return {"message": MESSAGE} @app.route('/custom-response', methods=['GET']) def custom_response(): return Response( status_code=204, body='', headers={ 'Content-Type': 'text/plain', 'Set-Cookie': ['key=value', 'foo=bar'], }, ) @app.route('/api-key-required', methods=['GET'], api_key_required=True) def api_key_required(): return {"success": True} @app.route('/binary', methods=['POST'], content_types=['application/octet-stream']) def binary_round_trip(): return Response( app.current_request.raw_body, headers={ 'Content-Type': 'application/octet-stream' }, status_code=200) @app.route('/custom-binary', methods=['POST'], content_types=['application/binary']) def custom_binary_round_trip(): return Response( app.current_request.raw_body, headers={ 'Content-Type': 'application/binary' }, status_code=200) @app.route('/get-binary', methods=['GET']) def binary_response(): return Response( body=b'\xDE\xAD\xBE\xEF', headers={ 'Content-Type': 'application/octet-stream' }, status_code=200) @app.route('/shared', methods=['GET']) def shared_get(): return {'method': 'GET'} @app.route('/shared', methods=['POST']) def shared_post(): return {'method': 'POST'} @app.route('/builtin-auth', authorizer=dummy_auth) def builtin_auth(): return {'success': True, 'context': app.current_request.context} # Testing a common use case where you can have read only GET access # but you need to be auth'd to POST. @app.route('/fake-profile', methods=['GET']) def fake_profile_read_only(): return {'success': True, 'context': app.current_request.context} @app.route('/fake-profile', authorizer=dummy_auth, methods=['POST']) def fake_profile_post(): return {'success': True, 'context': app.current_request.context} @app.route('/repr-raw-body', methods=['POST']) def repr_raw_body(): return {'repr-raw-body': app.current_request.raw_body.decode('utf-8')} SOCKET_MESSAGES = [] @app.on_ws_connect() def connect(event): pass @app.on_ws_message() def message(event): SOCKET_MESSAGES.append((event.connection_id, event.body)) app.websocket_api.send(event.connection_id, json.dumps(SOCKET_MESSAGES)) @app.on_ws_disconnect() def disconnect(event): pass ================================================ FILE: tests/aws/testapp/chalicelib/__init__.py ================================================ MESSAGE = "success" ================================================ FILE: tests/aws/testapp/requirements.txt ================================================ boto3==1.38.15 ================================================ FILE: tests/aws/testwebsocketapp/.chalice/config.json ================================================ { "version": "2.0", "app_name": "testwebsocketapp", "stages": { "dev": { "api_gateway_stage": "api", "environment_variables": {} } } } ================================================ FILE: tests/aws/testwebsocketapp/.gitignore ================================================ .chalice/deployments/ .chalice/venv/ ================================================ FILE: tests/aws/testwebsocketapp/app-redeploy.py ================================================ import os import boto3 from chalice import Chalice app = Chalice(app_name=os.environ['APP_NAME']) app.websocket_api.session = boto3.session.Session() app.experimental_feature_flags.update([ 'WEBSOCKETS' ]) ddb = boto3.client('dynamodb') # This comment is to cause a change which triggers a redeployment # of the Lambda Function, this is needed to properly test redeployment. @app.on_ws_message() def message(event): ddb.put_item( TableName=os.environ['APP_NAME'], Item={ 'entry': { 'N': event.body }, }, ) ================================================ FILE: tests/aws/testwebsocketapp/app.py ================================================ import os import boto3 from chalice import Chalice app = Chalice(app_name=os.environ['APP_NAME']) app.websocket_api.session = boto3.session.Session() app.experimental_feature_flags.update([ 'WEBSOCKETS' ]) ddb = boto3.client('dynamodb') @app.on_ws_message() def message(event): try: ddb.put_item( TableName=os.environ['APP_NAME'], Item={ 'entry': { 'N': event.body }, }, ) except Exception as e: # If we get an exception, we need to log it somehow. We can't # return this back to the user so we'll add something to the ddb # table to denote that we failed. ddb.put_item( TableName=os.environ['APP_NAME'], Item={ 'entry': { 'N': "-9999" }, 'errormsg': { 'S': '%s: %s,\noriginal event: %s' % ( e.__class__, e, event.to_dict()) } } ) ================================================ FILE: tests/aws/testwebsocketapp/requirements.txt ================================================ boto3==1.38.15 ================================================ FILE: tests/conftest.py ================================================ import sys import botocore.session from botocore.stub import Stubber import pytest from pytest import fixture def pytest_addoption(parser): parser.addoption('--skip-slow', action='store_true', help='Skip slow tests') def pytest_configure(config): config.addinivalue_line("markers", "slow: mark test as slow to run") config.addinivalue_line( "markers", ( "on_redeploy: mark an integration test to be run after " "the app is redeployed" ) ) def pytest_collection_modifyitems(config, items): if config.getoption("--skip-slow"): skip_slow = pytest.mark.skip(reason="Skipping due to --skip-slow") for item in items: if "slow" in item.keywords: item.add_marker(skip_slow) @fixture(autouse=True) def teardown_function(): sys.modules.pop('app', None) sys.path_importer_cache.clear() class StubbedSession(botocore.session.Session): def __init__(self, *args, **kwargs): super(StubbedSession, self).__init__(*args, **kwargs) self._cached_clients = {} self._client_stubs = {} def create_client(self, service_name, *args, **kwargs): if service_name not in self._cached_clients: client = self._create_stubbed_client(service_name, *args, **kwargs) self._cached_clients[service_name] = client return self._cached_clients[service_name] def _create_stubbed_client(self, service_name, *args, **kwargs): client = super(StubbedSession, self).create_client( service_name, *args, **kwargs) stubber = StubBuilder(ChaliceStubber(client)) self._client_stubs[service_name] = stubber return client def stub(self, service_name): if service_name not in self._client_stubs: self.create_client(service_name) return self._client_stubs[service_name] def activate_stubs(self): for stub in self._client_stubs.values(): stub.activate() def verify_stubs(self): for stub in self._client_stubs.values(): stub.assert_no_pending_responses() class StubBuilder(object): def __init__(self, stub): self.stub = stub self.activated = False self.pending_args = {} def __getattr__(self, name): if self.activated: # I want to be strict here to guide common test behavior. # This helps encourage the "record" "replay" "verify" # idiom in traditional mock frameworks. raise RuntimeError("Stub has already been activated: %s, " "you must set up your stub calls before " "calling .activate()" % self.stub) if not name.startswith('_'): # Assume it's an API call. self.pending_args['operation_name'] = name return self def assert_no_pending_responses(self): self.stub.assert_no_pending_responses() def activate(self): self.activated = True self.stub.activate() def returns(self, response): self.pending_args['service_response'] = response # returns() is essentially our "build()" method and triggers # creations of a stub response creation. p = self.pending_args self.stub.add_response(p['operation_name'], expected_params=p['expected_params'], service_response=p['service_response']) # And reset the pending_args for the next stub creation. self.pending_args = {} def raises_error(self, error_code=None, message=None, error=None): p = self.pending_args if error_code is not None and message is not None: self.stub.add_client_error(p['operation_name'], service_error_code=error_code, service_message=message) elif error is not None: self.stub.add_response_error(p['operation_name'], error) else: raise ValueError( 'Either error_code and message must be provided or ' 'error must be provided' ) # Reset pending args for next expectation. self.pending_args = {} def __call__(self, **kwargs): self.pending_args['expected_params'] = kwargs return self # TODO: Port this functionality to inject non-ClientErrors back to botocore class ChaliceStubber(Stubber): def add_response_error(self, method, error, expected_params=None): """Adds a custom exception to the response queue :type method: str :param method: The name of the service method to raise the error on. :type error: Exception :param error: The customer exception to raise :type expected_params: dict :param expected_params: A dictionary of the expected parameters to be called for the provided service response. The parameters match the names of keyword arguments passed to that client call. If any of the parameters differ a ``StubResponseError`` is thrown. You can use stub.ANY to indicate a particular parameter to ignore in validation. stub.ANY is only valid for top level params. """ operation_name = self.client.meta.method_to_api_mapping.get(method) response = { 'operation_name': operation_name, 'response': error, 'expected_params': expected_params, } self._queue.append(response) def _get_response_handler(self, model, params, **kwargs): response = super(ChaliceStubber, self)._get_response_handler( model, params, **kwargs) if isinstance(response, Exception): raise response return response @fixture def stubbed_session(): s = StubbedSession() return s @fixture def no_local_config(monkeypatch): """Ensure no local AWS configuration is used. This is useful for unit/functional tests so we can ensure that local configuration does not affect the results of the test. """ monkeypatch.setenv('AWS_DEFAULT_REGION', 'us-west-2') monkeypatch.setenv('AWS_ACCESS_KEY_ID', 'foo') monkeypatch.setenv('AWS_SECRET_ACCESS_KEY', 'bar') monkeypatch.delenv('AWS_PROFILE', raising=False) monkeypatch.delenv('AWS_DEFAULT_PROFILE', raising=False) # Ensure that the existing ~/.aws/{config,credentials} file # don't influence test results. monkeypatch.setenv('AWS_CONFIG_FILE', '/tmp/asdfasdfaf/does/not/exist') monkeypatch.setenv('AWS_SHARED_CREDENTIALS_FILE', '/tmp/asdfasdfaf/does/not/exist2') ================================================ FILE: tests/functional/__init__.py ================================================ ================================================ FILE: tests/functional/api/__init__.py ================================================ ================================================ FILE: tests/functional/api/test_package.py ================================================ import os import json import pytest from click.testing import CliRunner from chalice.cli import newproj from chalice.api import package_app @pytest.fixture def runner(): return CliRunner() @pytest.mark.parametrize('package_format,template_format,expected_filename', [ ('cloudformation', 'json', 'sam.json'), ('cloudformation', 'yaml', 'sam.yaml'), ('terraform', 'json', 'chalice.tf.json'), ]) def test_can_package_different_formats(runner, package_format, template_format, expected_filename): with runner.isolated_filesystem(): newproj.create_new_project_skeleton('testproject') package_app('testproject', output_dir='packagedir', stage='dev', package_format=package_format, template_format=template_format) app_contents = os.listdir('packagedir') assert expected_filename in app_contents assert 'deployment.zip' in app_contents def test_can_override_chalice_config(runner): with runner.isolated_filesystem(): newproj.create_new_project_skeleton('testproject') chalice_config = { 'environment_variables': { 'FOO': 'BAR', } } package_app('testproject', output_dir='packagedir', stage='dev', chalice_config=chalice_config) app_contents = os.listdir('packagedir') assert 'sam.json' in app_contents with open(os.path.join('packagedir', 'sam.json')) as f: data = json.loads(f.read()) properties = data['Resources']['APIHandler']['Properties'] assert properties['Environment'] == { 'Variables': { 'FOO': 'BAR', } } ================================================ FILE: tests/functional/basicapp/.chalice/config.json ================================================ { "version": "2.0", "app_name": "basicapp", "stages": { "dev": { "api_gateway_stage": "api" } } } ================================================ FILE: tests/functional/basicapp/.gitignore ================================================ .chalice/deployments/ .chalice/venv/ ================================================ FILE: tests/functional/basicapp/app.py ================================================ from chalice import Chalice app = Chalice(app_name='basicapp') @app.route('/') def index(): return {'version': 'original'} ================================================ FILE: tests/functional/basicapp/requirements.txt ================================================ ================================================ FILE: tests/functional/cdk/__init__.py ================================================ ================================================ FILE: tests/functional/cdk/test_construct.py ================================================ import os import sys import json import pytest from click.testing import CliRunner from chalice.cli import newproj try: from aws_cdk import core as cdk CDK_VERSION = 1 except ImportError: try: import aws_cdk as cdk CDK_VERSION = 2 except ImportError: pytestmark = pytest.mark.skip( "aws_cdk package needed to run CDK tests.") @pytest.fixture def runner(): return CliRunner() def verify_code_asset_exists_v1(uri_props): bucket_ref = uri_props['Bucket']['Ref'] assert bucket_ref.startswith('AssetParameters') def verify_code_asset_exists_v2(uri_props): bucket_ref = uri_props['Bucket']['Fn::Sub'] # Actual sub value will look something like this: # 'cdk-abcdefghi-assets-${AWS::AccountId}-${AWS::Region}'}, assert bucket_ref.startswith('cdk-') @pytest.fixture def verify_code_asset_exists(): if CDK_VERSION == 1: return verify_code_asset_exists_v1 elif CDK_VERSION == 2: return verify_code_asset_exists_v2 def load_chalice_construct(dirname, stack_name='testcdk'): try: sys.path.append(dirname) sys.modules.pop('stacks.chaliceapp', None) sys.modules.pop('stacks', None) import stacks.chaliceapp app = cdk.App() chalice_app = stacks.chaliceapp.ChaliceApp(app, stack_name) return app, chalice_app.chalice finally: sys.modules.pop('app', None) sys.modules.pop('stacks', None) sys.path.pop() sys.path_importer_cache.clear() def filter_resources(template, resource_type): return [(name, props) for name, props in template['Resources'].items() if props['Type'] == resource_type] def test_cdk_construct_api(runner): # The CDK loading/synth can take a while so we're testing the # various APIs out in one test to cut down on test time. with runner.isolated_filesystem(): newproj.create_new_project_skeleton( 'testcdk', project_type='cdk-ddb') dirname = os.path.abspath(os.path.join('testcdk', 'infrastructure')) os.chdir(dirname) cdk_app, chalice_app = load_chalice_construct(dirname, 'testcdk') api_handler = chalice_app.get_function('APIHandler') assert api_handler == chalice_app.get_function('APIHandler') role = chalice_app.get_role('DefaultRole') assert hasattr(role, 'role_name') def test_can_package_as_cdk_app(runner, verify_code_asset_exists): # The CDK loading/synth can take a while so we're testing the # various APIs out in one test to cut down on test time. with runner.isolated_filesystem(): newproj.create_new_project_skeleton( 'testcdkpackage', project_type='cdk-ddb') dirname = os.path.abspath(os.path.join('testcdkpackage', 'infrastructure')) os.chdir(dirname) cdk_app, chalice_app = load_chalice_construct( dirname, 'testcdkpackage') assembly = cdk_app.synth() stack = assembly.get_stack_by_name('testcdkpackage') cfn_template = stack.template resources = cfn_template['Resources'] # Sanity check that we have the resources from Chalice as well as the # resources from the ChaliceApp construct. assert 'APIHandler' in resources assert 'DefaultRole' in resources ddb_tables = filter_resources(cfn_template, 'AWS::DynamoDB::Table')[0] # CDK adds a random suffix to our resource name so we verify that # the name starts with our provided name "AppTable". assert ddb_tables[0].startswith('AppTable') # We also need to verify that we've replaces the CodUri with the # CDK specific assets. functions = filter_resources( cfn_template, 'AWS::Serverless::Function')[0] verify_code_asset_exists(functions[1]['Properties']['CodeUri']) def test_can_package_managed_layer(runner, verify_code_asset_exists): with runner.isolated_filesystem(): newproj.create_new_project_skeleton( 'testcdklayers', project_type='cdk-ddb') project_dir = os.path.abspath('testcdklayers') config_file = os.path.join(project_dir, 'runtime', '.chalice', 'config.json') with open(config_file) as f: config = json.load(f) config['automatic_layer'] = True with open(config_file, 'w') as f: f.write(json.dumps(config)) infrastructure_dir = os.path.join(project_dir, 'infrastructure') os.chdir(infrastructure_dir) cdk_app, chalice_app = load_chalice_construct(infrastructure_dir, 'testcdklayers') assembly = cdk_app.synth() stack = assembly.get_stack_by_name('testcdklayers') cfn_template = stack.template layers = filter_resources( cfn_template, 'AWS::Serverless::LayerVersion')[0] verify_code_asset_exists(layers[1]['Properties']['ContentUri']) ================================================ FILE: tests/functional/cli/__init__.py ================================================ ================================================ FILE: tests/functional/cli/test_cli.py ================================================ import json import zipfile import os import sys import re from unittest import mock import pytest from click.testing import CliRunner from botocore.exceptions import ClientError from chalice import cli from chalice.cli import factory from chalice.cli import newproj from chalice.config import Config, DeployedResources from chalice.utils import record_deployed_values from chalice.utils import PipeReader from chalice.constants import DEFAULT_APIGATEWAY_STAGE_NAME from chalice.logs import LogRetriever, LogRetrieveOptions from chalice.invoke import LambdaInvokeHandler from chalice.invoke import UnhandledLambdaError from chalice.awsclient import ReadTimeout from chalice.deploy.validate import ExperimentalFeatureError class FakeConfig(object): def __init__(self, deployed_resources): self._deployed_resources = deployed_resources def deployed_resources(self, chalice_stage_name): return self._deployed_resources @pytest.fixture def runner(): return CliRunner() @pytest.fixture def mock_cli_factory(): cli_factory = mock.Mock(spec=factory.CLIFactory) cli_factory.create_config_obj.return_value = Config.create(project_dir='.') cli_factory.create_botocore_session.return_value = mock.sentinel.Session return cli_factory def teardown_function(function): sys.modules.pop('app', None) sys.path_importer_cache.clear() def assert_chalice_app_structure_created(dirname): app_contents = os.listdir(os.path.join(os.getcwd(), dirname)) assert 'app.py' in app_contents assert 'requirements.txt' in app_contents assert '.chalice' in app_contents assert '.gitignore' in app_contents def _run_cli_command(runner, function, args, cli_factory=None): # Handles passing in 'obj' so we can get commands # that use @pass_context to work properly. # click doesn't support this natively so we have to duplicate # what 'def cli(...)' is doing. if cli_factory is None: cli_factory = factory.CLIFactory('.') result = runner.invoke( function, args, obj={'project_dir': '.', 'debug': False, 'factory': cli_factory}) return result def test_create_new_project_creates_app(runner): with runner.isolated_filesystem(): result = runner.invoke(cli.new_project, ['testproject'], obj={}) assert result.exit_code == 0 # The 'new-project' command creates a directory based on # the project name assert os.listdir(os.getcwd()) == ['testproject'] assert_chalice_app_structure_created(dirname='testproject') def test_create_project_with_prompted_app_name(runner): with runner.isolated_filesystem(): result = runner.invoke( cli.new_project, input=b'', obj={ 'prompter': lambda: {'project_name': 'testproject', 'project_type': 'legacy'} } ) print(result.stdout) assert result.exit_code == 0 assert os.listdir(os.getcwd()) == ['testproject'] assert_chalice_app_structure_created(dirname='testproject') def test_error_raised_if_dir_already_exists(runner): with runner.isolated_filesystem(): os.mkdir('testproject') result = runner.invoke(cli.new_project, ['testproject'], obj={}) assert result.exit_code == 1 assert 'Directory already exists: testproject' in result.output def test_can_load_project_config_after_project_creation(runner): with runner.isolated_filesystem(): result = runner.invoke(cli.new_project, ['testproject'], obj={}) assert result.exit_code == 0 config = factory.CLIFactory('testproject').load_project_config() assert config == { 'version': '2.0', 'app_name': 'testproject', 'stages': { 'dev': {'api_gateway_stage': DEFAULT_APIGATEWAY_STAGE_NAME}, } } def test_default_new_project_adds_index_route(runner): with runner.isolated_filesystem(): result = runner.invoke(cli.new_project, ['testproject'], obj={}) assert result.exit_code == 0 app = factory.CLIFactory('testproject').load_chalice_app() assert '/' in app.routes def test_gen_policy_command_creates_policy(runner): with runner.isolated_filesystem(): newproj.create_new_project_skeleton('testproject') os.chdir('testproject') result = runner.invoke(cli.cli, ['gen-policy'], obj={}) assert result.exit_code == 0 # The output should be valid JSON. parsed_policy = json.loads(result.output) # We don't want to validate the specific parts of the policy # (that's tested elsewhere), but we'll check to make sure # it looks like a policy document. assert 'Version' in parsed_policy assert 'Statement' in parsed_policy def test_does_fail_to_generate_swagger_if_no_rest_api(runner): with runner.isolated_filesystem(): newproj.create_new_project_skeleton('testproject') os.chdir('testproject') with open('app.py', 'w') as f: f.write( 'from chalice import Chalice\n' 'app = Chalice("myapp")\n' ) result = _run_cli_command(runner, cli.generate_models, []) assert result.exit_code == 1 assert result.output == ( 'No REST API found to generate model from.\n' 'Aborted!\n' ) def test_can_write_swagger_model(runner): with runner.isolated_filesystem(): newproj.create_new_project_skeleton('testproject') os.chdir('testproject') result = _run_cli_command(runner, cli.generate_models, []) assert result.exit_code == 0 model = json.loads(result.output) assert model == { "swagger": "2.0", "info": { "version": "1.0", "title": "testproject" }, "schemes": [ "https" ], "paths": { "/": { "get": { "consumes": [ "application/json" ], "produces": [ "application/json" ], "responses": { "200": { "description": "200 response", "schema": { "$ref": "#/definitions/Empty" } } }, "x-amazon-apigateway-integration": { "responses": { "default": { "statusCode": "200" } }, "uri": ( "arn:{partition}:apigateway:{region_name}" ":lambda:path/2015-03-31/functions/" "{api_handler_lambda_arn}/invocations" ), "passthroughBehavior": "when_no_match", "httpMethod": "POST", "contentHandling": "CONVERT_TO_TEXT", "type": "aws_proxy" } } } }, "definitions": { "Empty": { "type": "object", "title": "Empty Schema" } }, "x-amazon-apigateway-binary-media-types": [ "application/octet-stream", "application/x-tar", "application/zip", "audio/basic", "audio/ogg", "audio/mp4", "audio/mpeg", "audio/wav", "audio/webm", "image/png", "image/jpg", "image/jpeg", "image/gif", "video/ogg", "video/mpeg", "video/webm" ] } def test_can_package_command(runner, mock_cli_factory): with runner.isolated_filesystem(): newproj.create_new_project_skeleton('testproject') os.chdir('testproject') result = _run_cli_command(runner, cli.package, ['outdir']) assert result.exit_code == 0, result.output assert os.path.isdir('outdir') dir_contents = os.listdir('outdir') assert 'sam.json' in dir_contents assert 'deployment.zip' in dir_contents def test_can_package_with_yaml_command(runner): with runner.isolated_filesystem(): newproj.create_new_project_skeleton('testproject') os.chdir('testproject') result = _run_cli_command(runner, cli.package, ['--template-format', 'yaml', 'outdir']) assert result.exit_code == 0, result.output assert os.path.isdir('outdir') dir_contents = os.listdir('outdir') assert 'sam.yaml' in dir_contents assert 'deployment.zip' in dir_contents def test_case_insensitive_template_format(runner): with runner.isolated_filesystem(): newproj.create_new_project_skeleton('testproject') os.chdir('testproject') result = _run_cli_command(runner, cli.package, ['--template-format', 'YAML', 'outdir']) assert result.exit_code == 0, result.output assert os.path.isdir('outdir') assert 'sam.yaml' in os.listdir('outdir') def test_can_package_with_single_file(runner): with runner.isolated_filesystem(): newproj.create_new_project_skeleton('testproject') os.chdir('testproject') result = _run_cli_command( runner, cli.package, ['--single-file', 'package.zip']) assert result.exit_code == 0, result.output assert os.path.isfile('package.zip') with zipfile.ZipFile('package.zip', 'r') as f: assert sorted(f.namelist()) == [ 'deployment.zip', 'sam.json'] def test_package_terraform_err_with_single_file_or_merge(runner): with runner.isolated_filesystem(): newproj.create_new_project_skeleton('testproject') os.chdir('testproject') result = _run_cli_command( runner, cli.package, ['--pkg-format', 'terraform', '--single-file', 'module']) assert result.exit_code == 1, result.output assert "Terraform format does not support" in result.output result = _run_cli_command( runner, cli.package, ['--pkg-format', 'terraform', '--merge-template', 'foo.json', 'module']) assert result.exit_code == 1, result.output assert "Terraform format does not support" in result.output def test_debug_flag_enables_logging(runner): with runner.isolated_filesystem(): newproj.create_new_project_skeleton('testproject') os.chdir('testproject') result = runner.invoke( cli.cli, ['--debug', 'package', 'outdir'], obj={}) assert result.exit_code == 0 assert re.search('[DEBUG].*Creating deployment package', result.output) is not None def test_does_deploy_with_default_api_gateway_stage_name(runner, mock_cli_factory): with runner.isolated_filesystem(): newproj.create_new_project_skeleton('testproject') os.chdir('testproject') # This isn't perfect as we're assuming we know how to # create the config_obj like the deploy() command does, # it should give us more confidence that the api gateway # stage defaults are still working. cli_factory = factory.CLIFactory('.') config = cli_factory.create_config_obj( chalice_stage_name='dev', autogen_policy=None, api_gateway_stage=None ) assert config.api_gateway_stage == DEFAULT_APIGATEWAY_STAGE_NAME def test_can_specify_api_gateway_stage(runner, mock_cli_factory): with runner.isolated_filesystem(): newproj.create_new_project_skeleton('testproject') os.chdir('testproject') result = _run_cli_command(runner, cli.deploy, ['--api-gateway-stage', 'notdev'], cli_factory=mock_cli_factory) assert result.exit_code == 0 mock_cli_factory.create_config_obj.assert_called_with( autogen_policy=None, chalice_stage_name='dev', api_gateway_stage='notdev' ) def test_can_deploy_specify_connection_timeout(runner, mock_cli_factory): with runner.isolated_filesystem(): newproj.create_new_project_skeleton('testproject') os.chdir('testproject') result = _run_cli_command(runner, cli.deploy, ['--connection-timeout', 100], cli_factory=mock_cli_factory) assert result.exit_code == 0 mock_cli_factory.create_botocore_session.assert_called_with( connection_timeout=100 ) def test_can_retrieve_url(runner, mock_cli_factory): deployed_values_dev = { "schema_version": "2.0", "resources": [ {"rest_api_url": "https://dev-url/", "name": "rest_api", "resource_type": "rest_api"}, ] } deployed_values_prod = { "schema_version": "2.0", "resources": [ {"rest_api_url": "https://prod-url/", "name": "rest_api", "resource_type": "rest_api"}, ] } with runner.isolated_filesystem(): newproj.create_new_project_skeleton('testproject') os.chdir('testproject') deployed_dir = os.path.join('.chalice', 'deployed') os.makedirs(deployed_dir) record_deployed_values( deployed_values_dev, os.path.join(deployed_dir, 'dev.json') ) record_deployed_values( deployed_values_prod, os.path.join(deployed_dir, 'prod.json') ) result = _run_cli_command(runner, cli.url, [], cli_factory=mock_cli_factory) assert result.exit_code == 0 assert result.output == 'https://dev-url/\n' prod_result = _run_cli_command(runner, cli.url, ['--stage', 'prod'], cli_factory=mock_cli_factory) assert prod_result.exit_code == 0 assert prod_result.output == 'https://prod-url/\n' def test_error_when_no_deployed_record(runner, mock_cli_factory): with runner.isolated_filesystem(): newproj.create_new_project_skeleton('testproject') os.chdir('testproject') result = _run_cli_command(runner, cli.url, [], cli_factory=mock_cli_factory) assert result.exit_code == 2 assert 'not find' in result.output @pytest.mark.skipif( (3, 9) <= sys.version_info[:2] <= (3, 13), reason=("Cannot generate pipeline for python3.9 - python3.13"), ) def test_can_generate_pipeline_for_all(runner): with runner.isolated_filesystem(): newproj.create_new_project_skeleton('testproject') os.chdir('testproject') result = _run_cli_command( runner, cli.generate_pipeline, ['pipeline.json']) assert result.exit_code == 0, result.output assert os.path.isfile('pipeline.json') with open('pipeline.json', 'r') as f: template = json.load(f) # The actual contents are tested in the unit # tests. Just a sanity check that it looks right. assert "AWSTemplateFormatVersion" in template assert "Outputs" in template def test_no_errors_if_override_codebuild_image(runner): with runner.isolated_filesystem(): newproj.create_new_project_skeleton('testproject') os.chdir('testproject') result = _run_cli_command( runner, cli.generate_pipeline, ['-i', 'python:3.6.1', 'pipeline.json']) assert result.exit_code == 0, result.output assert os.path.isfile('pipeline.json') with open('pipeline.json', 'r') as f: template = json.load(f) # The actual contents are tested in the unit # tests. Just a sanity check that it looks right. image = template['Parameters']['CodeBuildImage']['Default'] assert image == 'python:3.6.1' def test_can_configure_github(runner): with runner.isolated_filesystem(): newproj.create_new_project_skeleton('testproject') os.chdir('testproject') # The -i option is provided so we don't have to skip this # test on python3.6 result = _run_cli_command( runner, cli.generate_pipeline, ['--source', 'github', '-i' 'python:3.6.1', 'pipeline.json']) assert result.exit_code == 0, result.output assert os.path.isfile('pipeline.json') with open('pipeline.json', 'r') as f: template = json.load(f) # The template is already tested in the unit tests # for template generation. We just want a basic # sanity check to make sure things are mapped # properly. assert 'GithubOwner' in template['Parameters'] assert 'GithubRepoName' in template['Parameters'] def test_can_extract_buildspec_yaml(runner): with runner.isolated_filesystem(): newproj.create_new_project_skeleton('testproject') os.chdir('testproject') result = _run_cli_command( runner, cli.generate_pipeline, ['--buildspec-file', 'buildspec.yml', '-i', 'python:3.6.1', 'pipeline.json']) assert result.exit_code == 0, result.output assert os.path.isfile('buildspec.yml') with open('buildspec.yml') as f: data = f.read() # The contents of this file are tested elsewhere, # we just want a basic sanity check here. assert 'chalice package' in data def test_can_specify_profile_for_logs(runner, mock_cli_factory): with runner.isolated_filesystem(): newproj.create_new_project_skeleton('testproject') os.chdir('testproject') result = _run_cli_command( runner, cli.logs, ['--profile', 'my-profile'], cli_factory=mock_cli_factory ) assert result.exit_code == 0 assert mock_cli_factory.profile == 'my-profile' def test_can_provide_lambda_name_for_logs(runner, mock_cli_factory): deployed_resources = DeployedResources({ "resources": [ {"name": "foo", "lambda_arn": "arn:aws:lambda::app-dev-foo", "resource_type": "lambda_function"}] }) mock_cli_factory.create_config_obj.return_value = FakeConfig( deployed_resources) log_retriever = mock.Mock(spec=LogRetriever) log_retriever.retrieve_logs.return_value = [] mock_cli_factory.create_log_retriever.return_value = log_retriever with runner.isolated_filesystem(): newproj.create_new_project_skeleton('testproject') os.chdir('testproject') result = _run_cli_command( runner, cli.logs, ['--name', 'foo'], cli_factory=mock_cli_factory ) assert result.exit_code == 0 log_retriever.retrieve_logs.assert_called_with( LogRetrieveOptions( include_lambda_messages=False, max_entries=None) ) mock_cli_factory.create_log_retriever.assert_called_with( mock.sentinel.Session, 'arn:aws:lambda::app-dev-foo', False ) def test_can_follow_logs_with_option(runner, mock_cli_factory): deployed_resources = DeployedResources({ "resources": [ {"name": "foo", "lambda_arn": "arn:aws:lambda::app-dev-foo", "resource_type": "lambda_function"}] }) mock_cli_factory.create_config_obj.return_value = FakeConfig( deployed_resources) log_retriever = mock.Mock(spec=LogRetriever) log_retriever.retrieve_logs.return_value = [] mock_cli_factory.create_log_retriever.return_value = log_retriever with runner.isolated_filesystem(): newproj.create_new_project_skeleton('testproject') os.chdir('testproject') result = _run_cli_command( runner, cli.logs, ['--name', 'foo', '--follow'], cli_factory=mock_cli_factory ) assert result.exit_code == 0 log_retriever.retrieve_logs.assert_called_with( LogRetrieveOptions( include_lambda_messages=False, max_entries=None) ) mock_cli_factory.create_log_retriever.assert_called_with( mock.sentinel.Session, 'arn:aws:lambda::app-dev-foo', True ) def test_can_call_invoke(runner, mock_cli_factory, monkeypatch): invoke_handler = mock.Mock(spec=LambdaInvokeHandler) mock_cli_factory.create_lambda_invoke_handler.return_value = invoke_handler mock_reader = mock.Mock(spec=PipeReader) mock_reader.read.return_value = 'barbaz' mock_cli_factory.create_stdin_reader.return_value = mock_reader with runner.isolated_filesystem(): newproj.create_new_project_skeleton('testproject') os.chdir('testproject') result = _run_cli_command(runner, cli.invoke, ['-n', 'foo'], cli_factory=mock_cli_factory) assert result.exit_code == 0 assert invoke_handler.invoke.call_args == mock.call('barbaz') def test_invoke_does_raise_if_service_error(runner, mock_cli_factory): deployed_resources = DeployedResources({"resources": []}) mock_cli_factory.create_config_obj.return_value = FakeConfig( deployed_resources) invoke_handler = mock.Mock(spec=LambdaInvokeHandler) invoke_handler.invoke.side_effect = ClientError( { 'Error': { 'Code': 'LambdaError', 'Message': 'Error message' } }, 'Invoke' ) mock_cli_factory.create_lambda_invoke_handler.return_value = invoke_handler mock_reader = mock.Mock(spec=PipeReader) mock_reader.read.return_value = 'barbaz' mock_cli_factory.create_stdin_reader.return_value = mock_reader with runner.isolated_filesystem(): newproj.create_new_project_skeleton('testproject') os.chdir('testproject') result = _run_cli_command(runner, cli.invoke, ['-n', 'foo'], cli_factory=mock_cli_factory) assert result.exit_code == 1 assert invoke_handler.invoke.call_args == mock.call('barbaz') assert ( "Error: got 'LambdaError' exception back from Lambda\n" "Error message" ) in result.output def test_invoke_does_raise_if_unhandled_error(runner, mock_cli_factory): deployed_resources = DeployedResources({"resources": []}) mock_cli_factory.create_config_obj.return_value = FakeConfig( deployed_resources) invoke_handler = mock.Mock(spec=LambdaInvokeHandler) invoke_handler.invoke.side_effect = UnhandledLambdaError('foo') mock_cli_factory.create_lambda_invoke_handler.return_value = invoke_handler mock_reader = mock.Mock(spec=PipeReader) mock_reader.read.return_value = 'barbaz' mock_cli_factory.create_stdin_reader.return_value = mock_reader with runner.isolated_filesystem(): newproj.create_new_project_skeleton('testproject') os.chdir('testproject') result = _run_cli_command(runner, cli.invoke, ['-n', 'foo'], cli_factory=mock_cli_factory) assert result.exit_code == 1 assert invoke_handler.invoke.call_args == mock.call('barbaz') assert 'Unhandled exception in Lambda function, details above.' \ in result.output def test_invoke_does_raise_if_read_timeout(runner, mock_cli_factory): mock_cli_factory.create_lambda_invoke_handler.side_effect = \ ReadTimeout('It took too long') with runner.isolated_filesystem(): newproj.create_new_project_skeleton('testproject') os.chdir('testproject') result = _run_cli_command(runner, cli.invoke, ['-n', 'foo'], cli_factory=mock_cli_factory) assert result.exit_code == 1 assert 'It took too long' in result.output def test_invoke_does_raise_if_no_function_found(runner, mock_cli_factory): mock_cli_factory.create_lambda_invoke_handler.side_effect = \ factory.NoSuchFunctionError('foo') with runner.isolated_filesystem(): newproj.create_new_project_skeleton('testproject') os.chdir('testproject') result = _run_cli_command(runner, cli.invoke, ['-n', 'foo'], cli_factory=mock_cli_factory) assert result.exit_code == 2 assert 'foo' in result.output def test_error_message_displayed_when_missing_feature_opt_in(runner): with runner.isolated_filesystem(): newproj.create_new_project_skeleton('testproject') with open(os.path.join('testproject', 'app.py'), 'w') as f: # Rather than pick an existing experimental feature, we're # manually injecting a feature flag into our app. This ensures # we don't have to update this test if a feature graduates # from trial to accepted. The '_features_used' is a "package # private" var for chalice code. f.write( 'from chalice import Chalice\n' 'app = Chalice("myapp")\n' 'app._features_used.add("MYTESTFEATURE")\n' ) os.chdir('testproject') result = _run_cli_command(runner, cli.package, ['out']) assert isinstance(result.exception, ExperimentalFeatureError) assert 'MYTESTFEATURE' in str(result.exception) @pytest.mark.parametrize( "path", [ None, '.', os.getcwd, ], ) def test_cli_with_absolute_path(runner, path): with runner.isolated_filesystem(): if callable(path): path = path() result = runner.invoke( cli.cli, ['--project-dir', path, 'new-project', 'testproject'], obj={}) assert result.exit_code == 0 assert os.listdir(os.getcwd()) == ['testproject'] assert_chalice_app_structure_created(dirname='testproject') def test_can_generate_dev_plan(runner, mock_cli_factory): with runner.isolated_filesystem(): newproj.create_new_project_skeleton('testproject') os.chdir('testproject') result = _run_cli_command(runner, cli.plan, [], cli_factory=mock_cli_factory) deployer = mock_cli_factory.create_plan_only_deployer.return_value call_args = deployer.deploy.call_args assert result.exit_code == 0 assert isinstance(call_args[0][0], Config) assert call_args[1] == {'chalice_stage_name': 'dev'} # The appgraph command actually works on py27, but due to a bug in click's # testing (https://github.com/pallets/click/issues/848), it assumes # stdout must be ascii. # stdout is a cStringIO.StringIO, which doesn't accept unicode. # See: https://docs.python.org/2/library/stringio.html#cStringIO.StringIO @pytest.mark.skipif(sys.version_info[0] == 2, reason="Click bug when writing unicode to stdout.") def test_can_generate_appgraph(runner, mock_cli_factory): with runner.isolated_filesystem(): newproj.create_new_project_skeleton('testproject') os.chdir('testproject') result = _run_cli_command(runner, cli.appgraph, []) assert result.exit_code == 0 # Just sanity checking some of the output assert 'Application' in result.output assert 'RestAPI(' in result.output def test_chalice_cli_mode_env_var_always_set(runner): with runner.isolated_filesystem(): result = runner.invoke(cli.new_project, ['testproject'], obj={}) assert result.exit_code == 0 assert os.environ['AWS_CHALICE_CLI_MODE'] == 'true' ================================================ FILE: tests/functional/cli/test_factory.py ================================================ import os import sys import json import logging import pytest from pytest import fixture from chalice.cli import factory from chalice.deploy.deployer import Deployer, DeploymentReporter from chalice.config import Config from chalice.config import DeployedResources from chalice import local from chalice.package import PackageOptions from chalice.utils import UI from chalice import Chalice from chalice.logs import LogRetriever from chalice.invoke import LambdaInvokeHandler @fixture def no_deployed_values(): return DeployedResources({'resources': [], 'schema_version': '2.0'}) @fixture def clifactory(tmpdir): appdir = tmpdir.mkdir('app') appdir.join('app.py').write( '# Test app\n' 'import chalice\n' 'app = chalice.Chalice(app_name="test")\n' ) chalice_dir = appdir.mkdir('.chalice') chalice_dir.join('config.json').write('{}') return factory.CLIFactory(str(appdir)) def assert_has_no_request_body_filter(log_name): log = logging.getLogger(log_name) assert not any( isinstance(f, factory.LargeRequestBodyFilter) for f in log.filters) def assert_request_body_filter_in_log(log_name): log = logging.getLogger(log_name) assert any( isinstance(f, factory.LargeRequestBodyFilter) for f in log.filters) def test_can_create_botocore_session(): session = factory.create_botocore_session() assert session.user_agent().startswith('aws-chalice/') assert session.get_default_client_config() is None def test_can_create_botocore_session_debug(): log_name = 'botocore.endpoint' assert_has_no_request_body_filter(log_name) factory.create_botocore_session(debug=True) assert_request_body_filter_in_log(log_name) assert logging.getLogger('').level == logging.DEBUG def test_can_create_botocore_session_connection_timeout(): session = factory.create_botocore_session(connection_timeout=100) assert vars(session.get_default_client_config())['connect_timeout'] == 100 def test_can_create_botocore_session_read_timeout(): session = factory.create_botocore_session(read_timeout=50) assert vars(session.get_default_client_config())['read_timeout'] == 50 def test_can_create_botocore_session_max_retries(): session = factory.create_botocore_session(max_retries=2) assert vars( session.get_default_client_config())['retries']['max_attempts'] == 2 def test_can_create_botocore_session_with_multiple_configs(): session = factory.create_botocore_session( connection_timeout=100, read_timeout=50, max_retries=5, ) assert vars(session.get_default_client_config())['connect_timeout'] == 100 assert vars(session.get_default_client_config())['read_timeout'] == 50 assert vars( session.get_default_client_config())['retries']['max_attempts'] == 5 def test_can_create_botocore_session_cli_factory(clifactory): clifactory.profile = 'myprofile' session = clifactory.create_botocore_session() assert session.profile == 'myprofile' def test_can_create_deletion_deployer(clifactory): session = clifactory.create_botocore_session() deployer = clifactory.create_deletion_deployer(session, UI()) assert isinstance(deployer, Deployer) def test_can_create_plan_only_deployer(clifactory): session = clifactory.create_botocore_session() config = clifactory.create_config_obj(chalice_stage_name='dev') deployer = clifactory.create_plan_only_deployer( session=session, config=config, ui=UI()) assert isinstance(deployer, Deployer) def test_can_create_config_obj(clifactory): obj = clifactory.create_config_obj() assert isinstance(obj, Config) def test_can_create_config_obj_default_autogen_policy_true(clifactory): config = clifactory.create_config_obj() assert config.autogen_policy is True def test_provided_autogen_policy_overrides_config_file(clifactory): config_file = os.path.join( clifactory.project_dir, '.chalice', 'config.json') with open(config_file, 'w') as f: f.write('{"autogen_policy": false}') config = clifactory.create_config_obj(autogen_policy=True) assert config.autogen_policy is True def test_can_create_config_obj_with_override_autogen(clifactory): config = clifactory.create_config_obj(autogen_policy=False) assert config.autogen_policy is False def test_config_file_override_autogen_policy(clifactory): config_file = os.path.join( clifactory.project_dir, '.chalice', 'config.json') with open(config_file, 'w') as f: f.write('{"autogen_policy": false}') config = clifactory.create_config_obj() assert config.autogen_policy is False def test_can_create_config_obj_with_api_gateway_stage(clifactory): config = clifactory.create_config_obj(api_gateway_stage='custom-stage') assert config.api_gateway_stage == 'custom-stage' def test_can_create_config_obj_with_default_api_gateway_stage(clifactory): config = clifactory.create_config_obj() assert config.api_gateway_stage == 'api' def test_cant_load_config_obj_with_bad_project(clifactory): clifactory.project_dir = 'nowhere-asdfasdfasdfas' with pytest.raises(RuntimeError): clifactory.create_config_obj() def test_error_raised_on_unknown_config_version(clifactory): filename = os.path.join( clifactory.project_dir, '.chalice', 'config.json') with open(filename, 'w') as f: f.write(json.dumps({"version": "100.0"})) with pytest.raises(factory.UnknownConfigFileVersion): clifactory.create_config_obj() def test_filename_and_lineno_included_in_syntax_error(clifactory): filename = os.path.join(clifactory.project_dir, 'app.py') with open(filename, 'w') as f: f.write("this is a syntax error\n") # If this app has been previously imported in another app # we need to remove it from the cached modules to ensure # we get the syntax error on import. with pytest.raises(RuntimeError) as excinfo: clifactory.load_chalice_app() message = str(excinfo.value) assert 'app.py' in message assert 'line 1' in message def test_can_import_vendor_package(clifactory): # Tests that vendor packages can be imported during config loading. vendor_lib = os.path.join(clifactory.project_dir, 'vendor') vendedlib_dir = os.path.join(vendor_lib, 'vendedlib') os.makedirs(vendedlib_dir) open(os.path.join(vendedlib_dir, '__init__.py'), 'a').close() with open(os.path.join(vendedlib_dir, 'submodule.py'), 'a') as f: f.write('CONST = "foo bar"\n') app_py = os.path.join(clifactory.project_dir, 'app.py') with open(app_py, 'a') as f: f.write('from vendedlib import submodule\n') f.write('app.imported_value = submodule.CONST\n') app = clifactory.load_chalice_app() assert app.imported_value == 'foo bar' assert sys.path[-1] == vendor_lib def test_error_raised_on_invalid_config_json(clifactory): filename = os.path.join( clifactory.project_dir, '.chalice', 'config.json') with open(filename, 'w') as f: f.write("INVALID_JSON") with pytest.raises(RuntimeError): clifactory.create_config_obj() def test_can_create_local_server(clifactory): app = clifactory.load_chalice_app() config = clifactory.create_config_obj() server = clifactory.create_local_server(app, config, '0.0.0.0', 8000) assert isinstance(server, local.LocalDevServer) assert server.host == '0.0.0.0' assert server.port == 8000 def test_can_create_deployment_reporter(clifactory): ui = UI() reporter = clifactory.create_deployment_reporter(ui=ui) assert isinstance(reporter, DeploymentReporter) def test_can_access_lazy_loaded_app(clifactory): config = clifactory.create_config_obj() assert isinstance(config.chalice_app, Chalice) def test_can_create_log_retriever(clifactory): session = clifactory.create_botocore_session() lambda_arn = ( 'arn:aws:lambda:us-west-2:1:function:app-dev-foo' ) logs = clifactory.create_log_retriever(session, lambda_arn, follow_logs=False) assert isinstance(logs, LogRetriever) def test_can_create_follow_logs_retriever(clifactory): session = clifactory.create_botocore_session() lambda_arn = ( 'arn:aws:lambda:us-west-2:1:function:app-dev-foo' ) logs = clifactory.create_log_retriever(session, lambda_arn, follow_logs=True) assert isinstance(logs, LogRetriever) def test_can_create_lambda_invoke_handler(clifactory): lambda_arn = ( 'arn:aws:lambda:us-west-2:1:function:app-dev-foo' ) stage = 'dev' deployed_dir = os.path.join(clifactory.project_dir, '.chalice', 'deployed') os.mkdir(deployed_dir) deployed_file = os.path.join(deployed_dir, '%s.json' % stage) with open(deployed_file, 'w') as f: f.write(json.dumps({ 'resources': [ { 'name': 'foobar', 'resource_type': 'lambda_function', 'lambda_arn': lambda_arn, }, ], 'schema_version': '2.0' })) invoker = clifactory.create_lambda_invoke_handler('foobar', stage) assert isinstance(invoker, LambdaInvokeHandler) def test_does_raise_not_found_error_when_no_function_found( clifactory, no_deployed_values): with pytest.raises(factory.NoSuchFunctionError) as e: clifactory.create_lambda_invoke_handler('function_name', 'stage') assert e.value.name == 'function_name' def test_does_raise_not_found_error_when_resource_is_not_lambda(clifactory): stage = 'dev' deployed_dir = os.path.join(clifactory.project_dir, '.chalice', 'deployed') os.mkdir(deployed_dir) deployed_file = os.path.join(deployed_dir, '%s.json' % stage) with open(deployed_file, 'w') as f: f.write(json.dumps({ 'resources': [ { 'name': 'foobar', 'resource_type': 'iam_role', 'role_arn': 'bazbuz', }, ], 'schema_version': '2.0' })) with pytest.raises(factory.NoSuchFunctionError) as e: clifactory.create_lambda_invoke_handler('foobar', stage) assert e.value.name == 'foobar' def test_can_create_package_options(clifactory): options = clifactory.create_package_options() assert isinstance(options, PackageOptions) ================================================ FILE: tests/functional/cli/test_reloader.py ================================================ import pytest from unittest import mock import threading import os import unittest import time from chalice.cli.filewatch.stat import StatWorkerProcess try: from chalice.cli.filewatch.eventbased import WatchdogWorkerProcess WATCHDOG_AVAILABLE = True except ImportError: WATCHDOG_AVAILABLE = False import chalice.local DEFAULT_DELAY = 0.1 SETTLE_DELAY = 1 MAX_TIMEOUT = 5.0 use_all_watcher_types = pytest.mark.parametrize( ['worker_class_type'], [('watchdog',), ('stat',)]) def modify_file_after_n_seconds(filename, contents, delay=DEFAULT_DELAY): t = threading.Timer(delay, function=modify_file, args=(filename, contents)) t.daemon = True t.start() def delete_file_after_n_seconds(filename, delay=DEFAULT_DELAY): t = threading.Timer(delay, function=os.remove, args=(filename,)) t.daemon = True t.start() def modify_file(filename, contents): if filename is None: return with open(filename, 'w') as f: f.write(contents) def assert_reload_happens(root_dir, when_modified_file, using_worker_class): http_thread = mock.Mock(spec=chalice.local.HTTPServerThread) worker_cls = get_worker_cls(using_worker_class) p = worker_cls(http_thread) if isinstance(when_modified_file, tuple): if when_modified_file[1] == 'is_deleted': delete_file_after_n_seconds(when_modified_file[0]) else: modify_file_after_n_seconds(when_modified_file, 'contents') rc = p.main(root_dir, MAX_TIMEOUT) assert rc == chalice.cli.filewatch.RESTART_REQUEST_RC def get_worker_cls(worker_class_name): if worker_class_name == 'watchdog': if not WATCHDOG_AVAILABLE: raise unittest.SkipTest("Test requires watchdog package.") else: return WatchdogWorkerProcess elif worker_class_name == 'stat': return StatWorkerProcess else: raise RuntimeError("Unknown worker class type name: %s" % worker_class_name) @use_all_watcher_types def test_can_reload_when_file_created(tmpdir, worker_class_type): top_level_file = str(tmpdir.join('foo')) assert_reload_happens(str(tmpdir), when_modified_file=top_level_file, using_worker_class=worker_class_type) @use_all_watcher_types def test_can_reload_when_subdir_file_created(tmpdir, worker_class_type): subdir_file = str(tmpdir.join('subdir').mkdir().join('foo.txt')) assert_reload_happens(str(tmpdir), when_modified_file=subdir_file, using_worker_class=worker_class_type) @use_all_watcher_types def test_can_reload_when_file_modified(tmpdir, worker_class_type): top_level_file = tmpdir.join('foo') top_level_file.write('original contents') # If you write to the file and immediately start the reloader, it # won't see the initial write() above. I tried out a few delay options, # and a separate SETTLE_DELAY was necessary in order to prevent # intermittent failures. time.sleep(SETTLE_DELAY) assert_reload_happens(str(tmpdir), when_modified_file=str(top_level_file), using_worker_class=worker_class_type) @use_all_watcher_types def test_can_reload_when_file_removed(tmpdir, worker_class_type): top_level_file = tmpdir.join('foo') top_level_file.write('original contents') assert_reload_happens( str(tmpdir), when_modified_file=(str(top_level_file), 'is_deleted'), using_worker_class=worker_class_type ) @use_all_watcher_types def test_rc_0_when_no_file_modified(tmpdir, worker_class_type): http_thread = mock.Mock(spec=chalice.local.HTTPServerThread) worker_cls = get_worker_cls(worker_class_type) p = worker_cls(http_thread) rc = p.main(str(tmpdir), timeout=0.2) assert rc == 0 ================================================ FILE: tests/functional/conftest.py ================================================ from pytest import fixture @fixture(autouse=True) def ensure_no_local_config(no_local_config): pass ================================================ FILE: tests/functional/envapp/.chalice/config.json ================================================ { "stages": { "dev": { "api_gateway_stage": "api", "environment_variables": {"FOO": "bar"} } }, "version": "2.0", "app_name": "env" } ================================================ FILE: tests/functional/envapp/.gitignore ================================================ .chalice/deployments/ .chalice/venv/ ================================================ FILE: tests/functional/envapp/app.py ================================================ import os import sys from chalice import Chalice app = Chalice(app_name='env') try: foo = os.environ['FOO'] except KeyError: raise AssertionError("Env vars were not loaded at import time.") @app.route('/') def index(): return {'hello': foo} sys.stderr.write("READY") sys.stderr.flush() ================================================ FILE: tests/functional/envapp/requirements.txt ================================================ ================================================ FILE: tests/functional/test_awsclient.py ================================================ import json import datetime import time from unittest import mock import pytest import botocore.exceptions from botocore.vendored.requests import ConnectionError as \ RequestsConnectionError from botocore.vendored.requests.exceptions import ReadTimeout as \ RequestsReadTimeout from botocore import stub from botocore.utils import datetime2timestamp from chalice.awsclient import TypedAWSClient from chalice.awsclient import ResourceDoesNotExistError from chalice.awsclient import DeploymentPackageTooLargeError from chalice.awsclient import LambdaClientError from chalice.awsclient import ReadTimeout def create_policy_statement(source_arn, service_name, statement_id, account_id=None): policy_statement = { 'Action': 'lambda:InvokeFunction', 'Condition': { 'ArnLike': { 'AWS:SourceArn': source_arn, } }, 'Effect': 'Allow', 'Principal': {'Service': '%s.amazonaws.com' % service_name}, 'Resource': 'function-arn', 'Sid': statement_id, } if account_id is not None: policy_statement['Condition']['StringEquals'] = { 'AWS:SourceAccount': account_id, } return policy_statement def test_region_name_is_exposed(stubbed_session): assert TypedAWSClient(stubbed_session).region_name == 'us-west-2' def test_deploy_rest_api(stubbed_session): stub_client = stubbed_session.stub('apigateway') stub_client.create_deployment( restApiId='api_id', stageName='stage', tracingEnabled=False).returns({}) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) awsclient.deploy_rest_api('api_id', 'stage', False) stubbed_session.verify_stubs() def test_defaults_to_false_if_none_deploy_rest_api(stubbed_session): stub_client = stubbed_session.stub('apigateway') stub_client.create_deployment( restApiId='api_id', stageName='stage', tracingEnabled=False).returns({}) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) awsclient.deploy_rest_api('api_id', 'stage', None) stubbed_session.verify_stubs() def test_put_role_policy(stubbed_session): stubbed_session.stub('iam').put_role_policy( RoleName='role_name', PolicyName='policy_name', PolicyDocument=json.dumps({'foo': 'bar'}, indent=2) ).returns({}) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) awsclient.put_role_policy('role_name', 'policy_name', {'foo': 'bar'}) stubbed_session.verify_stubs() def test_rest_api_exists(stubbed_session): stubbed_session.stub('apigateway').get_rest_api( restApiId='api').returns({'id': 'api'}) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) assert awsclient.get_rest_api('api') stubbed_session.verify_stubs() def test_rest_api_not_exists(stubbed_session): stubbed_session.stub('apigateway').get_rest_api( restApiId='api').raises_error( error_code='NotFoundException', message='ResourceNotFound') stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) assert not awsclient.get_rest_api('api') stubbed_session.verify_stubs() def test_can_get_function_configuration(stubbed_session): stubbed_session.stub('lambda').get_function_configuration( FunctionName='myfunction', ).returns({ "FunctionName": "myfunction", "MemorySize": 128, "Handler": "app.app", "Runtime": "python3.6", }) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) assert (awsclient.get_function_configuration('myfunction')['Runtime'] == 'python3.6') def test_can_iterate_logs(stubbed_session): stubbed_session.stub('logs').filter_log_events( logGroupName='loggroup', interleaved=True).returns({ "events": [{ "logStreamName": "logStreamName", "timestamp": 1501278366000, "message": "message", "ingestionTime": 1501278366000, "eventId": "eventId" }], }) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) logs = list(awsclient.iter_log_events('loggroup')) timestamp = datetime.datetime.utcfromtimestamp(1501278366) assert logs == [ {'logStreamName': 'logStreamName', # We should have converted the ints to timestamps. 'timestamp': timestamp, 'message': 'message', 'ingestionTime': timestamp, 'eventId': 'eventId'} ] stubbed_session.verify_stubs() def test_can_provide_optional_start_time_iter_logs(stubbed_session): timestamp = int(datetime2timestamp(datetime.datetime.utcnow()) * 1000) # We need to convert back from timestamp instead of using utcnow() directly # because the loss of precision in sub ms time. datetime_now = datetime.datetime.utcfromtimestamp(timestamp / 1000.0) stubbed_session.stub('logs').filter_log_events( logGroupName='loggroup', interleaved=True).returns({ "events": [{ "logStreamName": "logStreamName", "timestamp": timestamp, "message": "message", "ingestionTime": timestamp, "eventId": "eventId" }], }) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) logs = list(awsclient.iter_log_events('loggroup', start_time=datetime_now)) assert logs == [ {'logStreamName': 'logStreamName', 'timestamp': datetime_now, 'message': 'message', 'ingestionTime': datetime_now, 'eventId': 'eventId'} ] stubbed_session.verify_stubs() def test_missing_log_messages_doesnt_fail(stubbed_session): stubbed_session.stub('logs').filter_log_events( logGroupName='loggroup', interleaved=True).raises_error( error_code='ResourceNotFoundException', message='ResourceNotFound') stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) logs = list(awsclient.iter_log_events('loggroup')) assert logs == [] def test_can_call_filter_log_events(stubbed_session): stubbed_session.stub('logs').filter_log_events( logGroupName='loggroup', interleaved=True, nextToken='nexttoken', startTime=1577836800000.0 ).returns({ "events": [{ "logStreamName": "logStreamName", "timestamp": 1501278366000, "message": "message", "ingestionTime": 1501278366000, "eventId": "eventId" }], }) stubbed_session.activate_stubs() timestamp = datetime.datetime.utcfromtimestamp(1501278366) awsclient = TypedAWSClient(stubbed_session) assert awsclient.filter_log_events( log_group_name='loggroup', next_token='nexttoken', start_time=datetime.datetime(2020, 1, 1) ) == { 'events': [{ "logStreamName": "logStreamName", "timestamp": timestamp, "message": "message", "ingestionTime": timestamp, "eventId": "eventId" }] } def test_optional_kwarg_on_filter_logs_omitted(stubbed_session): stubbed_session.stub('logs').filter_log_events( logGroupName='loggroup', interleaved=True, ).returns({ "events": [{ "logStreamName": "logStreamName", "timestamp": 1501278366000, "message": "message", "ingestionTime": 1501278366000, "eventId": "eventId" }], }) stubbed_session.activate_stubs() timestamp = datetime.datetime.utcfromtimestamp(1501278366) awsclient = TypedAWSClient(stubbed_session) assert awsclient.filter_log_events( log_group_name='loggroup', ) == { 'events': [{ "logStreamName": "logStreamName", "timestamp": timestamp, "message": "message", "ingestionTime": timestamp, "eventId": "eventId" }] } def test_missing_log_events_returns_empty_response(stubbed_session): stubbed_session.stub('logs').filter_log_events( logGroupName='loggroup', interleaved=True).raises_error( error_code='ResourceNotFoundException', message='ResourceNotFound') stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) assert awsclient.filter_log_events( log_group_name='loggroup', ) == {'events': []} def test_rule_arn_requires_expression_or_pattern(stubbed_session): client = TypedAWSClient(stubbed_session) with pytest.raises(ValueError): client.get_or_create_rule_arn("foo") class TestLambdaLayer(object): def test_layer_exists(self, stubbed_session): stubbed_session.stub('lambda').get_layer_version_by_arn( Arn='arn:xyz').returns( {'LayerVersionArn': 'arn:xyz'}) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) assert awsclient.get_layer_version('arn:xyz') == { 'LayerVersionArn': 'arn:xyz'} def test_layer_exists_not_found_error(self, stubbed_session): stubbed_session.stub('lambda').get_layer_version_by_arn( Arn='arn:xyz').raises_error( error_code='ResourceNotFoundException', message='Not Found') stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) assert awsclient.get_layer_version('arn:xyz') == {} def test_layer_delete_not_found_error(self, stubbed_session): stubbed_session.stub('lambda').delete_layer_version( LayerName='xyz', VersionNumber=4).raises_error( error_code='ResourceNotFoundException', message='Not Found') stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) assert awsclient.delete_layer_version('arn:xyz:4') is None def test_publish_layer_propagate_error(self, stubbed_session): stubbed_session.stub('lambda').publish_layer_version( LayerName='name', CompatibleRuntimes=['python2.7'], Content={'ZipFile': b'foo'}, ).raises_error(error_code='UnexpectedError', message='Unknown') stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) with pytest.raises(LambdaClientError) as excinfo: awsclient.publish_layer( 'name', b'foo', 'python2.7') == 'arn:12345:name' assert isinstance( excinfo.value.original_error, botocore.exceptions.ClientError) stubbed_session.verify_stubs() def test_can_publish_layer(self, stubbed_session): stubbed_session.stub('lambda').publish_layer_version( LayerName='name', CompatibleRuntimes=['python2.7'], Content={'ZipFile': b'foo'}, ).returns({'LayerVersionArn': 'arn:12345:name:3'}) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) assert awsclient.publish_layer( 'name', b'foo', 'python2.7') == 'arn:12345:name:3' stubbed_session.verify_stubs() class TestLambdaFunctionExists(object): def test_can_query_lambda_function_exists(self, stubbed_session): stubbed_session.stub('lambda').get_function(FunctionName='myappname')\ .returns({'Code': {}, 'Configuration': {}}) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) assert awsclient.lambda_function_exists(name='myappname') stubbed_session.verify_stubs() def test_can_query_lambda_function_does_not_exist(self, stubbed_session): stubbed_session.stub('lambda').get_function(FunctionName='myappname')\ .raises_error(error_code='ResourceNotFoundException', message='ResourceNotFound') stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) assert not awsclient.lambda_function_exists(name='myappname') stubbed_session.verify_stubs() def test_lambda_function_bad_error_propagates(self, stubbed_session): stubbed_session.stub('lambda').get_function(FunctionName='myappname')\ .raises_error(error_code='UnexpectedError', message='Unknown') stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) with pytest.raises(botocore.exceptions.ClientError): awsclient.lambda_function_exists(name='myappname') stubbed_session.verify_stubs() class TestDeleteLambdaFunction(object): def test_lambda_delete_function(self, stubbed_session): stubbed_session.stub('lambda')\ .delete_function(FunctionName='name').returns({}) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) assert awsclient.delete_function('name') is None stubbed_session.verify_stubs() def test_lambda_delete_function_already_deleted(self, stubbed_session): stubbed_session.stub('lambda')\ .delete_function(FunctionName='name')\ .raises_error(error_code='ResourceNotFoundException', message='Unknown') stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) with pytest.raises(ResourceDoesNotExistError): assert awsclient.delete_function('name') class TestDeleteRestAPI(object): def test_rest_api_delete(self, stubbed_session): stubbed_session.stub('apigateway')\ .delete_rest_api(restApiId='name').returns({}) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) assert awsclient.delete_rest_api('name') is None stubbed_session.verify_stubs() def test_rest_api_delete_already_deleted(self, stubbed_session): stubbed_session.stub('apigateway')\ .delete_rest_api(restApiId='name')\ .raises_error(error_code='NotFoundException', message='Unknown') stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) with pytest.raises(ResourceDoesNotExistError): assert awsclient.delete_rest_api('name') class TestGetDomainName(object): def test_get_domain_name(self, stubbed_session): domain_name = 'test_domain' certificate_arn = 'arn:aws:acm:us-east-1:aws_id:certificate/12345' regional_name = 'test.execute-api.us-east-1.amazonaws.com' stubbed_session.stub('apigateway')\ .get_domain_name(domainName=domain_name)\ .returns({ 'domainName': 'test_domain', 'certificateUploadDate': datetime.datetime.now(), 'regionalDomainName': regional_name, 'regionalHostedZoneId': 'TEST1TEST1TESTQ1', 'regionalCertificateArn': certificate_arn, 'endpointConfiguration': { 'types': ['REGIONAL'] }, 'domainNameStatus': 'AVAILABLE', 'securityPolicy': 'TLS_1_0', 'tags': { 'some_key1': 'test_value1', 'some_key2': 'test_value2' } }) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) result = awsclient.get_domain_name(domain_name)['domainName'] assert result == domain_name def test_get_domain_name_failed(self, stubbed_session): domain_name = 'unknown_domain' stubbed_session.stub('apigateway') \ .get_domain_name(domainName=domain_name) \ .raises_error(error_code='NotFoundException', message='Unknown') stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) with pytest.raises(ResourceDoesNotExistError): assert awsclient.get_domain_name(domain_name) def test_domain_name_exists(self, stubbed_session): domain_name = 'test_domain' certificate_arn = 'arn:aws:acm:us-east-1:aws_id:certificate/12345' regional_name = 'test.execute-api.us-east-1.amazonaws.com' stubbed_session.stub('apigateway')\ .get_domain_name(domainName=domain_name)\ .returns({ 'domainName': 'test_domain', 'certificateUploadDate': datetime.datetime.now(), 'regionalDomainName': regional_name, 'regionalHostedZoneId': 'TEST1TEST1TESTQ1', 'regionalCertificateArn': certificate_arn, 'endpointConfiguration': { 'types': ['REGIONAL'] }, 'domainNameStatus': 'AVAILABLE', 'securityPolicy': 'TLS_1_0', 'tags': { 'some_key1': 'test_value1', 'some_key2': 'test_value2' } }) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) assert awsclient.domain_name_exists(domain_name) def test_domain_name_does_not_exist(self, stubbed_session): domain_name = 'unknown_domain' stubbed_session.stub('apigateway') \ .get_domain_name(domainName=domain_name) \ .raises_error(error_code='NotFoundException', message='Unknown') stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) assert not awsclient.domain_name_exists(domain_name) def test_domain_name_exists_v2(self, stubbed_session): domain_name = 'test_domain' certificate_arn = 'arn:aws:acm:us-east-1:aws_id:certificate/12345' regional_name = 'test.execute-api.us-east-1.amazonaws.com' stubbed_session.stub('apigatewayv2') \ .get_domain_name(DomainName=domain_name) \ .returns({ 'DomainName': 'test_domain', 'DomainNameConfigurations': [{ 'ApiGatewayDomainName': regional_name, 'CertificateArn': certificate_arn, 'EndpointType': 'REGIONAL', 'HostedZoneId': 'TEST1TEST1TESTQ1', 'SecurityPolicy': 'TLS_1_0', 'DomainNameStatus': 'AVAILABLE' }], 'Tags': { 'some_key1': 'some_value1', 'some_key2': 'some_value2' } }) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) assert awsclient.domain_name_exists_v2(domain_name) def test_domain_name_does_not_exist_v2(self, stubbed_session): domain_name = 'unknown_domain' stubbed_session.stub('apigatewayv2') \ .get_domain_name(DomainName=domain_name) \ .raises_error( error_code='NotFoundException', message='Unknown' ) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) assert not awsclient.domain_name_exists_v2(domain_name) class TestGetApiMapping(object): def test_api_mapping_exists(self, stubbed_session): domain_name = 'test_domain' path = '(none)' stubbed_session.stub('apigatewayv2') \ .get_api_mappings( DomainName=domain_name, ).returns({ 'Items': [{ 'ApiMappingKey': '(none)', 'ApiMappingId': 'mapping_id', 'ApiId': 'rest_api_id', 'Stage': 'test' }] }) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) assert awsclient.api_mapping_exists(domain_name, path) def test_api_mapping_not_exists(self, stubbed_session): domain_name = 'test_domain' path = 'path-key' stubbed_session.stub('apigatewayv2') \ .get_api_mappings( DomainName=domain_name, ).returns({ 'Items': [{ 'ApiMappingKey': '(none)', 'ApiMappingId': 'mapping_id', 'ApiId': 'rest_api_id', 'Stage': 'test' }] }) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) assert not awsclient.api_mapping_exists(domain_name, path) def test_api_mapping_does_not_exist(self, stubbed_session): domain_name = 'unknown_domain' path = '/unknown' stubbed_session.stub('apigatewayv2') \ .get_api_mappings( DomainName=domain_name, ).raises_error( error_code='NotFoundException', message='Unknown' ) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) assert not awsclient.api_mapping_exists(domain_name, path) class TestCreateApiMapping(object): def test_create_api_mapping(self, stubbed_session): domain_name = 'test_domain' path_key = '(none)' api_id = 'rest_api_id' stage = 'test' stubbed_session.stub('apigatewayv2') \ .create_api_mapping( DomainName=domain_name, ApiMappingKey=path_key, ApiId=api_id, Stage=stage ).returns({ 'ApiId': api_id, 'ApiMappingId': 'key_id', 'ApiMappingKey': '(none)', 'Stage': stage }) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) assert awsclient.create_api_mapping( domain_name=domain_name, path_key=path_key, api_id=api_id, stage=stage ) == { 'key': '/' } def test_create_api_mapping_with_path(self, stubbed_session): domain_name = 'test_domain' path_key = 'path-key' api_id = 'rest_api_id' stage = 'test' stubbed_session.stub('apigatewayv2') \ .create_api_mapping( DomainName=domain_name, ApiMappingKey=path_key, ApiId=api_id, Stage=stage ).returns({ 'ApiId': api_id, 'ApiMappingId': 'key_id', 'ApiMappingKey': 'path-key', 'Stage': stage }) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) assert awsclient.create_api_mapping( domain_name=domain_name, path_key=path_key, api_id=api_id, stage=stage ) == { 'key': '/path-key' } class TestCreateBasePathMapping(object): def test_create_base_path_mapping(self, stubbed_session): domain_name = 'test_domain' path_key = '(none)' api_id = 'rest_api_id' stage = 'test' stubbed_session.stub('apigateway') \ .create_base_path_mapping( domainName=domain_name, basePath=path_key, restApiId=api_id, stage=stage ).returns({ 'restApiId': api_id, 'basePath': '(none)', 'stage': stage }) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) assert awsclient.create_base_path_mapping( domain_name=domain_name, path_key=path_key, api_id=api_id, stage=stage ) == { 'key': '/' } def test_create_base_path_mapping_with_path(self, stubbed_session): domain_name = 'test_domain' path_key = 'path-key' api_id = 'rest_api_id' stage = 'test' stubbed_session.stub('apigateway') \ .create_base_path_mapping( domainName=domain_name, basePath=path_key, restApiId=api_id, stage=stage ).returns({ 'restApiId': api_id, 'basePath': 'path-key', 'stage': stage }) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) assert awsclient.create_base_path_mapping( domain_name=domain_name, path_key=path_key, api_id=api_id, stage=stage ) == { 'key': '/path-key' } def test_create_base_path_mapping_failed(self, stubbed_session): domain_name = 'test_domain' path_key = '/test' api_id = 'rest_api_id' stage = 'test' err_msg = 'An ApiMapping key may contain only letters, ' \ 'numbers and one of $-_.+!*\'()' stubbed_session.stub('apigateway') \ .create_base_path_mapping( domainName=domain_name, basePath=path_key, restApiId=api_id, stage=stage ).raises_error( error_code='BadRequestException', message=err_msg ) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) with pytest.raises(botocore.exceptions.ClientError): awsclient.create_base_path_mapping( domain_name=domain_name, path_key=path_key, api_id=api_id, stage=stage ) class TestCreateDomainName(object): def test_create_domain_name_with_unsupported_protocol( self, stubbed_session ): awsclient = TypedAWSClient(stubbed_session) params = { 'protocol': 'SOME_PROTOCOL', 'domain_name': 'test_domain', 'endpoint_type': 'REGIONAL', 'security_policy': 'TLS_1_0', 'certificate_arn': 'certificate_arn', 'tags': None } with pytest.raises(ValueError): awsclient.create_domain_name(**params) def test_create_rest_api_domain_name(self, stubbed_session): stubbed_session.stub('apigateway') \ .create_domain_name( domainName='test_domain', endpointConfiguration={ 'types': ['REGIONAL'] }, securityPolicy='TLS_1_0', tags={ 'some_key1': 'some_value1', 'some_key2': 'some_value2' }, regionalCertificateArn='certificate_arn', ).returns({ 'domainName': 'test_domain', 'regionalCertificateName': 'certificate_name', 'regionalCertificateArn': 'certificate_arn', 'regionalDomainName': 'regional_domain_name', 'regionalHostedZoneId': 'hosted_zone_id', 'endpointConfiguration': { 'types': ['REGIONAL'], }, 'domainNameStatus': 'AVAILABLE', 'domainNameStatusMessage': 'string', 'securityPolicy': 'TLS_1_0', 'tags': { 'some_key1': 'some_value1', 'some_key2': 'some_value2' } }) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) assert awsclient.create_domain_name( protocol='HTTP', domain_name='test_domain', endpoint_type='REGIONAL', security_policy='TLS_1_0', certificate_arn='certificate_arn', tags={ 'some_key1': 'some_value1', 'some_key2': 'some_value2' } ) == { 'domain_name': 'test_domain', 'security_policy': 'TLS_1_0', 'alias_domain_name': 'regional_domain_name', 'hosted_zone_id': 'hosted_zone_id', 'certificate_arn': 'certificate_arn' } stubbed_session.verify_stubs() def test_create_rest_api_domain_name_no_regional(self, stubbed_session): stubbed_session.stub('apigateway') \ .create_domain_name( domainName='test_domain', endpointConfiguration={ 'types': ['EDGE'] }, securityPolicy='TLS_1_0', tags={ 'some_key1': 'some_value1', 'some_key2': 'some_value2' }, certificateArn='certificate_arn', ).returns({ 'domainName': 'test_domain', 'certificateName': 'certificate_name', 'certificateArn': 'certificate_arn', 'certificateUploadDate': datetime.datetime.now(), 'endpointConfiguration': { 'types': ['EDGE'], }, 'distributionDomainName': 'dist_test_domain', 'distributionHostedZoneId': 'hosted_zone_id', 'domainNameStatus': 'AVAILABLE', 'domainNameStatusMessage': 'string', 'securityPolicy': 'TLS_1_0', 'tags': { 'some_key1': 'some_value1', 'some_key2': 'some_value2' } }) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) assert awsclient.create_domain_name( protocol='HTTP', domain_name='test_domain', endpoint_type='EDGE', security_policy='TLS_1_0', certificate_arn='certificate_arn', tags={ 'some_key1': 'some_value1', 'some_key2': 'some_value2' } ) == { 'domain_name': 'test_domain', 'security_policy': 'TLS_1_0', 'hosted_zone_id': 'hosted_zone_id', 'alias_domain_name': 'dist_test_domain', 'certificate_arn': 'certificate_arn' } stubbed_session.verify_stubs() def test_create_websocket_api_custom_domain(self, stubbed_session): stubbed_session.stub('apigatewayv2') \ .create_domain_name( DomainName='test_websocket_domain', DomainNameConfigurations=[{ 'ApiGatewayDomainName': 'test_websocket_domain', 'CertificateArn': 'certificate_arn', 'EndpointType': 'REGIONAL', 'SecurityPolicy': 'TLS_1_2', 'DomainNameStatus': 'AVAILABLE', }], Tags={ 'some_key1': 'some_value1', 'some_key2': 'some_value2' } ).returns({ 'DomainName': 'test_websocket_domain', 'DomainNameConfigurations': [ { 'ApiGatewayDomainName': 'd-1234', 'CertificateArn': 'certificate_arn', 'CertificateName': 'certificate_name', 'CertificateUploadDate': datetime.datetime.now(), 'EndpointType': 'REGIONAL', 'HostedZoneId': 'hosted_zone_id', 'SecurityPolicy': 'TLS_1_2', 'DomainNameStatus': 'AVAILABLE', }, ], 'Tags': { 'some_key1': 'some_value1', 'some_key2': 'some_value2' } }) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) assert awsclient.create_domain_name( protocol='WEBSOCKET', domain_name='test_websocket_domain', endpoint_type='REGIONAL', security_policy='TLS_1_2', certificate_arn='certificate_arn', tags={ 'some_key1': 'some_value1', 'some_key2': 'some_value2' } ) == { 'domain_name': 'test_websocket_domain', 'alias_domain_name': 'd-1234', 'security_policy': 'TLS_1_2', 'hosted_zone_id': 'hosted_zone_id', 'certificate_arn': 'certificate_arn' } stubbed_session.verify_stubs() def test_get_custom_domain_params_v2(self, stubbed_session): awsclient = TypedAWSClient(stubbed_session) result = awsclient.get_custom_domain_params_v2( domain_name='test_domain_name', endpoint_type='EDGE', security_policy='TLS_1_2', certificate_arn='certificate_arn', tags={ 'some_key1': 'some_value1', 'some_key2': 'some_value2' }, ) assert result == { 'DomainName': 'test_domain_name', 'DomainNameConfigurations': [ { 'ApiGatewayDomainName': 'test_domain_name', 'CertificateArn': 'certificate_arn', 'EndpointType': 'EDGE', 'SecurityPolicy': 'TLS_1_2', 'DomainNameStatus': 'AVAILABLE', }, ], 'Tags': { 'some_key1': 'some_value1', 'some_key2': 'some_value2' } } def test_create_domain_name_max_retries(self, stubbed_session): for _ in range(6): stubbed_session.stub('apigateway') \ .create_domain_name( domainName='test_domain', endpointConfiguration={ 'types': ['EDGE'] }, securityPolicy='TLS_1_0', tags={ 'some_key1': 'some_value1', 'some_key2': 'some_value2' }, certificateArn='certificate_arn' ).raises_error( error_code='TooManyRequestsException', message='Too Many Requests' ) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session, mock.Mock(spec=time.sleep)) with pytest.raises(botocore.exceptions.ClientError): awsclient.create_domain_name( protocol='HTTP', domain_name='test_domain', endpoint_type='EDGE', security_policy='TLS_1_0', certificate_arn='certificate_arn', tags={ 'some_key1': 'some_value1', 'some_key2': 'some_value2' } ) def test_create_domain_name_v2_max_retries(self, stubbed_session): for _ in range(6): stubbed_session.stub('apigatewayv2') \ .create_domain_name( DomainName='test_websocket_domain', DomainNameConfigurations=[{ 'ApiGatewayDomainName': 'test_websocket_domain', 'CertificateArn': 'certificate_arn', 'EndpointType': 'REGIONAL', 'SecurityPolicy': 'TLS_1_2', 'DomainNameStatus': 'AVAILABLE', }], Tags={ 'some_key1': 'some_value1', 'some_key2': 'some_value2' } ).raises_error( error_code='TooManyRequestsException', message='Too Many Requests' ) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session, mock.Mock(spec=time.sleep)) with pytest.raises(botocore.exceptions.ClientError): awsclient.create_domain_name( protocol='WEBSOCKET', domain_name='test_websocket_domain', endpoint_type='REGIONAL', security_policy='TLS_1_2', certificate_arn='certificate_arn', tags={ 'some_key1': 'some_value1', 'some_key2': 'some_value2' } ) class TestUpdateDomainName(object): def test_update_domain_name_websocket(self, stubbed_session): stubbed_session.stub('apigatewayv2') \ .update_domain_name( DomainName='test_domain', DomainNameConfigurations=[{ 'ApiGatewayDomainName': 'test_domain', 'CertificateArn': 'certificate_arn', 'EndpointType': 'REGIONAL', 'SecurityPolicy': 'TLS_1_2', 'DomainNameStatus': 'AVAILABLE', }] ).returns({ 'DomainName': 'test_domain', 'DomainNameConfigurations': [ { 'ApiGatewayDomainName': 'd-1234', 'CertificateArn': 'certificate_arn', 'CertificateName': 'certificate_name', 'CertificateUploadDate': datetime.datetime.now(), 'EndpointType': 'REGIONAL', 'HostedZoneId': 'hosted_zone_id', 'SecurityPolicy': 'TLS_1_2', 'DomainNameStatus': 'AVAILABLE', }, ] }) arn = 'arn:aws:apigateway:us-west-2::/domainnames/test_domain' stubbed_session.stub('apigatewayv2') \ .get_tags(ResourceArn=arn) \ .returns({ 'Tags': {} }) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) assert awsclient.update_domain_name( protocol='WEBSOCKET', domain_name='test_domain', endpoint_type='REGIONAL', security_policy='TLS_1_2', certificate_arn='certificate_arn', ) == { 'domain_name': 'test_domain', 'alias_domain_name': 'd-1234', 'security_policy': 'TLS_1_2', 'hosted_zone_id': 'hosted_zone_id', 'certificate_arn': 'certificate_arn' } stubbed_session.verify_stubs() def test_update_domain_name_failed(self, stubbed_session): err_msg = 'The resource specified in the request was not found.' stubbed_session.stub('apigatewayv2') \ .update_domain_name( DomainName='unknown_domain', DomainNameConfigurations=[{ 'ApiGatewayDomainName': 'unknown_domain', 'CertificateArn': 'certificate_arn', 'EndpointType': 'REGIONAL', 'SecurityPolicy': 'TLS_1_2', 'DomainNameStatus': 'AVAILABLE', }]).raises_error( error_code='NotFoundException', message=err_msg ) awsclient = TypedAWSClient(stubbed_session) with pytest.raises(botocore.exceptions.ClientError): awsclient.update_domain_name( protocol='WEBSOCKET', domain_name='unknown_domain', endpoint_type='REGIONAL', security_policy='TLS_1_2', certificate_arn='certificate_arn', ) def test_update_domain_v2_name_max_retries(self, stubbed_session): for _ in range(6): stubbed_session.stub('apigatewayv2') \ .update_domain_name( DomainName='test_domain', DomainNameConfigurations=[{ 'ApiGatewayDomainName': 'test_domain', 'CertificateArn': 'certificate_arn', 'EndpointType': 'REGIONAL', 'SecurityPolicy': 'TLS_1_2', 'DomainNameStatus': 'AVAILABLE', }], Tags={ 'some_key1': 'some_value1', 'some_key2': 'some_value2' } ).raises_error( error_code='TooManyRequestsException', message='Too Many Requests' ) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session, mock.Mock(spec=time.sleep)) with pytest.raises(botocore.exceptions.ClientError): awsclient.update_domain_name( protocol='WEBSOCKET', domain_name='test_domain', endpoint_type='REGIONAL', security_policy='TLS_1_2', certificate_arn='certificate_arn', tags={ 'some_key1': 'some_value1', 'some_key2': 'some_value2' } ) def test_unsupported_protocol(self, stubbed_session): awsclient = TypedAWSClient(stubbed_session) with pytest.raises(ValueError) as err: awsclient.update_domain_name( protocol='unsupported', domain_name='unknown_domain', endpoint_type='REGIONAL', security_policy='TLS_1_2', certificate_arn='certificate_arn', ) assert str(err.value) == 'Unsupported protocol value.' def test_get_custom_domain_patch_operations(self, stubbed_session): awsclient = TypedAWSClient(stubbed_session) patch_operations = awsclient.get_custom_domain_patch_operations( security_policy='TLS_1_0', certificate_arn='certificate_arn', endpoint_type='EDGE', ) assert patch_operations == [ { 'op': 'replace', 'path': '/securityPolicy', 'value': 'TLS_1_0', }, { 'op': 'replace', 'path': '/certificateArn', 'value': 'certificate_arn', } ] def test_get_custom_domain_patch_operations_regional( self, stubbed_session ): awsclient = TypedAWSClient(stubbed_session) patch_operations = awsclient.get_custom_domain_patch_operations( security_policy='TLS_1_2', certificate_arn='regional_certificate_arn', endpoint_type='REGIONAL', ) assert patch_operations == [ { 'op': 'replace', 'path': '/securityPolicy', 'value': 'TLS_1_2', }, { 'op': 'replace', 'path': '/regionalCertificateArn', 'value': 'regional_certificate_arn', } ] def test_update_domain_name_http_protocol_regional( self, stubbed_session ): stubbed_session.stub('apigateway') \ .update_domain_name( domainName='test_domain', patchOperations=[ { 'op': 'replace', 'path': '/securityPolicy', 'value': 'TLS_1_2', }, ] ).returns({ 'domainName': 'test_domain', 'regionalHostedZoneId': 'hosted_zone_id', 'regionalDomainName': 'regional_domain_name', 'regionalCertificateArn': 'old_regional_certificate_arn', 'endpointConfiguration': { 'types': [ 'REGIONAL', ], }, 'domainNameStatus': 'AVAILABLE', 'securityPolicy': 'TLS_1_2' }) stubbed_session.stub('apigateway') \ .update_domain_name( domainName='test_domain', patchOperations=[ { 'op': 'replace', 'path': '/regionalCertificateArn', 'value': 'regional_certificate_arn', } ] ).returns({ 'domainName': 'test_domain', 'regionalHostedZoneId': 'hosted_zone_id', 'regionalCertificateArn': 'regional_certificate_arn', 'regionalDomainName': 'regional_domain_name', 'endpointConfiguration': { 'types': [ 'REGIONAL', ], }, 'domainNameStatus': 'AVAILABLE', 'securityPolicy': 'TLS_1_2' }) arn = 'arn:aws:apigateway:us-west-2::/domainnames/test_domain' stubbed_session.stub('apigatewayv2') \ .get_tags(ResourceArn=arn) \ .returns({ 'Tags': {} }) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) assert awsclient.update_domain_name( protocol='HTTP', domain_name='test_domain', endpoint_type='REGIONAL', security_policy='TLS_1_2', certificate_arn='regional_certificate_arn', ) == { 'domain_name': 'test_domain', 'alias_domain_name': 'regional_domain_name', 'security_policy': 'TLS_1_2', 'hosted_zone_id': 'hosted_zone_id', 'certificate_arn': 'regional_certificate_arn' } stubbed_session.verify_stubs() def test_update_domain_name_http_protocol( self, stubbed_session ): stubbed_session.stub('apigateway') \ .update_domain_name( domainName='test_domain', patchOperations=[ { 'op': 'replace', 'path': '/securityPolicy', 'value': 'TLS_1_0', }, ] ).returns({ 'domainName': 'test_domain', 'distributionHostedZoneId': 'hosted_zone_id', 'certificateArn': 'old_certificate_arn', 'distributionDomainName': 'dist_domain_name', 'endpointConfiguration': { 'types': [ 'EDGE', ], }, 'domainNameStatus': 'AVAILABLE', 'securityPolicy': 'TLS_1_0' }) stubbed_session.stub('apigateway') \ .update_domain_name( domainName='test_domain', patchOperations=[ { 'op': 'replace', 'path': '/certificateArn', 'value': 'certificate_arn', } ] ).returns({ 'domainName': 'test_domain', 'distributionHostedZoneId': 'hosted_zone_id', 'distributionDomainName': 'dist_domain_name', 'certificateArn': 'certificate_arn', 'endpointConfiguration': { 'types': [ 'EDGE', ], }, 'domainNameStatus': 'AVAILABLE', 'securityPolicy': 'TLS_1_0' }) arn = 'arn:aws:apigateway:us-west-2::/domainnames/test_domain' stubbed_session.stub('apigatewayv2') \ .get_tags(ResourceArn=arn) \ .returns({ 'Tags': {} }) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) assert awsclient.update_domain_name( protocol='HTTP', domain_name='test_domain', endpoint_type='EDGE', security_policy='TLS_1_0', certificate_arn='certificate_arn', ) == { 'domain_name': 'test_domain', 'security_policy': 'TLS_1_0', 'alias_domain_name': 'dist_domain_name', 'hosted_zone_id': 'hosted_zone_id', 'certificate_arn': 'certificate_arn' } stubbed_session.verify_stubs() def test_update_domain_name_govcloud(self, stubbed_session): stubbed_session.create_client( 'apigateway', region_name='us-gov-west-1') apig = stubbed_session.stub('apigateway') self._setup_expected_update_calls(apig) # Verify we use the aws-us-gov partition in our ARN. arn = ( 'arn:aws-us-gov:apigateway:us-gov-west-1::/domainnames/test_domain' ) stubbed_session.stub('apigatewayv2') \ .get_tags(ResourceArn=arn) \ .returns({ 'Tags': {} }) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) awsclient.update_domain_name( protocol='HTTP', domain_name='test_domain', endpoint_type='EDGE', security_policy='TLS_1_0', certificate_arn='certificate_arn', ) stubbed_session.verify_stubs() def _setup_expected_update_calls(self, apig): apig.update_domain_name( domainName='test_domain', patchOperations=[ { 'op': 'replace', 'path': '/securityPolicy', 'value': 'TLS_1_0', }, ] ).returns({ 'domainName': 'test_domain', 'distributionHostedZoneId': 'hosted_zone_id', 'certificateArn': 'old_certificate_arn', 'distributionDomainName': 'dist_domain_name', 'endpointConfiguration': { 'types': [ 'EDGE', ], }, 'domainNameStatus': 'AVAILABLE', 'securityPolicy': 'TLS_1_0' }) apig.update_domain_name( domainName='test_domain', patchOperations=[ { 'op': 'replace', 'path': '/certificateArn', 'value': 'certificate_arn', } ] ).returns({ 'domainName': 'test_domain', 'distributionHostedZoneId': 'hosted_zone_id', 'distributionDomainName': 'dist_domain_name', 'certificateArn': 'certificate_arn', 'endpointConfiguration': { 'types': [ 'EDGE', ], }, 'domainNameStatus': 'AVAILABLE', 'securityPolicy': 'TLS_1_0' }) def test_update_domain_name_max_retries(self, stubbed_session): for _ in range(6): stubbed_session.stub('apigateway') \ .update_domain_name( domainName='test_domain', patchOperations=[ { 'op': 'replace', 'path': '/certificateArn', 'value': 'certificate_arn', } ] ).raises_error( error_code='TooManyRequestsException', message='Too Many Requests' ) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session, mock.Mock(spec=time.sleep)) with pytest.raises(botocore.exceptions.ClientError): awsclient.update_domain_name( protocol='HTTP', domain_name='test_domain', endpoint_type='EDGE', security_policy='TLS_1_0', certificate_arn='certificate_arn', ) def test_update_resource_tags(self, stubbed_session): arn = 'arn:aws:apigateway:us-west-2::/domainnames/test_domain' stubbed_session.stub('apigatewayv2') \ .get_tags( ResourceArn=arn ).returns({ 'Tags': { 'key': 'value', 'key1': 'value1' } }) stubbed_session.stub('apigatewayv2') \ .untag_resource( ResourceArn=arn, TagKeys=['key1'] ).returns({}) stubbed_session.stub('apigatewayv2') \ .tag_resource( ResourceArn=arn, Tags={ 'key2': 'value2' } ).returns({}) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) tags = { 'key': 'value', 'key2': 'value2' } awsclient._update_resource_tags(arn, tags) stubbed_session.verify_stubs() class TestDeleteDomainName(object): def test_delete_domain_name(self, stubbed_session): domain_name = 'test_domain' stubbed_session.stub('apigatewayv2') \ .delete_domain_name(DomainName=domain_name).returns({}) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) awsclient.delete_domain_name(domain_name=domain_name) stubbed_session.verify_stubs() def test_delete_domain_name_failed(self, stubbed_session): domain_name = 'unknown_domain' err_msg = 'The resource specified in the request was not found.' stubbed_session.stub('apigatewayv2') \ .delete_domain_name(DomainName=domain_name) \ .raises_error( error_code='NotFoundException', message=err_msg ) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) with pytest.raises(botocore.exceptions.ClientError): awsclient.delete_domain_name(domain_name=domain_name) def test_delete_domain_name_max_retries(self, stubbed_session): for _ in range(6): stubbed_session.stub('apigatewayv2') \ .delete_domain_name( domainName='test_domain', ).raises_error( error_code='TooManyRequestsException', message='Too Many Requests' ) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session, mock.Mock(spec=time.sleep)) with pytest.raises(botocore.exceptions.ClientError): awsclient.delete_domain_name(domain_name='test_domain') class TestDeleteApiMapping(object): def test_delete_api_mapping(self, stubbed_session): domain_name = 'test_domain' stubbed_session.stub('apigateway') \ .delete_base_path_mapping( domainName=domain_name, basePath='foo' ).returns({}) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) awsclient.delete_api_mapping( domain_name=domain_name, path_key='foo' ) stubbed_session.verify_stubs() def test_delete_api_mapping_failed(self, stubbed_session): domain_name = 'unknown_domain' err_msg = 'The resource specified in the request was not found.' stubbed_session.stub('apigateway') \ .delete_base_path_mapping( domainName=domain_name, basePath='foo' ).raises_error( error_code='NotFoundException', message=err_msg ) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) with pytest.raises(botocore.exceptions.ClientError): awsclient.delete_api_mapping( domain_name=domain_name, path_key='foo' ) class TestGetRestAPI(object): def test_rest_api_exists(self, stubbed_session): desired_name = 'myappname' stubbed_session.stub('apigateway').get_rest_apis()\ .returns( {'items': [ {'createdDate': 1, 'id': 'wrongid1', 'name': 'wrong1'}, {'createdDate': 2, 'id': 'correct', 'name': desired_name}, {'createdDate': 3, 'id': 'wrongid3', 'name': 'wrong3'}, ]}) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) assert awsclient.get_rest_api_id(desired_name) == 'correct' stubbed_session.verify_stubs() def test_rest_api_does_not_exist(self, stubbed_session): stubbed_session.stub('apigateway').get_rest_apis()\ .returns( {'items': [ {'createdDate': 1, 'id': 'wrongid1', 'name': 'wrong1'}, {'createdDate': 2, 'id': 'wrongid1', 'name': 'wrong2'}, {'createdDate': 3, 'id': 'wrongid3', 'name': 'wrong3'}, ]}) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) assert awsclient.get_rest_api_id('myappname') is None stubbed_session.verify_stubs() class TestGetRoleArn(object): def test_get_role_arn_for_name_found(self, stubbed_session): # Need len(20) to pass param validation. good_arn = 'good_arn' * 3 role_id = 'abcd' * 4 today = datetime.datetime.today() stubbed_session.stub('iam').get_role(RoleName='Yes').returns({ 'Role': { 'Path': '/', 'RoleName': 'Yes', 'RoleId': role_id, 'CreateDate': today, 'Arn': good_arn } }) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) assert awsclient.get_role_arn_for_name(name='Yes') == good_arn stubbed_session.verify_stubs() def test_got_role_arn_not_found_raises_value_error(self, stubbed_session): stubbed_session.stub('iam').get_role(RoleName='Yes').raises_error( error_code='NoSuchEntity', message='Foo') stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) with pytest.raises(ResourceDoesNotExistError): awsclient.get_role_arn_for_name(name='Yes') stubbed_session.verify_stubs() def test_unexpected_error_is_propagated(self, stubbed_session): stubbed_session.stub('iam').get_role(RoleName='Yes').raises_error( error_code='InternalError', message='Foo') stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) with pytest.raises(botocore.exceptions.ClientError): awsclient.get_role_arn_for_name(name='Yes') stubbed_session.verify_stubs() class TestGetRole(object): def test_get_role_success(self, stubbed_session): today = datetime.datetime.today() response = { 'Role': { 'Path': '/', 'RoleName': 'Yes', 'RoleId': 'abcd' * 4, 'CreateDate': today, 'Arn': 'good_arn' * 3, } } stubbed_session.stub('iam').get_role(RoleName='Yes').returns(response) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) actual = awsclient.get_role(name='Yes') assert actual == response['Role'] stubbed_session.verify_stubs() def test_get_role_raises_exception_when_no_exists(self, stubbed_session): stubbed_session.stub('iam').get_role(RoleName='Yes').raises_error( error_code='NoSuchEntity', message='Foo') stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) with pytest.raises(ResourceDoesNotExistError): awsclient.get_role(name='Yes') stubbed_session.verify_stubs() class TestCreateRole(object): def test_create_role(self, stubbed_session): arn = 'good_arn' * 3 role_id = 'abcd' * 4 today = datetime.datetime.today() stubbed_session.stub('iam').create_role( RoleName='role_name', AssumeRolePolicyDocument=json.dumps({'trust': 'policy'}) ).returns({'Role': { 'RoleName': 'No', 'Arn': arn, 'Path': '/', 'RoleId': role_id, 'CreateDate': today}} ) stubbed_session.stub('iam').put_role_policy( RoleName='role_name', PolicyName='role_name', PolicyDocument=json.dumps({'policy': 'document'}, indent=2) ).returns({}) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) actual = awsclient.create_role( 'role_name', {'trust': 'policy'}, {'policy': 'document'}) assert actual == arn stubbed_session.verify_stubs() def test_create_role_raises_error_on_failure(self, stubbed_session): arn = 'good_arn' * 3 role_id = 'abcd' * 4 today = datetime.datetime.today() stubbed_session.stub('iam').create_role( RoleName='role_name', AssumeRolePolicyDocument=json.dumps({'trust': 'policy'}) ).returns({'Role': { 'RoleName': 'No', 'Arn': arn, 'Path': '/', 'RoleId': role_id, 'CreateDate': today}} ) stubbed_session.stub('iam').put_role_policy( RoleName='role_name', PolicyName='role_name', PolicyDocument={'policy': 'document'} ).raises_error( error_code='MalformedPolicyDocumentException', message='MalformedPolicyDocument' ) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) with pytest.raises(botocore.exceptions.ClientError): awsclient.create_role( 'role_name', {'trust': 'policy'}, {'policy': 'document'}) stubbed_session.verify_stubs() class TestInvokeLambdaFunction(object): def test_invoke_no_payload_no_context(self, stubbed_session): stubbed_session.stub('lambda').invoke( FunctionName='name', InvocationType='RequestResponse', ).returns({}) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) assert awsclient.invoke_function('name') == {} stubbed_session.verify_stubs() def test_invoke_payload_provided(self, stubbed_session): stubbed_session.stub('lambda').invoke( FunctionName='name', Payload=b'payload', InvocationType='RequestResponse', ).returns({}) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) assert awsclient.invoke_function('name', payload=b'payload') == {} stubbed_session.verify_stubs() def test_invoke_read_timeout_raises_correct_error(self, stubbed_session): stubbed_session.stub('lambda').invoke( FunctionName='name', Payload=b'payload', InvocationType='RequestResponse', ).raises_error(error=RequestsReadTimeout()) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) with pytest.raises(ReadTimeout): awsclient.invoke_function('name', payload=b'payload') == {} class TestCreateLambdaFunction(object): SUCCESS_RESPONSE = { 'FunctionArn': 'arn:12345:name', 'State': 'Active', } def test_create_function_succeeds_first_try(self, stubbed_session): stubbed_session.stub('lambda').create_function( FunctionName='name', Runtime='python2.7', Code={'ZipFile': b'foo'}, Handler='app.app', Role='myarn' ).returns(self.SUCCESS_RESPONSE) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) assert awsclient.create_function( 'name', 'myarn', b'foo', 'python2.7', 'app.app') == 'arn:12345:name' stubbed_session.verify_stubs() def test_create_function_with_non_python2_runtime(self, stubbed_session): stubbed_session.stub('lambda').create_function( FunctionName='name', Runtime='python3.6', Code={'ZipFile': b'foo'}, Handler='app.app', Role='myarn', ).returns(self.SUCCESS_RESPONSE) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) assert awsclient.create_function( 'name', 'myarn', b'foo', runtime='python3.6', handler='app.app') == 'arn:12345:name' stubbed_session.verify_stubs() def test_create_function_wait_for_active_state(self, stubbed_session, monkeypatch): client = stubbed_session.stub('lambda') client.create_function( FunctionName='name', Runtime='python2.7', Code={'ZipFile': b'foo'}, Handler='app.app', Role='myarn' ).returns({'FunctionArn': 'arn:12345:name', 'State': 'Pending'}) client.get_function_configuration( FunctionName='name', ).returns({'State': 'Pending'}) client.get_function_configuration( FunctionName='name', ).returns({'State': 'Active'}) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) monkeypatch.setattr(time, 'sleep', mock.Mock(spec=time.sleep)) assert awsclient.create_function( 'name', 'myarn', b'foo', 'python2.7', 'app.app') == 'arn:12345:name' stubbed_session.verify_stubs() def test_create_function_with_environment_variables(self, stubbed_session): stubbed_session.stub('lambda').create_function( FunctionName='name', Runtime='python2.7', Code={'ZipFile': b'foo'}, Handler='app.app', Role='myarn', Environment={'Variables': {'FOO': 'BAR'}} ).returns(self.SUCCESS_RESPONSE) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) assert awsclient.create_function( 'name', 'myarn', b'foo', 'python2.7', handler='app.app', environment_variables={'FOO': 'BAR'}) == 'arn:12345:name' stubbed_session.verify_stubs() def test_create_function_with_tags(self, stubbed_session): stubbed_session.stub('lambda').create_function( FunctionName='name', Runtime='python2.7', Code={'ZipFile': b'foo'}, Handler='app.app', Role='myarn', Timeout=240 ).returns(self.SUCCESS_RESPONSE) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) assert awsclient.create_function( 'name', 'myarn', b'foo', 'python2.7', 'app.app', timeout=240) == 'arn:12345:name' stubbed_session.verify_stubs() def test_create_function_with_timeout(self, stubbed_session): stubbed_session.stub('lambda').create_function( FunctionName='name', Runtime='python2.7', Code={'ZipFile': b'foo'}, Handler='app.app', Role='myarn', Tags={'mykey': 'myvalue'} ).returns(self.SUCCESS_RESPONSE) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) assert awsclient.create_function( 'name', 'myarn', b'foo', 'python2.7', 'app.app', tags={'mykey': 'myvalue'}) == 'arn:12345:name' stubbed_session.verify_stubs() def test_create_function_with_memory_size(self, stubbed_session): stubbed_session.stub('lambda').create_function( FunctionName='name', Runtime='python2.7', Code={'ZipFile': b'foo'}, Handler='app.app', Role='myarn', MemorySize=256 ).returns(self.SUCCESS_RESPONSE) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) assert awsclient.create_function( 'name', 'myarn', b'foo', 'python2.7', 'app.app', memory_size=256) == 'arn:12345:name' stubbed_session.verify_stubs() def test_create_function_with_vpc_config(self, stubbed_session): stubbed_session.stub('lambda').create_function( FunctionName='name', Runtime='python2.7', Code={'ZipFile': b'foo'}, Handler='app.app', Role='myarn', VpcConfig={ 'SecurityGroupIds': ['sg1', 'sg2'], 'SubnetIds': ['sn1', 'sn2'] } ).returns(self.SUCCESS_RESPONSE) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) assert awsclient.create_function( 'name', 'myarn', b'foo', 'python2.7', 'app.app', subnet_ids=['sn1', 'sn2'], security_group_ids=['sg1', 'sg2'], ) == 'arn:12345:name' stubbed_session.verify_stubs() def test_create_function_with_layers(self, stubbed_session): layers = ['arn:aws:lambda:us-east-1:111:layer:test_layer:1'] stubbed_session.stub('lambda').create_function( FunctionName='name', Runtime='python2.7', Code={'ZipFile': b'foo'}, Handler='app.app', Role='myarn', Layers=layers ).returns(self.SUCCESS_RESPONSE) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) assert awsclient.create_function( 'name', 'myarn', b'foo', 'python2.7', 'app.app', layers=layers ) == 'arn:12345:name' stubbed_session.verify_stubs() def test_create_function_is_retried_and_succeeds(self, stubbed_session): kwargs = { 'FunctionName': 'name', 'Runtime': 'python2.7', 'Code': {'ZipFile': b'foo'}, 'Handler': 'app.app', 'Role': 'myarn', } stubbed_session.stub('lambda').create_function( **kwargs).raises_error( error_code='InvalidParameterValueException', message=('The role defined for the function cannot ' 'be assumed by Lambda.')) stubbed_session.stub('lambda').create_function( **kwargs).raises_error( error_code='InvalidParameterValueException', message=('The role defined for the function cannot ' 'be assumed by Lambda.')) stubbed_session.stub('lambda').create_function( **kwargs).returns(self.SUCCESS_RESPONSE) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session, mock.Mock(spec=time.sleep)) assert awsclient.create_function( 'name', 'myarn', b'foo', 'python2.7', 'app.app') == 'arn:12345:name' stubbed_session.verify_stubs() def test_create_function_retries_on_kms_errors(self, stubbed_session): # You'll sometimes get this message when you first create a role. # We want to ensure that we're trying when this happens. error_code = 'InvalidParameterValueException' error_message = ( 'Lambda was unable to configure access to your ' 'environment variables because the KMS key ' 'is invalid for CreateGrant. Please ' 'check your KMS key settings. ' 'KMS Exception: InvalidArnException KMS Message: ' 'ARN does not refer to a valid principal' ) kwargs = { 'FunctionName': 'name', 'Runtime': 'python2.7', 'Code': {'ZipFile': b'foo'}, 'Handler': 'app.app', 'Role': 'myarn', } client = stubbed_session.stub('lambda') client.create_function(**kwargs).raises_error( error_code=error_code, message=error_message ) client.create_function(**kwargs).returns(self.SUCCESS_RESPONSE) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session, mock.Mock(spec=time.sleep)) assert awsclient.create_function( 'name', 'myarn', b'foo', 'python2.7', 'app.app') == 'arn:12345:name' stubbed_session.verify_stubs() def test_retry_happens_on_insufficient_permissions(self, stubbed_session): # This can happen if we deploy a lambda in a VPC. Instead of the role # not being able to be assumed, we can instead not have permissions # to modify ENIs. These can be retried. kwargs = { 'FunctionName': 'name', 'Runtime': 'python2.7', 'Code': {'ZipFile': b'foo'}, 'Handler': 'app.app', 'Role': 'myarn', 'VpcConfig': {'SubnetIds': ['sn-1'], 'SecurityGroupIds': ['sg-1']}, } stubbed_session.stub('lambda').create_function( **kwargs).raises_error( error_code='InvalidParameterValueException', message=('The provided execution role does not have permissions ' 'to call CreateNetworkInterface on EC2 be assumed by ' 'Lambda.')) stubbed_session.stub('lambda').create_function( **kwargs).returns(self.SUCCESS_RESPONSE) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session, mock.Mock(spec=time.sleep)) assert awsclient.create_function( 'name', 'myarn', b'foo', 'python2.7', 'app.app', security_group_ids=['sg-1'], subnet_ids=['sn-1']) == 'arn:12345:name' stubbed_session.verify_stubs() def test_create_function_fails_after_max_retries(self, stubbed_session): kwargs = { 'FunctionName': 'name', 'Runtime': 'python2.7', 'Code': {'ZipFile': b'foo'}, 'Handler': 'app.app', 'Role': 'myarn', } for _ in range(TypedAWSClient.LAMBDA_CREATE_ATTEMPTS): stubbed_session.stub('lambda').create_function( **kwargs).raises_error( error_code='InvalidParameterValueException', message=('The role defined for the function cannot ' 'be assumed by Lambda.') ) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session, mock.Mock(spec=time.sleep)) with pytest.raises(LambdaClientError) as excinfo: awsclient.create_function('name', 'myarn', b'foo', 'python2.7', 'app.app') assert isinstance( excinfo.value.original_error, botocore.exceptions.ClientError) stubbed_session.verify_stubs() def test_can_pass_python_runtime(self, stubbed_session): stubbed_session.stub('lambda').create_function( FunctionName='name', Runtime='python3.6', Code={'ZipFile': b'foo'}, Handler='app.app', Role='myarn', ).returns(self.SUCCESS_RESPONSE) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) assert awsclient.create_function( 'name', 'myarn', b'foo', runtime='python3.6', handler='app.app') == 'arn:12345:name' stubbed_session.verify_stubs() def test_create_function_propagates_unknown_error(self, stubbed_session): kwargs = { 'FunctionName': 'name', 'Runtime': 'python2.7', 'Code': {'ZipFile': b'foo'}, 'Handler': 'app.app', 'Role': 'myarn', } stubbed_session.stub('lambda').create_function( **kwargs).raises_error( error_code='UnknownException', message='') stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session, mock.Mock(spec=time.sleep)) with pytest.raises(LambdaClientError) as excinfo: awsclient.create_function('name', 'myarn', b'foo', 'pytohn2.7', 'app.app') assert isinstance( excinfo.value.original_error, botocore.exceptions.ClientError) stubbed_session.verify_stubs() def test_can_provide_tags(self, stubbed_session): stubbed_session.stub('lambda').create_function( FunctionName='name', Runtime='python2.7', Code={'ZipFile': b'foo'}, Handler='app.app', Role='myarn', Tags={'key': 'value'}, ).returns(self.SUCCESS_RESPONSE) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) assert awsclient.create_function( function_name='name', role_arn='myarn', zip_contents=b'foo', runtime='python2.7', tags={'key': 'value'}, handler='app.app') == 'arn:12345:name' stubbed_session.verify_stubs() def test_raises_large_deployment_error_for_connection_error( self, stubbed_session): too_large_content = b'a' * 60 * (1024 ** 2) kwargs = { 'FunctionName': 'name', 'Runtime': 'python2.7', 'Code': {'ZipFile': too_large_content}, 'Handler': 'app.app', 'Role': 'myarn', } stubbed_session.stub('lambda').create_function( **kwargs).raises_error(error=RequestsConnectionError()) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session, mock.Mock(spec=time.sleep)) with pytest.raises(DeploymentPackageTooLargeError) as excinfo: awsclient.create_function('name', 'myarn', too_large_content, 'python2.7', 'app.app') stubbed_session.verify_stubs() assert excinfo.value.context.function_name == 'name' assert excinfo.value.context.client_method_name == 'create_function' assert excinfo.value.context.deployment_size == 60 * (1024 ** 2) def test_no_raise_large_deployment_error_when_small_deployment_size( self, stubbed_session): kwargs = { 'FunctionName': 'name', 'Runtime': 'python2.7', 'Code': {'ZipFile': b'foo'}, 'Handler': 'app.app', 'Role': 'myarn', } stubbed_session.stub('lambda').create_function( **kwargs).raises_error(error=RequestsConnectionError()) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session, mock.Mock(spec=time.sleep)) with pytest.raises(LambdaClientError) as excinfo: awsclient.create_function('name', 'myarn', b'foo', 'python2.7', 'app.app') stubbed_session.verify_stubs() assert not isinstance(excinfo.value, DeploymentPackageTooLargeError) assert isinstance( excinfo.value.original_error, RequestsConnectionError) def test_raises_large_deployment_error_request_entity_to_large( self, stubbed_session): kwargs = { 'FunctionName': 'name', 'Runtime': 'python2.7', 'Code': {'ZipFile': b'foo'}, 'Handler': 'app.app', 'Role': 'myarn', } stubbed_session.stub('lambda').create_function( **kwargs).raises_error( error_code='RequestEntityTooLargeException', message='') stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session, mock.Mock(spec=time.sleep)) with pytest.raises(DeploymentPackageTooLargeError): awsclient.create_function('name', 'myarn', b'foo', 'python2.7', 'app.app') stubbed_session.verify_stubs() def test_raises_large_deployment_error_for_too_large_unzip( self, stubbed_session): kwargs = { 'FunctionName': 'name', 'Runtime': 'python2.7', 'Code': {'ZipFile': b'foo'}, 'Handler': 'app.app', 'Role': 'myarn', } stubbed_session.stub('lambda').create_function( **kwargs).raises_error( error_code='InvalidParameterValueException', message='Unzipped size must be smaller than ...') stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session, mock.Mock(spec=time.sleep)) with pytest.raises(DeploymentPackageTooLargeError): awsclient.create_function('name', 'myarn', b'foo', 'python2.7', 'app.app') stubbed_session.verify_stubs() class TestUpdateLambdaFunction(object): SUCCESS_RESPONSE = { 'LastUpdateStatus': 'Successful', } def test_always_update_function_code(self, stubbed_session): lambda_client = stubbed_session.stub('lambda') lambda_client.update_function_code( FunctionName='name', ZipFile=b'foo').returns(self.SUCCESS_RESPONSE) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) awsclient.update_function('name', b'foo') stubbed_session.verify_stubs() def test_update_function_code_with_runtime(self, stubbed_session): lambda_client = stubbed_session.stub('lambda') lambda_client.update_function_code( FunctionName='name', ZipFile=b'foo').returns(self.SUCCESS_RESPONSE) lambda_client.update_function_configuration( FunctionName='name', Runtime='python3.6').returns(self.SUCCESS_RESPONSE) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) awsclient.update_function('name', b'foo', runtime='python3.6') stubbed_session.verify_stubs() def test_update_function_code_with_environment_vars(self, stubbed_session): lambda_client = stubbed_session.stub('lambda') lambda_client.update_function_code( FunctionName='name', ZipFile=b'foo').returns(self.SUCCESS_RESPONSE) lambda_client.update_function_configuration( FunctionName='name', Environment={'Variables': {"FOO": "BAR"}}).returns( self.SUCCESS_RESPONSE) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) awsclient.update_function( 'name', b'foo', {"FOO": "BAR"}) stubbed_session.verify_stubs() def test_update_function_code_with_timeout(self, stubbed_session): lambda_client = stubbed_session.stub('lambda') lambda_client.update_function_code( FunctionName='name', ZipFile=b'foo').returns(self.SUCCESS_RESPONSE) lambda_client.update_function_configuration( FunctionName='name', Timeout=240).returns(self.SUCCESS_RESPONSE) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) awsclient.update_function('name', b'foo', timeout=240) stubbed_session.verify_stubs() def test_update_function_code_with_memory(self, stubbed_session): lambda_client = stubbed_session.stub('lambda') lambda_client.update_function_code( FunctionName='name', ZipFile=b'foo').returns(self.SUCCESS_RESPONSE) lambda_client.update_function_configuration( FunctionName='name', MemorySize=256).returns(self.SUCCESS_RESPONSE) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) awsclient.update_function('name', b'foo', memory_size=256) stubbed_session.verify_stubs() def test_update_function_with_vpc_config(self, stubbed_session): lambda_client = stubbed_session.stub('lambda') lambda_client.update_function_code( FunctionName='name', ZipFile=b'foo').returns(self.SUCCESS_RESPONSE) lambda_client.update_function_configuration( FunctionName='name', VpcConfig={ 'SecurityGroupIds': ['sg1', 'sg2'], 'SubnetIds': ['sn1', 'sn2'] } ).returns(self.SUCCESS_RESPONSE) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) awsclient.update_function( 'name', b'foo', subnet_ids=['sn1', 'sn2'], security_group_ids=['sg1', 'sg2'], ) stubbed_session.verify_stubs() def test_update_function_with_layers_config(self, stubbed_session): layers = ['arn:aws:lambda:us-east-1:111:layer:test_layer:1'] lambda_client = stubbed_session.stub('lambda') lambda_client.update_function_code( FunctionName='name', ZipFile=b'foo').returns(self.SUCCESS_RESPONSE) lambda_client.update_function_configuration( FunctionName='name', Layers=layers ).returns(self.SUCCESS_RESPONSE) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) awsclient.update_function( 'name', b'foo', layers=layers ) stubbed_session.verify_stubs() def test_update_function_with_adding_tags(self, stubbed_session): function_arn = 'arn' lambda_client = stubbed_session.stub('lambda') lambda_client.update_function_code( FunctionName='name', ZipFile=b'foo').returns( {'FunctionArn': function_arn, 'LastUpdateStatus': 'Successful'}) lambda_client.list_tags( Resource=function_arn).returns({'Tags': {}}) lambda_client.tag_resource( Resource=function_arn, Tags={'MyKey': 'MyValue'}).returns({}) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) awsclient.update_function('name', b'foo', tags={'MyKey': 'MyValue'}) stubbed_session.verify_stubs() def test_update_function_with_updating_tags(self, stubbed_session): function_arn = 'arn' lambda_client = stubbed_session.stub('lambda') lambda_client.update_function_code( FunctionName='name', ZipFile=b'foo').returns( {'FunctionArn': function_arn, 'LastUpdateStatus': 'Successful'}) lambda_client.list_tags( Resource=function_arn).returns({'Tags': {'MyKey': 'MyOrigValue'}}) lambda_client.tag_resource( Resource=function_arn, Tags={'MyKey': 'MyNewValue'}).returns({}) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) awsclient.update_function('name', b'foo', tags={'MyKey': 'MyNewValue'}) stubbed_session.verify_stubs() def test_update_function_with_removing_tags(self, stubbed_session): function_arn = 'arn' lambda_client = stubbed_session.stub('lambda') lambda_client.update_function_code( FunctionName='name', ZipFile=b'foo').returns( {'FunctionArn': function_arn, 'LastUpdateStatus': 'Successful'}) lambda_client.list_tags( Resource=function_arn).returns( {'Tags': {'KeyToRemove': 'Value'}}) lambda_client.untag_resource( Resource=function_arn, TagKeys=['KeyToRemove']).returns({}) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) awsclient.update_function('name', b'foo', tags={}) stubbed_session.verify_stubs() def test_update_function_with_no_tag_updates_needed(self, stubbed_session): function_arn = 'arn' lambda_client = stubbed_session.stub('lambda') lambda_client.update_function_code( FunctionName='name', ZipFile=b'foo').returns( {'FunctionArn': function_arn, 'LastUpdateStatus': 'Successful'}) lambda_client.list_tags( Resource=function_arn).returns({'Tags': {'MyKey': 'SameValue'}}) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) awsclient.update_function('name', b'foo', tags={'MyKey': 'SameValue'}) stubbed_session.verify_stubs() def test_update_function_with_iam_role(self, stubbed_session): function_arn = 'arn' lambda_client = stubbed_session.stub('lambda') lambda_client.update_function_code( FunctionName='name', ZipFile=b'foo').returns( {'FunctionArn': function_arn, 'LastUpdateStatus': 'Successful'}) lambda_client.update_function_configuration( FunctionName='name', Role='role-arn').returns(self.SUCCESS_RESPONSE) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) awsclient.update_function('name', b'foo', role_arn='role-arn') stubbed_session.verify_stubs() def test_update_function_is_retried_and_succeeds(self, stubbed_session): stubbed_session.stub('lambda').update_function_code( FunctionName='name', ZipFile=b'foo').returns( {'FunctionArn': 'arn', 'LastUpdateStatus': 'Successful'}) update_config_kwargs = { 'FunctionName': 'name', 'Role': 'role-arn' } # This should fail two times with retryable exceptions and # then succeed to update the lambda function. stubbed_session.stub('lambda').update_function_configuration( **update_config_kwargs).raises_error( error_code='InvalidParameterValueException', message=('The role defined for the function cannot ' 'be assumed by Lambda.')) stubbed_session.stub('lambda').update_function_configuration( **update_config_kwargs).raises_error( error_code='InvalidParameterValueException', message=('The role defined for the function cannot ' 'be assumed by Lambda.')) stubbed_session.stub('lambda').update_function_configuration( **update_config_kwargs).returns(self.SUCCESS_RESPONSE) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session, mock.Mock(spec=time.sleep)) awsclient.update_function('name', b'foo', role_arn='role-arn') stubbed_session.verify_stubs() def test_update_function_fails_after_max_retries(self, stubbed_session): stubbed_session.stub('lambda').update_function_code( FunctionName='name', ZipFile=b'foo').returns(self.SUCCESS_RESPONSE) update_config_kwargs = { 'FunctionName': 'name', 'Role': 'role-arn' } for _ in range(TypedAWSClient.LAMBDA_CREATE_ATTEMPTS): stubbed_session.stub('lambda').update_function_configuration( **update_config_kwargs).raises_error( error_code='InvalidParameterValueException', message=('The role defined for the function cannot ' 'be assumed by Lambda.')) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session, mock.Mock(spec=time.sleep)) with pytest.raises(botocore.exceptions.ClientError): awsclient.update_function('name', b'foo', role_arn='role-arn') stubbed_session.verify_stubs() def test_raises_large_deployment_error_for_connection_error( self, stubbed_session): too_large_content = b'a' * 60 * (1024 ** 2) stubbed_session.stub('lambda').update_function_code( FunctionName='name', ZipFile=too_large_content).raises_error( error=RequestsConnectionError()) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session, mock.Mock(spec=time.sleep)) with pytest.raises(DeploymentPackageTooLargeError) as excinfo: awsclient.update_function('name', too_large_content) stubbed_session.verify_stubs() assert excinfo.value.context.function_name == 'name' assert ( excinfo.value.context.client_method_name == 'update_function_code') assert excinfo.value.context.deployment_size == 60 * (1024 ** 2) def test_no_raise_large_deployment_error_when_small_deployment_size( self, stubbed_session): stubbed_session.stub('lambda').update_function_code( FunctionName='name', ZipFile=b'foo').raises_error( error=RequestsConnectionError()) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session, mock.Mock(spec=time.sleep)) with pytest.raises(LambdaClientError) as excinfo: awsclient.update_function('name', b'foo') stubbed_session.verify_stubs() assert not isinstance(excinfo.value, DeploymentPackageTooLargeError) assert isinstance( excinfo.value.original_error, RequestsConnectionError) def test_raises_large_deployment_error_request_entity_to_large( self, stubbed_session): stubbed_session.stub('lambda').update_function_code( FunctionName='name', ZipFile=b'foo').raises_error( error_code='RequestEntityTooLargeException', message='') stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session, mock.Mock(spec=time.sleep)) with pytest.raises(DeploymentPackageTooLargeError): awsclient.update_function('name', b'foo') stubbed_session.verify_stubs() def test_raises_large_deployment_error_for_too_large_unzip( self, stubbed_session): stubbed_session.stub('lambda').update_function_code( FunctionName='name', ZipFile=b'foo').raises_error( error_code='InvalidParameterValueException', message='Unzipped size must be smaller than ...') stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session, mock.Mock(spec=time.sleep)) with pytest.raises(DeploymentPackageTooLargeError): awsclient.update_function('name', b'foo') stubbed_session.verify_stubs() def test_update_function_waits_for_active(self, stubbed_session, monkeypatch): lambda_client = stubbed_session.stub('lambda') lambda_client.update_function_code( FunctionName='name', ZipFile=b'foo').returns({ 'LastUpdateStatus': 'InProgress', }) lambda_client.get_function_configuration( FunctionName='name', ).returns({'LastUpdateStatus': 'InProgress'}) lambda_client.get_function_configuration( FunctionName='name', ).returns({'LastUpdateStatus': 'Successful'}) stubbed_session.activate_stubs() monkeypatch.setattr(time, 'sleep', mock.Mock(spec=time.sleep)) awsclient = TypedAWSClient(stubbed_session) awsclient.update_function('name', b'foo') stubbed_session.verify_stubs() def test_update_function_config_waits_for_active(self, stubbed_session, monkeypatch): lambda_client = stubbed_session.stub('lambda') lambda_client.update_function_code( FunctionName='name', ZipFile=b'foo').returns(self.SUCCESS_RESPONSE) lambda_client.update_function_configuration( FunctionName='name', Role='role-arn').returns({'LastUpdateStatus': 'InProgress'}) lambda_client.get_function_configuration( FunctionName='name', ).returns({'LastUpdateStatus': 'InProgress'}) lambda_client.get_function_configuration( FunctionName='name', ).returns({'LastUpdateStatus': 'Successful'}) stubbed_session.activate_stubs() monkeypatch.setattr(time, 'sleep', mock.Mock(spec=time.sleep)) awsclient = TypedAWSClient(stubbed_session) awsclient.update_function('name', b'foo', role_arn='role-arn') stubbed_session.verify_stubs() class TestPutFunctionConcurrency(object): def test_put_function_concurrency(self, stubbed_session): lambda_client = stubbed_session.stub('lambda') lambda_client.put_function_concurrency( FunctionName='name', ReservedConcurrentExecutions=5).returns({}) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) awsclient.put_function_concurrency('name', 5) stubbed_session.verify_stubs() class TestDeleteFunctionConcurrency(object): def test_delete_function_concurrency(self, stubbed_session): lambda_client = stubbed_session.stub('lambda') lambda_client.delete_function_concurrency( FunctionName='name').returns({}) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) awsclient.delete_function_concurrency('name') stubbed_session.verify_stubs() class TestCanDeleteRolePolicy(object): def test_can_delete_role_policy(self, stubbed_session): stubbed_session.stub('iam').delete_role_policy( RoleName='myrole', PolicyName='mypolicy' ).returns({}) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) awsclient.delete_role_policy('myrole', 'mypolicy') stubbed_session.verify_stubs() class TestCanDeleteRole(object): def test_can_delete_role(self, stubbed_session): stubbed_session.stub('iam').list_role_policies( RoleName='myrole').returns({ 'PolicyNames': ['mypolicy'] }) stubbed_session.stub('iam').delete_role_policy( RoleName='myrole', PolicyName='mypolicy').returns({}) stubbed_session.stub('iam').delete_role( RoleName='myrole' ).returns({}) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) awsclient.delete_role('myrole') stubbed_session.verify_stubs() class TestAddPermissionsForAPIGateway(object): def should_call_add_permission(self, lambda_stub, statement_id=stub.ANY): lambda_stub.add_permission( Action='lambda:InvokeFunction', FunctionName='name', StatementId=statement_id, Principal='apigateway.amazonaws.com', SourceArn='arn:aws:execute-api:us-west-2:123:rest-api-id/*', ).returns({}) def test_can_add_permission_for_apigateway_needed(self, stubbed_session): # An empty policy means we need to add permissions. lambda_stub = stubbed_session.stub('lambda') lambda_stub.get_policy(FunctionName='name').returns({'Policy': '{}'}) self.should_call_add_permission(lambda_stub) stubbed_session.activate_stubs() client = TypedAWSClient(stubbed_session) client.add_permission_for_apigateway( 'name', 'us-west-2', '123', 'rest-api-id') stubbed_session.verify_stubs() def test_can_add_permission_random_id_optional(self, stubbed_session): lambda_stub = stubbed_session.stub('lambda') lambda_stub.get_policy(FunctionName='name').returns({'Policy': '{}'}) self.should_call_add_permission(lambda_stub) stubbed_session.activate_stubs() client = TypedAWSClient(stubbed_session) client.add_permission_for_apigateway( 'name', 'us-west-2', '123', 'rest-api-id') stubbed_session.verify_stubs() def test_can_add_permission_for_apigateway_not_needed(self, stubbed_session): source_arn = 'arn:aws:execute-api:us-west-2:123:rest-api-id/*' wrong_action = { 'Action': 'lambda:NotInvoke', 'Condition': { 'ArnLike': { 'AWS:SourceArn': source_arn, } }, 'Effect': 'Allow', 'Principal': {'Service': 'apigateway.amazonaws.com'}, 'Resource': 'arn:aws:lambda:us-west-2:account_id:function:name', 'Sid': 'e4755709-067e-4254-b6ec-e7f9639e6f7b', } wrong_service_name = { 'Action': 'lambda:Invoke', 'Condition': { 'ArnLike': { 'AWS:SourceArn': source_arn, } }, 'Effect': 'Allow', 'Principal': {'Service': 'NOT-apigateway.amazonaws.com'}, 'Resource': 'arn:aws:lambda:us-west-2:account_id:function:name', 'Sid': 'e4755709-067e-4254-b6ec-e7f9639e6f7b', } correct_statement = { 'Action': 'lambda:InvokeFunction', 'Condition': { 'ArnLike': { 'AWS:SourceArn': source_arn, } }, 'Effect': 'Allow', 'Principal': {'Service': 'apigateway.amazonaws.com'}, 'Resource': 'arn:aws:lambda:us-west-2:account_id:function:name', 'Sid': 'e4755709-067e-4254-b6ec-e7f9639e6f7b', } policy = { 'Id': 'default', 'Statement': [ wrong_action, wrong_service_name, correct_statement, ], 'Version': '2012-10-17' } stubbed_session.stub('lambda').get_policy( FunctionName='name').returns({'Policy': json.dumps(policy)}) # Because the policy above indicates that API gateway already has the # necessary permissions, we should not call add_permission. stubbed_session.activate_stubs() client = TypedAWSClient(stubbed_session) client.add_permission_for_apigateway( 'name', 'us-west-2', '123', 'rest-api-id') stubbed_session.verify_stubs() def test_can_add_permission_when_policy_does_not_exist(self, stubbed_session): # It's also possible to receive a ResourceNotFoundException # if you call get_policy() on a lambda function with no policy. lambda_stub = stubbed_session.stub('lambda') lambda_stub.get_policy(FunctionName='name').raises_error( error_code='ResourceNotFoundException', message='Does not exist.') self.should_call_add_permission(lambda_stub) stubbed_session.activate_stubs() client = TypedAWSClient(stubbed_session) client.add_permission_for_apigateway( 'name', 'us-west-2', '123', 'rest-api-id', 'random-id') stubbed_session.verify_stubs() class TestAddPermissionsForAPIGatewayV2(object): def should_call_add_permission(self, lambda_stub, statement_id=stub.ANY): lambda_stub.add_permission( Action='lambda:InvokeFunction', FunctionName='name', StatementId=statement_id, Principal='apigateway.amazonaws.com', SourceArn='arn:aws:execute-api:us-west-2:123:websocket-api-id/*', ).returns({}) def test_can_add_permission_for_apigateway_v2_needed(self, stubbed_session): # An empty policy means we need to add permissions. lambda_stub = stubbed_session.stub('lambda') lambda_stub.get_policy(FunctionName='name').returns({'Policy': '{}'}) self.should_call_add_permission(lambda_stub) stubbed_session.activate_stubs() client = TypedAWSClient(stubbed_session) client.add_permission_for_apigateway_v2( 'name', 'us-west-2', '123', 'websocket-api-id') stubbed_session.verify_stubs() def test_can_add_permission_random_id_optional(self, stubbed_session): lambda_stub = stubbed_session.stub('lambda') lambda_stub.get_policy(FunctionName='name').returns({'Policy': '{}'}) self.should_call_add_permission(lambda_stub) stubbed_session.activate_stubs() client = TypedAWSClient(stubbed_session) client.add_permission_for_apigateway_v2( 'name', 'us-west-2', '123', 'websocket-api-id') stubbed_session.verify_stubs() def test_can_add_permission_for_apigateway_v2_not_needed(self, stubbed_session): source_arn = 'arn:aws:execute-api:us-west-2:123:websocket-api-id/*' wrong_action = { 'Action': 'lambda:NotInvoke', 'Condition': { 'ArnLike': { 'AWS:SourceArn': source_arn, } }, 'Effect': 'Allow', 'Principal': {'Service': 'apigateway.amazonaws.com'}, 'Resource': 'arn:aws:lambda:us-west-2:account_id:function:name', 'Sid': 'e4755709-067e-4254-b6ec-e7f9639e6f7b', } wrong_service_name = { 'Action': 'lambda:Invoke', 'Condition': { 'ArnLike': { 'AWS:SourceArn': source_arn, } }, 'Effect': 'Allow', 'Principal': {'Service': 'NOT-apigateway.amazonaws.com'}, 'Resource': 'arn:aws:lambda:us-west-2:account_id:function:name', 'Sid': 'e4755709-067e-4254-b6ec-e7f9639e6f7b', } correct_statement = { 'Action': 'lambda:InvokeFunction', 'Condition': { 'ArnLike': { 'AWS:SourceArn': source_arn, } }, 'Effect': 'Allow', 'Principal': {'Service': 'apigateway.amazonaws.com'}, 'Resource': 'arn:aws:lambda:us-west-2:account_id:function:name', 'Sid': 'e4755709-067e-4254-b6ec-e7f9639e6f7b', } policy = { 'Id': 'default', 'Statement': [ wrong_action, wrong_service_name, correct_statement, ], 'Version': '2012-10-17' } stubbed_session.stub('lambda').get_policy( FunctionName='name').returns({'Policy': json.dumps(policy)}) # Because the policy above indicates that API gateway already has the # necessary permissions, we should not call add_permission. stubbed_session.activate_stubs() client = TypedAWSClient(stubbed_session) client.add_permission_for_apigateway( 'name', 'us-west-2', '123', 'websocket-api-id') stubbed_session.verify_stubs() def test_can_add_permission_when_policy_does_not_exist(self, stubbed_session): # It's also possible to receive a ResourceNotFoundException # if you call get_policy() on a lambda function with no policy. lambda_stub = stubbed_session.stub('lambda') lambda_stub.get_policy(FunctionName='name').raises_error( error_code='ResourceNotFoundException', message='Does not exist.') self.should_call_add_permission(lambda_stub) stubbed_session.activate_stubs() client = TypedAWSClient(stubbed_session) client.add_permission_for_apigateway_v2( 'name', 'us-west-2', '123', 'websocket-api-id', 'random-id') stubbed_session.verify_stubs() class TestWebsocketAPI(object): def test_can_create_websocket_api(self, stubbed_session): stubbed_session.stub('apigatewayv2').create_api( Name='name', ProtocolType='WEBSOCKET', RouteSelectionExpression='$request.body.action', ).returns({'ApiId': 'id'}) stubbed_session.activate_stubs() client = TypedAWSClient(stubbed_session) api_id = client.create_websocket_api('name') stubbed_session.verify_stubs() assert api_id == 'id' def test_can_get_websocket_api(self, stubbed_session): stubbed_session.stub('apigatewayv2').get_apis( ).returns({ 'Items': [ {'Name': 'some-other-api', 'ApiId': 'foo bar', 'RouteSelectionExpression': 'unused', 'ProtocolType': 'WEBSOCKET'}, {'Name': 'target-api', 'ApiId': 'id', 'RouteSelectionExpression': 'unused', 'ProtocolType': 'WEBSOCKET'}, ], }) stubbed_session.activate_stubs() client = TypedAWSClient(stubbed_session) api_id = client.get_websocket_api_id('target-api') stubbed_session.verify_stubs() assert api_id == 'id' def test_does_return_none_on_websocket_api_missing(self, stubbed_session): stubbed_session.stub('apigatewayv2').get_apis( ).returns({ 'Items': [], }) stubbed_session.activate_stubs() client = TypedAWSClient(stubbed_session) api_id = client.get_websocket_api_id('target-api') stubbed_session.verify_stubs() assert api_id is None def test_can_check_get_websocket_api_exists(self, stubbed_session): stubbed_session.stub('apigatewayv2').get_api( ApiId='api-id', ).returns({}) stubbed_session.activate_stubs() client = TypedAWSClient(stubbed_session) exists = client.websocket_api_exists('api-id') stubbed_session.verify_stubs() assert exists is True def test_can_check_get_websocket_api_not_exists(self, stubbed_session): stubbed_session.stub('apigatewayv2').get_api( ApiId='api-id', ).raises_error( error_code='NotFoundException', message='Does not exists.', ) stubbed_session.activate_stubs() client = TypedAWSClient(stubbed_session) exists = client.websocket_api_exists('api-id') stubbed_session.verify_stubs() assert exists is False def test_can_delete_websocket_api(self, stubbed_session): stubbed_session.stub('apigatewayv2').delete_api( ApiId='id', ).returns({}) stubbed_session.activate_stubs() client = TypedAWSClient(stubbed_session) client.delete_websocket_api('id') stubbed_session.verify_stubs() def test_rest_api_delete_already_deleted(self, stubbed_session): stubbed_session.stub('apigatewayv2')\ .delete_api(ApiId='name')\ .raises_error(error_code='NotFoundException', message='Unknown') stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) with pytest.raises(ResourceDoesNotExistError): assert awsclient.delete_websocket_api('name') def test_can_create_integration(self, stubbed_session): stubbed_session.stub('apigatewayv2').create_integration( ApiId='api-id', ConnectionType='INTERNET', ContentHandlingStrategy='CONVERT_TO_TEXT', Description='connect', IntegrationType='AWS_PROXY', IntegrationUri='arn:aws:lambda', ).returns({'IntegrationId': 'integration-id'}) stubbed_session.activate_stubs() client = TypedAWSClient(stubbed_session) integration_id = client.create_websocket_integration( api_id='api-id', lambda_function='arn:aws:lambda', handler_type='connect', ) stubbed_session.verify_stubs() assert integration_id == 'integration-id' def test_can_create_route(self, stubbed_session): stubbed_session.stub('apigatewayv2').create_route( ApiId='api-id', RouteKey='route-key', RouteResponseSelectionExpression='$default', Target='integrations/integration-id', ).returns({}) stubbed_session.activate_stubs() client = TypedAWSClient(stubbed_session) client.create_websocket_route( api_id='api-id', route_key='route-key', integration_id='integration-id', ) stubbed_session.verify_stubs() def test_can_delete_all_websocket_routes(self, stubbed_session): stubbed_session.stub('apigatewayv2').delete_route( ApiId='api-id', RouteId='route-id', ).returns({}) stubbed_session.stub('apigatewayv2').delete_route( ApiId='api-id', RouteId='old-route-id', ).returns({}) stubbed_session.activate_stubs() client = TypedAWSClient(stubbed_session) client.delete_websocket_routes( api_id='api-id', routes=['route-id', 'old-route-id'], ) stubbed_session.verify_stubs() def test_can_delete_all_websocket_integrations(self, stubbed_session): stubbed_session.stub('apigatewayv2').delete_integration( ApiId='api-id', IntegrationId='integration-id', ).returns({}) stubbed_session.stub('apigatewayv2').delete_integration( ApiId='api-id', IntegrationId='old-integration-id', ).returns({}) stubbed_session.activate_stubs() client = TypedAWSClient(stubbed_session) client.delete_websocket_integrations( api_id='api-id', integrations=['integration-id', 'old-integration-id'], ) stubbed_session.verify_stubs() def test_can_deploy_websocket_api(self, stubbed_session): stubbed_session.stub('apigatewayv2').create_deployment( ApiId='api-id', ).returns({'DeploymentId': 'deployment-id'}) stubbed_session.activate_stubs() client = TypedAWSClient(stubbed_session) deployment_id = client.deploy_websocket_api( api_id='api-id', ) stubbed_session.verify_stubs() assert deployment_id == 'deployment-id' def test_can_get_routes(self, stubbed_session): stubbed_session.stub('apigatewayv2').get_routes( ApiId='api-id', ).returns( { 'Items': [ {'RouteKey': 'route-key-foo', 'RouteId': 'route-id-foo'}, {'RouteKey': 'route-key-bar', 'RouteId': 'route-id-bar'}, ], } ) stubbed_session.activate_stubs() client = TypedAWSClient(stubbed_session) routes = client.get_websocket_routes( api_id='api-id', ) stubbed_session.verify_stubs() assert routes == ['route-id-foo', 'route-id-bar'] def test_can_get_integrations(self, stubbed_session): stubbed_session.stub('apigatewayv2').get_integrations( ApiId='api-id', ).returns( { 'Items': [ { 'Description': 'connect', 'IntegrationId': 'connect-integration-id' }, { 'Description': 'message', 'IntegrationId': 'message-integration-id' }, { 'Description': 'disconnect', 'IntegrationId': 'disconnect-integration-id' }, ] } ) stubbed_session.activate_stubs() client = TypedAWSClient(stubbed_session) integration_ids = client.get_websocket_integrations( api_id='api-id', ) stubbed_session.verify_stubs() assert integration_ids == [ 'connect-integration-id', 'message-integration-id', 'disconnect-integration-id', ] def test_can_create_stage(self, stubbed_session): stubbed_session.stub('apigatewayv2').create_stage( ApiId='api-id', StageName='stage-name', DeploymentId='deployment-id', ).returns({}) stubbed_session.activate_stubs() client = TypedAWSClient(stubbed_session) client.create_stage( api_id='api-id', stage_name='stage-name', deployment_id='deployment-id', ) stubbed_session.verify_stubs() class TestAddPermissionsForAuthorizer(object): FUNCTION_ARN = ( 'arn:aws:lambda:us-west-2:1:function:app-dev-name' ) GOOD_ARN = ( 'arn:aws:apigateway:us-west-2:lambda:path/2015-03-31/functions/' '%s/invocations' % FUNCTION_ARN ) def test_can_add_permission_for_authorizer(self, stubbed_session): apigateway = stubbed_session.stub('apigateway') apigateway.get_authorizers(restApiId='rest-api-id').returns({ 'items': [ {'authorizerUri': 'not:arn', 'id': 'bad'}, {'authorizerUri': self.GOOD_ARN, 'id': 'good'}, ] }) source_arn = ( 'arn:aws:execute-api:us-west-2:1:rest-api-id/authorizers/good' ) # We should call the appropriate add_permission call. lambda_client = stubbed_session.stub('lambda') lambda_client.add_permission( Action='lambda:InvokeFunction', FunctionName='app-dev-name', StatementId='random-id', Principal='apigateway.amazonaws.com', SourceArn=source_arn ).returns({}) stubbed_session.activate_stubs() TypedAWSClient(stubbed_session).add_permission_for_authorizer( 'rest-api-id', self.FUNCTION_ARN, 'random-id' ) stubbed_session.verify_stubs() def test_random_id_can_be_omitted(self, stubbed_session): stubbed_session.stub('apigateway').get_authorizers( restApiId='rest-api-id').returns({ 'items': [{'authorizerUri': self.GOOD_ARN, 'id': 'good'}]}) source_arn = ( 'arn:aws:execute-api:us-west-2:1:rest-api-id/authorizers/good' ) stubbed_session.stub('lambda').add_permission( Action='lambda:InvokeFunction', FunctionName='app-dev-name', # Autogenerated value here. StatementId=stub.ANY, Principal='apigateway.amazonaws.com', SourceArn=source_arn ).returns({}) stubbed_session.activate_stubs() # Note the omission of the random id. TypedAWSClient(stubbed_session).add_permission_for_authorizer( 'rest-api-id', self.FUNCTION_ARN ) stubbed_session.verify_stubs() def test_value_error_raised_for_unknown_function(self, stubbed_session): apigateway = stubbed_session.stub('apigateway') apigateway.get_authorizers(restApiId='rest-api-id').returns({ 'items': [ {'authorizerUri': 'not:arn', 'id': 'bad'}, {'authorizerUri': 'also-not:arn', 'id': 'alsobad'}, ] }) stubbed_session.activate_stubs() unknown_function_arn = 'function:arn' with pytest.raises(ResourceDoesNotExistError): TypedAWSClient(stubbed_session).add_permission_for_authorizer( 'rest-api-id', unknown_function_arn, 'random-id' ) stubbed_session.verify_stubs() def test_get_sdk(stubbed_session): apig = stubbed_session.stub('apigateway') apig.get_sdk( restApiId='rest-api-id', stageName='dev', sdkType='javascript').returns({'body': 'foo'}) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) response = awsclient.get_sdk_download_stream( 'rest-api-id', 'dev', 'javascript') stubbed_session.verify_stubs() assert response == 'foo' def test_import_rest_api(stubbed_session): apig = stubbed_session.stub('apigateway') swagger_doc = {'swagger': 'doc'} apig.import_rest_api( parameters={'endpointConfigurationTypes': 'EDGE'}, body=json.dumps(swagger_doc, indent=2)).returns( {'id': 'rest_api_id'}) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) rest_api_id = awsclient.import_rest_api(swagger_doc, 'EDGE') stubbed_session.verify_stubs() assert rest_api_id == 'rest_api_id' def test_update_api_from_swagger(stubbed_session): apig = stubbed_session.stub('apigateway') swagger_doc = {'swagger': 'doc'} apig.put_rest_api( restApiId='rest_api_id', mode='overwrite', body=json.dumps(swagger_doc, indent=2)).returns({}) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) awsclient.update_api_from_swagger('rest_api_id', swagger_doc) stubbed_session.verify_stubs() def test_update_rest_api(stubbed_session): apig = stubbed_session.stub('apigateway') patch_operations = [{'op': 'replace', 'path': '/minimumCompressionSize', 'value': '2'}] apig.update_rest_api( restApiId='rest_api_id', patchOperations=patch_operations).returns({}) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) awsclient.update_rest_api('rest_api_id', patch_operations) stubbed_session.verify_stubs() def test_can_get_or_create_rule_arn_with_pattern(stubbed_session): events = stubbed_session.stub('events') events.put_rule( Name='rule-name', EventPattern='{"source": ["aws.ec2"]}').returns({ 'RuleArn': 'rule-arn', }) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) result = awsclient.get_or_create_rule_arn( rule_name='rule-name', event_pattern='{"source": ["aws.ec2"]}') stubbed_session.verify_stubs() assert result == 'rule-arn' def test_can_get_or_create_rule_arn(stubbed_session): events = stubbed_session.stub('events') events.put_rule( Name='rule-name', Description='rule-description', ScheduleExpression='rate(1 hour)').returns({ 'RuleArn': 'rule-arn', }) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) result = awsclient.get_or_create_rule_arn( 'rule-name', schedule_expression='rate(1 hour)', rule_description='rule-description' ) stubbed_session.verify_stubs() assert result == 'rule-arn' def test_can_connect_rule_to_lambda(stubbed_session): events = stubbed_session.stub('events') events.put_targets( Rule='rule-name', Targets=[{'Id': '1', 'Arn': 'function-arn'}]).returns({}) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) awsclient.connect_rule_to_lambda('rule-name', 'function-arn') stubbed_session.verify_stubs() def test_add_permission_for_scheduled_event(stubbed_session): lambda_client = stubbed_session.stub('lambda') lambda_client.get_policy(FunctionName='function-arn').returns( {'Policy': '{}'}) lambda_client.add_permission( Action='lambda:InvokeFunction', FunctionName='function-arn', StatementId=stub.ANY, Principal='events.amazonaws.com', SourceArn='arn:aws:events:us-east-1:123456789012:rule/MyScheduledRule' ).returns({}) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) awsclient.add_permission_for_cloudwatch_event( 'arn:aws:events:us-east-1:123456789012:rule/MyScheduledRule', 'function-arn') stubbed_session.verify_stubs() def test_skip_if_permission_already_granted(stubbed_session): lambda_client = stubbed_session.stub('lambda') policy = { 'Id': 'default', 'Statement': [ {'Action': 'lambda:InvokeFunction', 'Condition': { 'ArnLike': { 'AWS:SourceArn': 'arn:aws:events:us-east-1' ':123456789012:rule/MyScheduledRule', } }, 'Effect': 'Allow', 'Principal': {'Service': 'events.amazonaws.com'}, 'Resource': 'resource-arn', 'Sid': 'statement-id'}, ], 'Version': '2012-10-17' } lambda_client.get_policy( FunctionName='function-arn').returns({'Policy': json.dumps(policy)}) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) awsclient.add_permission_for_cloudwatch_event( 'arn:aws:events:us-east-1:123456789012:rule/MyScheduledRule', 'function-arn') stubbed_session.verify_stubs() def test_can_delete_rule(stubbed_session): events = stubbed_session.stub('events') events.remove_targets( Rule='rule-name', Ids=['1']).returns({}) events.delete_rule(Name='rule-name').returns({}) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) awsclient.delete_rule('rule-name') stubbed_session.verify_stubs() def test_can_connect_bucket_to_lambda_new_config(stubbed_session): s3 = stubbed_session.stub('s3') s3.get_bucket_notification_configuration(Bucket='mybucket').returns({ 'ResponseMetadata': {}, }) s3.put_bucket_notification_configuration( Bucket='mybucket', NotificationConfiguration={ 'LambdaFunctionConfigurations': [{ 'LambdaFunctionArn': 'function-arn', 'Events': ['s3:ObjectCreated:*'], }] } ).returns({}) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) awsclient.connect_s3_bucket_to_lambda( 'mybucket', 'function-arn', ['s3:ObjectCreated:*']) stubbed_session.verify_stubs() def test_can_connect_bucket_with_prefix_and_suffix(stubbed_session): s3 = stubbed_session.stub('s3') s3.get_bucket_notification_configuration(Bucket='mybucket').returns({}) s3.put_bucket_notification_configuration( Bucket='mybucket', NotificationConfiguration={ 'LambdaFunctionConfigurations': [{ 'LambdaFunctionArn': 'function-arn', 'Filter': { 'Key': { 'FilterRules': [ { 'Name': 'Prefix', 'Value': 'images/' }, { 'Name': 'Suffix', 'Value': '.jpg' } ] } }, 'Events': ['s3:ObjectCreated:*'], }] } ).returns({}) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) awsclient.connect_s3_bucket_to_lambda( 'mybucket', 'function-arn', ['s3:ObjectCreated:*'], prefix='images/', suffix='.jpg', ) stubbed_session.verify_stubs() def test_can_merge_s3_notification_config(stubbed_session): s3 = stubbed_session.stub('s3') s3.get_bucket_notification_configuration(Bucket='mybucket').returns({ 'LambdaFunctionConfigurations': [ {'Events': ['s3:ObjectCreated:*'], 'LambdaFunctionArn': 'other-function-arn'}], }) s3.put_bucket_notification_configuration( Bucket='mybucket', NotificationConfiguration={ 'LambdaFunctionConfigurations': [ # The existing function arn remains untouched. {'LambdaFunctionArn': 'other-function-arn', 'Events': ['s3:ObjectCreated:*']}, # This is the new function arn that we've injected. {'LambdaFunctionArn': 'function-arn', 'Events': ['s3:ObjectCreated:*']}, ] } ).returns({}) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) awsclient.connect_s3_bucket_to_lambda( 'mybucket', 'function-arn', ['s3:ObjectCreated:*']) stubbed_session.verify_stubs() def test_can_replace_existing_config(stubbed_session): s3 = stubbed_session.stub('s3') s3.get_bucket_notification_configuration(Bucket='mybucket').returns({ 'LambdaFunctionConfigurations': [ {'Events': ['s3:ObjectRemoved:*'], 'LambdaFunctionArn': 'function-arn'}], }) s3.put_bucket_notification_configuration( Bucket='mybucket', NotificationConfiguration={ 'LambdaFunctionConfigurations': [ # Note the event is replaced from ObjectRemoved # to ObjectCreated. {'LambdaFunctionArn': 'function-arn', 'Events': ['s3:ObjectCreated:*']}, ] } ).returns({}) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) awsclient.connect_s3_bucket_to_lambda( 'mybucket', 'function-arn', ['s3:ObjectCreated:*']) stubbed_session.verify_stubs() def test_add_permission_for_s3_event(stubbed_session): lambda_client = stubbed_session.stub('lambda') lambda_client.get_policy(FunctionName='function-arn').returns( {'Policy': '{}'}) lambda_client.add_permission( Action='lambda:InvokeFunction', FunctionName='function-arn', StatementId=stub.ANY, Principal='s3.amazonaws.com', SourceArn='arn:aws:s3:::mybucket', SourceAccount='12345', ).returns({}) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) awsclient.add_permission_for_s3_event( 'mybucket', 'function-arn', '12345') stubbed_session.verify_stubs() def test_skip_if_permission_already_granted_to_s3(stubbed_session): lambda_client = stubbed_session.stub('lambda') policy = { 'Id': 'default', 'Statement': [{ 'Action': 'lambda:InvokeFunction', 'Condition': { 'StringEquals': { 'AWS:SourceAccount': '12345', }, 'ArnLike': { 'AWS:SourceArn': 'arn:aws:s3:::mybucket', } }, 'Effect': 'Allow', 'Principal': {'Service': 's3.amazonaws.com'}, 'Resource': 'resource-arn', 'Sid': 'statement-id', }], 'Version': '2012-10-17' } lambda_client.get_policy( FunctionName='function-arn').returns({'Policy': json.dumps(policy)}) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) awsclient.add_permission_for_s3_event( 'mybucket', 'function-arn', '12345') stubbed_session.verify_stubs() def test_can_disconnect_bucket_to_lambda_merged(stubbed_session): s3 = stubbed_session.stub('s3') s3.get_bucket_notification_configuration(Bucket='mybucket').returns({ 'LambdaFunctionConfigurations': [ {'Events': ['s3:ObjectRemoved:*'], 'LambdaFunctionArn': 'function-arn-1'}, {'Events': ['s3:ObjectCreated:*'], 'LambdaFunctionArn': 'function-arn-2'} ], 'ResponseMetadata': {}, }) s3.put_bucket_notification_configuration( Bucket='mybucket', NotificationConfiguration={ 'LambdaFunctionConfigurations': [ {'Events': ['s3:ObjectCreated:*'], 'LambdaFunctionArn': 'function-arn-2'} ], }, ).returns({}) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) awsclient.disconnect_s3_bucket_from_lambda( 'mybucket', 'function-arn-1') stubbed_session.verify_stubs() def test_can_disconnect_bucket_to_lambda_not_exists(stubbed_session): s3 = stubbed_session.stub('s3') s3.get_bucket_notification_configuration(Bucket='mybucket').returns({ 'LambdaFunctionConfigurations': [ {'Events': ['s3:ObjectRemoved:*'], 'LambdaFunctionArn': 'function-arn-1'}, {'Events': ['s3:ObjectCreated:*'], 'LambdaFunctionArn': 'function-arn-2'} ], }) s3.put_bucket_notification_configuration( Bucket='mybucket', NotificationConfiguration={ 'LambdaFunctionConfigurations': [ {'Events': ['s3:ObjectRemoved:*'], 'LambdaFunctionArn': 'function-arn-1'}, {'Events': ['s3:ObjectCreated:*'], 'LambdaFunctionArn': 'function-arn-2'} ], }, ).returns({}) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) awsclient.disconnect_s3_bucket_from_lambda('mybucket', 'some-other-arn') stubbed_session.verify_stubs() def test_add_permission_for_sns_publish(stubbed_session): lambda_client = stubbed_session.stub('lambda') lambda_client.get_policy(FunctionName='function-arn').returns( {'Policy': '{"Statement": []}'} ) lambda_client.add_permission( Action='lambda:InvokeFunction', FunctionName='function-arn', StatementId=stub.ANY, Principal='sns.amazonaws.com', SourceArn='arn:aws:sns:us-west-2:12345:my-demo-topic', ).returns({}) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) awsclient.add_permission_for_sns_topic( 'arn:aws:sns:us-west-2:12345:my-demo-topic', 'function-arn') stubbed_session.verify_stubs() def test_subscribe_function_to_arn(stubbed_session): sns_client = stubbed_session.stub('sns') topic_arn = 'arn:aws:sns:topic-arn' sns_client.subscribe( TopicArn=topic_arn, Protocol='lambda', Endpoint='function-arn' ).returns({'SubscriptionArn': 'subscribe-arn'}) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) awsclient.subscribe_function_to_topic( 'arn:aws:sns:topic-arn', 'function-arn') stubbed_session.verify_stubs() def test_can_unsubscribe_from_topic(stubbed_session): sns_client = stubbed_session.stub('sns') subscription_arn = 'arn:aws:sns:subscribe-arn' sns_client.unsubscribe( SubscriptionArn=subscription_arn, ).returns({}) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) awsclient.unsubscribe_from_topic(subscription_arn) stubbed_session.verify_stubs() @pytest.mark.parametrize('topic_arn,function_arn,is_verified', [ ('arn:aws:sns:mytopic', 'arn:aws:lambda:myfunction', True), ('arn:aws:sns:NEW-TOPIC', 'arn:aws:lambda:myfunction', False), ('arn:aws:sns:mytopic', 'arn:aws:lambda:NEW-FUNCTION', False), ('arn:aws:sns:NEW-TOPIC', 'arn:aws:lambda:NEW-FUNCTION', False), ]) def test_subscription_exists(stubbed_session, topic_arn, function_arn, is_verified): sns_client = stubbed_session.stub('sns') subscription_arn = 'arn:aws:sns:subscribe-arn' sns_client.get_subscription_attributes( SubscriptionArn=subscription_arn, ).returns({ "Attributes": { "Owner": "12345", "RawMessageDelivery": "false", "TopicArn": topic_arn, "Endpoint": function_arn, "Protocol": "lambda", "PendingConfirmation": "false", "ConfirmationWasAuthenticated": "true", "SubscriptionArn": subscription_arn, } }) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) assert awsclient.verify_sns_subscription_current( subscription_arn, topic_name='mytopic', function_arn='arn:aws:lambda:myfunction', ) == is_verified stubbed_session.verify_stubs() def test_subscription_not_exists(stubbed_session): sns_client = stubbed_session.stub('sns') subscription_arn = 'arn:aws:sns:subscribe-arn' sns_client.get_subscription_attributes( SubscriptionArn=subscription_arn, ).raises_error(error_code='NotFound', message='Does not exists.') stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) assert not awsclient.verify_sns_subscription_current( subscription_arn, 'topic-arn', 'function-arn') stubbed_session.verify_stubs() def test_can_remove_lambda_sns_permission(stubbed_session): topic_arn = 'arn:aws:sns:us-west-2:12345:my-demo-topic' policy = { 'Id': 'default', 'Statement': [create_policy_statement(topic_arn, service_name='sns', statement_id='12345')], 'Version': '2012-10-17' } lambda_stub = stubbed_session.stub('lambda') lambda_stub.get_policy( FunctionName='name').returns({'Policy': json.dumps(policy)}) lambda_stub.remove_permission( FunctionName='name', StatementId='12345', ).returns({}) # Because the policy above indicates that API gateway already has the # necessary permissions, we should not call add_permission. stubbed_session.activate_stubs() client = TypedAWSClient(stubbed_session) client.remove_permission_for_sns_topic( topic_arn, 'name') stubbed_session.verify_stubs() def test_can_remove_s3_permission(stubbed_session): policy = { 'Id': 'default', 'Statement': [create_policy_statement('arn:aws:s3:::mybucket', service_name='s3', statement_id='12345', account_id='67890')], 'Version': '2012-10-17' } lambda_stub = stubbed_session.stub('lambda') lambda_stub.get_policy( FunctionName='name').returns({'Policy': json.dumps(policy)}) lambda_stub.remove_permission( FunctionName='name', StatementId='12345', ).returns({}) # Because the policy above indicates that API gateway already has the # necessary permissions, we should not call add_permission. stubbed_session.activate_stubs() client = TypedAWSClient(stubbed_session) client.remove_permission_for_s3_event( 'mybucket', 'name', '67890') stubbed_session.verify_stubs() def test_can_create_kinesis_event_source(stubbed_session): kinesis_arn = 'arn:aws:kinesis:us-west-2:...:stream/MyStream' function_name = 'myfunction' batch_size = 100 starting_position = 'TRIM_HORIZON' lambda_stub = stubbed_session.stub('lambda') lambda_stub.create_event_source_mapping( EventSourceArn=kinesis_arn, FunctionName=function_name, BatchSize=batch_size, StartingPosition=starting_position, MaximumBatchingWindowInSeconds=0 ).returns({'UUID': 'my-uuid'}) stubbed_session.activate_stubs() client = TypedAWSClient(stubbed_session) result = client.create_lambda_event_source( kinesis_arn, function_name, batch_size, starting_position ) assert result == 'my-uuid' stubbed_session.verify_stubs() def test_can_create_kinesis_event_source_batching_window(stubbed_session): kinesis_arn = 'arn:aws:kinesis:us-west-2:...:stream/MyStream' function_name = 'myfunction' batch_size = 100 starting_position = 'TRIM_HORIZON' maximum_batching_window_in_seconds = 60 lambda_stub = stubbed_session.stub('lambda') lambda_stub.create_event_source_mapping( EventSourceArn=kinesis_arn, FunctionName=function_name, BatchSize=batch_size, StartingPosition=starting_position, MaximumBatchingWindowInSeconds=maximum_batching_window_in_seconds ).returns({'UUID': 'my-uuid'}) stubbed_session.activate_stubs() client = TypedAWSClient(stubbed_session) result = client.create_lambda_event_source( kinesis_arn, function_name, batch_size, starting_position, maximum_batching_window_in_seconds ) assert result == 'my-uuid' stubbed_session.verify_stubs() def test_can_create_sqs_event_source(stubbed_session): queue_arn = 'arn:sqs:queue-name' function_name = 'myfunction' batch_size = 100 maximum_batching_window_in_seconds = 60 lambda_stub = stubbed_session.stub('lambda') lambda_stub.create_event_source_mapping( EventSourceArn=queue_arn, FunctionName=function_name, BatchSize=batch_size, MaximumBatchingWindowInSeconds=maximum_batching_window_in_seconds ).returns({'UUID': 'my-uuid'}) stubbed_session.activate_stubs() client = TypedAWSClient(stubbed_session) result = client.create_lambda_event_source( queue_arn, function_name, batch_size, maximum_batching_window_in_seconds=maximum_batching_window_in_seconds ) assert result == 'my-uuid' stubbed_session.verify_stubs() def test_can_retry_create_sqs_event_source(stubbed_session): queue_arn = 'arn:sqs:queue-name' function_name = 'myfunction' batch_size = 100 lambda_stub = stubbed_session.stub('lambda') lambda_stub.create_event_source_mapping( EventSourceArn=queue_arn, FunctionName=function_name, BatchSize=batch_size, MaximumBatchingWindowInSeconds=0 ).raises_error( error_code='InvalidParameterValueException', message=('The provided execution role does not ' 'have permissions to call ReceiveMessage on SQS') ) lambda_stub.create_event_source_mapping( EventSourceArn=queue_arn, FunctionName=function_name, BatchSize=batch_size, MaximumBatchingWindowInSeconds=0 ).returns({'UUID': 'my-uuid'}) stubbed_session.activate_stubs() client = TypedAWSClient(stubbed_session, mock.Mock(spec=time.sleep)) result = client.create_lambda_event_source( queue_arn, function_name, batch_size ) assert result == 'my-uuid' stubbed_session.verify_stubs() def test_can_delete_sqs_event_source(stubbed_session): lambda_stub = stubbed_session.stub('lambda') lambda_stub.delete_event_source_mapping( UUID='my-uuid', ).returns({}) stubbed_session.activate_stubs() client = TypedAWSClient(stubbed_session, mock.Mock(spec=time.sleep)) client.remove_lambda_event_source( 'my-uuid', ) stubbed_session.verify_stubs() def test_can_retry_delete_event_source(stubbed_session): lambda_stub = stubbed_session.stub('lambda') lambda_stub.delete_event_source_mapping( UUID='my-uuid', ).raises_error( error_code='ResourceInUseException', message=('Cannot update the event source mapping ' 'because it is in use.') ) lambda_stub.delete_event_source_mapping( UUID='my-uuid', ).returns({}) stubbed_session.activate_stubs() client = TypedAWSClient(stubbed_session, mock.Mock(spec=time.sleep)) client.remove_lambda_event_source( 'my-uuid', ) stubbed_session.verify_stubs() def test_only_retry_settling_errors(stubbed_session): lambda_stub = stubbed_session.stub('lambda') lambda_stub.delete_event_source_mapping( UUID='my-uuid', ).raises_error( error_code='ResourceInUseException', message='Wrong message' ) stubbed_session.activate_stubs() client = TypedAWSClient(stubbed_session, mock.Mock(spec=time.sleep)) with pytest.raises(botocore.exceptions.ClientError): client.remove_lambda_event_source('my-uuid') stubbed_session.verify_stubs() def test_can_retry_update_event_source(stubbed_session): lambda_stub = stubbed_session.stub('lambda') lambda_stub.update_event_source_mapping( UUID='my-uuid', BatchSize=5, MaximumBatchingWindowInSeconds=0, ).returns({}) stubbed_session.activate_stubs() client = TypedAWSClient(stubbed_session) client.update_lambda_event_source( event_uuid='my-uuid', batch_size=5 ) stubbed_session.verify_stubs() def test_can_retry_update_event_source_batching_window(stubbed_session): lambda_stub = stubbed_session.stub('lambda') lambda_stub.update_event_source_mapping( UUID='my-uuid', BatchSize=5, MaximumBatchingWindowInSeconds=60, ).returns({}) stubbed_session.activate_stubs() client = TypedAWSClient(stubbed_session) client.update_lambda_event_source( event_uuid='my-uuid', batch_size=5, maximum_batching_window_in_seconds=60 ) stubbed_session.verify_stubs() @pytest.mark.parametrize('resource_name,service_name,is_verified', [ ('queue-name', 'sqs', True), ('queue-name', 'not-sqs', False), ('not-queue-name', 'sqs', False), ('not-queue-name', 'not-sqs', False), ]) def test_verify_event_source_current(stubbed_session, resource_name, service_name, is_verified): client = stubbed_session.stub('lambda') uuid = 'uuid-12345' client.get_event_source_mapping( UUID=uuid, ).returns({ 'UUID': uuid, 'BatchSize': 10, 'EventSourceArn': 'arn:aws:sqs:us-west-2:123:queue-name', 'FunctionArn': 'arn:aws:lambda:function-arn', 'LastModified': '2018-07-02T18:19:03.958000-07:00', 'State': 'Enabled', 'StateTransitionReason': 'USER_INITIATED' }) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) assert awsclient.verify_event_source_current( uuid, resource_name=resource_name, service_name=service_name, function_arn='arn:aws:lambda:function-arn', ) == is_verified stubbed_session.verify_stubs() def test_verify_event_source_arn_current(stubbed_session): client = stubbed_session.stub('lambda') uuid = 'uuid-12345' client.get_event_source_mapping( UUID=uuid, ).returns({ 'UUID': uuid, 'BatchSize': 10, 'EventSourceArn': 'arn:aws:dynamodb:...:table/MyTable/stream/2020', 'FunctionArn': 'arn:aws:lambda:function-arn', 'LastModified': '2018-07-02T18:19:03.958000-07:00', 'State': 'Enabled', 'StateTransitionReason': 'USER_INITIATED' }) stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) assert awsclient.verify_event_source_arn_current( uuid, event_source_arn='arn:aws:dynamodb:...:table/MyTable/stream/2020', function_arn='arn:aws:lambda:function-arn', ) stubbed_session.verify_stubs() def test_event_source_uuid_does_not_exist(stubbed_session): client = stubbed_session.stub('lambda') uuid = 'uuid-12345' client.get_event_source_mapping( UUID=uuid, ).raises_error(error_code='ResourceNotFoundException', message='Does not exists.') stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) assert not awsclient.verify_event_source_arn_current( uuid, event_source_arn='arn:aws:dynamodb:...', function_arn='arn:aws:lambda:...', ) stubbed_session.verify_stubs() def test_event_source_does_not_exist(stubbed_session): client = stubbed_session.stub('lambda') uuid = 'uuid-12345' client.get_event_source_mapping( UUID=uuid, ).raises_error(error_code='ResourceNotFoundException', message='Does not exists.') stubbed_session.activate_stubs() awsclient = TypedAWSClient(stubbed_session) assert not awsclient.verify_event_source_current( uuid, 'myqueue', 'sqs', 'function-arn') stubbed_session.verify_stubs() def test_can_update_lambda_event_source(stubbed_session): lambda_stub = stubbed_session.stub('lambda') lambda_stub.update_event_source_mapping( UUID='my-uuid', BatchSize=5, MaximumBatchingWindowInSeconds=60, ).returns({}) stubbed_session.activate_stubs() client = TypedAWSClient(stubbed_session) client.update_lambda_event_source( event_uuid='my-uuid', batch_size=5, maximum_batching_window_in_seconds=60 ) stubbed_session.verify_stubs() def test_can_create_log_group(stubbed_session): logs_stub = stubbed_session.stub('logs') logs_stub.create_log_group( logGroupName='mygroup' ).returns({}) stubbed_session.activate_stubs() client = TypedAWSClient(stubbed_session) client.create_log_group(log_group_name='mygroup') stubbed_session.verify_stubs() def test_can_delete_log_group(stubbed_session): logs_stub = stubbed_session.stub('logs') logs_stub.delete_log_group( logGroupName='mygroup' ).returns({}) stubbed_session.activate_stubs() client = TypedAWSClient(stubbed_session) client.delete_log_group(log_group_name='mygroup') stubbed_session.verify_stubs() def test_can_delete_retention_policy(stubbed_session): logs_stub = stubbed_session.stub('logs') logs_stub.delete_retention_policy( logGroupName='mygroup' ).returns({}) stubbed_session.activate_stubs() client = TypedAWSClient(stubbed_session) client.delete_retention_policy(log_group_name='mygroup') stubbed_session.verify_stubs() ================================================ FILE: tests/functional/test_deployer.py ================================================ import os import zipfile import json from unittest import mock import hashlib from pytest import fixture import pytest import chalice.deploy.deployer import chalice.deploy.packager from chalice.awsclient import TypedAWSClient import chalice.utils from chalice.config import Config from chalice import Chalice from chalice.deploy.packager import MissingDependencyError from chalice.deploy.packager import EmptyPackageError from chalice.deploy.packager import LambdaDeploymentPackager from chalice.deploy.packager import DependencyBuilder from chalice.deploy.packager import Package slow = pytest.mark.slow @fixture def chalice_deployer(): ui = chalice.utils.UI() osutils = chalice.utils.OSUtils() dependency_builder = mock.Mock(spec=DependencyBuilder) d = chalice.deploy.packager.LambdaDeploymentPackager( osutils=osutils, dependency_builder=dependency_builder, ui=ui ) return d @fixture def app_only_packager(): ui = chalice.utils.UI() osutils = chalice.utils.OSUtils() dependency_builder = mock.Mock(spec=DependencyBuilder) d = chalice.deploy.packager.AppOnlyDeploymentPackager( osutils=osutils, dependency_builder=dependency_builder, ui=ui ) return d, dependency_builder @fixture def layer_packager(): ui = chalice.utils.UI() osutils = chalice.utils.OSUtils() dependency_builder = mock.Mock(spec=DependencyBuilder) d = chalice.deploy.packager.LayerDeploymentPackager( osutils=osutils, dependency_builder=dependency_builder, ui=ui ) return d, dependency_builder def _create_app_structure(tmpdir): appdir = tmpdir.mkdir('app') appdir.join('app.py').write('# Test app') appdir.mkdir('.chalice') return appdir @slow def test_can_create_deployment_package(tmpdir, chalice_deployer): appdir = _create_app_structure(tmpdir) appdir.join('app.py').write('# Test app') chalice_dir = appdir.join('.chalice') chalice_deployer.create_deployment_package( str(appdir), 'python3.11') # There should now be a zip file created. contents = chalice_dir.join('deployments').listdir() assert len(contents) == 1 assert str(contents[0]).endswith('.zip') @slow def test_can_inject_latest_app(tmpdir, chalice_deployer): appdir = _create_app_structure(tmpdir) appdir.join('app.py').write('# Test app v1') chalice_dir = appdir.join('.chalice') name = chalice_deployer.create_deployment_package( str(appdir), 'python3.11') # Now suppose we update our app code but not any deps. # We can use inject_latest_app. appdir.join('app.py').write('# Test app NEW VERSION') # There should now be a zip file created. chalice_deployer.inject_latest_app(name, str(appdir)) contents = chalice_dir.join('deployments').listdir() assert len(contents) == 1 assert str(contents[0]) == name with zipfile.ZipFile(name) as f: contents = f.read('app.py') assert contents == b'# Test app NEW VERSION' @slow def test_zipfile_hash_only_based_on_contents(tmpdir, chalice_deployer): appdir = _create_app_structure(tmpdir) appdir.join('app.py').write('# Test app v1') name = chalice_deployer.create_deployment_package( str(appdir), 'python3.11') with open(name, 'rb') as f: original_checksum = hashlib.md5(f.read()).hexdigest() # Now we'll modify the file our app file with the same contents # but it will change the mtime. app_file = appdir.join('app.py') app_file.write('# Test app v1') # Set the mtime to something different (1990-1-1T00:00:00). # This would normally result in the zipfile having a different # checksum. os.utime(str(app_file), (631152000.0, 631152000.0)) name = chalice_deployer.create_deployment_package( str(appdir), 'python3.11') with open(name, 'rb') as f: new_checksum = hashlib.md5(f.read()).hexdigest() assert new_checksum == original_checksum @slow def test_app_injection_still_compresses_file(tmpdir, chalice_deployer): appdir = _create_app_structure(tmpdir) appdir.join('app.py').write('# Test app v1') name = chalice_deployer.create_deployment_package( str(appdir), 'python3.11') original_size = os.path.getsize(name) appdir.join('app.py').write('# Test app v2') chalice_deployer.inject_latest_app(name, str(appdir)) new_size = os.path.getsize(name) # The new_size won't be exactly the same as the original, # we just want to make sure it wasn't converted to # ZIP_STORED, so there's a 5% tolerance. assert new_size < (original_size * 1.05) @slow def test_no_error_message_printed_on_empty_reqs_file(tmpdir, chalice_deployer, capfd): appdir = _create_app_structure(tmpdir) appdir.join('app.py').write('# Foo') appdir.join('requirements.txt').write('\n') chalice_deployer.create_deployment_package( str(appdir), 'python3.11') out, err = capfd.readouterr() assert err.strip() == '' def test_osutils_proxies_os_functions(tmpdir): appdir = _create_app_structure(tmpdir) appdir.join('app.py').write(b'hello') osutils = chalice.utils.OSUtils() app_file = str(appdir.join('app.py')) assert osutils.file_exists(app_file) assert osutils.get_file_contents(app_file) == b'hello' assert osutils.open(app_file, 'rb').read() == b'hello' osutils.remove_file(app_file) # Removing again doesn't raise an error. osutils.remove_file(app_file) assert not osutils.file_exists(app_file) @slow def test_includes_app_and_chalicelib_dir(tmpdir, chalice_deployer): appdir = _create_app_structure(tmpdir) # We're now also going to create additional files chalicelib = appdir.mkdir('chalicelib') appdir.join('chalicelib', '__init__.py').write('# Test package') appdir.join('chalicelib', 'mymodule.py').write('# Test module') appdir.join('chalicelib', 'config.json').write('{"test": "config"}') # Should also include sub directories subdir = chalicelib.mkdir('subdir') subdir.join('submodule.py').write('# Test submodule') subdir.join('subconfig.json').write('{"test": "subconfig"}') name = chalice_deployer.create_deployment_package( str(appdir), 'python3.11') with zipfile.ZipFile(name) as f: _assert_in_zip('chalicelib/__init__.py', b'# Test package', f) _assert_in_zip('chalicelib/mymodule.py', b'# Test module', f) _assert_in_zip('chalicelib/config.json', b'{"test": "config"}', f) _assert_in_zip('chalicelib/subdir/submodule.py', b'# Test submodule', f) _assert_in_zip('chalicelib/subdir/subconfig.json', b'{"test": "subconfig"}', f) def _assert_in_zip(path, contents, zip): allfiles = zip.namelist() assert path in allfiles assert zip.read(path) == contents def _assert_not_in_zip(path, zip): allfiles = zip.namelist() assert path not in allfiles @slow def test_subsequent_deploy_replaces_chalicelib(tmpdir, chalice_deployer): appdir = _create_app_structure(tmpdir) chalicelib = appdir.mkdir('chalicelib') appdir.join('chalicelib', '__init__.py').write('# Test package') subdir = chalicelib.mkdir('subdir') subdir.join('submodule.py').write('# Test submodule') name = chalice_deployer.create_deployment_package( str(appdir), 'python3.11') subdir.join('submodule.py').write('# Test submodule v2') appdir.join('chalicelib', '__init__.py').remove() chalice_deployer.inject_latest_app(name, str(appdir)) with zipfile.ZipFile(name) as f: _assert_in_zip('chalicelib/subdir/submodule.py', b'# Test submodule v2', f) # And chalicelib/__init__.py should no longer be # in the zipfile because we deleted it in the appdir. assert 'chalicelib/__init__.py' not in f.namelist() @slow def test_vendor_dir_included(tmpdir, chalice_deployer): appdir = _create_app_structure(tmpdir) vendor = appdir.mkdir('vendor') extra_package = vendor.mkdir('mypackage') extra_package.join('__init__.py').write('# Test package') name = chalice_deployer.create_deployment_package( str(appdir), 'python3.11') with zipfile.ZipFile(name) as f: _assert_in_zip('mypackage/__init__.py', b'# Test package', f) @slow def test_no_vendor_in_app_only_packager(tmpdir, app_only_packager): packager, deps_builder = app_only_packager appdir = _create_app_structure(tmpdir) appdir.mkdir('chalicelib') appdir.join('requirements.txt').write('boto3') appdir.join('chalicelib', '__init__.py').write('# Test package') vendor = appdir.mkdir('vendor') extra_package = vendor.mkdir('mypackage') extra_package.join('__init__.py').write('# Test package') name = packager.create_deployment_package( str(appdir), 'python3.11') with zipfile.ZipFile(name) as f: _assert_not_in_zip('mypackage/__init__.py', f) _assert_in_zip('chalicelib/__init__.py', b'# Test package', f) _assert_in_zip('app.py', b'# Test app', f) assert not deps_builder.build_site_packages.called @slow def test_py_deps_in_layer_package(tmpdir, layer_packager): packager, deps_builder = layer_packager appdir = _create_app_structure(tmpdir) appdir.mkdir('chalicelib') appdir.join('requirements.txt').write('boto3') appdir.join('chalicelib', '__init__.py').write('# Test package') vendor = appdir.mkdir('vendor') extra_package = vendor.mkdir('mypackage') extra_package.join('__init__.py').write('# Test package') name = packager.create_deployment_package( str(appdir), 'python3.11') assert os.path.basename(name).startswith('managed-layer-') with zipfile.ZipFile(name) as f: prefix = 'python/lib/python3.11/site-packages' _assert_in_zip( '%s/mypackage/__init__.py' % prefix, b'# Test package', f) _assert_not_in_zip('%s/chalicelib/__init__.py' % prefix, f) _assert_not_in_zip('%s/app.py' % prefix, f) deps_builder.build_site_packages.assert_called_with( 'cp311', str(appdir.join('requirements.txt')), mock.ANY ) def test_empty_layer_package_raises_error(tmpdir, layer_packager): packager, deps_builder = layer_packager appdir = _create_app_structure(tmpdir) appdir.mkdir('chalicelib') appdir.join('requirements.txt').write('') appdir.join('chalicelib', '__init__.py').write('# Test package') filename = packager.deployment_package_filename(str(appdir), 'python3.11') with pytest.raises(EmptyPackageError): packager.create_deployment_package( str(appdir), 'python3.11') # We should also verify that the file does not exist so it doesn't # get reused in subsequent caches. This shouldn't affect anything, # we're just trying to cleanup properly. assert not os.path.isfile(filename) @slow def test_subsequent_deploy_replaces_vendor_dir(tmpdir, chalice_deployer): appdir = _create_app_structure(tmpdir) vendor = appdir.mkdir('vendor') extra_package = vendor.mkdir('mypackage') extra_package.join('__init__.py').write('# v1') name = chalice_deployer.create_deployment_package( str(appdir), 'python3.11') # Now we update a package in vendor/ with a new version. extra_package.join('__init__.py').write('# v2') name = chalice_deployer.create_deployment_package( str(appdir), 'python3.11') with zipfile.ZipFile(name) as f: _assert_in_zip('mypackage/__init__.py', b'# v2', f) @slow def test_vendor_symlink_included(tmpdir, chalice_deployer): appdir = _create_app_structure(tmpdir) extra_package = tmpdir.mkdir('mypackage') extra_package.join('__init__.py').write('# Test package') vendor = appdir.mkdir('vendor') os.symlink(str(extra_package), str(vendor.join('otherpackage'))) name = chalice_deployer.create_deployment_package( str(appdir), 'python3.11') with zipfile.ZipFile(name) as f: _assert_in_zip('otherpackage/__init__.py', b'# Test package', f) @slow def test_subsequent_deploy_replaces_vendor_symlink(tmpdir, chalice_deployer): appdir = _create_app_structure(tmpdir) extra_package = tmpdir.mkdir('mypackage') extra_package.join('__init__.py').write('# v1') vendor = appdir.mkdir('vendor') os.symlink(str(extra_package), str(vendor.join('otherpackage'))) name = chalice_deployer.create_deployment_package( str(appdir), 'python3.11') with zipfile.ZipFile(name) as f: _assert_in_zip('otherpackage/__init__.py', b'# v1', f) # Now we update a package in vendor/ with a new version. extra_package.join('__init__.py').write('# v2') name = chalice_deployer.create_deployment_package( str(appdir), 'python3.11') with zipfile.ZipFile(name) as f: _assert_in_zip('otherpackage/__init__.py', b'# v2', f) def test_zip_filename_changes_on_vendor_update(tmpdir, chalice_deployer): appdir = _create_app_structure(tmpdir) vendor = appdir.mkdir('vendor') extra_package = vendor.mkdir('mypackage') extra_package.join('__init__.py').write('# v1') first = chalice_deployer.deployment_package_filename( str(appdir), 'python3.6') extra_package.join('__init__.py').write('# v2') second = chalice_deployer.deployment_package_filename( str(appdir), 'python3.6') assert first != second def test_zip_filename_changes_on_vendor_symlink(tmpdir, chalice_deployer): appdir = _create_app_structure(tmpdir) vendor = appdir.mkdir('vendor') extra_package = tmpdir.mkdir('mypackage') extra_package.join('__init__.py').write('# v1') os.symlink(str(extra_package), str(vendor.join('otherpackage'))) first = chalice_deployer.deployment_package_filename( str(appdir), 'python3.6') extra_package.join('__init__.py').write('# v2') second = chalice_deployer.deployment_package_filename( str(appdir), 'python3.6') assert first != second @slow def test_chalice_runtime_injected_on_change(tmpdir, chalice_deployer): appdir = _create_app_structure(tmpdir) name = chalice_deployer.create_deployment_package( str(appdir), 'python3.11') # We're verifying that we always inject the chalice runtime # but we can't actually modify the runtime in this repo, so # instead we'll modify the deployment package and change the # runtime. # We'll then verify when we inject the latest app the runtime # has been re-added. This should give us enough confidence # that the runtime is always being inserted. _remove_runtime_from_deployment_package(name) with zipfile.ZipFile(name) as z: assert 'chalice/app.py' not in z.namelist() chalice_deployer.inject_latest_app(name, str(appdir)) with zipfile.ZipFile(name) as z: assert 'chalice/app.py' in z.namelist() def test_does_handle_missing_dependency_error(tmpdir): appdir = _create_app_structure(tmpdir) builder = mock.Mock(spec=DependencyBuilder) fake_package = mock.Mock(spec=Package) fake_package.identifier = 'foo==1.2' builder.build_site_packages.side_effect = MissingDependencyError( set([fake_package])) ui = mock.Mock(spec=chalice.utils.UI) osutils = chalice.utils.OSUtils() packager = LambdaDeploymentPackager( osutils=osutils, dependency_builder=builder, ui=ui, ) packager.create_deployment_package(str(appdir), 'python3.11') output = ''.join([call[0][0] for call in ui.write.call_args_list]) assert 'Could not install dependencies:\nfoo==1.2' in output def _remove_runtime_from_deployment_package(filename): new_filename = os.path.join(os.path.dirname(filename), 'new.zip') with zipfile.ZipFile(filename, 'r') as original: with zipfile.ZipFile(new_filename, 'w', compression=zipfile.ZIP_DEFLATED) as z: for item in original.infolist(): if item.filename.startswith('chalice/'): continue contents = original.read(item.filename) z.writestr(item, contents) os.remove(filename) os.rename(new_filename, filename) def test_can_delete_app(tmpdir): # This is just a sanity check that deletions are working # as expected now that there's no separate interface # for deletion at the deployer layer. appdir = _create_app_structure(tmpdir) appdir.join('app.py').write( 'from chalice import Chalice\n' 'app = Chalice("testapp")' ) deployed_json = { 'schema_version': '2.0', 'backend': 'api', 'resources': [ {'name': 'role-index', 'resource_type': 'iam_role', 'role_name': 'testapp-dev-index', 'role_arn': 'arn:aws:iam::1:role/testapp-dev-index'}, {'lambda_arn': 'arn:aws:lambda:r:1:f:testapp-dev-index', 'name': 'index', 'resource_type': 'lambda_function'}, {'name': 'role-james', 'resource_type': 'iam_role', 'role_name': 'testapp-dev-foo', 'role_arn': 'arn:aws:iam::1:role/testapp-dev-foo'}, {'lambda_arn': 'arn:aws:lambda:r:1:f:testapp-dev-foo', 'name': 'james', 'resource_type': 'lambda_function'} ] } deployed_dir = appdir.join('.chalice', 'deployed') deployed_dir.mkdir() deployed_dir.join('dev.json').write( json.dumps(deployed_json)) mock_client = mock.Mock(spec=TypedAWSClient) ui = mock.Mock(spec=chalice.utils.UI) d = chalice.deploy.deployer.create_deletion_deployer(mock_client, ui) config = Config( chalice_stage='dev', user_provided_params={ 'chalice_app': Chalice('testapp'), 'project_dir': str(appdir), }, config_from_disk={}, default_params={} ) returned_values = d.deploy(config, 'dev') assert returned_values == { 'schema_version': '2.0', 'backend': 'api', 'resources': [], } call = mock.call expected_calls = [ call.delete_function( function_name=u'arn:aws:lambda:r:1:f:testapp-dev-foo'), call.delete_role(name=u'testapp-dev-foo'), call.delete_function( function_name=u'arn:aws:lambda:r:1:f:testapp-dev-index'), call.delete_role(name=u'testapp-dev-index') ] assert expected_calls == mock_client.method_calls ================================================ FILE: tests/functional/test_local.py ================================================ import os import socket import time import contextlib from threading import Thread from threading import Event from threading import Lock import json import subprocess from contextlib import contextmanager import pytest import requests from urllib3.util.retry import Retry from requests.adapters import HTTPAdapter from chalice import app from chalice.local import create_local_server from chalice.config import Config from chalice.utils import OSUtils APPS_DIR = os.path.dirname(os.path.abspath(__file__)) ENV_APP_DIR = os.path.join(APPS_DIR, 'envapp') BASIC_APP = os.path.join(APPS_DIR, 'basicapp') NEW_APP_VERSION = """ from chalice import Chalice app = Chalice(app_name='basicapp') @app.route('/') def index(): return {'version': 'reloaded'} """ @contextmanager def cd(path): try: original_dir = os.getcwd() os.chdir(path) yield finally: os.chdir(original_dir) @pytest.fixture() def basic_app(tmpdir): tmpdir = str(tmpdir.mkdir('basicapp')) OSUtils().copytree(BASIC_APP, tmpdir) return tmpdir class ThreadedLocalServer(Thread): def __init__(self, port, host='localhost'): super(ThreadedLocalServer, self).__init__() self._app_object = None self._config = None self._host = host self._port = port self._server = None self._server_ready = Event() def wait_for_server_ready(self): self._server_ready.wait() def configure(self, app_object, config): self._app_object = app_object self._config = config def run(self): self._server = create_local_server( self._app_object, self._config, self._host, self._port) self._server_ready.set() self._server.serve_forever() def make_call(self, method, path, port, timeout=0.5): self._server_ready.wait() return method('http://{host}:{port}{path}'.format( path=path, host=self._host, port=port), timeout=timeout) def shutdown(self): if self._server is not None: self._server.server.shutdown() @pytest.fixture def config(): return Config() @pytest.fixture() def unused_tcp_port(): with contextlib.closing(socket.socket()) as sock: sock.bind(('127.0.0.1', 0)) return sock.getsockname()[1] @pytest.fixture() def http_session(): session = requests.Session() retry = Retry( # How many connection-related errors to retry on. connect=10, # A backoff factor to apply between attempts after the second try. backoff_factor=2, allowed_methods=['GET', 'POST', 'PUT'], ) session.mount('http://', HTTPAdapter(max_retries=retry)) return HTTPFetcher(session) class HTTPFetcher(object): def __init__(self, session): self.session = session def json_get(self, url): response = self.session.get(url) response.raise_for_status() return json.loads(response.content) @pytest.fixture() def local_server_factory(unused_tcp_port): threaded_server = ThreadedLocalServer(unused_tcp_port) def create_server(app_object, config): threaded_server.configure(app_object, config) threaded_server.start() return threaded_server, unused_tcp_port try: yield create_server finally: threaded_server.shutdown() @pytest.fixture def sample_app(): demo = app.Chalice('demo-app') thread_safety_check = [] lock = Lock() @demo.route('/', methods=['GET']) def index(): return {'hello': 'world'} @demo.route('/test-cors', methods=['POST'], cors=True) def test_cors(): return {'hello': 'world'} @demo.route('/count', methods=['POST']) def record_counter(): # An extra delay helps ensure we consistently fail if we're # not thread safe. time.sleep(0.001) count = int(demo.current_request.json_body['counter']) with lock: thread_safety_check.append(count) @demo.route('/count', methods=['GET']) def get_record_counter(): return thread_safety_check[:] return demo def test_has_thread_safe_current_request(config, sample_app, local_server_factory): local_server, port = local_server_factory(sample_app, config) local_server.wait_for_server_ready() num_requests = 25 num_threads = 5 # The idea here is that each requests.post() has a unique 'counter' # integer. If the current request is thread safe we should see a number # for each 0 - (num_requests * num_threads). If it's not thread safe # we'll see missing numbers and/or duplicates. def make_requests(counter_start): for i in range(counter_start * num_requests, (counter_start + 1) * num_requests): # We're slowing the sending rate down a bit. The threaded # http server is good, but not great. You can still overwhelm # it pretty easily. time.sleep(0.001) requests.post( 'http://localhost:%s/count' % port, json={'counter': i}) threads = [] for i in range(num_threads): threads.append(Thread(target=make_requests, args=(i,))) for thread in threads: thread.start() for thread in threads: thread.join() response = requests.get('http://localhost:%s/count' % port) assert len(response.json()) == len(range(num_requests * num_threads)) assert sorted(response.json()) == list(range(num_requests * num_threads)) def test_can_accept_get_request(config, sample_app, local_server_factory): local_server, port = local_server_factory(sample_app, config) response = local_server.make_call(requests.get, '/', port) assert response.status_code == 200 assert response.text == '{"hello":"world"}' def test_can_get_unicode_string_content_length( config, local_server_factory): demo = app.Chalice('app-name') @demo.route('/') def index_view(): return u'\u2713' local_server, port = local_server_factory(demo, config) response = local_server.make_call(requests.get, '/', port) assert response.headers['Content-Length'] == '3' def test_can_accept_options_request(config, sample_app, local_server_factory): local_server, port = local_server_factory(sample_app, config) response = local_server.make_call(requests.options, '/test-cors', port) assert response.headers['Content-Length'] == '0' assert response.headers['Access-Control-Allow-Methods'] == 'POST,OPTIONS' assert response.text == '' def test_can_accept_multiple_options_request(config, sample_app, local_server_factory): local_server, port = local_server_factory(sample_app, config) response = local_server.make_call(requests.options, '/test-cors', port) assert response.headers['Content-Length'] == '0' assert response.headers['Access-Control-Allow-Methods'] == 'POST,OPTIONS' assert response.text == '' response = local_server.make_call(requests.options, '/test-cors', port) assert response.headers['Content-Length'] == '0' assert response.headers['Access-Control-Allow-Methods'] == 'POST,OPTIONS' assert response.text == '' def test_can_accept_multiple_connections(config, sample_app, local_server_factory): # When a GET request is made to Chalice from a browser, it will send the # connection keep-alive header in order to hold the connection open and # reuse it for subsequent requests. If the conncetion close header is sent # back by the server the connection will be closed, but the browser will # reopen a new connection just in order to have it ready when needed. # In this case, since it does not send any content we do not have the # opportunity to send a connection close header back in a response to # force it to close the socket. # This is an issue in Chalice since the single threaded local server will # now be blocked waiting for IO from the browser socket. If a request from # any other source is made it will be blocked until the browser sends # another request through, giving us a chance to read from another socket. local_server, port = local_server_factory(sample_app, config) local_server.wait_for_server_ready() # We create a socket here to emulate a browser's open connection and then # make a request. The request should succeed. socket.create_connection(('localhost', port), timeout=1) try: response = local_server.make_call(requests.get, '/', port) except requests.exceptions.ReadTimeout: assert False, ( 'Read timeout occurred, the socket is blocking the next request ' 'from going though.' ) assert response.status_code == 200 assert response.text == '{"hello":"world"}' def test_can_import_env_vars(unused_tcp_port, http_session): with cd(ENV_APP_DIR): p = subprocess.Popen(['chalice', 'local', '--port', str(unused_tcp_port)], stdout=subprocess.PIPE, stderr=subprocess.PIPE) _wait_for_server_ready(p) try: _assert_env_var_loaded(unused_tcp_port, http_session) finally: p.terminate() def _wait_for_server_ready(process): if process.poll() is not None: raise AssertionError( 'Local server immediately exited with rc: %s' % process.poll() ) def _assert_env_var_loaded(port_number, http_session): response = http_session.json_get('http://localhost:%s/' % port_number) assert response == {'hello': 'bar'} def test_can_reload_server(unused_tcp_port, basic_app, http_session): with cd(basic_app): p = subprocess.Popen(['chalice', 'local', '--port', str(unused_tcp_port)], stdout=subprocess.PIPE, stderr=subprocess.PIPE) _wait_for_server_ready(p) url = 'http://localhost:%s/' % unused_tcp_port try: assert http_session.json_get(url) == {'version': 'original'} # Updating the app should trigger a reload. with open(os.path.join(basic_app, 'app.py'), 'w') as f: f.write(NEW_APP_VERSION) time.sleep(2) assert http_session.json_get(url) == {'version': 'reloaded'} finally: p.terminate() ================================================ FILE: tests/functional/test_package.py ================================================ import os import zipfile import tarfile import io from unittest import mock from collections import defaultdict, namedtuple import pytest from chalice.awsclient import TypedAWSClient from chalice.config import Config from chalice import Chalice from chalice import package from chalice.deploy.packager import PipRunner from chalice.deploy.packager import DependencyBuilder from chalice.deploy.packager import Package from chalice.deploy.packager import MissingDependencyError from chalice.deploy.packager import SubprocessPip from chalice.deploy.packager import SDistMetadataFetcher from chalice.deploy.packager import InvalidSourceDistributionNameError from chalice.deploy.packager import UnsupportedPackageError from chalice.compat import pip_no_compile_c_env_vars from chalice.compat import pip_no_compile_c_shim from chalice.package import PackageOptions from chalice.utils import OSUtils FakePipCall = namedtuple('FakePipEntry', ['args', 'env_vars', 'shim']) def _create_app_structure(tmpdir): appdir = tmpdir.mkdir('app') appdir.join('app.py').write('# Test app') appdir.mkdir('.chalice') return appdir def sample_app(): app = Chalice("sample_app") @app.route('/') def index(): return {"hello": "world"} return app @pytest.fixture def sdist_reader(): return SDistMetadataFetcher() @pytest.fixture def sdist_builder(): s = FakeSdistBuilder() return s class FakeSdistBuilder(object): _SETUP_PY = ( 'from setuptools import setup\n' 'setup(\n' ' name="%s",\n' ' version="%s"\n' ')\n' ) def write_fake_sdist(self, directory, name, version): filename = '%s-%s.zip' % (name, version) path = '%s/%s' % (directory, filename) with zipfile.ZipFile(path, 'w', compression=zipfile.ZIP_DEFLATED) as z: z.writestr('sdist/setup.py', self._SETUP_PY % (name, version)) return directory, filename class PathArgumentEndingWith(object): def __init__(self, filename): self._filename = filename def __eq__(self, other): if isinstance(other, str): filename = os.path.split(other)[-1] return self._filename == filename return False class FakePip(object): def __init__(self): self._calls = defaultdict(lambda: []) self._call_history = [] self._side_effects = defaultdict(lambda: []) self._return_tuple = (0, b'', b'') def main(self, args, env_vars=None, shim=None): cmd, args = args[0], args[1:] self._calls[cmd].append((args, env_vars, shim)) try: side_effects = self._side_effects[cmd].pop(0) for side_effect in side_effects: self._call_history.append(( FakePipCall(args, env_vars, shim), FakePipCall(side_effect.expected_args, side_effect.expected_env_vars, side_effect.expected_shim))) side_effect.execute(args) except IndexError: pass return self._return_tuple def set_return_tuple(self, rc, out, err): self._return_tuple = (rc, out, err) def packages_to_download(self, expected_args, packages, whl_contents=None): side_effects = [PipSideEffect(pkg, '--dest', expected_args, whl_contents) for pkg in packages] self._side_effects['download'].append(side_effects) def wheels_to_build(self, expected_args, wheels_to_build, expected_env_vars=None, expected_shim=None): # The SubprocessPip class handles injecting the # subprocess_python_base_environ into the env vars if needed, # so at this level of abstraction the env vars just default # to an empty dict if None is provided. if expected_env_vars is None: expected_env_vars = {} if expected_shim is None: expected_shim = '' side_effects = [PipSideEffect(pkg, '--wheel-dir', expected_args, expected_env_vars=expected_env_vars, expected_shim=expected_shim) for pkg in wheels_to_build] self._side_effects['wheel'].append(side_effects) @property def calls(self): return self._calls def validate(self): for calls in self._call_history: actual_call, expected_call = calls assert actual_call.args == expected_call.args assert actual_call.env_vars == expected_call.env_vars assert actual_call.shim == expected_call.shim class PipSideEffect(object): def __init__(self, filename, dirarg, expected_args, whl_contents=None, expected_env_vars=None, expected_shim=None): self._filename = filename self._package_name = filename.split('-')[0] self._dirarg = dirarg self.expected_args = expected_args self.expected_env_vars = expected_env_vars self.expected_shim = expected_shim if whl_contents is None: whl_contents = ['{package_name}/placeholder'] self._whl_contents = whl_contents def _build_fake_whl(self, directory, filename): filepath = os.path.join(directory, filename) if not os.path.isfile(filepath): package = Package(directory, filename) with zipfile.ZipFile(filepath, 'w') as z: for content_path in self._whl_contents: z.writestr(content_path.format( package_name=self._package_name, data_dir=package.data_dir ), b'') def _build_fake_sdist(self, filepath): # tar.gz is the same no reason to test it here as it is tested in # unit.deploy.TestSdistMetadataFetcher assert filepath.endswith('.zip') components = os.path.split(filepath) prefix, filename = components[:-1], components[-1] directory = os.path.join(*prefix) filename_without_ext = filename[:-4] pkg_name, pkg_version = filename_without_ext.split('-') builder = FakeSdistBuilder() builder.write_fake_sdist(directory, pkg_name, pkg_version) def execute(self, args): """Generate the file in the target_dir.""" if self._dirarg: target_dir = None for i, arg in enumerate(args): if arg == self._dirarg: target_dir = args[i+1] if target_dir: filepath = os.path.join(target_dir, self._filename) if filepath.endswith('.whl'): self._build_fake_whl(target_dir, self._filename) else: self._build_fake_sdist(filepath) @pytest.fixture def osutils(): return OSUtils() @pytest.fixture def empty_env_osutils(): class EmptyEnv(object): def environ(self): return {} return EmptyEnv() @pytest.fixture def pip_runner(empty_env_osutils): pip = FakePip() pip_runner = PipRunner(pip, osutils=empty_env_osutils) return pip, pip_runner class TestDependencyBuilder(object): def _write_requirements_txt(self, packages, directory): contents = '\n'.join(packages) filepath = os.path.join(directory, 'requirements.txt') with open(filepath, 'w') as f: f.write(contents) def _make_appdir_and_dependency_builder(self, reqs, tmpdir, runner): appdir = str(_create_app_structure(tmpdir)) self._write_requirements_txt(reqs, appdir) builder = DependencyBuilder(OSUtils(), runner) return appdir, builder def test_can_build_local_dir_as_whl(self, tmpdir, pip_runner): reqs = ['../foo'] pip, runner = pip_runner appdir, builder = self._make_appdir_and_dependency_builder( reqs, tmpdir, runner) requirements_file = os.path.join(appdir, 'requirements.txt') pip.set_return_tuple(0, (b"Processing ../foo\n" b" Link is a directory," b" ignoring download_dir"), b'') pip.wheels_to_build( expected_args=['--no-deps', '--wheel-dir', mock.ANY, '../foo'], wheels_to_build=[ 'foo-1.2-cp36-none-any.whl' ] ) site_packages = os.path.join(appdir, '.chalice.', 'site-packages') builder.build_site_packages('cp36m', requirements_file, site_packages) installed_packages = os.listdir(site_packages) pip.validate() assert ['foo'] == installed_packages def test_can_get_sdist_if_missing_initially(self, tmpdir, pip_runner): reqs = ['foo'] pip, runner = pip_runner appdir, builder = self._make_appdir_and_dependency_builder( reqs, tmpdir, runner) requirements_file = os.path.join(appdir, 'requirements.txt') # Initial download yields an incompatible wheel pip.packages_to_download( expected_args=['-r', requirements_file, '--dest', mock.ANY], packages=[ 'foo-1.2-cp36-cp36m-macosx_10_6_intel.whl' ] ) # Secondary download for a compatible only one fails pip.packages_to_download( expected_args=[ '--only-binary=:all:', '--no-deps', '--platform', 'manylinux2014_x86_64', '--implementation', 'cp', '--abi', 'cp36m', '--dest', mock.ANY, 'foo==1.2' ], packages=[] ) # Third download for an sdist succeeds pip.packages_to_download( expected_args=[ '--no-binary=:all:', '--no-deps', '--dest', mock.ANY, 'foo==1.2' ], packages=[ 'foo-1.2.zip', ] ) # Wheel successfully builds pip.wheels_to_build( expected_args=['--no-deps', '--wheel-dir', mock.ANY, PathArgumentEndingWith('foo-1.2.zip')], wheels_to_build=[ 'foo-1.2-cp36-none-any.whl' ] ) site_packages = os.path.join(appdir, '.chalice.', 'site-packages') builder.build_site_packages('cp36m', requirements_file, site_packages) installed_packages = os.listdir(site_packages) pip.validate() for req in reqs: assert req in installed_packages def test_can_get_whls_all_manylinux(self, tmpdir, pip_runner): reqs = ['foo', 'bar'] pip, runner = pip_runner appdir, builder = self._make_appdir_and_dependency_builder( reqs, tmpdir, runner) requirements_file = os.path.join(appdir, 'requirements.txt') pip.packages_to_download( expected_args=['-r', requirements_file, '--dest', mock.ANY], packages=[ 'foo-1.2-cp36-cp36m-manylinux1_x86_64.whl', 'bar-1.2-cp36-cp36m-manylinux1_x86_64.whl' ] ) site_packages = os.path.join(appdir, '.chalice.', 'site-packages') builder.build_site_packages('cp36m', requirements_file, site_packages) installed_packages = os.listdir(site_packages) pip.validate() for req in reqs: assert req in installed_packages def test_can_support_new_wheel_tags(self, tmpdir, pip_runner): reqs = ['numpy'] pip, runner = pip_runner appdir, builder = self._make_appdir_and_dependency_builder( reqs, tmpdir, runner) requirements_file = os.path.join(appdir, 'requirements.txt') # This is the actual filename from numpy v1.20.3. pip.packages_to_download( expected_args=['-r', requirements_file, '--dest', mock.ANY], packages=[ 'numpy-1.20.3-cp37-cp37m-manylinux_2_12_x86_64.whl', ] ) site_packages = os.path.join(appdir, '.chalice.', 'site-packages') builder.build_site_packages('cp37m', requirements_file, site_packages) installed_packages = os.listdir(site_packages) pip.validate() for req in reqs: assert req in installed_packages def test_can_support_compressed_tags(self, tmpdir, pip_runner): reqs = ['numpy'] pip, runner = pip_runner appdir, builder = self._make_appdir_and_dependency_builder( reqs, tmpdir, runner) requirements_file = os.path.join(appdir, 'requirements.txt') # This is the actual filename from numpy v1.20.3. pip.packages_to_download( expected_args=['-r', requirements_file, '--dest', mock.ANY], packages=[ 'numpy-1.20.3-cp37-cp37m-manylinux_2_12_x86_64' '.manylinux2010_x86_64.whl', ] ) site_packages = os.path.join(appdir, '.chalice.', 'site-packages') builder.build_site_packages('cp37m', requirements_file, site_packages) installed_packages = os.listdir(site_packages) pip.validate() for req in reqs: assert req in installed_packages def test_can_use_abi3_whl_for_any_python3(self, tmpdir, pip_runner): reqs = ['foo', 'bar', 'baz', 'qux'] pip, runner = pip_runner appdir, builder = self._make_appdir_and_dependency_builder( reqs, tmpdir, runner) requirements_file = os.path.join(appdir, 'requirements.txt') pip.packages_to_download( expected_args=['-r', requirements_file, '--dest', mock.ANY], packages=[ 'foo-1.2-cp33-abi3-manylinux1_x86_64.whl', 'bar-1.2-cp34-abi3-manylinux1_x86_64.whl', 'baz-1.2-cp35-abi3-manylinux1_x86_64.whl', 'qux-1.2-cp36-abi3-manylinux1_x86_64.whl', ] ) site_packages = os.path.join(appdir, '.chalice.', 'site-packages') builder.build_site_packages('cp36m', requirements_file, site_packages) installed_packages = os.listdir(site_packages) pip.validate() for req in reqs: assert req in installed_packages def test_can_expand_purelib_whl(self, tmpdir, pip_runner): reqs = ['foo'] pip, runner = pip_runner appdir, builder = self._make_appdir_and_dependency_builder( reqs, tmpdir, runner) requirements_file = os.path.join(appdir, 'requirements.txt') pip.packages_to_download( expected_args=['-r', requirements_file, '--dest', mock.ANY], packages=[ 'foo-1.2-cp36-cp36m-manylinux1_x86_64.whl' ], whl_contents=['foo-1.2.data/purelib/foo/'] ) site_packages = os.path.join(appdir, '.chalice.', 'site-packages') builder.build_site_packages('cp36m', requirements_file, site_packages) installed_packages = os.listdir(site_packages) pip.validate() for req in reqs: assert req in installed_packages def test_can_normalize_dirname_for_purelib_whl(self, tmpdir, pip_runner): reqs = ['foo'] pip, runner = pip_runner appdir, builder = self._make_appdir_and_dependency_builder( reqs, tmpdir, runner) requirements_file = os.path.join(appdir, 'requirements.txt') pip.packages_to_download( expected_args=['-r', requirements_file, '--dest', mock.ANY], packages=[ 'foo-1.2-cp36-cp36m-manylinux1_x86_64.whl' ], whl_contents=['Foo-1.2.data/purelib/foo/'] ) site_packages = os.path.join(appdir, '.chalice.', 'site-packages') builder.build_site_packages('cp36m', requirements_file, site_packages) installed_packages = os.listdir(site_packages) pip.validate() for req in reqs: assert req in installed_packages def test_can_expand_platlib_whl(self, tmpdir, pip_runner): reqs = ['foo'] pip, runner = pip_runner appdir, builder = self._make_appdir_and_dependency_builder( reqs, tmpdir, runner) requirements_file = os.path.join(appdir, 'requirements.txt') pip.packages_to_download( expected_args=['-r', requirements_file, '--dest', mock.ANY], packages=[ 'foo-1.2-cp36-cp36m-manylinux1_x86_64.whl' ], whl_contents=['Foo-1.2.data/platlib/foo/'] ) site_packages = os.path.join(appdir, '.chalice.', 'site-packages') builder.build_site_packages('cp36m', requirements_file, site_packages) installed_packages = os.listdir(site_packages) pip.validate() for req in reqs: assert req in installed_packages def test_can_expand_platlib_and_purelib(self, tmpdir, pip_runner): # This wheel installs two importable libraries foo and bar, one from # the wheels purelib and one from its platlib. reqs = ['foo', 'bar'] pip, runner = pip_runner appdir, builder = self._make_appdir_and_dependency_builder( reqs, tmpdir, runner) requirements_file = os.path.join(appdir, 'requirements.txt') pip.packages_to_download( expected_args=['-r', requirements_file, '--dest', mock.ANY], packages=[ 'foo-1.2-cp36-cp36m-manylinux1_x86_64.whl' ], whl_contents=[ 'foo-1.2.data/platlib/foo/', 'foo-1.2.data/purelib/bar/' ] ) site_packages = os.path.join(appdir, '.chalice.', 'site-packages') builder.build_site_packages('cp36m', requirements_file, site_packages) installed_packages = os.listdir(site_packages) pip.validate() for req in reqs: assert req in installed_packages def test_does_ignore_data(self, tmpdir, pip_runner): # Make sure the wheel installer does not copy the data directory # up to the root. reqs = ['foo'] pip, runner = pip_runner appdir, builder = self._make_appdir_and_dependency_builder( reqs, tmpdir, runner) requirements_file = os.path.join(appdir, 'requirements.txt') pip.packages_to_download( expected_args=['-r', requirements_file, '--dest', mock.ANY], packages=[ 'foo-1.2-cp36-cp36m-manylinux1_x86_64.whl' ], whl_contents=[ 'foo/placeholder', 'foo-1.2.data/data/bar/' ] ) site_packages = os.path.join(appdir, '.chalice.', 'site-packages') builder.build_site_packages('cp36m', requirements_file, site_packages) installed_packages = os.listdir(site_packages) pip.validate() for req in reqs: assert req in installed_packages assert 'bar' not in installed_packages def test_does_ignore_include(self, tmpdir, pip_runner): # Make sure the wheel installer does not copy the includes directory # up to the root. reqs = ['foo'] pip, runner = pip_runner appdir, builder = self._make_appdir_and_dependency_builder( reqs, tmpdir, runner) requirements_file = os.path.join(appdir, 'requirements.txt') pip.packages_to_download( expected_args=['-r', requirements_file, '--dest', mock.ANY], packages=[ 'foo-1.2-cp36-cp36m-manylinux1_x86_64.whl' ], whl_contents=[ 'foo/placeholder', 'foo.1.2.data/includes/bar/' ] ) site_packages = os.path.join(appdir, '.chalice.', 'site-packages') builder.build_site_packages('cp36m', requirements_file, site_packages) installed_packages = os.listdir(site_packages) pip.validate() for req in reqs: assert req in installed_packages assert 'bar' not in installed_packages def test_does_ignore_scripts(self, tmpdir, pip_runner): # Make sure the wheel isntaller does not copy the scripts directory # up to the root. reqs = ['foo'] pip, runner = pip_runner appdir, builder = self._make_appdir_and_dependency_builder( reqs, tmpdir, runner) requirements_file = os.path.join(appdir, 'requirements.txt') pip.packages_to_download( expected_args=['-r', requirements_file, '--dest', mock.ANY], packages=[ 'foo-1.2-cp36-cp36m-manylinux1_x86_64.whl' ], whl_contents=[ '{package_name}/placeholder', '{data_dir}/scripts/bar/placeholder' ] ) site_packages = os.path.join(appdir, '.chalice.', 'site-packages') builder.build_site_packages('cp36m', requirements_file, site_packages) installed_packages = os.listdir(site_packages) pip.validate() for req in reqs: assert req in installed_packages assert 'bar' not in installed_packages def test_can_expand_platlib_and_platlib_and_root(self, tmpdir, pip_runner): # This wheel installs three import names foo, bar and baz. # they are from the root install directory and the platlib and purelib # subdirectories in the platlib. reqs = ['foo', 'bar', 'baz'] pip, runner = pip_runner appdir, builder = self._make_appdir_and_dependency_builder( reqs, tmpdir, runner) requirements_file = os.path.join(appdir, 'requirements.txt') pip.packages_to_download( expected_args=['-r', requirements_file, '--dest', mock.ANY], packages=[ 'foo-1.2-cp36-cp36m-manylinux1_x86_64.whl' ], whl_contents=[ '{package_name}/placeholder', '{data_dir}/platlib/bar/placeholder', '{data_dir}/purelib/baz/placeholder' ] ) site_packages = os.path.join(appdir, '.chalice.', 'site-packages') builder.build_site_packages('cp36m', requirements_file, site_packages) installed_packages = os.listdir(site_packages) pip.validate() for req in reqs: assert req in installed_packages def test_can_get_whls_mixed_compat(self, tmpdir, osutils, pip_runner): reqs = ['foo', 'bar', 'baz'] pip, runner = pip_runner appdir, builder = self._make_appdir_and_dependency_builder( reqs, tmpdir, runner) requirements_file = os.path.join(appdir, 'requirements.txt') pip.packages_to_download( expected_args=['-r', requirements_file, '--dest', mock.ANY], packages=[ 'foo-1.0-cp36-none-any.whl', 'bar-1.2-cp36-cp36m-manylinux1_x86_64.whl', 'baz-1.5-cp36-cp36m-linux_x86_64.whl' ] ) site_packages = os.path.join(appdir, '.chalice.', 'site-packages') builder.build_site_packages('cp36m', requirements_file, site_packages) installed_packages = os.listdir(site_packages) pip.validate() for req in reqs: assert req in installed_packages def test_can_get_py27_whls(self, tmpdir, osutils, pip_runner): reqs = ['foo', 'bar', 'baz'] pip, runner = pip_runner appdir, builder = self._make_appdir_and_dependency_builder( reqs, tmpdir, runner) requirements_file = os.path.join(appdir, 'requirements.txt') pip.packages_to_download( expected_args=['-r', requirements_file, '--dest', mock.ANY], packages=[ 'foo-1.0-cp27-none-any.whl', 'bar-1.2-cp27-none-manylinux1_x86_64.whl', 'baz-1.5-cp27-cp27mu-linux_x86_64.whl' ] ) site_packages = os.path.join(appdir, '.chalice.', 'site-packages') builder.build_site_packages('cp27mu', requirements_file, site_packages) installed_packages = os.listdir(site_packages) pip.validate() for req in reqs: assert req in installed_packages def test_does_fail_on_invalid_local_package(self, tmpdir, osutils, pip_runner): reqs = ['../foo'] pip, runner = pip_runner appdir, builder = self._make_appdir_and_dependency_builder( reqs, tmpdir, runner) requirements_file = os.path.join(appdir, 'requirements.txt') pip.set_return_tuple(0, (b"Processing ../foo\n" b" Link is a directory," b" ignoring download_dir"), b'') pip.wheels_to_build( expected_args=['--no-deps', '--wheel-dir', mock.ANY, '../foo'], wheels_to_build=[ 'foo-1.2-cp36-cp36m-macosx_10_6_intel.whl' ] ) site_packages = os.path.join(appdir, '.chalice.', 'site-packages') with pytest.raises(MissingDependencyError) as e: builder.build_site_packages( 'cp36m', requirements_file, site_packages) installed_packages = os.listdir(site_packages) missing_packages = list(e.value.missing) pip.validate() assert len(missing_packages) == 1 assert missing_packages[0].identifier == 'foo==1.2' assert len(installed_packages) == 0 def test_does_fail_on_narrow_py27_unicode(self, tmpdir, osutils, pip_runner): reqs = ['baz'] pip, runner = pip_runner appdir, builder = self._make_appdir_and_dependency_builder( reqs, tmpdir, runner) requirements_file = os.path.join(appdir, 'requirements.txt') pip.packages_to_download( expected_args=['-r', requirements_file, '--dest', mock.ANY], packages=[ 'baz-1.5-cp27-cp27m-linux_x86_64.whl' ] ) site_packages = os.path.join(appdir, '.chalice.', 'site-packages') with pytest.raises(MissingDependencyError) as e: builder.build_site_packages( 'cp27mu', requirements_file, site_packages) installed_packages = os.listdir(site_packages) missing_packages = list(e.value.missing) pip.validate() assert len(missing_packages) == 1 assert missing_packages[0].identifier == 'baz==1.5' assert len(installed_packages) == 0 def test_does_fail_on_python_1_whl(self, tmpdir, osutils, pip_runner): reqs = ['baz'] pip, runner = pip_runner appdir, builder = self._make_appdir_and_dependency_builder( reqs, tmpdir, runner) requirements_file = os.path.join(appdir, 'requirements.txt') pip.packages_to_download( expected_args=['-r', requirements_file, '--dest', mock.ANY], packages=[ 'baz-1.5-cp14-cp14m-linux_x86_64.whl' ] ) site_packages = os.path.join(appdir, '.chalice.', 'site-packages') with pytest.raises(MissingDependencyError) as e: builder.build_site_packages( 'cp36m', requirements_file, site_packages) installed_packages = os.listdir(site_packages) missing_packages = list(e.value.missing) pip.validate() assert len(missing_packages) == 1 assert missing_packages[0].identifier == 'baz==1.5' assert len(installed_packages) == 0 def test_can_replace_incompat_whl(self, tmpdir, osutils, pip_runner): reqs = ['foo', 'bar'] pip, runner = pip_runner appdir, builder = self._make_appdir_and_dependency_builder( reqs, tmpdir, runner) requirements_file = os.path.join(appdir, 'requirements.txt') pip.packages_to_download( expected_args=['-r', requirements_file, '--dest', mock.ANY], packages=[ 'foo-1.0-cp36-none-any.whl', 'bar-1.2-cp36-cp36m-macosx_10_6_intel.whl' ] ) # Once the initial download has 1 incompatible whl file. The second, # more targeted download, finds manylinux1_x86_64 and downloads that. pip.packages_to_download( expected_args=[ '--only-binary=:all:', '--no-deps', '--platform', 'manylinux2014_x86_64', '--implementation', 'cp', '--abi', 'cp36m', '--dest', mock.ANY, 'bar==1.2' ], packages=[ 'bar-1.2-cp36-cp36m-manylinux1_x86_64.whl' ] ) site_packages = os.path.join(appdir, '.chalice.', 'site-packages') builder.build_site_packages('cp36m', requirements_file, site_packages) installed_packages = os.listdir(site_packages) pip.validate() for req in reqs: assert req in installed_packages @pytest.mark.parametrize( 'package,package_filename', [ # package: The name you would provide in requirements.txt # package_filename: The package name used in the .whl file. ('sqlalchemy', 'SQLAlchemy'), ('pyyaml', 'PyYAML'), ] ) def test_whitelist_sqlalchemy(self, tmpdir, osutils, pip_runner, package, package_filename): reqs = ['%s==1.1.18' % package] abi = 'cp36m' pip, runner = pip_runner appdir, builder = self._make_appdir_and_dependency_builder( reqs, tmpdir, runner) requirements_file = os.path.join(appdir, 'requirements.txt') pip.packages_to_download( expected_args=['-r', requirements_file, '--dest', mock.ANY], packages=[ '%s-1.1.18-cp36-cp36m-macosx_10_11_x86_64.whl' % package_filename ] ) pip.packages_to_download( expected_args=[ '--only-binary=:all:', '--no-deps', '--platform', 'manylinux2014_x86_64', '--implementation', 'cp', '--abi', abi, '--dest', mock.ANY, '%s==1.1.18' % package ], packages=[ '%s-1.1.18-cp36-cp36m-macosx_10_11_x86_64.whl' % package_filename ] ) site_packages = os.path.join(appdir, '.chalice.', 'site-packages') builder.build_site_packages(abi, requirements_file, site_packages) installed_packages = os.listdir(site_packages) pip.validate() assert installed_packages == [package_filename] def test_can_build_sdist(self, tmpdir, osutils, pip_runner): reqs = ['foo', 'bar'] pip, runner = pip_runner appdir, builder = self._make_appdir_and_dependency_builder( reqs, tmpdir, runner) requirements_file = os.path.join(appdir, 'requirements.txt') pip.packages_to_download( expected_args=['-r', requirements_file, '--dest', mock.ANY], packages=[ 'foo-1.2.zip', 'bar-1.2-cp36-cp36m-manylinux1_x86_64.whl' ] ) # Foo is built from and is pure python so it yields a compatible # wheel file. pip.wheels_to_build( expected_args=['--no-deps', '--wheel-dir', mock.ANY, PathArgumentEndingWith('foo-1.2.zip')], wheels_to_build=[ 'foo-1.2-cp36-none-any.whl' ] ) site_packages = os.path.join(appdir, '.chalice.', 'site-packages') builder.build_site_packages('cp36m', requirements_file, site_packages) installed_packages = os.listdir(site_packages) pip.validate() for req in reqs: assert req in installed_packages def test_build_sdist_makes_incompatible_whl(self, tmpdir, osutils, pip_runner): reqs = ['foo', 'bar'] pip, runner = pip_runner appdir, builder = self._make_appdir_and_dependency_builder( reqs, tmpdir, runner) requirements_file = os.path.join(appdir, 'requirements.txt') pip.packages_to_download( expected_args=['-r', requirements_file, '--dest', mock.ANY], packages=[ 'foo-1.2.zip', 'bar-1.2-cp36-cp36m-manylinux1_x86_64.whl' ] ) # foo is compiled since downloading it failed to get any wheels. And # the second download for manylinux1_x86_64 wheels failed as well. # building in this case yields a platform specific wheel file that is # not compatible. In this case currently there is nothing that chalice # can do to install this package. pip.wheels_to_build( expected_args=['--no-deps', '--wheel-dir', mock.ANY, PathArgumentEndingWith('foo-1.2.zip')], wheels_to_build=[ 'foo-1.2-cp36-cp36m-macosx_10_6_intel.whl' ] ) site_packages = os.path.join(appdir, '.chalice.', 'site-packages') with pytest.raises(MissingDependencyError) as e: builder.build_site_packages( 'cp36m', requirements_file, site_packages) installed_packages = os.listdir(site_packages) # bar should succeed and foo should failed. missing_packages = list(e.value.missing) pip.validate() assert len(missing_packages) == 1 assert missing_packages[0].identifier == 'foo==1.2' assert installed_packages == ['bar'] def test_can_build_package_with_optional_c_speedups_and_no_wheel( self, tmpdir, osutils, pip_runner): reqs = ['foo'] pip, runner = pip_runner appdir, builder = self._make_appdir_and_dependency_builder( reqs, tmpdir, runner) requirements_file = os.path.join(appdir, 'requirements.txt') # In this scenario we are downloading a package that has no wheel files # at all, and optional c speedups. The initial download will yield an # sdist since there were no wheels. pip.packages_to_download( expected_args=['-r', requirements_file, '--dest', mock.ANY], packages=['foo-1.2.zip'] ) # Chalice should now try and build this into a wheel file. Since it has # optional c speedups it will build a platform dependent wheel file # which is not compatible with lambda. pip.wheels_to_build( expected_args=['--no-deps', '--wheel-dir', mock.ANY, PathArgumentEndingWith('foo-1.2.zip')], wheels_to_build=[ 'foo-1.2-cp36-cp36m-macosx_10_6_intel.whl' ] ) # Now chalice should make a last ditch effort to build the package by # trying once again to build the sdist, but this time it will prevent # c extensions from compiling by force. If the package had optional # c speedups (which in this scenario it did) then it will # successfully fall back to building a pure python wheel file. pip.wheels_to_build( expected_args=['--no-deps', '--wheel-dir', mock.ANY, PathArgumentEndingWith('foo-1.2.zip')], expected_env_vars=pip_no_compile_c_env_vars, expected_shim=pip_no_compile_c_shim, wheels_to_build=[ 'foo-1.2-cp36-none-any.whl' ] ) site_packages = os.path.join(appdir, '.chalice.', 'site-packages') builder.build_site_packages('cp36m', requirements_file, site_packages) installed_packages = os.listdir(site_packages) # Now we should have successfully built the foo package. pip.validate() assert installed_packages == ['foo'] def test_build_into_existing_dir_with_preinstalled_packages( self, tmpdir, osutils, pip_runner): # Same test as above so we should get foo failing and bar succeeding # but in this test we started with a .chalice/site-packages directory # with both foo and bar already installed. It should still fail since # they may be there by happenstance, or from an incompatible version # of python. reqs = ['foo', 'bar'] abi = 'cp36m' pip, runner = pip_runner appdir, builder = self._make_appdir_and_dependency_builder( reqs, tmpdir, runner) requirements_file = os.path.join(appdir, 'requirements.txt') pip.packages_to_download( expected_args=['-r', requirements_file, '--dest', mock.ANY], packages=[ 'foo-1.2.zip', 'bar-1.2-cp36-cp36m-manylinux1_x86_64.whl' ] ) pip.packages_to_download( expected_args=[ '--only-binary=:all:', '--no-deps', '--platform', 'manylinux2014_x86_64', '--implementation', 'cp', '--abi', abi, '--dest', mock.ANY, 'foo==1.2' ], packages=[ 'foo-1.2-cp36-cp36m-macosx_10_6_intel.whl' ] ) # Add two fake packages foo and bar that have previously been # installed in the site-packages directory. site_packages = os.path.join(appdir, '.chalice', 'site-packages') foo = os.path.join(site_packages, 'foo') os.makedirs(foo) bar = os.path.join(site_packages, 'bar') os.makedirs(bar) with pytest.raises(MissingDependencyError) as e: builder.build_site_packages( abi, requirements_file, site_packages) installed_packages = os.listdir(site_packages) # bar should succeed and foo should failed. missing_packages = list(e.value.missing) pip.validate() assert len(missing_packages) == 1 assert missing_packages[0].identifier == 'foo==1.2' assert installed_packages == ['bar'] def test_can_create_app_packager_with_no_autogen(tmpdir, stubbed_session): appdir = _create_app_structure(tmpdir) outdir = tmpdir.mkdir('outdir') default_params = {'autogen_policy': True} config = Config.create(project_dir=str(appdir), chalice_app=sample_app(), **default_params) options = PackageOptions(TypedAWSClient(session=stubbed_session)) p = package.create_app_packager(config, options) p.package_app(config, str(outdir), 'dev') # We're not concerned with the contents of the files # (those are tested in the unit tests), we just want to make # sure they're written to disk and look (mostly) right. contents = os.listdir(str(outdir)) assert 'deployment.zip' in contents assert 'sam.json' in contents def test_can_create_app_packager_with_yaml_extention(tmpdir, stubbed_session): appdir = _create_app_structure(tmpdir) outdir = tmpdir.mkdir('outdir') default_params = {'autogen_policy': True} extras_file = tmpdir.join('extras.yaml') extras_file.write("foo: bar") config = Config.create(project_dir=str(appdir), chalice_app=sample_app(), **default_params) options = PackageOptions(TypedAWSClient(session=stubbed_session)) p = package.create_app_packager(config, options, merge_template=str(extras_file)) p.package_app(config, str(outdir), 'dev') contents = os.listdir(str(outdir)) assert 'deployment.zip' in contents assert 'sam.yaml' in contents def test_can_specify_yaml_output(tmpdir, stubbed_session): appdir = _create_app_structure(tmpdir) outdir = tmpdir.mkdir('outdir') default_params = {'autogen_policy': True} config = Config.create(project_dir=str(appdir), chalice_app=sample_app(), **default_params) options = PackageOptions(TypedAWSClient(session=stubbed_session)) p = package.create_app_packager(config, options, template_format='yaml') p.package_app(config, str(outdir), 'dev') contents = os.listdir(str(outdir)) assert 'deployment.zip' in contents assert 'sam.yaml' in contents def test_will_create_outdir_if_needed(tmpdir, stubbed_session): appdir = _create_app_structure(tmpdir) outdir = str(appdir.join('outdir')) default_params = {'autogen_policy': True} config = Config.create(project_dir=str(appdir), chalice_app=sample_app(), **default_params) options = PackageOptions(TypedAWSClient(session=stubbed_session)) p = package.create_app_packager(config, options) p.package_app(config, str(outdir), 'dev') contents = os.listdir(str(outdir)) assert 'deployment.zip' in contents assert 'sam.json' in contents def test_includes_layer_package_with_sam(tmpdir, stubbed_session): appdir = _create_app_structure(tmpdir) appdir.mkdir('vendor').join('hello').write('hello\n') outdir = str(appdir.join('outdir')) default_params = {'autogen_policy': True} config = Config.create(project_dir=str(appdir), chalice_app=sample_app(), automatic_layer=True, **default_params) options = PackageOptions(TypedAWSClient(session=stubbed_session)) p = package.create_app_packager(config, options) p.package_app(config, str(outdir), 'dev') contents = os.listdir(str(outdir)) assert 'deployment.zip' in contents assert 'layer-deployment.zip' in contents assert 'sam.json' in contents def test_includes_layer_package_with_terraform(tmpdir, stubbed_session): appdir = _create_app_structure(tmpdir) appdir.mkdir('vendor').join('hello').write('hello\n') outdir = str(appdir.join('outdir')) default_params = {'autogen_policy': True} config = Config.create(project_dir=str(appdir), chalice_app=sample_app(), automatic_layer=True, **default_params) options = PackageOptions(TypedAWSClient(session=stubbed_session)) p = package.create_app_packager(config, options, package_format='terraform') p.package_app(config, str(outdir), 'dev') contents = os.listdir(str(outdir)) assert 'deployment.zip' in contents assert 'layer-deployment.zip' in contents assert 'chalice.tf.json' in contents class TestSubprocessPip(object): def test_can_invoke_pip(self): pip = SubprocessPip() rc, out, err = pip.main(['--version']) # Simple assertion that we can execute pip and it gives us some output # and nothing on stderr. print(out, err) assert rc == 0 assert err == b'' def test_does_error_code_propagate(self): pip = SubprocessPip() rc, _, err = pip.main(['badcommand']) assert rc != 0 # Don't want to depend on a particular error message from pip since it # may change if we pin a differnet version to Chalice at some point. # But there should be a non-empty error message of some kind. assert err != b'' class TestSdistMetadataFetcher(object): _SETUPTOOLS = 'from setuptools import setup' _DISTUTILS = 'from distutils.core import setup' _BOTH = ( 'try:\n' ' from setuptools import setup\n' 'except ImportError:\n' ' from distutils.core import setuptools\n' ) _SETUP_PY = ( '%s\n' 'setup(\n' ' name="%s",\n' ' version="%s"\n' ')\n' ) _VALID_TAR_FORMATS = ['tar.gz', 'tar.bz2'] def _write_fake_sdist(self, setup_py, directory, ext, pkg_info_contents=None): filename = 'sdist.%s' % ext path = '%s/%s' % (directory, filename) if ext == 'zip': with zipfile.ZipFile(path, 'w', compression=zipfile.ZIP_DEFLATED) as z: z.writestr('sdist/setup.py', setup_py) if pkg_info_contents is not None: z.writestr('sdist/PKG-INFO', pkg_info_contents) elif ext in self._VALID_TAR_FORMATS: compression_format = ext.split('.')[1] with tarfile.open(path, 'w:%s' % compression_format) as tar: tarinfo = tarfile.TarInfo('sdist/setup.py') tarinfo.size = len(setup_py) tar.addfile(tarinfo, io.BytesIO(setup_py.encode())) if pkg_info_contents is not None: tarinfo = tarfile.TarInfo('sdist/PKG-INFO') tarinfo.size = len(pkg_info_contents) tar.addfile(tarinfo, io.BytesIO(pkg_info_contents.encode())) else: open(path, 'a').close() filepath = os.path.join(directory, filename) return filepath def test_setup_tar_gz(self, osutils, sdist_reader): setup_py = self._SETUP_PY % ( self._SETUPTOOLS, 'foo', '1.0' ) with osutils.tempdir() as tempdir: filepath = self._write_fake_sdist(setup_py, tempdir, 'tar.gz') name, version = sdist_reader.get_package_name_and_version( filepath) assert name == 'foo' assert version == '1.0' def test_setup_tar_bz2(self, osutils, sdist_reader): setup_py = self._SETUP_PY % ( self._SETUPTOOLS, 'foo', '1.0' ) with osutils.tempdir() as tempdir: filepath = self._write_fake_sdist(setup_py, tempdir, 'tar.bz2') name, version = sdist_reader.get_package_name_and_version( filepath) assert name == 'foo' assert version == '1.0' def test_setup_zip(self, osutils, sdist_reader): setup_py = self._SETUP_PY % ( self._SETUPTOOLS, 'foo', '1.0' ) with osutils.tempdir() as tempdir: filepath = self._write_fake_sdist(setup_py, tempdir, 'zip') name, version = sdist_reader.get_package_name_and_version( filepath) assert name == 'foo' assert version == '1.0' def test_distutil_tar_gz(self, osutils, sdist_reader): setup_py = self._SETUP_PY % ( self._DISTUTILS, 'foo', '1.0' ) with osutils.tempdir() as tempdir: filepath = self._write_fake_sdist(setup_py, tempdir, 'tar.gz') name, version = sdist_reader.get_package_name_and_version( filepath) assert name == 'foo' assert version == '1.0' def test_distutil_tar_bz2(self, osutils, sdist_reader): setup_py = self._SETUP_PY % ( self._DISTUTILS, 'foo', '1.0' ) with osutils.tempdir() as tempdir: filepath = self._write_fake_sdist(setup_py, tempdir, 'tar.bz2') name, version = sdist_reader.get_package_name_and_version( filepath) assert name == 'foo' assert version == '1.0' def test_distutil_zip(self, osutils, sdist_reader): setup_py = self._SETUP_PY % ( self._DISTUTILS, 'foo', '1.0' ) with osutils.tempdir() as tempdir: filepath = self._write_fake_sdist(setup_py, tempdir, 'zip') name, version = sdist_reader.get_package_name_and_version( filepath) assert name == 'foo' assert version == '1.0' def test_both_zip(self, osutils, sdist_reader): setup_py = self._SETUP_PY % ( self._BOTH, 'foo', '1.0' ) with osutils.tempdir() as tempdir: filepath = self._write_fake_sdist(setup_py, tempdir, 'zip') name, version = sdist_reader.get_package_name_and_version( filepath) assert name == 'foo' assert version == '1.0' def test_bad_format(self, osutils, sdist_reader): setup_py = self._SETUP_PY % ( self._BOTH, 'foo', '1.0' ) with osutils.tempdir() as tempdir: filepath = self._write_fake_sdist(setup_py, tempdir, 'tar.gz2') with pytest.raises(InvalidSourceDistributionNameError): name, version = sdist_reader.get_package_name_and_version( filepath) def test_cant_get_egg_info_filename(self, osutils, sdist_reader): # In this scenario the setup.py file will fail with an import # error so we should verify we try a fallback to look for # PKG-INFO. bad_setup_py = self._SETUP_PY % ( 'import some_build_dependency', 'foo', '1.0', ) pkg_info_file = ( 'Name: foo\n' 'Version: 1.0\n' ) with osutils.tempdir() as tempdir: filepath = self._write_fake_sdist(bad_setup_py, tempdir, 'zip', pkg_info_file) name, version = sdist_reader.get_package_name_and_version( filepath) assert name == 'foo' assert version == '1.0' def test_pkg_info_fallback_fails_raises_error(self, osutils, sdist_reader): setup_py = self._SETUP_PY % ( 'import build_time_dependency', 'foo', '1.0' ) with osutils.tempdir() as tempdir: filepath = self._write_fake_sdist(setup_py, tempdir, 'tar.gz') with pytest.raises(UnsupportedPackageError): sdist_reader.get_package_name_and_version(filepath) class TestPackage(object): def test_same_pkg_sdist_and_wheel_collide(self, osutils, sdist_builder): with osutils.tempdir() as tempdir: sdist_builder.write_fake_sdist(tempdir, 'foobar', '1.0') pkgs = set() pkgs.add(Package('', 'foobar-1.0-py3-none-any.whl')) pkgs.add(Package(tempdir, 'foobar-1.0.zip')) assert len(pkgs) == 1 def test_ensure_sdist_name_normalized_for_comparison(self, osutils, sdist_builder): with osutils.tempdir() as tempdir: sdist_builder.write_fake_sdist(tempdir, 'Foobar', '1.0') pkgs = set() pkgs.add(Package('', 'foobar-1.0-py3-none-any.whl')) pkgs.add(Package(tempdir, 'Foobar-1.0.zip')) assert len(pkgs) == 1 def test_ensure_wheel_name_normalized_for_comparison(self, osutils, sdist_builder): with osutils.tempdir() as tempdir: sdist_builder.write_fake_sdist(tempdir, 'foobar', '1.0') pkgs = set() pkgs.add(Package('', 'Foobar-1.0-py3-none-any.whl')) pkgs.add(Package(tempdir, 'foobar-1.0.zip')) assert len(pkgs) == 1 ================================================ FILE: tests/functional/test_utils.py ================================================ import zipfile import json import os import io import tarfile import pytest from chalice import utils @pytest.fixture def osutils(): return utils.OSUtils() def test_can_zip_single_file(tmpdir): source = tmpdir.mkdir('sourcedir') source.join('hello.txt').write(b'hello world') outfile = str(tmpdir.join('out.zip')) utils.create_zip_file(source_dir=str(source), outfile=outfile) with zipfile.ZipFile(outfile) as f: contents = f.read('hello.txt') assert contents == b'hello world' assert f.namelist() == ['hello.txt'] def test_can_zip_recursive_contents(tmpdir): source = tmpdir.mkdir('sourcedir') source.join('hello.txt').write(b'hello world') subdir = source.mkdir('subdir') subdir.join('sub.txt').write(b'sub.txt') subdir.join('sub2.txt').write(b'sub2.txt') subsubdir = subdir.mkdir('subsubdir') subsubdir.join('leaf.txt').write(b'leaf.txt') outfile = str(tmpdir.join('out.zip')) utils.create_zip_file(source_dir=str(source), outfile=outfile) with zipfile.ZipFile(outfile) as f: assert sorted(f.namelist()) == sorted([ 'hello.txt', 'subdir/sub.txt', 'subdir/sub2.txt', 'subdir/subsubdir/leaf.txt', ]) assert f.read('subdir/subsubdir/leaf.txt') == b'leaf.txt' def test_can_write_recorded_values(tmpdir): filename = str(tmpdir.join('deployed.json')) utils.record_deployed_values({'dev': {'deployed': 'foo'}}, filename) with open(filename, 'r') as f: assert json.load(f) == {'dev': {'deployed': 'foo'}} def test_can_merge_recorded_values(tmpdir): filename = str(tmpdir.join('deployed.json')) first = {'dev': {'deployed': 'values'}} second = {'prod': {'deployed': 'values'}} utils.record_deployed_values(first, filename) utils.record_deployed_values(second, filename) combined = first.copy() combined.update(second) with open(filename, 'r') as f: data = json.load(f) assert data == combined def test_can_remove_stage_from_deployed_values(tmpdir): filename = str(tmpdir.join('deployed.json')) deployed = { 'dev': {'deployed': 'values'}, } left_after_removal = { 'prod': {'deployed': 'values'} } deployed.update(left_after_removal) with open(filename, 'wb') as f: f.write(json.dumps(deployed).encode('utf-8')) utils.remove_stage_from_deployed_values('dev', filename) with open(filename, 'r') as f: data = json.load(f) assert data == left_after_removal def test_remove_stage_from_deployed_values_already_removed(tmpdir): filename = str(tmpdir.join('deployed.json')) deployed = { 'dev': {'deployed': 'values'}, 'prod': {'deployed': 'values'} } with open(filename, 'wb') as f: f.write(json.dumps(deployed).encode('utf-8')) utils.remove_stage_from_deployed_values('fake_key', filename) with open(filename, 'r') as f: data = json.load(f) assert data == deployed def test_remove_stage_from_deployed_values_no_file(tmpdir): filename = str(tmpdir.join('deployed.json')) utils.remove_stage_from_deployed_values('fake_key', filename) # Make sure it doesn't create the file if it didn't already exist assert not os.path.isfile(filename) def test_error_raised_on_tar_out_of_extract_dir(tmp_path, osutils): filepath = tmp_path / 'badfile' filepath.write_text('single file') badtarpath = tmp_path / 'badtar.tar.gz' extractdir = tmp_path / 'nest1' / 'nest2' / 'nest3' with tarfile.open(badtarpath, 'w:gz') as tar: tar.add(filepath, arcname='../../escaped-dir.txt') with pytest.raises(RuntimeError): osutils.extract_tarfile(str(badtarpath), extractdir) def test_error_raise_tar_symlink_out_of_extract_dir(tmp_path, osutils): dir_with_symlink = tmp_path / 'nest1' / 'nest2' dir_with_symlink.mkdir(parents=True, exist_ok=True) outside_file = tmp_path / 'outside.txt' outside_file.write_text('outside of dir') symlink_file = dir_with_symlink / 'myfile.txt' os.symlink(outside_file, symlink_file) tarpath = dir_with_symlink / 'badtar.tar.gz' with tarfile.open(tarpath, 'w:gz') as tar: tar.add(symlink_file) with pytest.raises(RuntimeError): osutils.extract_tarfile(str(tarpath), dir_with_symlink) class TestOSUtils(object): def test_can_read_unicode(self, tmpdir, osutils): filename = str(tmpdir.join('file.txt')) checkmark = u'\2713' with io.open(filename, 'w', encoding='utf-16') as f: f.write(checkmark) content = osutils.get_file_contents(filename, binary=False, encoding='utf-16') assert content == checkmark ================================================ FILE: tests/integration/__init__.py ================================================ ================================================ FILE: tests/integration/conftest.py ================================================ from pytest import fixture @fixture(autouse=True) def ensure_no_local_config(no_local_config): pass ================================================ FILE: tests/integration/test_cli.py ================================================ import os import subprocess import pytest from chalice.utils import OSUtils CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) PROJECT_DIR = os.path.join( os.path.dirname(CURRENT_DIR), 'aws', 'testapp', ) @pytest.fixture def local_app(tmpdir): temp_dir_path = str(tmpdir) OSUtils().copytree(PROJECT_DIR, temp_dir_path) old_dir = os.getcwd() try: os.chdir(temp_dir_path) yield temp_dir_path finally: os.chdir(old_dir) def test_stack_trace_printed_on_error(local_app): app_file = os.path.join(local_app, 'app.py') with open(app_file, 'w') as f: f.write( 'from chalice import Chalice\n' 'app = Chalice(app_name="test")\n' 'foobarbaz\n' ) p = subprocess.Popen(['chalice', 'local', '--no-autoreload'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) stderr = p.communicate()[1].decode('ascii') rc = p.returncode assert rc == 2 assert 'Traceback' in stderr assert 'foobarbaz' in stderr ================================================ FILE: tests/integration/test_package.py ================================================ import os import sys import stat import uuid import fnmatch from zipfile import ZipFile import hashlib from contextlib import contextmanager from click.testing import CliRunner import pytest from chalice import cli from chalice.cli import factory from chalice.cli.newproj import create_new_project_skeleton from chalice.deploy.packager import NoSuchPackageError PY_VERSION = sys.version_info[:2] VERSION_CUTOFF = (3, 11) # We're being cautious here, but we want to fix the package versions we # try to install on older versions of python. # If the python version being tested is less than or equal to VERSION_CUTOFF, # then we'll install the `legacy_version` in the packages below. This is to # ensure we don't regress on being able to package older package versions on # older versions on python. Any python version above the VERSION_CUTOFF will # install the `version` identifier. That way newer versions of python won't # need to update this list as long as a package can still be installed on # versions greater than VERSION_CUTOFF. PACKAGES_TO_TEST = { 'pandas': { 'version': '2.2.3', 'legacy_version': '1.5.3', 'contents': [ 'pandas/*__init__.py', 'pandas/*cpython-*-x86_64-linux-gnu.so' ], }, 'SQLAlchemy': { 'version': '2.0.40', 'legacy_version': '1.4.47', 'contents': [ 'sqlalchemy/__init__.py', 'sqlalchemy/*cpython-*-x86_64-linux-gnu.so' ], }, 'numpy': { 'version': '2.2.5', 'legacy_version': '1.23.3', 'contents': [ 'numpy/__init__.py', 'numpy/*cpython-*-x86_64-linux-gnu.so' ], }, 'cryptography': { 'version': '44.0.3', 'legacy_version': '39.0.0', 'contents': [ 'cryptography/__init__.py', 'cryptography/*.so' ], }, 'Jinja2': { 'version': '3.1.6', 'legacy_version': '2.11.2', 'contents': ['jinja2/__init__.py'], }, 'Mako': { 'version': '1.3.10', 'legacy_version': '1.1.3', 'contents': ['mako/__init__.py'], }, 'MarkupSafe': { 'version': '3.0.2', 'legacy_version': '1.1.1', 'contents': ['markupsafe/__init__.py'], }, 'scipy': { 'version': '1.15.3', 'legacy_version': '1.10.1', 'contents': [ 'scipy/__init__.py', 'scipy/cluster/_hierarchy.cpython-*-x86_64-linux-gnu.so' ], }, 'cffi': { 'version': '1.17.1', 'legacy_version': '1.15.1', 'contents': ['_cffi_backend.cpython-*-x86_64-linux-gnu.so'], }, 'pygit2': { 'version': '1.17.0', 'legacy_version': '1.10.1', 'contents': ['pygit2/_pygit2.cpython-*-x86_64-linux-gnu.so'], }, 'pyrsistent': { 'version': '0.20.0', 'legacy_version': '0.17.3', 'contents': ['pyrsistent/__init__.py'], }, } @contextmanager def cd(path): original_dir = os.getcwd() try: os.chdir(path) yield finally: os.chdir(original_dir) @pytest.fixture def runner(): return CliRunner() @pytest.fixture def app_skeleton(tmpdir, runner): project_name = 'deployment-integ-test' with cd(str(tmpdir)): create_new_project_skeleton(project_name) return str(tmpdir.join(project_name)) def _get_random_package_name(): return 'foobar-%s' % str(uuid.uuid4())[:8] def _get_package_install_test_cases(): testcases = [] if PY_VERSION <= VERSION_CUTOFF: version_key = 'legacy_version' else: version_key = 'version' for package, config in PACKAGES_TO_TEST.items(): package_version = f'{package}=={config[version_key]}' testcases.append( (package_version, config['contents']) ) return testcases # This test can take a while, but you can set this env var to make sure that # the commonly used python packages can be packaged successfully. @pytest.mark.skipif(not os.environ.get('CHALICE_TEST_EXTENDED_PACKAGING'), reason='Set CHALICE_TEST_EXTENDED_PACKAGING for extended ' 'packaging tests.') @pytest.mark.parametrize('package,contents', _get_package_install_test_cases()) def test_package_install_smoke_tests(package, contents, runner, app_skeleton): assert_can_package_dependency(runner, app_skeleton, package, contents) def assert_can_package_dependency( runner, app_skeleton, package, contents): req = os.path.join(app_skeleton, 'requirements.txt') with open(req, 'w') as f: f.write('%s\n' % package) cli_factory = factory.CLIFactory(app_skeleton) package_output_location = os.path.join(app_skeleton, 'pkg') result = runner.invoke( cli.package, [package_output_location], obj={'project_dir': app_skeleton, 'debug': False, 'factory': cli_factory}) if result.exit_code != 0: raise AssertionError( f"Non-zero RC when packaging {package}") from result.exception assert result.exit_code == 0 assert result.output.strip() == 'Creating deployment package.' package_path = os.path.join(app_skeleton, 'pkg', 'deployment.zip') package_file = ZipFile(package_path) package_content = package_file.namelist() for content in contents: assert any(fnmatch.fnmatch(filename, content) for filename in package_content), ( "No match found for %s" % content) class TestPackage(object): def test_can_package_with_dashes_in_name(self, runner, app_skeleton, no_local_config): assert_can_package_dependency( runner, app_skeleton, 'googleapis-common-protos==1.5.2', contents=[ 'google/api/__init__.py', ], ) def test_can_package_simplejson(self, runner, app_skeleton, no_local_config): assert_can_package_dependency( runner, app_skeleton, 'simplejson==3.17.0', contents=[ 'simplejson/__init__.py', ], ) def test_can_package_sqlalchemy(self, runner, app_skeleton, no_local_config): # SQLAlchemy is used quite often with Chalice so we want to ensure # we can package it correctly. assert_can_package_dependency( runner, app_skeleton, 'SQLAlchemy==1.3.13', contents=[ 'sqlalchemy/__init__.py', ], ) def test_can_package_pandas(self, runner, app_skeleton, no_local_config): version = '2.2.3' if sys.version_info[1] >= 10 else '2.0.3' assert_can_package_dependency( runner, app_skeleton, 'pandas==' + version, contents=[ 'pandas/_libs/__init__.py', ], ) def test_does_not_package_bad_requirements_file( self, runner, app_skeleton): req = os.path.join(app_skeleton, 'requirements.txt') package = _get_random_package_name() with open(req, 'w') as f: f.write('%s\n' % package) cli_factory = factory.CLIFactory(app_skeleton) # Try to build a deployment package from the bad requirements file. # It should fail with a NoSuchPackageError error since the package # should not exist. result = runner.invoke( cli.package, ['package'], obj={'project_dir': app_skeleton, 'debug': False, 'factory': cli_factory}) assert result.exception is not None ex = result.exception assert isinstance(ex, NoSuchPackageError) assert str(ex) == 'Could not satisfy the requirement: %s' % package def test_packaging_requirements_keeps_same_hash(self, runner, app_skeleton, no_local_config): req = os.path.join(app_skeleton, 'requirements.txt') package = 'botocore==1.12.202' with open(req, 'w') as f: f.write('%s\n' % package) cli_factory = factory.CLIFactory(app_skeleton) package_output_location = os.path.join(app_skeleton, 'pkg') self._run_package_cmd(package_output_location, app_skeleton, cli_factory, runner) original_checksum = self._calculate_checksum(package_output_location) self._run_package_cmd(package_output_location, app_skeleton, cli_factory, runner) new_checksum = self._calculate_checksum(package_output_location) assert original_checksum == new_checksum def test_preserves_executable_permissions(self, runner, app_skeleton, no_local_config): vendor = os.path.join(app_skeleton, 'vendor') os.makedirs(vendor) executable_file = os.path.join(vendor, 'myscript.sh') with open(executable_file, 'w') as f: f.write('#!/bin/bash\necho foo\n') os.chmod(executable_file, 0o755) cli_factory = factory.CLIFactory(app_skeleton) package_output_location = os.path.join(app_skeleton, 'pkg') self._run_package_cmd(package_output_location, app_skeleton, cli_factory, runner) self._verify_file_is_executable(package_output_location, 'myscript.sh') original_checksum = self._calculate_checksum(package_output_location) self._run_package_cmd(package_output_location, app_skeleton, cli_factory, runner) new_checksum = self._calculate_checksum(package_output_location) assert original_checksum == new_checksum def _calculate_checksum(self, package_output_location): zip_filename = os.path.join(package_output_location, 'deployment.zip') with open(zip_filename, 'rb') as f: return hashlib.md5(f.read()).hexdigest() def _run_package_cmd(self, package_output_location, app_skeleton, cli_factory, runner, expected_exit_code=0): result = runner.invoke( cli.package, [package_output_location], obj={'project_dir': app_skeleton, 'debug': False, 'factory': cli_factory}) assert result.exit_code == expected_exit_code return result def _verify_file_is_executable(self, package_output_location, filename): zip_filename = os.path.join(package_output_location, 'deployment.zip') with ZipFile(zip_filename) as zip: zipinfo = zip.getinfo(filename) assert (zipinfo.external_attr >> 16) & stat.S_IXUSR ================================================ FILE: tests/plugins/codelinter.py ================================================ # These are linting checks used in the chalice codebase itself. # These are used to enforce specific coding standards and constraints. from pylint.checkers import BaseChecker from astroid.exceptions import InferenceError import astroid def register(linter): linter.register_checker(ConditionalImports(linter)) class ConditionalImports(BaseChecker): # This is used to ensure that any imports that rely on conditional # dependencies must be wrapped in a try/except ImportError. name = 'must-catch-import-error' msgs = { 'C9997': ('Importing this module must catch ImportError.', 'must-catch-import-error', 'Importing this module must catch ImportError.'), } def visit_import(self, node): names = [name[0] for name in node.names] if 'chalice.cli.filewatch.eventbased' in names: if not self._is_in_try_except_import_error(node): self.add_message('must-catch-import-error', node=node) return def visit_importfrom(self, node): if node.modname == 'chalice.cli.filewatch.eventbased': names = [name[0] for name in node.names] if 'WatchdogWorkerProcess' in names: # Ensure this is wrapped in a try/except. # Technically we should ensure anywhere in the call stack # we're wrapped in a try/except, but in practice we'll just # enforce you did that in the same scope as your import. if not self._is_in_try_except_import_error(node): self.add_message('must-catch-import-error', node=node) return def _is_in_try_except_import_error(self, node): if not isinstance(node.parent, astroid.Try): return False caught_exceptions = [ handler.type.name for handler in node.parent.handlers] if 'ImportError' not in caught_exceptions: # They wrapped a try/except but aren't catching # ImportError. return False return True ================================================ FILE: tests/plugins/testlinter.py ================================================ from pylint.checkers import BaseChecker from astroid.exceptions import InferenceError def register(linter): linter.register_checker(PatchChecker(linter)) linter.register_checker(MocksUseSpecArg(linter)) class PatchChecker(BaseChecker): name = 'patching-banned' msgs = { 'C9999': ('Use of mock.patch is not allowed', 'patch-call', 'Use of mock.patch not allowed') } patch_pytype = 'unittest.mock._patch' def visit_call(self, node): try: for inferred_type in node.infer(): if inferred_type.pytype() == self.patch_pytype: self.add_message('patch-call', node=node) except InferenceError: # It's ok if we can't work out what type the function # call is. pass class MocksUseSpecArg(BaseChecker): name = 'mocks-use-spec' msgs = { 'C9998': ('mock.Mock() must provide "spec=" argument', 'mock-missing-spec', 'mock.Mock() must provide "spec=" argument') } mock_pytype = 'unittest.mock.Mock' required_kwarg = 'spec' def visit_call(self, node): try: for inferred_type in node.infer(): if inferred_type.pytype() == self.mock_pytype: self._verify_spec_arg_provided(node) except InferenceError: pass def _verify_spec_arg_provided(self, node): if not node.keywords: self.add_message('mock-missing-spec', node=node) return kwargs = [kwarg.arg for kwarg in node.keywords] if self.required_kwarg not in kwargs: self.add_message('mock-missing-spec', node=node) ================================================ FILE: tests/unit/__init__.py ================================================ ================================================ FILE: tests/unit/cli/__init__.py ================================================ ================================================ FILE: tests/unit/cli/filewatch/test_eventbased.py ================================================ import threading from subprocess import Popen from unittest import mock import pytest try: from watchdog.events import FileSystemEvent, DirModifiedEvent from chalice.cli.filewatch.eventbased import WatchdogRestarter HAS_WATCHDOG = True except ImportError: HAS_WATCHDOG = False import chalice.local from chalice.cli import reloader from chalice.local import LocalDevServer # This will skip all the tests in this module if watchdog is not installed. pytestmark = pytest.mark.skipif(not HAS_WATCHDOG, reason='Tests require watchdog package.') # NOTE: Most of the reloader module relies on threads, subprocesses, # and process exiting with specific return codes. This is quite difficult # to unit test, so the more realistic tests are over in function/test_local.py. class RecordingPopen(object): def __init__(self, process, return_codes=None): self.process = process self.recorded_args = [] if return_codes is None: return_codes = [] self.return_codes = return_codes def __call__(self, *args, **kwargs): self.recorded_args.append((args, kwargs)) if self.return_codes: rc = self.return_codes.pop(0) self.process.returncode = rc return self.process def test_restarter_triggers_event(): restart_event = threading.Event() restarter = WatchdogRestarter(restart_event) app_modified = FileSystemEvent(src_path='./app.py') restarter.on_any_event(app_modified) assert restart_event.is_set() def test_directory_events_ignored(): restart_event = threading.Event() restarter = WatchdogRestarter(restart_event) app_modified = DirModifiedEvent(src_path='./') restarter.on_any_event(app_modified) assert not restart_event.is_set() def test_http_server_thread_starts_server_and_shutsdown(): server = mock.Mock(spec=LocalDevServer) thread = chalice.local.HTTPServerThread(lambda: server) thread.run() thread.shutdown() server.serve_forever.assert_called_with() server.shutdown.assert_called_with() def test_shutdown_noop_if_server_not_started(): server = mock.Mock(spec=LocalDevServer) thread = chalice.local.HTTPServerThread(lambda: server) thread.shutdown() assert not server.shutdown.called def test_parent_process_starts_child_with_worker_env_var(): process = mock.Mock(spec=Popen) process.returncode = 0 popen = RecordingPopen(process) env = {'original-env': 'foo'} parent = reloader.ParentProcess(env, popen) parent.main() assert len(popen.recorded_args) == 1 kwargs = popen.recorded_args[-1][1] assert kwargs == {'env': {'original-env': 'foo', 'CHALICE_WORKER': 'true'}} def test_assert_child_restarted_until_not_restart_rc(): process = mock.Mock(spec=Popen) popen = RecordingPopen( process, return_codes=[chalice.cli.filewatch.RESTART_REQUEST_RC, 0]) parent = reloader.ParentProcess({}, popen) parent.main() # The child process should have been invoked twice, the first one # was with RESTART_REQUEST_RC so that should trigger a restart, # then second one was rc 0 the process should just exit. assert len(popen.recorded_args) == 2 def test_ctrl_c_kill_child_process(): process = mock.Mock(spec=Popen) process.communicate.side_effect = KeyboardInterrupt popen = RecordingPopen(process) parent = reloader.ParentProcess({}, popen) with pytest.raises(KeyboardInterrupt): parent.main() assert process.terminate.called ================================================ FILE: tests/unit/cli/filewatch/test_stat.py ================================================ import os import time from chalice.cli.filewatch import stat class FakeOSUtils(object): def __init__(self): self.initial_scan = True def walk(self, rootdir): yield 'rootdir', [], ['bad-file', 'baz'] if self.initial_scan: self.initial_scan = False def joinpath(self, *parts): return os.path.join(*parts) def mtime(self, path): if self.initial_scan: return 1 if path.endswith('bad-file'): raise OSError("Bad file") return 2 def test_can_ignore_stat_errors(): calls = [] def callback(*args, **kwargs): calls.append((args, kwargs)) watcher = stat.StatFileWatcher(FakeOSUtils()) watcher.watch_for_file_changes('rootdir', callback) for _ in range(10): if len(calls) == 1: break time.sleep(0.2) else: raise AssertionError("Expected callback to be invoked but was not.") ================================================ FILE: tests/unit/cli/test_cli.py ================================================ from unittest import mock import pytest import re from chalice import cli from chalice.cli.factory import CLIFactory from chalice.local import LocalDevServer def test_cannot_run_local_mode_with_trailing_slash_route(): local_stage_test = 'local_test' factory = mock.Mock(spec=CLIFactory) factory.create_config_obj.return_value.environment_variables = {} factory.create_config_obj.return_value.chalice_app.routes = { 'foobar/': None } local_server = mock.Mock(spec=LocalDevServer) factory.create_local_server.return_value = local_server with pytest.raises(ValueError) as e: cli.run_local_server(factory, 'localhost', 8000, local_stage_test) assert str(e.value) == 'Route cannot end with a trailing slash: foobar/' def test_get_system_info(): system_info = cli.get_system_info() assert re.match(r'python\s*([\d.]+),?\s*(.*) (.*)', system_info) ================================================ FILE: tests/unit/cli/test_newproj.py ================================================ import os import pytest from chalice.cli import newproj class InMemoryOSUtils(object): def __init__(self, filemap=None): if filemap is None: filemap = {} self.filemap = filemap self.walk_return_val = None def dirname(self, name): return os.path.dirname(name) def get_directory_contents(self, dirname): full_paths = [f for f in self.filemap if f.startswith(dirname)] return [p.split(os.sep)[1] for p in full_paths] def file_exists(self, filename): return filename in self.filemap def joinpath(self, *args): return os.path.join(*args) def walk(self, root_dir): return self.walk_return_value def directory_exists(self, dirname): return True def get_file_contents(self, filename, binary=True): return self.filemap[filename] def set_file_contents(self, filename, contents, binary=True): self.filemap[filename] = contents @pytest.mark.parametrize( 'contents,template_kwargs,expected', [ ('{{myvar}}', {'myvar': 'foo'}, 'foo'), ('{{myvar}}', {'myvar': 'foo', 'myvar2': 'bar'}, 'foo'), ('before {{myvar}} after', {'myvar': 'foo'}, 'before foo after'), ('newlines\n{{myvar}}\nbar', {'myvar': 'foo'}, 'newlines\nfoo\nbar'), ('NAME = "{{myvar}}"', {'myvar': 'foo'}, 'NAME = "foo"'), ('{{one}}{{two}}', {'one': 'foo', 'two': 'bar'}, 'foobar'), ('{nomatch}', {'nomatch': 'bar'}, '{nomatch}'), ('no template', {'nomatch': 'bar'}, 'no template'), ('', {}, ''), ('{{noclose', {}, '{{noclose'), ('nostart}}', {}, 'nostart}}'), ('{{unknown_var}}', {}, newproj.BadTemplateError()), ] ) def test_can_get_templated_content(contents, template_kwargs, expected): if isinstance(expected, Exception): with pytest.raises(expected.__class__): newproj.get_templated_content(contents, template_kwargs) else: newproj.get_templated_content(contents, template_kwargs) == expected def test_newproj_copies_and_templates_files(): fake_osutils = InMemoryOSUtils() fake_osutils.walk_return_value = [ ('source_dir', [], ['foo', 'bar']), ] fake_osutils.filemap = { os.path.join('source_dir', 'foo'): 'hello', os.path.join('source_dir', 'bar'): '{{who}}', } creator = newproj.ProjectCreator(fake_osutils) creator.create_new_project('source_dir', 'dest_dir', {'who': 'world'}) assert fake_osutils.filemap[os.path.join('dest_dir', 'foo')] == 'hello' assert fake_osutils.filemap[os.path.join('dest_dir', 'bar')] == 'world' def test_can_list_available_projects(): fake_osutils = InMemoryOSUtils() join = os.path.join first_dir = join('template-dir', '0001-first-proj') second_dir = join('template-dir', '0002-second-proj') fake_osutils.filemap = { join(first_dir, 'metadata.json'): '{"description": "First template"}', join(second_dir, 'metadata.json'): '{"description": "Second"}', } results = newproj.list_available_projects('template-dir', fake_osutils) assert results == [ newproj.ProjectTemplate( dirname='0001-first-proj', metadata={'description': 'First template'}, key='first-proj', ), newproj.ProjectTemplate( dirname='0002-second-proj', metadata={'description': 'Second'}, key='second-proj', ), ] ================================================ FILE: tests/unit/conftest.py ================================================ import json import os from pytest import fixture from hypothesis import settings, HealthCheck from chalice.app import Chalice # From: # http://hypothesis.readthedocs.io/en/latest/settings.html#settings-profiles # On travis we'll have it run through more iterations. from chalice.deploy import models settings.register_profile( 'ci', settings(max_examples=2000, suppress_health_check=[HealthCheck.too_slow]), ) # When you're developing locally, we'll only run a few examples # to keep unit tests fast. If you want to run more iterations # locally just set HYPOTHESIS_PROFILE=ci. settings.register_profile('dev', settings(max_examples=10)) settings.load_profile(os.getenv('HYPOTHESIS_PROFILE', 'dev')) print("HYPOTHESIS PROFILE: %s" % os.environ.get("HYPOTHESIS_PROFILE")) @fixture(autouse=True) def ensure_no_local_config(no_local_config): pass @fixture def sample_app(): app = Chalice('sample') @app.route('/') def foo(): return {} return app @fixture def sample_app_with_auth(): app = Chalice('sampleauth') @app.authorizer('myauth') def myauth(auth_request): pass @app.route('/', authorizer=myauth) def foo(): return {} return app @fixture def sample_app_schedule_only(): app = Chalice('schedule_only') @app.schedule('rate(5 minutes)') def cron(event): pass return app @fixture def sample_sqs_event_app(): app = Chalice('sqs-event') @app.on_sqs_message(queue='myqueue') def handler(event): pass return app @fixture def sample_kinesis_event_app(): app = Chalice('kinesis-event') @app.on_kinesis_record(stream='mystream') def handler(event): pass return app @fixture def sample_ddb_event_app(): app = Chalice('ddb-event') @app.on_dynamodb_record(stream_arn='arn:aws:...:stream') def handler(event): pass return app @fixture def sample_app_lambda_only(): app = Chalice('lambda_only') @app.lambda_function() def myfunction(event, context): pass return app @fixture def sample_websocket_app(): app = Chalice('sample') @app.on_ws_connect() def connect(): pass @app.on_ws_message() def message(): pass @app.on_ws_disconnect() def disconnect(): pass return app @fixture def sample_s3_event_app(): app = Chalice('s3-event') @app.on_s3_event(bucket='mybucket') def handler(event): pass return app @fixture def sample_sns_event_app(): app = Chalice('sns-event') @app.on_sns_message(topic='mytopic') def handler(event): pass return app @fixture def sample_cloudwatch_event_app(): app = Chalice('cloudwatch-event') @app.on_cw_event({'source': {'source': ['aws.ec2']}}) def foo(event): return event return app @fixture def create_event(): def create_event_inner(uri, method, path, content_type='application/json'): return { 'requestContext': { 'httpMethod': method, 'resourcePath': uri, }, 'headers': { 'Content-Type': content_type, }, 'pathParameters': path, 'multiValueQueryStringParameters': None, 'body': "", 'stageVariables': {}, } return create_event_inner @fixture def create_websocket_event(): def create_event_inner( route_key, body='', endpoint='abcd1234.execute-api.us-west-2.amazonaws.com'): return { 'requestContext': { 'routeKey': route_key, 'domainName': endpoint, 'stage': 'api', 'connectionId': 'ABCD1234=', 'apiId': 'abcd1234', }, 'body': body, } return create_event_inner @fixture def create_empty_header_event(): def create_empty_header_event_inner(uri, method, path, content_type='application/json'): return { 'requestContext': { 'httpMethod': method, 'resourcePath': uri, }, 'headers': None, 'pathParameters': path, 'multiValueQueryStringParameters': None, 'body': "", 'stageVariables': {}, } return create_empty_header_event_inner @fixture def create_event_with_body(create_event): def create_event_with_body_inner(body, uri='/', method='POST', content_type='application/json'): event = create_event(uri, method, {}, content_type) if content_type == 'application/json': body = json.dumps(body) event['body'] = body return event return create_event_with_body_inner @fixture def lambda_function(): return models.LambdaFunction( resource_name='foo', function_name='app-stage-foo', deployment_package=None, environment_variables={}, runtime='python2.7', handler='app.app', tags={}, timeout=None, memory_size=None, role=models.PreCreatedIAMRole(role_arn='foobar'), security_group_ids=[], subnet_ids=[], layers=[], reserved_concurrency=None, xray=None, ) ================================================ FILE: tests/unit/deploy/__init__.py ================================================ ================================================ FILE: tests/unit/deploy/test_appgraph.py ================================================ import pytest from chalice.app import Chalice from chalice.config import Config from chalice.constants import LAMBDA_TRUST_POLICY from chalice.deploy import models from chalice.deploy.appgraph import ApplicationGraphBuilder, ChaliceBuildError from chalice.deploy.deployer import BuildStage, PolicyGenerator from chalice.utils import serialize_to_json, OSUtils @pytest.fixture def websocket_app_without_connect(): app = Chalice('websocket-event-no-connect') @app.on_ws_message() def message(event): pass @app.on_ws_disconnect() def disconnect(event): pass return app @pytest.fixture def websocket_app_without_message(): app = Chalice('websocket-event-no-message') @app.on_ws_connect() def connect(event): pass @app.on_ws_disconnect() def disconnect(event): pass return app @pytest.fixture def websocket_app_without_disconnect(): app = Chalice('websocket-event-no-disconnect') @app.on_ws_connect() def connect(event): pass @app.on_ws_message() def message(event): pass return app class TestApplicationGraphBuilder(object): def create_config(self, app, app_name='lambda-only', iam_role_arn=None, policy_file=None, api_gateway_stage='api', autogen_policy=False, security_group_ids=None, subnet_ids=None, reserved_concurrency=None, layers=None, automatic_layer=False, api_gateway_endpoint_type=None, api_gateway_endpoint_vpce=None, api_gateway_policy_file=None, api_gateway_custom_domain=None, websocket_api_custom_domain=None, log_retention_in_days=None, project_dir='.'): kwargs = { 'chalice_app': app, 'app_name': app_name, 'project_dir': project_dir, 'automatic_layer': automatic_layer, 'api_gateway_stage': api_gateway_stage, 'api_gateway_policy_file': api_gateway_policy_file, 'api_gateway_endpoint_type': api_gateway_endpoint_type, 'api_gateway_endpoint_vpce': api_gateway_endpoint_vpce, 'api_gateway_custom_domain': api_gateway_custom_domain, 'websocket_api_custom_domain': websocket_api_custom_domain, } if iam_role_arn is not None: # We want to use an existing role. # This will skip all the autogen-policy # and role creation. kwargs['manage_iam_role'] = False kwargs['iam_role_arn'] = 'role:arn' elif policy_file is not None: # Otherwise this setting is when a user wants us to # manage the role, but they've written a policy file # they'd like us to use. kwargs['autogen_policy'] = False kwargs['iam_policy_file'] = policy_file elif autogen_policy: kwargs['autogen_policy'] = True if security_group_ids is not None and subnet_ids is not None: kwargs['security_group_ids'] = security_group_ids kwargs['subnet_ids'] = subnet_ids if reserved_concurrency is not None: kwargs['reserved_concurrency'] = reserved_concurrency if log_retention_in_days is not None: kwargs['log_retention_in_days'] = log_retention_in_days kwargs['layers'] = layers config = Config.create(**kwargs) return config def test_can_build_single_lambda_function_app(self, sample_app_lambda_only): # This is the simplest configuration we can get. builder = ApplicationGraphBuilder() config = self.create_config(sample_app_lambda_only, automatic_layer=False, iam_role_arn='role:arn') application = builder.build(config, stage_name='dev') # The top level resource is always an Application. assert isinstance(application, models.Application) assert len(application.resources) == 1 assert application.resources[0] == models.LambdaFunction( resource_name='myfunction', function_name='lambda-only-dev-myfunction', environment_variables={}, runtime=config.lambda_python_version, handler='app.myfunction', tags=config.tags, timeout=None, memory_size=None, deployment_package=models.DeploymentPackage( models.Placeholder.BUILD_STAGE), role=models.PreCreatedIAMRole('role:arn'), security_group_ids=[], subnet_ids=[], layers=[], reserved_concurrency=None, managed_layer=None, xray=None, ) def test_can_build_single_lambda_function_app_with_log_retention( self, sample_app_lambda_only): # This is the simplest configuration we can get. builder = ApplicationGraphBuilder() config = self.create_config(sample_app_lambda_only, automatic_layer=False, iam_role_arn='role:arn', log_retention_in_days=14) application = builder.build(config, stage_name='dev') # The top level resource is always an Application. assert isinstance(application, models.Application) assert len(application.resources) == 1 assert isinstance(application.resources[0].log_group, models.LogGroup) assert application.resources[0] == models.LambdaFunction( resource_name='myfunction', function_name='lambda-only-dev-myfunction', environment_variables={}, runtime=config.lambda_python_version, handler='app.myfunction', tags=config.tags, timeout=None, memory_size=None, deployment_package=models.DeploymentPackage( models.Placeholder.BUILD_STAGE), role=models.PreCreatedIAMRole('role:arn'), security_group_ids=[], subnet_ids=[], layers=[], reserved_concurrency=None, managed_layer=None, xray=None, log_group=models.LogGroup( resource_name='myfunction-log-group', log_group_name='/aws/lambda/%s-%s-%s' % (config.app_name, 'dev', 'myfunction'), retention_in_days=14) ) def test_can_build_single_lambda_function_app_with_managed_layer( self, sample_app_lambda_only): # This is the simplest configuration we can get. builder = ApplicationGraphBuilder() config = self.create_config( sample_app_lambda_only, iam_role_arn='role:arn', automatic_layer=True) application = builder.build(config, stage_name='dev') # The top level resource is always an Application. assert isinstance(application, models.Application) assert len(application.resources) == 1 assert application.resources[0] == models.LambdaFunction( resource_name='myfunction', function_name='lambda-only-dev-myfunction', environment_variables={}, runtime=config.lambda_python_version, handler='app.myfunction', tags=config.tags, timeout=None, memory_size=None, deployment_package=models.DeploymentPackage( models.Placeholder.BUILD_STAGE), role=models.PreCreatedIAMRole('role:arn'), security_group_ids=[], subnet_ids=[], layers=[], managed_layer=models.LambdaLayer( resource_name='managed-layer', layer_name='lambda-only-dev-managed-layer', runtime=config.lambda_python_version, deployment_package=models.DeploymentPackage( models.Placeholder.BUILD_STAGE, ) ), reserved_concurrency=None, xray=None, ) def test_all_lambda_functions_share_managed_layer( self, sample_app_lambda_only): @sample_app_lambda_only.lambda_function() def second(event, context): pass builder = ApplicationGraphBuilder() config = self.create_config( sample_app_lambda_only, iam_role_arn='role:arn', automatic_layer=True) application = builder.build(config, stage_name='dev') assert len(application.resources) == 2 first_layer = application.resources[0].managed_layer second_layer = application.resources[1].managed_layer assert first_layer == second_layer def test_can_build_lambda_function_with_layers(self, sample_app_lambda_only): # This is the simplest configuration we can get. builder = ApplicationGraphBuilder() layers = ['arn:aws:lambda:us-east-1:111:layer:test_layer:1'] config = self.create_config(sample_app_lambda_only, iam_role_arn='role:arn', layers=layers) application = builder.build(config, stage_name='dev') # The top level resource is always an Application. assert isinstance(application, models.Application) assert len(application.resources) == 1 assert application.resources[0] == models.LambdaFunction( resource_name='myfunction', function_name='lambda-only-dev-myfunction', environment_variables={}, runtime=config.lambda_python_version, handler='app.myfunction', tags=config.tags, timeout=None, memory_size=None, deployment_package=models.DeploymentPackage( models.Placeholder.BUILD_STAGE), role=models.PreCreatedIAMRole('role:arn'), security_group_ids=[], subnet_ids=[], layers=layers, reserved_concurrency=None, xray=None, ) def test_can_build_app_with_domain_name(self, sample_app): domain_name = { 'domain_name': 'example.com', 'tls_version': 'TLS_1_0', 'certificate_arn': 'certificate_arn', 'tags': { 'some_key1': 'some_value1', 'some_key2': 'some_value2' }, 'url_prefix': '/' } config = self.create_config(sample_app, app_name='rest-api-app', api_gateway_endpoint_type='REGIONAL', api_gateway_custom_domain=domain_name, ) builder = ApplicationGraphBuilder() application = builder.build(config, stage_name='dev') rest_api = application.resources[0] assert isinstance(rest_api, models.RestAPI) domain_name = rest_api.domain_name api_mapping = domain_name.api_mapping assert isinstance(domain_name, models.DomainName) assert isinstance(api_mapping, models.APIMapping) assert api_mapping.mount_path == '(none)' def test_can_build_lambda_function_app_with_vpc_config( self, sample_app_lambda_only ): @sample_app_lambda_only.lambda_function() def foo(event, context): pass builder = ApplicationGraphBuilder() config = self.create_config(sample_app_lambda_only, iam_role_arn='role:arn', security_group_ids=['sg1', 'sg2'], subnet_ids=['sn1', 'sn2']) application = builder.build(config, stage_name='dev') assert application.resources[0] == models.LambdaFunction( resource_name='myfunction', function_name='lambda-only-dev-myfunction', environment_variables={}, runtime=config.lambda_python_version, handler='app.myfunction', tags=config.tags, timeout=None, memory_size=None, deployment_package=models.DeploymentPackage( models.Placeholder.BUILD_STAGE), role=models.PreCreatedIAMRole('role:arn'), security_group_ids=['sg1', 'sg2'], subnet_ids=['sn1', 'sn2'], layers=[], reserved_concurrency=None, xray=None, ) def test_vpc_trait_added_when_vpc_configured(self, sample_app_lambda_only): @sample_app_lambda_only.lambda_function() def foo(event, context): pass builder = ApplicationGraphBuilder() config = self.create_config(sample_app_lambda_only, autogen_policy=True, security_group_ids=['sg1', 'sg2'], subnet_ids=['sn1', 'sn2']) application = builder.build(config, stage_name='dev') policy = application.resources[0].role.policy assert policy == models.AutoGenIAMPolicy( document=models.Placeholder.BUILD_STAGE, traits=set([models.RoleTraits.VPC_NEEDED]), ) def test_exception_raised_when_missing_vpc_params(self, sample_app_lambda_only): @sample_app_lambda_only.lambda_function() def foo(event, context): pass builder = ApplicationGraphBuilder() config = self.create_config(sample_app_lambda_only, iam_role_arn='role:arn', security_group_ids=['sg1', 'sg2'], subnet_ids=[]) with pytest.raises(ChaliceBuildError): builder.build(config, stage_name='dev') def test_can_build_lambda_function_app_with_reserved_concurrency( self, sample_app_lambda_only): # This is the simplest configuration we can get. builder = ApplicationGraphBuilder() config = self.create_config(sample_app_lambda_only, iam_role_arn='role:arn', reserved_concurrency=5) application = builder.build(config, stage_name='dev') # The top level resource is always an Application. assert isinstance(application, models.Application) assert len(application.resources) == 1 assert application.resources[0] == models.LambdaFunction( resource_name='myfunction', function_name='lambda-only-dev-myfunction', environment_variables={}, runtime=config.lambda_python_version, handler='app.myfunction', tags=config.tags, timeout=None, memory_size=None, deployment_package=models.DeploymentPackage( models.Placeholder.BUILD_STAGE), role=models.PreCreatedIAMRole('role:arn'), security_group_ids=[], subnet_ids=[], layers=[], reserved_concurrency=5, xray=None, ) def test_multiple_lambda_functions_share_role_and_package( self, sample_app_lambda_only): # We're going to add another lambda_function to our app. @sample_app_lambda_only.lambda_function() def bar(event, context): return {} builder = ApplicationGraphBuilder() config = self.create_config(sample_app_lambda_only, iam_role_arn='role:arn') application = builder.build(config, stage_name='dev') assert len(application.resources) == 2 # The lambda functions by default share the same role assert application.resources[0].role == application.resources[1].role # Not just in equality but the exact same role objects. assert application.resources[0].role is application.resources[1].role # And all lambda functions share the same deployment package. assert (application.resources[0].deployment_package == application.resources[1].deployment_package) def test_autogen_policy_for_function(self, sample_app_lambda_only): # This test is just a sanity test that verifies all the params # for an ManagedIAMRole. The various combinations for role # configuration is all tested via RoleTestCase. config = self.create_config(sample_app_lambda_only, autogen_policy=True) builder = ApplicationGraphBuilder() application = builder.build(config, stage_name='dev') function = application.resources[0] role = function.role # We should have linked a ManagedIAMRole assert isinstance(role, models.ManagedIAMRole) assert role == models.ManagedIAMRole( resource_name='default-role', role_name='lambda-only-dev', trust_policy=LAMBDA_TRUST_POLICY, policy=models.AutoGenIAMPolicy(models.Placeholder.BUILD_STAGE), ) def test_cloudwatch_event_models(self, sample_cloudwatch_event_app): config = self.create_config(sample_cloudwatch_event_app, app_name='cloudwatch-event', autogen_policy=True) builder = ApplicationGraphBuilder() application = builder.build(config, stage_name='dev') assert len(application.resources) == 1 event = application.resources[0] assert isinstance(event, models.CloudWatchEvent) assert event.resource_name == 'foo-event' assert event.rule_name == 'cloudwatch-event-dev-foo-event' assert isinstance(event.lambda_function, models.LambdaFunction) assert event.lambda_function.resource_name == 'foo' def test_scheduled_event_models(self, sample_app_schedule_only): config = self.create_config(sample_app_schedule_only, app_name='scheduled-event', autogen_policy=True) builder = ApplicationGraphBuilder() application = builder.build(config, stage_name='dev') assert len(application.resources) == 1 event = application.resources[0] assert isinstance(event, models.ScheduledEvent) assert event.resource_name == 'cron-event' assert event.rule_name == 'scheduled-event-dev-cron-event' assert isinstance(event.lambda_function, models.LambdaFunction) assert event.lambda_function.resource_name == 'cron' def test_can_build_private_rest_api(self, sample_app): config = self.create_config(sample_app, app_name='sample-app', api_gateway_endpoint_type='PRIVATE', api_gateway_endpoint_vpce='vpce-abc123') builder = ApplicationGraphBuilder() application = builder.build(config, stage_name='dev') rest_api = application.resources[0] assert isinstance(rest_api, models.RestAPI) assert rest_api.policy.document == { 'Version': '2012-10-17', 'Statement': [ {'Action': 'execute-api:Invoke', 'Effect': 'Allow', 'Principal': '*', 'Resource': 'arn:*:execute-api:*:*:*', 'Condition': { 'StringEquals': { 'aws:SourceVpce': 'vpce-abc123'}}}, ] } def test_can_build_private_rest_api_custom_policy( self, tmpdir, sample_app): config = self.create_config(sample_app, app_name='rest-api-app', api_gateway_policy_file='foo.json', api_gateway_endpoint_type='PRIVATE', project_dir=str(tmpdir)) tmpdir.mkdir('.chalice').join('foo.json').write( serialize_to_json({'Version': '2012-10-17', 'Statement': []})) application_builder = ApplicationGraphBuilder() build_stage = BuildStage( steps=[ PolicyGenerator(osutils=OSUtils(), policy_gen=None) ] ) application = application_builder.build(config, stage_name='dev') build_stage.execute(config, application.resources) rest_api = application.resources[0] assert rest_api.policy.document == { 'Version': '2012-10-17', 'Statement': [] } def test_can_build_rest_api(self, sample_app): config = self.create_config(sample_app, app_name='sample-app', autogen_policy=True) builder = ApplicationGraphBuilder() application = builder.build(config, stage_name='dev') assert len(application.resources) == 1 rest_api = application.resources[0] assert isinstance(rest_api, models.RestAPI) assert rest_api.resource_name == 'rest_api' assert rest_api.api_gateway_stage == 'api' assert rest_api.lambda_function.resource_name == 'api_handler' assert rest_api.lambda_function.function_name == 'sample-app-dev' # The swagger document is validated elsewhere so we just # make sure it looks right. assert rest_api.swagger_doc == models.Placeholder.BUILD_STAGE def test_can_build_rest_api_with_authorizer(self, sample_app_with_auth): config = self.create_config(sample_app_with_auth, app_name='rest-api-app', autogen_policy=True) builder = ApplicationGraphBuilder() application = builder.build(config, stage_name='dev') rest_api = application.resources[0] assert len(rest_api.authorizers) == 1 assert isinstance(rest_api.authorizers[0], models.LambdaFunction) def test_can_create_s3_event_handler(self, sample_s3_event_app): # TODO: don't require app name, get it from app obj. config = self.create_config(sample_s3_event_app, app_name='s3-event-app', autogen_policy=True) builder = ApplicationGraphBuilder() application = builder.build(config, stage_name='dev') assert len(application.resources) == 1 s3_event = application.resources[0] assert isinstance(s3_event, models.S3BucketNotification) assert s3_event.resource_name == 'handler-s3event' assert s3_event.bucket == 'mybucket' assert s3_event.events == ['s3:ObjectCreated:*'] lambda_function = s3_event.lambda_function assert lambda_function.resource_name == 'handler' assert lambda_function.handler == 'app.handler' def test_can_create_sns_event_handler(self, sample_sns_event_app): config = self.create_config(sample_sns_event_app, app_name='s3-event-app', autogen_policy=True) builder = ApplicationGraphBuilder() application = builder.build(config, stage_name='dev') assert len(application.resources) == 1 sns_event = application.resources[0] assert isinstance(sns_event, models.SNSLambdaSubscription) assert sns_event.resource_name == 'handler-sns-subscription' assert sns_event.topic == 'mytopic' lambda_function = sns_event.lambda_function assert lambda_function.resource_name == 'handler' assert lambda_function.handler == 'app.handler' def test_can_create_sqs_event_handler(self, sample_sqs_event_app): config = self.create_config(sample_sqs_event_app, app_name='sqs-event-app', autogen_policy=True) builder = ApplicationGraphBuilder() application = builder.build(config, stage_name='dev') assert len(application.resources) == 1 sqs_event = application.resources[0] assert isinstance(sqs_event, models.SQSEventSource) assert sqs_event.resource_name == 'handler-sqs-event-source' assert sqs_event.queue == 'myqueue' lambda_function = sqs_event.lambda_function assert lambda_function.resource_name == 'handler' assert lambda_function.handler == 'app.handler' def test_can_create_sqs_handler_with_queue_arn(self, sample_sqs_event_app): @sample_sqs_event_app.on_sqs_message(queue_arn='arn:my:queue') def new_handler(event): pass config = self.create_config(sample_sqs_event_app, app_name='sqs-event-app', autogen_policy=True) builder = ApplicationGraphBuilder() application = builder.build(config, stage_name='dev') sqs_event = application.resources[1] assert sqs_event.queue == models.QueueARN(arn='arn:my:queue') lambda_function = sqs_event.lambda_function assert lambda_function.resource_name == 'new_handler' assert lambda_function.handler == 'app.new_handler' def test_can_create_kinesis_event_handler(self, sample_kinesis_event_app): config = self.create_config(sample_kinesis_event_app, app_name='kinesis-event-app', autogen_policy=True) builder = ApplicationGraphBuilder() application = builder.build(config, stage_name='dev') assert len(application.resources) == 1 kinesis_event = application.resources[0] assert isinstance(kinesis_event, models.KinesisEventSource) assert kinesis_event.resource_name == 'handler-kinesis-event-source' assert kinesis_event.stream == 'mystream' lambda_function = kinesis_event.lambda_function assert lambda_function.resource_name == 'handler' assert lambda_function.handler == 'app.handler' def test_can_create_ddb_event_handler(self, sample_ddb_event_app): config = self.create_config(sample_ddb_event_app, app_name='ddb-event-app', autogen_policy=True) builder = ApplicationGraphBuilder() application = builder.build(config, stage_name='dev') assert len(application.resources) == 1 ddb_event = application.resources[0] assert isinstance(ddb_event, models.DynamoDBEventSource) assert ddb_event.resource_name == 'handler-dynamodb-event-source' assert ddb_event.stream_arn == 'arn:aws:...:stream' lambda_function = ddb_event.lambda_function assert lambda_function.resource_name == 'handler' assert lambda_function.handler == 'app.handler' def test_can_create_websocket_event_handler(self, sample_websocket_app): config = self.create_config(sample_websocket_app, app_name='websocket-app', autogen_policy=True) builder = ApplicationGraphBuilder() application = builder.build(config, stage_name='dev') assert len(application.resources) == 1 websocket_api = application.resources[0] assert isinstance(websocket_api, models.WebsocketAPI) assert websocket_api.resource_name == 'websocket_api' assert sorted(websocket_api.routes) == sorted( ['$connect', '$default', '$disconnect']) assert websocket_api.api_gateway_stage == 'api' connect_function = websocket_api.connect_function assert connect_function.resource_name == 'websocket_connect' assert connect_function.handler == 'app.connect' message_function = websocket_api.message_function assert message_function.resource_name == 'websocket_message' assert message_function.handler == 'app.message' disconnect_function = websocket_api.disconnect_function assert disconnect_function.resource_name == 'websocket_disconnect' assert disconnect_function.handler == 'app.disconnect' def test_can_create_websocket_api_with_domain_name(self, sample_websocket_app): domain_name = { 'domain_name': 'example.com', 'tls_version': 'TLS_1_2', 'certificate_arn': 'certificate_arn', 'tags': { 'tag_key1': 'tag_value1', 'tag_key2': 'tag_value2' } } config = self.create_config(sample_websocket_app, app_name='websocket-app', autogen_policy=True, websocket_api_custom_domain=domain_name) builder = ApplicationGraphBuilder() application = builder.build(config, stage_name='dev') websocket_api = application.resources[0] assert isinstance(websocket_api, models.WebsocketAPI) domain_name = websocket_api.domain_name assert isinstance(domain_name, models.DomainName) assert isinstance(domain_name.api_mapping, models.APIMapping) assert domain_name.api_mapping.mount_path == '(none)' def test_can_create_websocket_app_missing_connect( self, websocket_app_without_connect): config = self.create_config(websocket_app_without_connect, app_name='websocket-app', autogen_policy=True) builder = ApplicationGraphBuilder() application = builder.build(config, stage_name='dev') assert len(application.resources) == 1 websocket_api = application.resources[0] assert isinstance(websocket_api, models.WebsocketAPI) assert websocket_api.resource_name == 'websocket_api' assert sorted(websocket_api.routes) == sorted( ['$default', '$disconnect']) assert websocket_api.api_gateway_stage == 'api' connect_function = websocket_api.connect_function assert connect_function is None message_function = websocket_api.message_function assert message_function.resource_name == 'websocket_message' assert message_function.handler == 'app.message' disconnect_function = websocket_api.disconnect_function assert disconnect_function.resource_name == 'websocket_disconnect' assert disconnect_function.handler == 'app.disconnect' def test_can_create_websocket_app_missing_message( self, websocket_app_without_message): config = self.create_config(websocket_app_without_message, app_name='websocket-app', autogen_policy=True) builder = ApplicationGraphBuilder() application = builder.build(config, stage_name='dev') assert len(application.resources) == 1 websocket_api = application.resources[0] assert isinstance(websocket_api, models.WebsocketAPI) assert websocket_api.resource_name == 'websocket_api' assert sorted(websocket_api.routes) == sorted( ['$connect', '$disconnect']) assert websocket_api.api_gateway_stage == 'api' connect_function = websocket_api.connect_function assert connect_function.resource_name == 'websocket_connect' assert connect_function.handler == 'app.connect' disconnect_function = websocket_api.disconnect_function assert disconnect_function.resource_name == 'websocket_disconnect' assert disconnect_function.handler == 'app.disconnect' def test_can_create_websocket_app_missing_disconnect( self, websocket_app_without_disconnect): config = self.create_config(websocket_app_without_disconnect, app_name='websocket-app', autogen_policy=True) builder = ApplicationGraphBuilder() application = builder.build(config, stage_name='dev') assert len(application.resources) == 1 websocket_api = application.resources[0] assert isinstance(websocket_api, models.WebsocketAPI) assert websocket_api.resource_name == 'websocket_api' assert sorted(websocket_api.routes) == sorted( ['$connect', '$default']) assert websocket_api.api_gateway_stage == 'api' connect_function = websocket_api.connect_function assert connect_function.resource_name == 'websocket_connect' assert connect_function.handler == 'app.connect' message_function = websocket_api.message_function assert message_function.resource_name == 'websocket_message' assert message_function.handler == 'app.message' ================================================ FILE: tests/unit/deploy/test_deployer.py ================================================ from __future__ import annotations import os from dataclasses import dataclass import socket import botocore.session import pytest from unittest import mock from botocore.stub import Stubber from botocore.vendored.requests import ConnectionError as \ RequestsConnectionError from pytest import fixture from chalice.app import Chalice from chalice.awsclient import LambdaClientError, AWSClientError from chalice.awsclient import DeploymentPackageTooLargeError from chalice.awsclient import LambdaErrorContext from chalice.config import Config from chalice.policy import AppPolicyGenerator from chalice.deploy.deployer import ChaliceDeploymentError from chalice.utils import UI import unittest from chalice.awsclient import TypedAWSClient from chalice.utils import OSUtils, serialize_to_json from chalice.deploy import models from chalice.deploy import packager from chalice.deploy.deployer import create_default_deployer, \ create_deletion_deployer, Deployer, BaseDeployStep, \ InjectDefaults, DeploymentPackager, SwaggerBuilder, \ PolicyGenerator, BuildStage, ResultsRecorder, DeploymentReporter, \ ManagedLayerDeploymentPackager from chalice.deploy.appgraph import ApplicationGraphBuilder, \ DependencyBuilder from chalice.deploy.executor import Executor from chalice.deploy.swagger import SwaggerGenerator, TemplatedSwaggerGenerator from chalice.deploy.planner import PlanStage from chalice.deploy.planner import StringFormat from chalice.deploy.sweeper import ResourceSweeper from chalice.deploy.models import APICall from chalice.constants import VPC_ATTACH_POLICY from chalice.constants import SQS_EVENT_SOURCE_POLICY from chalice.constants import KINESIS_EVENT_SOURCE_POLICY from chalice.constants import DDB_EVENT_SOURCE_POLICY from chalice.constants import POST_TO_WEBSOCKET_CONNECTION_POLICY from chalice.deploy.deployer import LambdaEventSourcePolicyInjector from chalice.deploy.deployer import WebsocketPolicyInjector _SESSION = None class InMemoryOSUtils(object): def __init__(self, filemap=None): if filemap is None: filemap = {} self.filemap = filemap def file_exists(self, filename): return filename in self.filemap def get_file_contents(self, filename, binary=True): return self.filemap[filename] def set_file_contents(self, filename, contents, binary=True): self.filemap[filename] = contents @fixture def in_memory_osutils(): return InMemoryOSUtils() def stubbed_client(service_name): global _SESSION if _SESSION is None: _SESSION = botocore.session.get_session() client = _SESSION.create_client(service_name, region_name='us-west-2') stubber = Stubber(client) return client, stubber @fixture def config_obj(sample_app): config = Config.create( chalice_app=sample_app, stage='dev', api_gateway_stage='api', ) return config @fixture def ui(): return mock.Mock(spec=UI) class TestChaliceDeploymentError(object): def test_general_exception(self): general_exception = Exception('My Exception') deploy_error = ChaliceDeploymentError(general_exception) deploy_error_msg = str(deploy_error) assert ( 'ERROR - While deploying your chalice application' in deploy_error_msg ) assert 'My Exception' in deploy_error_msg def test_lambda_client_error(self): lambda_error = LambdaClientError( Exception('My Exception'), context=LambdaErrorContext( function_name='foo', client_method_name='create_function', deployment_size=1024 ** 2 ) ) deploy_error = ChaliceDeploymentError(lambda_error) deploy_error_msg = str(deploy_error) assert ( 'ERROR - While sending your chalice handler code to ' 'Lambda to create function \n"foo"' in deploy_error_msg ) assert 'My Exception' in deploy_error_msg def test_lambda_client_error_wording_for_update(self): lambda_error = LambdaClientError( Exception('My Exception'), context=LambdaErrorContext( function_name='foo', client_method_name='update_function_code', deployment_size=1024 ** 2 ) ) deploy_error = ChaliceDeploymentError(lambda_error) deploy_error_msg = str(deploy_error) assert ( 'sending your chalice handler code to ' 'Lambda to update function' in deploy_error_msg ) def test_gives_where_and_suggestion_for_too_large_deployment_error(self): too_large_error = DeploymentPackageTooLargeError( Exception('Too large of deployment pacakge'), context=LambdaErrorContext( function_name='foo', client_method_name='create_function', deployment_size=1024 ** 2, ) ) deploy_error = ChaliceDeploymentError(too_large_error) deploy_error_msg = str(deploy_error) assert ( 'ERROR - While sending your chalice handler code to ' 'Lambda to create function \n"foo"' in deploy_error_msg ) assert 'Too large of deployment pacakge' in deploy_error_msg assert ( 'To avoid this error, decrease the size of your chalice ' 'application ' in deploy_error_msg ) def test_include_size_context_for_too_large_deployment_error(self): too_large_error = DeploymentPackageTooLargeError( Exception('Too large of deployment pacakge'), context=LambdaErrorContext( function_name='foo', client_method_name='create_function', deployment_size=58 * (1024 ** 2), ) ) deploy_error = ChaliceDeploymentError( too_large_error) deploy_error_msg = str(deploy_error) print(repr(deploy_error_msg)) assert 'deployment package is 58.0 MB' in deploy_error_msg assert '50.0 MB or less' in deploy_error_msg assert 'To avoid this error' in deploy_error_msg def test_error_msg_for_general_connection(self): lambda_error = DeploymentPackageTooLargeError( RequestsConnectionError( Exception( 'Connection aborted.', socket.error('Some vague reason') ) ), context=LambdaErrorContext( function_name='foo', client_method_name='create_function', deployment_size=1024 ** 2 ) ) deploy_error = ChaliceDeploymentError(lambda_error) deploy_error_msg = str(deploy_error) assert 'Connection aborted.' in deploy_error_msg assert 'Some vague reason' not in deploy_error_msg def test_simplifies_error_msg_for_broken_pipe(self): lambda_error = DeploymentPackageTooLargeError( RequestsConnectionError( Exception( 'Connection aborted.', socket.error(32, 'Broken pipe') ) ), context=LambdaErrorContext( function_name='foo', client_method_name='create_function', deployment_size=1024 ** 2 ) ) deploy_error = ChaliceDeploymentError(lambda_error) deploy_error_msg = str(deploy_error) assert ( 'Connection aborted. Lambda closed the connection' in deploy_error_msg ) def test_simplifies_error_msg_for_timeout(self): lambda_error = DeploymentPackageTooLargeError( RequestsConnectionError( Exception( 'Connection aborted.', socket.timeout('The write operation timed out') ) ), context=LambdaErrorContext( function_name='foo', client_method_name='create_function', deployment_size=1024 ** 2 ) ) deploy_error = ChaliceDeploymentError(lambda_error) deploy_error_msg = str(deploy_error) assert ( 'Connection aborted. Timed out sending your app to Lambda.' in deploy_error_msg ) @dataclass class FooResource(models.Model): name: str leaf: LeafResource def dependencies(self): if not isinstance(self.leaf, list): return [self.leaf] return self.leaf @dataclass class LeafResource(models.Model): name: str @fixture def mock_client(): return mock.Mock(spec=TypedAWSClient) @fixture def mock_osutils(): return mock.Mock(spec=OSUtils) def create_function_resource(name): return models.LambdaFunction( resource_name=name, function_name='appname-dev-%s' % name, environment_variables={}, runtime='python2.7', handler='app.app', tags={}, timeout=60, memory_size=128, deployment_package=models.DeploymentPackage( models.Placeholder.BUILD_STAGE ), xray=False, role=models.PreCreatedIAMRole(role_arn='role:arn'), security_group_ids=[], subnet_ids=[], layers=[], reserved_concurrency=None, ) class TestDependencyBuilder(object): def test_can_build_resource_with_single_dep(self): role = models.PreCreatedIAMRole(role_arn='foo') app = models.Application(stage='dev', resources=[role]) dep_builder = DependencyBuilder() deps = dep_builder.build_dependencies(app) assert deps == [role] def test_can_build_resource_with_dag_deps(self): shared_leaf = LeafResource(name='leaf-resource') first_parent = FooResource(name='first', leaf=shared_leaf) second_parent = FooResource(name='second', leaf=shared_leaf) app = models.Application( stage='dev', resources=[first_parent, second_parent]) dep_builder = DependencyBuilder() deps = dep_builder.build_dependencies(app) assert deps == [shared_leaf, first_parent, second_parent] def test_is_first_element_in_list(self): shared_leaf = LeafResource(name='leaf-resource') first_parent = FooResource(name='first', leaf=shared_leaf) app = models.Application( stage='dev', resources=[first_parent, shared_leaf], ) dep_builder = DependencyBuilder() deps = dep_builder.build_dependencies(app) assert deps == [shared_leaf, first_parent] def test_can_compares_with_identity_not_equality(self): first_leaf = LeafResource(name='same-name') second_leaf = LeafResource(name='same-name') first_parent = FooResource(name='first', leaf=first_leaf) second_parent = FooResource(name='second', leaf=second_leaf) app = models.Application( stage='dev', resources=[first_parent, second_parent]) dep_builder = DependencyBuilder() deps = dep_builder.build_dependencies(app) assert deps == [first_leaf, first_parent, second_leaf, second_parent] def test_no_duplicate_depedencies(self): leaf = LeafResource(name='leaf') second_parent = FooResource(name='second', leaf=leaf) first_parent = FooResource(name='first', leaf=[leaf, second_parent]) app = models.Application( stage='dev', resources=[first_parent]) dep_builder = DependencyBuilder() deps = dep_builder.build_dependencies(app) assert deps == [leaf, second_parent, first_parent] class RoleTestCase(object): def __init__(self, given, roles, app_name='appname'): self.given = given self.roles = roles self.app_name = app_name def build(self): app = Chalice(self.app_name) for name in self.given: def foo(event, context): return {} foo.__name__ = name app.lambda_function(name)(foo) user_provided_params = { 'chalice_app': app, 'app_name': self.app_name, 'project_dir': '.', } lambda_functions = {} for key, value in self.given.items(): lambda_functions[key] = value config_from_disk = { 'stages': { 'dev': { 'lambda_functions': lambda_functions, } } } config = Config(chalice_stage='dev', user_provided_params=user_provided_params, config_from_disk=config_from_disk) return app, config def assert_required_roles_created(self, application): resources = application.resources assert len(resources) == len(self.given) functions_by_name = { f.function_name: f for f in resources if isinstance(f, models.LambdaFunction)} # Roles that have the same name/arn should be the same # object. If we encounter a role that's already in # roles_by_identifier, we'll verify that it's the exact same object. roles_by_identifier = {} for function_name, expected in self.roles.items(): full_name = 'appname-dev-%s' % function_name assert full_name in functions_by_name actual_role = functions_by_name[full_name].role expectations = self.roles[function_name] if not expectations.get('managed_role', True): actual_role_arn = actual_role.role_arn assert isinstance(actual_role, models.PreCreatedIAMRole) assert expectations['iam_role_arn'] == actual_role_arn if actual_role_arn in roles_by_identifier: assert roles_by_identifier[actual_role_arn] is actual_role roles_by_identifier[actual_role_arn] = actual_role continue actual_name = actual_role.role_name assert expectations['name'] == actual_name if actual_name in roles_by_identifier: assert roles_by_identifier[actual_name] is actual_role roles_by_identifier[actual_name] = actual_role is_autogenerated = expectations.get('autogenerated', False) policy_file = expectations.get('policy_file') if is_autogenerated: assert isinstance(actual_role, models.ManagedIAMRole) assert isinstance(actual_role.policy, models.AutoGenIAMPolicy) if policy_file is not None and not is_autogenerated: assert isinstance(actual_role, models.ManagedIAMRole) assert isinstance(actual_role.policy, models.FileBasedIAMPolicy) assert actual_role.policy.filename == os.path.join( '.', '.chalice', expectations['policy_file']) # How to read these tests: # 'given' is a mapping of lambda function name to config values. # 'roles' is a mapping of lambda function to expected attributes # of the role associated with the given function. # The first test case is explained in more detail as an example. ROLE_TEST_CASES = [ # Default case, we use the shared 'appname-dev' role. RoleTestCase( # Given we have a lambda function in our app.py named 'a', # and we have our config file state that the 'a' function # should have an autogen'd policy, given={'a': {'autogen_policy': True}}, # then we expect the IAM role associated with the lambda # function 'a' should be named 'appname-dev', and it should # be an autogenerated role/policy. roles={'a': {'name': 'appname-dev', 'autogenerated': True}}), # If you specify an explicit policy, we generate a function # specific role. RoleTestCase( given={'a': {'autogen_policy': False, 'iam_policy_file': 'mypolicy.json'}}, roles={'a': {'name': 'appname-dev-a', 'autogenerated': False, 'policy_file': 'mypolicy.json'}}), # Multiple lambda functions that use autogen policies share # the same 'appname-dev' role. RoleTestCase( given={'a': {'autogen_policy': True}, 'b': {'autogen_policy': True}}, roles={'a': {'name': 'appname-dev'}, 'b': {'name': 'appname-dev'}}), # Multiple lambda functions with separate policies result # in separate roles. RoleTestCase( given={'a': {'autogen_policy': False, 'iam_policy_file': 'a.json'}, 'b': {'autogen_policy': False, 'iam_policy_file': 'b.json'}}, roles={'a': {'name': 'appname-dev-a', 'autogenerated': False, 'policy_file': 'a.json'}, 'b': {'name': 'appname-dev-b', 'autogenerated': False, 'policy_file': 'b.json'}}), # You can mix autogen and explicit policy files. Autogen will # always use the '{app}-{stage}' role. RoleTestCase( given={'a': {'autogen_policy': True}, 'b': {'autogen_policy': False, 'iam_policy_file': 'b.json'}}, roles={'a': {'name': 'appname-dev', 'autogenerated': True}, 'b': {'name': 'appname-dev-b', 'autogenerated': False, 'policy_file': 'b.json'}}), # Default location if no policy file is given is # policy-dev.json RoleTestCase( given={'a': {'autogen_policy': False}}, roles={'a': {'name': 'appname-dev-a', 'autogenerated': False, 'policy_file': 'policy-dev.json'}}), # As soon as autogen_policy is false, we will *always* # create a function specific role. RoleTestCase( given={'a': {'autogen_policy': False}, 'b': {'autogen_policy': True}}, roles={'a': {'name': 'appname-dev-a', 'autogenerated': False, 'policy_file': 'policy-dev.json'}, 'b': {'name': 'appname-dev'}}), RoleTestCase( given={'a': {'manage_iam_role': False, 'iam_role_arn': 'role:arn'}}, # 'managed_role' will verify the associated role is a # models.PreCreatedIAMRoleType with the provided iam_role_arn. roles={'a': {'managed_role': False, 'iam_role_arn': 'role:arn'}}), # Verify that we can use the same non-managed role for multiple # lambda functions. RoleTestCase( given={'a': {'manage_iam_role': False, 'iam_role_arn': 'role:arn'}, 'b': {'manage_iam_role': False, 'iam_role_arn': 'role:arn'}}, roles={'a': {'managed_role': False, 'iam_role_arn': 'role:arn'}, 'b': {'managed_role': False, 'iam_role_arn': 'role:arn'}}), RoleTestCase( given={'a': {'manage_iam_role': False, 'iam_role_arn': 'role:arn'}, 'b': {'autogen_policy': True}}, roles={'a': {'managed_role': False, 'iam_role_arn': 'role:arn'}, 'b': {'name': 'appname-dev', 'autogenerated': True}}), # Functions that mix all four options: RoleTestCase( # 2 functions with autogen'd policies. given={ 'a': {'autogen_policy': True}, 'b': {'autogen_policy': True}, # 2 functions with various iam role arns. 'c': {'manage_iam_role': False, 'iam_role_arn': 'role:arn'}, 'd': {'manage_iam_role': False, 'iam_role_arn': 'role:arn2'}, # A function with a default filename for a policy. 'e': {'autogen_policy': False}, # Even though this uses the same policy as 'e', we will # still create a new role. This could be optimized in the # future. 'f': {'autogen_policy': False}, # And finally 2 functions that have their own policy files. 'g': {'autogen_policy': False, 'iam_policy_file': 'g.json'}, 'h': {'autogen_policy': False, 'iam_policy_file': 'h.json'} }, roles={ 'a': {'name': 'appname-dev', 'autogenerated': True}, 'b': {'name': 'appname-dev', 'autogenerated': True}, 'c': {'managed_role': False, 'iam_role_arn': 'role:arn'}, 'd': {'managed_role': False, 'iam_role_arn': 'role:arn2'}, 'e': {'name': 'appname-dev-e', 'autogenerated': False, 'policy_file': 'policy-dev.json'}, 'f': {'name': 'appname-dev-f', 'autogenerated': False, 'policy_file': 'policy-dev.json'}, 'g': {'name': 'appname-dev-g', 'autogenerated': False, 'policy_file': 'g.json'}, 'h': {'name': 'appname-dev-h', 'autogenerated': False, 'policy_file': 'h.json'}, }), ] @pytest.mark.parametrize('case', ROLE_TEST_CASES) def test_role_creation(case): _, config = case.build() builder = ApplicationGraphBuilder() application = builder.build(config, stage_name='dev') case.assert_required_roles_created(application) class TestDefaultsInjector(object): def test_inject_when_values_are_none(self): injector = InjectDefaults( lambda_timeout=100, lambda_memory_size=512, ) function = models.LambdaFunction( # The timeout/memory_size are set to # None, so the injector should fill them # in the with the default values above. timeout=None, memory_size=None, resource_name='foo', function_name='app-dev-foo', environment_variables={}, runtime='python2.7', handler='app.app', tags={}, xray=None, deployment_package=None, role=None, security_group_ids=[], subnet_ids=[], layers=[], reserved_concurrency=None, ) config = Config.create() injector.handle(config, function) assert function.timeout == 100 assert function.memory_size == 512 def test_no_injection_when_values_are_set(self): injector = InjectDefaults( lambda_timeout=100, lambda_memory_size=512, ) function = models.LambdaFunction( # The timeout/memory_size are set to # None, so the injector should fill them # in the with the default values above. timeout=1, memory_size=1, resource_name='foo', function_name='app-stage-foo', environment_variables={}, runtime='python2.7', handler='app.app', tags={}, xray=None, deployment_package=None, role=None, security_group_ids=[], subnet_ids=[], layers=[], reserved_concurrency=None, ) config = Config.create() injector.handle(config, function) assert function.timeout == 1 assert function.memory_size == 1 def test_default_tls_version_on_domain_name(self): injector = InjectDefaults(tls_version='TLS_1_2') domain_name = models.DomainName( resource_name='my_domain_name', domain_name='example.com', protocol=models.APIType.HTTP, certificate_arn='myarn', api_mapping=models.APIMapping(resource_name='mymapping', mount_path='(none)', api_gateway_stage='api') ) config = Config.create() injector.handle(config, domain_name) assert domain_name.tls_version == models.TLSVersion.TLS_1_2 class TestPolicyGeneratorStage(object): def setup_method(self): self.osutils = mock.Mock(spec=OSUtils) def create_policy_generator(self, generator=None): if generator is None: generator = mock.Mock(spec=AppPolicyGenerator) p = PolicyGenerator(generator, self.osutils) return p def test_invokes_policy_generator(self): generator = mock.Mock(spec=AppPolicyGenerator) generator.generate_policy.return_value = {'policy': 'doc'} policy = models.AutoGenIAMPolicy(models.Placeholder.BUILD_STAGE) config = Config.create() p = self.create_policy_generator(generator) p.handle(config, policy) assert policy.document == {'policy': 'doc'} def test_no_policy_generated_if_exists(self): generator = mock.Mock(spec=AppPolicyGenerator) generator.generate_policy.return_value = {'policy': 'new'} policy = models.AutoGenIAMPolicy(document={'policy': 'original'}) config = Config.create() p = self.create_policy_generator(generator) p.handle(config, policy) assert policy.document == {'policy': 'original'} assert not generator.generate_policy.called def test_policy_loaded_from_file_if_needed(self): p = self.create_policy_generator() policy = models.FileBasedIAMPolicy( filename='foo.json', document=models.Placeholder.BUILD_STAGE) self.osutils.get_file_contents.return_value = '{"iam": "policy"}' p.handle(Config.create(), policy) assert policy.document == {'iam': 'policy'} self.osutils.get_file_contents.assert_called_with('foo.json') def test_error_raised_if_file_policy_not_exists(self): p = self.create_policy_generator() policy = models.FileBasedIAMPolicy( filename='foo.json', document=models.Placeholder.BUILD_STAGE) self.osutils.get_file_contents.side_effect = IOError() with pytest.raises(RuntimeError): p.handle(Config.create(), policy) def test_vpc_policy_inject_if_needed(self): generator = mock.Mock(spec=AppPolicyGenerator) generator.generate_policy.return_value = {'Statement': []} policy = models.AutoGenIAMPolicy( document=models.Placeholder.BUILD_STAGE, traits=set([models.RoleTraits.VPC_NEEDED]), ) config = Config.create() p = self.create_policy_generator(generator) p.handle(config, policy) assert policy.document['Statement'][0] == VPC_ATTACH_POLICY class TestSwaggerBuilder(object): def test_can_generate_swagger_builder(self): generator = mock.Mock(spec=SwaggerGenerator) generator.generate_swagger.return_value = {'swagger': '2.0'} rest_api = models.RestAPI( resource_name='foo', swagger_doc=models.Placeholder.BUILD_STAGE, minimum_compression='', endpoint_type='EDGE', api_gateway_stage='api', lambda_function=None, xray=False, ) app = Chalice(app_name='foo') config = Config.create(chalice_app=app) p = SwaggerBuilder(generator) p.handle(config, rest_api) assert rest_api.swagger_doc == {'swagger': '2.0'} generator.generate_swagger.assert_called_with(app, rest_api) class TestDeploymentPackager(object): def test_can_generate_layer_package(self): function = create_function_resource('myfunction') function.managed_layer = models.LambdaLayer( resource_name='managed-layer', layer_name='appname-dev-managed-layer', runtime='python2.7', deployment_package=models.DeploymentPackage( models.Placeholder.BUILD_STAGE ) ) lambda_packager = mock.Mock(spec=packager.BaseLambdaDeploymentPackager) layer_packager = mock.Mock(spec=packager.BaseLambdaDeploymentPackager) lambda_packager.create_deployment_package.return_value = 'package.zip' layer_packager.create_deployment_package.return_value = ( 'package-layer.zip') config = Config.create(project_dir='.') p = ManagedLayerDeploymentPackager(lambda_packager, layer_packager) p.handle(config, function.managed_layer) p.handle(config, function) assert function.deployment_package.filename == 'package.zip' lambda_packager.create_deployment_package.assert_called_with( '.', config.lambda_python_version ) assert function.managed_layer.deployment_package.filename == ( 'package-layer.zip' ) layer_packager.create_deployment_package.assert_called_with( '.', config.lambda_python_version ) def test_layer_package_not_generated_if_filename_populated(self): generator = mock.Mock(spec=packager.BaseLambdaDeploymentPackager) function = create_function_resource('myfunction') layer = models.LambdaLayer( resource_name='layer', layer_name='name', runtime='python2.7', deployment_package=models.DeploymentPackage( filename='original.zip') ) function.managed_layer = layer config = Config.create(project_dir='.') p = ManagedLayerDeploymentPackager(None, generator) p.handle(config, layer) assert layer.deployment_package.filename == 'original.zip' assert not generator.create_deployment_package.called def test_managed_layer_removed_if_no_deps(self): function = create_function_resource('myfunction') function.managed_layer = models.LambdaLayer( resource_name='managed-layer', layer_name='appname-dev-managed-layer', runtime='python2.7', deployment_package=models.DeploymentPackage( models.Placeholder.BUILD_STAGE ) ) lambda_packager = mock.Mock(spec=packager.BaseLambdaDeploymentPackager) layer_packager = mock.Mock(spec=packager.BaseLambdaDeploymentPackager) lambda_packager.create_deployment_package.return_value = 'package.zip' layer_packager.create_deployment_package.side_effect = \ packager.EmptyPackageError() config = Config.create(project_dir='.') p = ManagedLayerDeploymentPackager(lambda_packager, layer_packager) p.handle(config, function.managed_layer) p.handle(config, function) # If the deployment package for layers would result in an empty # deployment package, we expect that resource to be removed, it can't # be created on the service. assert function.managed_layer is None def test_can_generate_package(self): generator = mock.Mock(spec=packager.LambdaDeploymentPackager) generator.create_deployment_package.return_value = 'package.zip' package = models.DeploymentPackage(models.Placeholder.BUILD_STAGE) config = Config.create() p = DeploymentPackager(generator) p.handle(config, package) assert package.filename == 'package.zip' def test_package_not_generated_if_filename_populated(self): generator = mock.Mock(spec=packager.LambdaDeploymentPackager) generator.create_deployment_package.return_value = 'NEWPACKAGE.zip' package = models.DeploymentPackage(filename='original-name.zip') config = Config.create() p = DeploymentPackager(generator) p.handle(config, package) assert package.filename == 'original-name.zip' assert not generator.create_deployment_package.called def test_build_stage(): first = mock.Mock(spec=BaseDeployStep) second = mock.Mock(spec=BaseDeployStep) build = BuildStage([first, second]) foo_resource = mock.sentinel.foo_resource bar_resource = mock.sentinel.bar_resource config = Config.create() build.execute(config, [foo_resource, bar_resource]) assert first.handle.call_args_list == [ mock.call(config, foo_resource), mock.call(config, bar_resource), ] assert second.handle.call_args_list == [ mock.call(config, foo_resource), mock.call(config, bar_resource), ] class TestDeployer(unittest.TestCase): def setUp(self): self.resource_builder = mock.Mock(spec=ApplicationGraphBuilder) self.deps_builder = mock.Mock(spec=DependencyBuilder) self.build_stage = mock.Mock(spec=BuildStage) self.plan_stage = mock.Mock(spec=PlanStage) self.sweeper = mock.Mock(spec=ResourceSweeper) self.executor = mock.Mock(spec=Executor) self.recorder = mock.Mock(spec=ResultsRecorder) self.chalice_app = Chalice(app_name='foo') def create_deployer(self): return Deployer( self.resource_builder, self.deps_builder, self.build_stage, self.plan_stage, self.sweeper, self.executor, self.recorder, ) def test_deploy_delegates_properly(self): app = mock.Mock(spec=models.Application) resources = [mock.Mock(spec=models.Model)] api_calls = [mock.Mock(spec=APICall)] self.resource_builder.build.return_value = app self.deps_builder.build_dependencies.return_value = resources self.plan_stage.execute.return_value = api_calls self.executor.resource_values = {'foo': {'name': 'bar'}} deployer = self.create_deployer() config = Config.create(project_dir='.', chalice_app=self.chalice_app) result = deployer.deploy(config, 'dev') self.resource_builder.build.assert_called_with(config, 'dev') self.deps_builder.build_dependencies.assert_called_with(app) self.build_stage.execute.assert_called_with(config, resources) self.plan_stage.execute.assert_called_with(resources) self.sweeper.execute.assert_called_with(api_calls, config) self.executor.execute.assert_called_with(api_calls) expected_result = { 'resources': {'foo': {'name': 'bar'}}, 'schema_version': '2.0', 'backend': 'api', } self.recorder.record_results.assert_called_with( expected_result, 'dev', '.') assert result == expected_result def test_deploy_errors_raises_chalice_error(self): self.resource_builder.build.side_effect = AWSClientError() deployer = self.create_deployer() config = Config.create(project_dir='.', chalice_app=self.chalice_app) with pytest.raises(ChaliceDeploymentError): deployer.deploy(config, 'dev') def test_validation_errors_raise_failure(self): @self.chalice_app.route('') def bad_route_empty_string(): return {} deployer = self.create_deployer() config = Config.create(project_dir='.', chalice_app=self.chalice_app) with pytest.raises(ChaliceDeploymentError): deployer.deploy(config, 'dev') def test_can_create_default_deployer(): session = botocore.session.get_session() deployer = create_default_deployer(session, Config.create( project_dir='.', chalice_stage='dev', ), UI()) assert isinstance(deployer, Deployer) def test_can_create_deployer_with_layer_builds(): session = botocore.session.get_session() deployer = create_default_deployer(session, Config.create( project_dir='.', chalice_stage='dev', automatic_layer=True, ), UI()) assert isinstance(deployer, Deployer) def test_can_create_deletion_deployer(): session = botocore.session.get_session() deployer = create_deletion_deployer(TypedAWSClient(session), UI()) assert isinstance(deployer, Deployer) def test_templated_swagger_generator(sample_app): doc = TemplatedSwaggerGenerator().generate_swagger(sample_app) uri = doc['paths']['/']['get']['x-amazon-apigateway-integration']['uri'] assert isinstance(uri, StringFormat) assert uri.template == ( 'arn:{partition}:apigateway:{region_name}:lambda:path' '/2015-03-31/functions/{api_handler_lambda_arn}/invocations' ) assert uri.variables == ['partition', 'region_name', 'api_handler_lambda_arn'] def test_templated_swagger_with_auth_uri(sample_app_with_auth): doc = TemplatedSwaggerGenerator().generate_swagger(sample_app_with_auth) uri = doc['securityDefinitions']['myauth'][ 'x-amazon-apigateway-authorizer']['authorizerUri'] assert isinstance(uri, StringFormat) assert uri.template == ( 'arn:{partition}:apigateway:{region_name}:lambda:path' '/2015-03-31/functions/{myauth_lambda_arn}/invocations' ) assert uri.variables == ['partition', 'region_name', 'myauth_lambda_arn'] class TestRecordResults(object): def setup_method(self): self.osutils = mock.Mock(spec=OSUtils) self.recorder = ResultsRecorder(self.osutils) self.deployed_values = { 'stages': { 'dev': {'resources': []}, }, 'schema_version': '2.0', } self.osutils.joinpath = os.path.join self.deployed_dir = os.path.join('.', '.chalice', 'deployed') def test_can_record_results_initial_deploy(self): expected_filename = os.path.join(self.deployed_dir, 'dev.json') self.osutils.file_exists.return_value = False self.osutils.directory_exists.return_value = False self.recorder.record_results( self.deployed_values, 'dev', '.', ) expected_contents = serialize_to_json(self.deployed_values) # Verify we created the deployed dir on an initial deploy. self.osutils.makedirs.assert_called_with(self.deployed_dir) self.osutils.set_file_contents.assert_called_with( filename=expected_filename, contents=expected_contents, binary=False ) class TestDeploymentReporter(object): def setup_method(self): self.ui = mock.Mock(spec=UI) self.reporter = DeploymentReporter(ui=self.ui) def test_can_generate_report(self): certificate_arn = "arn:aws:acm:us-east-1:account_id:" \ "certificate/e2600f49-f6b7-4105-aaf6-63b2f018a030" deployed_values = { "resources": [ {"role_name": "james2-dev", "role_arn": "my-role-arn", "name": "default-role", "resource_type": "iam_role"}, {"resource_type": "lambda_layer", "name": "layer", "layer_version_arn": "arn:layer:4"}, {"lambda_arn": "lambda-arn-foo", "name": "foo", "resource_type": "lambda_function"}, {"lambda_arn": "lambda-arn-dev", "name": "api_handler", "resource_type": "lambda_function"}, {"name": "rest_api", "rest_api_id": "rest_api_id", "rest_api_url": "https://host/api", "resource_type": "rest_api"}, {"name": "websocket_api", "websocket_api_id": "websocket_api_id", "websocket_api_url": "wss://host/api", "resource_type": "websocket_api"}, {"name": "api_gateway_custom_domain", "resource_type": "domain_name", "hosted_zone_id": "A1FDTDATADATA0", "certificate_arn": certificate_arn, "alias_domain_name": "alias.domain.com", "security_policy": "TLS_1_0", "domain_name": "api.domain", "api_mapping": [ { "key": "/test1" } ]} ], } report = self.reporter.generate_report(deployed_values) assert report == ( "Resources deployed:\n" " - Lambda Layer ARN: arn:layer:4\n" " - Lambda ARN: lambda-arn-foo\n" " - Lambda ARN: lambda-arn-dev\n" " - Rest API URL: https://host/api\n" " - Websocket API URL: wss://host/api\n" " - Custom domain name:\n" " HostedZoneId: A1FDTDATADATA0\n" " AliasDomainName: alias.domain.com\n" ) def test_can_display_report(self): deployed_values = { 'resources': [] } self.reporter.display_report(deployed_values) self.ui.write.assert_called_with('Resources deployed:\n') class TestLambdaEventSourcePolicyInjector(object): def create_model_from_app(self, app, config): builder = ApplicationGraphBuilder() application = builder.build(config, stage_name='dev') return application.resources[0] def test_can_inject_policy(self, sample_sqs_event_app): config = Config.create(chalice_app=sample_sqs_event_app, autogen_policy=True, project_dir='.') event_source = self.create_model_from_app(sample_sqs_event_app, config) role = event_source.lambda_function.role role.policy.document = {'Statement': []} injector = LambdaEventSourcePolicyInjector() injector.handle(config, event_source) assert role.policy.document == { 'Statement': [SQS_EVENT_SOURCE_POLICY.copy()], } def test_no_inject_if_not_autogen_policy(self, sample_sqs_event_app): config = Config.create(chalice_app=sample_sqs_event_app, autogen_policy=False, project_dir='.') event_source = self.create_model_from_app(sample_sqs_event_app, config) role = event_source.lambda_function.role role.policy.document = {'Statement': []} injector = LambdaEventSourcePolicyInjector() injector.handle(config, event_source) assert role.policy.document == {'Statement': []} def test_no_inject_is_already_injected(self, sample_sqs_event_app): @sample_sqs_event_app.on_sqs_message(queue='second-queue') def second_handler(event): pass config = Config.create(chalice_app=sample_sqs_event_app, autogen_policy=True, project_dir='.') builder = ApplicationGraphBuilder() application = builder.build(config, stage_name='dev') event_sources = application.resources role = event_sources[1].lambda_function.role role.policy.document = {'Statement': []} injector = LambdaEventSourcePolicyInjector() injector.handle(config, event_sources[0]) injector.handle(config, event_sources[1]) # Even though we have two queue handlers, we only need to # inject the policy once. assert role.policy.document == { 'Statement': [SQS_EVENT_SOURCE_POLICY.copy()], } def test_can_inject_policy_for_kinesis(self, sample_kinesis_event_app): config = Config.create(chalice_app=sample_kinesis_event_app, autogen_policy=True, project_dir='.') event_source = self.create_model_from_app(sample_kinesis_event_app, config) role = event_source.lambda_function.role role.policy.document = {'Statement': []} injector = LambdaEventSourcePolicyInjector() injector.handle(config, event_source) assert role.policy.document == { 'Statement': [KINESIS_EVENT_SOURCE_POLICY], } def test_can_inject_policy_for_ddb(self, sample_ddb_event_app): config = Config.create(chalice_app=sample_ddb_event_app, autogen_policy=True, project_dir='.') event_source = self.create_model_from_app(sample_ddb_event_app, config) role = event_source.lambda_function.role role.policy.document = {'Statement': []} injector = LambdaEventSourcePolicyInjector() injector.handle(config, event_source) assert role.policy.document == { 'Statement': [DDB_EVENT_SOURCE_POLICY], } class TestWebsocketPolicyInjector(object): def create_model_from_app(self, app, config): builder = ApplicationGraphBuilder() application = builder.build(config, stage_name='dev') return application.resources[0] def test_can_inject_policy(self, sample_websocket_app): config = Config.create(chalice_app=sample_websocket_app, autogen_policy=True, project_dir='.') event_source = self.create_model_from_app( sample_websocket_app, config) role = event_source.connect_function.role role.policy.document = {'Statement': []} injector = WebsocketPolicyInjector() injector.handle(config, event_source) assert role.policy.document == { 'Statement': [POST_TO_WEBSOCKET_CONNECTION_POLICY.copy()], } def test_no_inject_if_not_autogen_policy(self, sample_websocket_app): config = Config.create(chalice_app=sample_websocket_app, autogen_policy=False, project_dir='.') event_source = self.create_model_from_app(sample_websocket_app, config) role = event_source.connect_function.role role.policy.document = {'Statement': []} injector = LambdaEventSourcePolicyInjector() injector.handle(config, event_source) assert role.policy.document == {'Statement': []} ================================================ FILE: tests/unit/deploy/test_executor.py ================================================ import re from unittest import mock import pytest from chalice.awsclient import TypedAWSClient from chalice.deploy import models from chalice.deploy.executor import Executor, UnresolvedValueError, \ VariableResolver, DisplayOnlyExecutor from chalice.deploy.models import APICall, RecordResourceVariable, \ RecordResourceValue, StoreValue, JPSearch, BuiltinFunction, Instruction, \ CopyVariable from chalice.deploy.planner import Variable, StringFormat, KeyDataVariable from chalice.utils import UI class TestExecutor(object): def setup_method(self): self.mock_client = mock.Mock(spec=TypedAWSClient) self.mock_client.endpoint_dns_suffix.return_value = 'amazonaws.com' self.ui = mock.Mock(spec=UI) self.executor = Executor(self.mock_client, self.ui) def execute(self, instructions, messages=None): if messages is None: messages = {} self.executor.execute(models.Plan(instructions, messages)) def test_can_invoke_api_call_with_no_output(self): params = {'name': 'foo', 'trust_policy': {'trust': 'policy'}, 'policy': {'iam': 'policy'}} call = APICall('create_role', params) self.execute([call]) self.mock_client.create_role.assert_called_with(**params) def test_can_store_api_result(self): params = {'name': 'foo', 'trust_policy': {'trust': 'policy'}, 'policy': {'iam': 'policy'}} apicall = APICall('create_role', params, output_var='my_variable_name') self.mock_client.create_role.return_value = 'myrole:arn' self.execute([apicall]) assert self.executor.variables['my_variable_name'] == 'myrole:arn' def test_can_store_multiple_value(self): instruction = models.StoreMultipleValue( name='list_data', value=['first_elem'] ) self.execute([instruction]) assert self.executor.variables['list_data'] == ['first_elem'] instruction = models.StoreMultipleValue( name='list_data', value=['second_elem'] ) self.execute([instruction]) assert self.executor.variables['list_data'] == [ 'first_elem', 'second_elem' ] def test_can_reference_stored_results_in_api_calls(self): params = { 'name': Variable('role_name'), 'trust_policy': {'trust': 'policy'}, 'policy': {'iam': 'policy'} } call = APICall('create_role', params) self.mock_client.create_role.return_value = 'myrole:arn' self.executor.variables['role_name'] = 'myrole-name' self.execute([call]) self.mock_client.create_role.assert_called_with( name='myrole-name', trust_policy={'trust': 'policy'}, policy={'iam': 'policy'}, ) def test_can_return_created_resources(self): params = {} call = APICall('create_function', params, output_var='myfunction_arn') self.mock_client.create_function.return_value = 'function:arn' record_instruction = RecordResourceVariable( resource_type='lambda_function', resource_name='myfunction', name='myfunction_arn', variable_name='myfunction_arn', ) self.execute([call, record_instruction]) assert self.executor.resource_values == [{ 'name': 'myfunction', 'myfunction_arn': 'function:arn', 'resource_type': 'lambda_function', }] def test_can_reference_varname(self): self.mock_client.create_function.return_value = 'function:arn' self.execute([ APICall('create_function', {}, output_var='myvarname'), RecordResourceVariable( resource_type='lambda_function', resource_name='myfunction', name='myfunction_arn', variable_name='myvarname', ), ]) assert self.executor.resource_values == [{ 'name': 'myfunction', 'resource_type': 'lambda_function', 'myfunction_arn': 'function:arn', }] def test_can_record_value_directly(self): self.execute([ RecordResourceValue( resource_type='lambda_function', resource_name='myfunction', name='myfunction_arn', value='arn:foo', ) ]) assert self.executor.resource_values == [{ 'name': 'myfunction', 'resource_type': 'lambda_function', 'myfunction_arn': 'arn:foo', }] def test_can_aggregate_multiple_resource_values(self): self.execute([ RecordResourceValue( resource_type='lambda_function', resource_name='myfunction', name='key1', value='value1', ), RecordResourceValue( resource_type='lambda_function', resource_name='myfunction', name='key2', value='value2', ) ]) assert self.executor.resource_values == [{ 'name': 'myfunction', 'resource_type': 'lambda_function', 'key1': 'value1', 'key2': 'value2', }] def test_new_keys_override_old_keys(self): self.execute([ RecordResourceValue( resource_type='lambda_function', resource_name='myfunction', name='key1', value='OLD', ), RecordResourceValue( resource_type='lambda_function', resource_name='myfunction', name='key1', value='NEW', ) ]) assert self.executor.resource_values == [{ 'name': 'myfunction', 'resource_type': 'lambda_function', 'key1': 'NEW', }] def test_validates_no_unresolved_deploy_vars(self): params = {'zip_contents': models.Placeholder.BUILD_STAGE} call = APICall('create_function', params) self.mock_client.create_function.return_value = 'function:arn' # We should raise an exception because a param has # a models.Placeholder.BUILD_STAGE value which should have # been handled in an earlier stage. with pytest.raises(UnresolvedValueError): self.execute([call]) def test_can_jp_search(self): self.execute([ StoreValue(name='searchval', value={'foo': {'bar': 'baz'}}), JPSearch('foo.bar', input_var='searchval', output_var='result'), ]) assert self.executor.variables['result'] == 'baz' def test_can_copy_variable(self): self.execute([ StoreValue(name='foo', value='bar'), CopyVariable(from_var='foo', to_var='baz'), ]) assert self.executor.variables['baz'] == 'bar' def test_can_call_builtin_function(self): self.execute([ StoreValue( name='my_arn', value='arn:aws:lambda:us-west-2:123:function:name'), BuiltinFunction( function_name='parse_arn', args=[Variable('my_arn')], output_var='result', ) ]) assert self.executor.variables['result'] == { 'partition': 'aws', 'account_id': '123', 'region': 'us-west-2', 'service': 'lambda', 'dns_suffix': 'amazonaws.com' } def test_built_in_function_interrogate_profile(self): self.mock_client.region_name = 'us-west-2' self.mock_client.partition_name = 'aws' self.execute([ BuiltinFunction( function_name='interrogate_profile', args=[], output_var='result', ) ]) assert self.executor.variables['result'] == { 'partition': 'aws', 'region': 'us-west-2', 'dns_suffix': 'amazonaws.com' } def test_built_in_function_service_principal(self): self.mock_client.region_name = 'us-west-2' self.mock_client.partition_name = 'aws' self.mock_client.service_principal.return_value = \ 'apigateway.amazonaws.com' self.execute([ BuiltinFunction( function_name='service_principal', args=['apigateway'], output_var='result', ) ]) self.mock_client.service_principal \ .assert_called_once_with('apigateway', 'us-west-2', 'amazonaws.com') assert self.executor.variables['result'] == { 'principal': 'apigateway.amazonaws.com' } def test_errors_out_on_unknown_function(self): with pytest.raises(ValueError): self.execute([ BuiltinFunction( function_name='unknown_foo', args=[], output_var=None, ) ]) def test_can_print_ui_messages(self): params = {'name': 'foo', 'trust_policy': {'trust': 'policy'}, 'policy': {'iam': 'policy'}} call = APICall('create_role', params) messages = {id(call): 'Creating role'} self.execute([call], messages) self.mock_client.create_role.assert_called_with(**params) self.ui.write.assert_called_with('Creating role') def test_error_out_on_unknown_instruction(self): class CustomInstruction(Instruction): pass with pytest.raises(RuntimeError): self.execute([CustomInstruction()]) class TestDisplayOnlyExecutor(object): # Note: This executor doesn't have any guarantees on its output, # it's primarily to help debug/understand chalice. The tests here # check the basic structure of the output, but try to not be overly strict. def setup_method(self): self.mock_client = mock.Mock(spec=TypedAWSClient) self.ui = mock.Mock(spec=UI) self.executor = DisplayOnlyExecutor(self.mock_client, self.ui) def execute(self, instructions, messages=None): if messages is None: messages = {} self.executor.execute(models.Plan(instructions, messages)) def get_plan_output(self, instructions): self.executor.execute(models.Plan(instructions, {})) return ''.join(args[0][0] for args in self.ui.write.call_args_list) def test_can_display_plan(self): params = {'name': 'foo', 'trust_policy': {'trust': 'policy'}, 'policy': {'iam': 'policy'}} call = APICall('create_role', params) plan_output = self.get_plan_output([call]) # Should have a plan title. assert plan_output.startswith('Plan\n====') # Should print the api call in upper camel case. assert 'API_CALL' in plan_output # Should print the name of the method in the plan. assert 'method_name: create_role' in plan_output # Should print out the api call arguments in output. assert 'name: foo' in plan_output # The values for these are in the tests for the variable pool. assert 'trust_policy: ' in plan_output assert 'policy: ' in plan_output def test_variable_pool_printed_if_needed(self): params = {'name': 'foo', 'policy': {'iam': 'policy'}} call = APICall('create_role', params) plan_output = self.get_plan_output([call]) # Dictionaries for param values are printed at the end so they # don't clutter the plan output. We should see a placeholder here. assert 'policy: ${POLICY_0}' in plan_output assert 'Variable Pool' in plan_output assert "${POLICY_0}:\n{'iam': 'policy'}" in plan_output def test_variable_pool_omitted_if_empty(self): params = {'name': 'foo'} call = APICall('create_role', params) plan_output = self.get_plan_output([call]) assert 'Variable Pool' not in plan_output def test_byte_value_replaced_if_over_length(self): params = {'name': 'foo', 'zip_contents': b'\x01' * 50} call = APICall('create_role', params) plan_output = self.get_plan_output([call]) assert 'zip_contents: ' in plan_output def test_can_print_multiple_instructions(self): instructions = [ JPSearch(expression='foo.bar', input_var='in1', output_var='out1'), JPSearch(expression='foo.baz', input_var='in2', output_var='out2'), ] plan_output = self.get_plan_output(instructions) # Use a regex to ensure they're printed in order. assert re.search( 'JP_SEARCH.*expression: foo.bar.*' 'JP_SEARCH.*expression: foo.baz', plan_output, re.MULTILINE | re.DOTALL ) is not None def test_empty_values_omitted(self): params = {'name': 'foo', 'empty_list': [], 'empty_dict': {}, 'empty_str': ''} call = APICall('create_role', params) plan_output = self.get_plan_output([call]) assert 'empty_list' not in plan_output assert 'empty_dict' not in plan_output assert 'empty_str' not in plan_output class TestResolveVariables(object): def resolve_vars(self, params, variables): return VariableResolver().resolve_variables( params, variables ) def test_resolve_top_level_vars(self): assert self.resolve_vars( {'foo': Variable('myvar')}, {'myvar': 'value'} ) == {'foo': 'value'} def test_can_resolve_multiple_vars(self): assert self.resolve_vars( {'foo': Variable('myvar'), 'bar': Variable('myvar')}, {'myvar': 'value'} ) == {'foo': 'value', 'bar': 'value'} def test_unsolved_error_raises_error(self): with pytest.raises(UnresolvedValueError) as excinfo: self.resolve_vars({'foo': models.Placeholder.BUILD_STAGE}, {}) raised_exception = excinfo.value assert raised_exception.key == 'foo' assert raised_exception.value == models.Placeholder.BUILD_STAGE def test_can_resolve_nested_variable_refs(self): assert self.resolve_vars( {'foo': {'bar': Variable('myvar')}}, {'myvar': 'value'} ) == {'foo': {'bar': 'value'}} def test_can_resolve_vars_in_list(self): assert self.resolve_vars( {'foo': [0, 1, Variable('myvar')]}, {'myvar': 2} ) == {'foo': [0, 1, 2]} def test_deeply_nested(self): nested = { 'a': { 'b': { 'c': { 'd': [{'e': {'f': Variable('foo')}}], } } } } variables = {'foo': 'value'} assert self.resolve_vars(nested, variables) == { 'a': { 'b': { 'c': { 'd': [{'e': {'f': 'value'}}], } } } } def test_can_handle_format_string(self): params = {'bar': StringFormat('value: {my_var}', ['my_var'])} variables = {'my_var': 'foo'} assert self.resolve_vars(params, variables) == { 'bar': 'value: foo', } def test_can_handle_deeply_nested_format_string(self): nested = { 'a': { 'b': { 'c': { 'd': [{'e': {'f': StringFormat( 'foo: {myvar}', ['myvar'])}}], } } } } variables = {'myvar': 'value'} assert self.resolve_vars(nested, variables) == { 'a': { 'b': { 'c': { 'd': [{'e': {'f': 'foo: value'}}], } } } } def test_can_handle_dict_value_by_key(self): variables = { 'domain_name': { 'base_path_mapping': { 'path': '/' } } } assert self.resolve_vars( KeyDataVariable('domain_name', 'base_path_mapping'), variables ) == {'path': '/'} ================================================ FILE: tests/unit/deploy/test_models.py ================================================ from dataclasses import replace from chalice.deploy import models def test_can_instantiate_empty_application(): app = models.Application(stage='dev', resources=[]) assert app.dependencies() == [] def test_can_instantiate_app_with_deps(): role = models.PreCreatedIAMRole(role_arn='foo') app = models.Application(stage='dev', resources=[role]) assert app.dependencies() == [role] def test_can_default_to_no_auths_in_rest_api(lambda_function): rest_api = models.RestAPI( resource_name='rest_api', swagger_doc={'swagger': '2.0'}, minimum_compression='', api_gateway_stage='api', endpoint_type='EDGE', lambda_function=lambda_function, xray=False ) assert rest_api.dependencies() == [lambda_function] def test_can_add_authorizers_to_dependencies(lambda_function): auth1 = replace(lambda_function, resource_name='auth1') auth2 = replace(lambda_function, resource_name='auth2') rest_api = models.RestAPI( resource_name='rest_api', swagger_doc={'swagger': '2.0'}, minimum_compression='', api_gateway_stage='api', endpoint_type='EDGE', lambda_function=lambda_function, xray=False, authorizers=[auth1, auth2], ) assert rest_api.dependencies() == [lambda_function, auth1, auth2] def test_can_add_connect_to_dependencies(lambda_function): api = models.WebsocketAPI( resource_name='websocket_api', name='name', api_gateway_stage='api', routes=['$connect'], connect_function=lambda_function, message_function=None, disconnect_function=None, ) assert api.dependencies() == [lambda_function] def test_can_add_message_to_dependencies(lambda_function): api = models.WebsocketAPI( resource_name='websocket_api', name='name', api_gateway_stage='api', routes=['$default'], connect_function=None, message_function=lambda_function, disconnect_function=None, ) assert api.dependencies() == [lambda_function] def test_can_add_disconnect_to_dependencies(lambda_function): api = models.WebsocketAPI( resource_name='websocket_api', name='name', api_gateway_stage='api', routes=['$disconnect'], connect_function=None, message_function=None, disconnect_function=lambda_function, ) assert api.dependencies() == [lambda_function] ================================================ FILE: tests/unit/deploy/test_packager.py ================================================ import pytest from collections import namedtuple from chalice.utils import OSUtils from chalice.compat import pip_no_compile_c_env_vars from chalice.compat import pip_no_compile_c_shim from chalice.deploy.packager import Package from chalice.deploy.packager import PipRunner from chalice.deploy.packager import SubprocessPip from chalice.deploy.packager import InvalidSourceDistributionNameError from chalice.deploy.packager import NoSuchPackageError from chalice.deploy.packager import PackageDownloadError FakePipCall = namedtuple('FakePipEntry', ['args', 'env_vars', 'shim']) class FakePip(object): def __init__(self): self._calls = [] self._returns = [] def main(self, args, env_vars=None, shim=None): self._calls.append(FakePipCall(args, env_vars, shim)) if self._returns: return self._returns.pop(0) # Return an rc of 0 and an empty stderr and stdout return 0, b'', b'' def add_return(self, return_pair): self._returns.append(return_pair) @property def calls(self): return self._calls @pytest.fixture def pip_factory(): def create_pip_runner(osutils=None): pip = FakePip() pip_runner = PipRunner(pip, osutils=osutils) return pip, pip_runner return create_pip_runner class CustomEnv(OSUtils): def __init__(self, env): self._env = env def environ(self): return self._env @pytest.fixture def osutils(): return OSUtils() class FakePopen(object): def __init__(self, rc, out, err): self.returncode = 0 self._out = out self._err = err def communicate(self): return self._out, self._err class FakePopenOSUtils(OSUtils): def __init__(self, processes): self.popens = [] self._processes = processes def popen(self, *args, **kwargs): self.popens.append((args, kwargs)) return self._processes.pop() class TestPackage(object): def test_can_create_package_with_custom_osutils(self, osutils): pkg = Package('', 'foobar-1.0-py3-none-any.whl', osutils) assert pkg._osutils == osutils def test_wheel_package(self): filename = 'foobar-1.0-py3-none-any.whl' pkg = Package('', filename) assert pkg.dist_type == 'wheel' assert pkg.filename == filename assert pkg.identifier == 'foobar==1.0' assert str(pkg) == 'foobar==1.0(wheel)' def test_invalid_package(self): with pytest.raises(InvalidSourceDistributionNameError): Package('', 'foobar.jpg') def test_diff_pkg_sdist_and_whl_do_not_collide(self): pkgs = set() pkgs.add(Package('', 'foobar-1.0-py3-none-any.whl')) pkgs.add(Package('', 'badbaz-1.0-py3-none-any.whl')) assert len(pkgs) == 2 def test_same_pkg_is_eq(self): pkg = Package('', 'foobar-1.0-py3-none-any.whl') assert pkg == pkg def test_pkg_is_eq_to_similar_pkg(self): pure_pkg = Package('', 'foobar-1.0-py3-none-any.whl') plat_pkg = Package('', 'foobar-1.0-py3-py36m-manylinux1_x86_64.whl') assert pure_pkg == plat_pkg def test_pkg_is_not_equal_to_different_type(self): pkg = Package('', 'foobar-1.0-py3-none-any.whl') non_package_type = 1 assert not (pkg == non_package_type) def test_pkg_repr(self): pkg = Package('', 'foobar-1.0-py3-none-any.whl') assert repr(pkg) == 'foobar==1.0(wheel)' def test_wheel_data_dir(self): pkg = Package('', 'foobar-2.0-py3-none-any.whl') assert pkg.data_dir == 'foobar-2.0.data' def test_can_read_packages_with_underscore_in_name(self): pkg = Package('', 'foo_bar-2.0-py3-none-any.whl') assert pkg.identifier == 'foo-bar==2.0' def test_can_read_packages_with_period_in_name(self): pkg = Package('', 'foo.bar-2.0-py3-none-any.whl') assert pkg.identifier == 'foo-bar==2.0' def test_can_normalize_data_dir(self): pkg = Package('', 'Foobar-2.0-py3-none-any.whl') assert pkg.data_dir == 'foobar-2.0.data' def test_can_normalize_dirname_comparisons(self): pkg = Package('', 'Foobar-2.0-py3-none-any.whl') assert pkg.matches_data_dir('Foobar-2.0.data') assert pkg.matches_data_dir('foobar-2.0.data') assert not pkg.matches_data_dir('other-2.0.data') assert not pkg.matches_data_dir('foobar-2.0.datastuff') assert not pkg.matches_data_dir('foobar-2.0') class TestPipRunner(object): def test_does_propagate_env_vars(self, pip_factory): osutils = CustomEnv({'foo': 'bar'}) pip, runner = pip_factory(osutils) wheel = 'foobar-1.2-py3-none-any.whl' directory = 'directory' runner.build_wheel(wheel, directory) call = pip.calls[0] assert 'foo' in call.env_vars assert call.env_vars['foo'] == 'bar' def test_build_wheel(self, pip_factory): # Test that `pip wheel` is called with the correct params pip, runner = pip_factory() wheel = 'foobar-1.0-py3-none-any.whl' directory = 'directory' runner.build_wheel(wheel, directory) assert len(pip.calls) == 1 call = pip.calls[0] assert call.args == ['wheel', '--no-deps', '--wheel-dir', directory, wheel] for compile_env_var in pip_no_compile_c_env_vars: assert compile_env_var not in call.env_vars assert call.shim == '' def test_build_wheel_without_c_extensions(self, pip_factory): # Test that `pip wheel` is called with the correct params when we # call it with compile_c=False. These will differ by platform. pip, runner = pip_factory() wheel = 'foobar-1.0-py3-none-any.whl' directory = 'directory' runner.build_wheel(wheel, directory, compile_c=False) assert len(pip.calls) == 1 call = pip.calls[0] assert call.args == ['wheel', '--no-deps', '--wheel-dir', directory, wheel] for compile_env_var in pip_no_compile_c_env_vars: assert compile_env_var in call.env_vars assert call.shim == pip_no_compile_c_shim def test_download_all_deps(self, pip_factory): # Make sure that `pip download` is called with the correct arguments # for getting all sdists. pip, runner = pip_factory() runner.download_all_dependencies('requirements.txt', 'directory') assert len(pip.calls) == 1 call = pip.calls[0] assert call.args == ['download', '-r', 'requirements.txt', '--dest', 'directory'] assert call.env_vars is None assert call.shim is None def test_download_sdist(self, pip_factory): pip, runner = pip_factory() packages = ['foo', 'bar', 'baz'] runner.download_sdists(packages, 'directory') expected_prefix = ['download', '--no-binary=:all:', '--no-deps', '--dest', 'directory'] for i, package in enumerate(packages): assert pip.calls[i].args == expected_prefix + [package] assert pip.calls[i].env_vars is None assert pip.calls[i].shim is None def test_download_wheels(self, pip_factory): # Make sure that `pip download` is called with the correct arguments # for getting lambda compatible wheels. pip, runner = pip_factory() packages = ['foo', 'bar', 'baz'] abi = 'cp37m' runner.download_manylinux_wheels(abi, packages, 'directory') expected_prefix = ['download', '--only-binary=:all:', '--no-deps', '--platform', 'manylinux2014_x86_64', '--implementation', 'cp', '--abi', abi, '--dest', 'directory'] for i, package in enumerate(packages): assert pip.calls[i].args == expected_prefix + [package] assert pip.calls[i].env_vars is None assert pip.calls[i].shim is None def test_download_wheels_no_wheels(self, pip_factory): pip, runner = pip_factory() runner.download_manylinux_wheels('cp36m', [], 'directory') assert len(pip.calls) == 0 def test_does_find_local_directory(self, pip_factory): pip, runner = pip_factory() pip.add_return((0, (b"Processing ../local-dir\n" b" Link is a directory," b" ignoring download_dir"), b'')) runner.download_all_dependencies('requirements.txt', 'directory') assert len(pip.calls) == 2 assert pip.calls[1].args == ['wheel', '--no-deps', '--wheel-dir', 'directory', '../local-dir'] def test_does_find_multiple_local_directories(self, pip_factory): pip, runner = pip_factory() pip.add_return((0, (b"Processing ../local-dir-1\n" b" Link is a directory," b" ignoring download_dir" b"\nsome pip output...\n" b"Processing ../local-dir-2\n" b" Link is a directory," b" ignoring download_dir"), b'')) runner.download_all_dependencies('requirements.txt', 'directory') assert len(pip.calls) == 3 assert pip.calls[1].args == ['wheel', '--no-deps', '--wheel-dir', 'directory', '../local-dir-1'] assert pip.calls[2].args == ['wheel', '--no-deps', '--wheel-dir', 'directory', '../local-dir-2'] def test_raise_no_such_package_error(self, pip_factory): pip, runner = pip_factory() pip.add_return((1, b'', (b'Could not find a version that satisfies the ' b'requirement BadPackageName '))) with pytest.raises(NoSuchPackageError) as einfo: runner.download_all_dependencies('requirements.txt', 'directory') assert str(einfo.value) == ('Could not satisfy the requirement: ' 'BadPackageName') def test_raise_other_unknown_error_during_downloads(self, pip_factory): pip, runner = pip_factory() pip.add_return((1, b'', b'SomeNetworkingError: Details here.')) with pytest.raises(PackageDownloadError) as einfo: runner.download_all_dependencies('requirements.txt', 'directory') assert str(einfo.value) == 'SomeNetworkingError: Details here.' def test_inject_unknown_error_if_no_stderr(self, pip_factory): pip, runner = pip_factory() pip.add_return((1, None, None)) with pytest.raises(PackageDownloadError) as einfo: runner.download_all_dependencies('requirements.txt', 'directory') assert str(einfo.value) == 'Unknown error' class TestSubprocessPip(object): def test_does_use_custom_pip_import_string(self): fake_osutils = FakePopenOSUtils([FakePopen(0, '', '')]) expected_import_statement = 'foobarbaz' pip = SubprocessPip(osutils=fake_osutils, import_string=expected_import_statement) pip.main(['--version']) pip_execution_string = fake_osutils.popens[0][0][0][2] import_statement = pip_execution_string.split(';')[1].strip() assert import_statement == expected_import_statement ================================================ FILE: tests/unit/deploy/test_planner.py ================================================ from unittest import mock from dataclasses import replace, dataclass from typing import Tuple # noqa import pytest from chalice.awsclient import TypedAWSClient, ResourceDoesNotExistError from chalice.deploy import models from chalice.config import DeployedResources from chalice.utils import OSUtils from chalice.deploy.planner import PlanStage, Variable, RemoteState, \ KeyDataVariable from chalice.deploy.planner import StringFormat from chalice.deploy.models import APICall from chalice.deploy.sweeper import ResourceSweeper def create_function_resource(name, function_name=None, environment_variables=None, runtime='python2.7', handler='app.app', tags=None, timeout=60, memory_size=128, deployment_package=None, role=None, layers=None, managed_layer=None): if function_name is None: function_name = 'appname-dev-%s' % name if environment_variables is None: environment_variables = {} if tags is None: tags = {} if deployment_package is None: deployment_package = models.DeploymentPackage(filename='foo') if role is None: role = models.PreCreatedIAMRole(role_arn='role:arn') return models.LambdaFunction( resource_name=name, function_name=function_name, environment_variables=environment_variables, runtime=runtime, handler=handler, tags=tags, timeout=timeout, memory_size=memory_size, xray=None, deployment_package=deployment_package, role=role, security_group_ids=[], subnet_ids=[], layers=layers, reserved_concurrency=None, managed_layer=managed_layer, ) def create_managed_layer(): layer = models.LambdaLayer( resource_name='layer', layer_name='bar', runtime='python2.7', deployment_package=models.DeploymentPackage( filename='foo') ) return layer def create_api_mapping(): return models.APIMapping( resource_name='api_mapping', mount_path='(none)', api_gateway_stage='dev' ) def create_http_domain_name(): return models.DomainName( protocol=models.APIType.HTTP, resource_name='api_gateway_custom_domain', domain_name='example.com', tls_version=models.TLSVersion.TLS_1_0, api_mapping=create_api_mapping(), certificate_arn='certificate_arn', ) def create_websocket_domain_name(): return models.DomainName( protocol=models.APIType.WEBSOCKET, resource_name='websocket_api_custom_domain', domain_name='example.com', tls_version=models.TLSVersion.TLS_1_0, api_mapping=create_api_mapping(), certificate_arn='certificate_arn', ) @pytest.fixture def no_deployed_values(): return DeployedResources({'resources': [], 'schema_version': '2.0'}) class FakeConfig(object): def __init__(self, deployed_values): self._deployed_values = deployed_values self.chalice_stage = 'dev' self.api_gateway_stage = 'dev' def deployed_resources(self, chalice_stage_name): return DeployedResources(self._deployed_values) class InMemoryRemoteState(object): def __init__(self, known_resources=None): if known_resources is None: known_resources = {} self.known_resources = known_resources self.deployed_values = {} def resource_exists(self, resource, *args): if resource.resource_type == 'api_mapping': return ( (resource.resource_type, resource.mount_path) in self.known_resources ) return ( (resource.resource_type, resource.resource_name) in self.known_resources ) def get_remote_model(self, resource): key = (resource.resource_type, resource.resource_name) return self.known_resources.get(key) def declare_resource_exists(self, resource, **deployed_values): key = (resource.resource_type, resource.resource_name) self.known_resources[key] = resource if deployed_values: deployed_values['name'] = resource.resource_name self.deployed_values[resource.resource_name] = deployed_values if resource.resource_type == 'domain_name': key = (resource.api_mapping.resource_type, resource.api_mapping.mount_path) self.known_resources[key] = resource def declare_no_resources_exists(self): self.known_resources = {} def resource_deployed_values(self, resource): return self.deployed_values[resource.resource_name] class BasePlannerTests(object): def setup_method(self): self.osutils = mock.Mock(spec=OSUtils) self.remote_state = InMemoryRemoteState() self.last_plan = None def assert_apicall_equals(self, expected, actual_api_call): # models.APICall has its own __eq__ method from attrs, # but in practice the assertion errors are unreadable and # it's not always clear which part of the API call object is # wrong. To get better error messages each field is individually # compared. assert isinstance(expected, models.APICall) assert isinstance(actual_api_call, models.APICall) assert expected.method_name == actual_api_call.method_name assert expected.params == actual_api_call.params def determine_plan(self, resource): planner = PlanStage(self.remote_state, self.osutils) self.last_plan = planner.execute([resource]) return self.last_plan.instructions def filter_api_calls(self, plan): api_calls = [] for instruction in plan: if isinstance(instruction, models.APICall): api_calls.append(instruction) return api_calls def assert_recorded_values(self, plan, resource_type, resource_name, expected_mapping): actual = {} for step in plan: if isinstance(step, models.RecordResourceValue): actual[step.name] = step.value elif isinstance(step, models.RecordResourceVariable): actual[step.name] = Variable(step.variable_name) assert actual == expected_mapping class TestPlanManagedRole(BasePlannerTests): def test_can_plan_for_iam_role_creation(self): self.remote_state.declare_no_resources_exists() resource = models.ManagedIAMRole( resource_name='default-role', role_name='myrole', trust_policy={'trust': 'policy'}, policy=models.AutoGenIAMPolicy(document={'iam': 'policy'}), ) plan = self.determine_plan(resource) expected = models.APICall( method_name='create_role', params={'name': 'myrole', 'trust_policy': Variable('lambda_trust_policy'), 'policy': {'iam': 'policy'}}, ) self.assert_apicall_equals(plan[4], expected) assert list(self.last_plan.messages.values()) == [ 'Creating IAM role: myrole\n' ] def test_can_create_plan_for_filebased_role(self): self.remote_state.declare_no_resources_exists() resource = models.ManagedIAMRole( resource_name='default-role', role_name='myrole', trust_policy={'trust': 'policy'}, policy=models.FileBasedIAMPolicy( filename='foo.json', document={'iam': 'policy'}), ) plan = self.determine_plan(resource) expected = models.APICall( method_name='create_role', params={'name': 'myrole', 'trust_policy': Variable('lambda_trust_policy'), 'policy': {'iam': 'policy'}}, ) self.assert_apicall_equals(plan[4], expected) assert list(self.last_plan.messages.values()) == [ 'Creating IAM role: myrole\n' ] def test_can_update_managed_role(self): role = models.ManagedIAMRole( resource_name='resource_name', role_name='myrole', trust_policy={}, policy=models.AutoGenIAMPolicy(document={'role': 'policy'}), ) self.remote_state.declare_resource_exists( role, role_arn='myrole:arn') plan = self.determine_plan(role) assert plan[0] == models.StoreValue( name='myrole_role_arn', value='myrole:arn') self.assert_apicall_equals( plan[1], models.APICall( method_name='put_role_policy', params={'role_name': 'myrole', 'policy_name': 'myrole', 'policy_document': {'role': 'policy'}}, ) ) assert plan[-2].variable_name == 'myrole_role_arn' assert plan[-1].value == 'myrole' assert list(self.last_plan.messages.values()) == [ 'Updating policy for IAM role: myrole\n' ] def test_can_update_file_based_policy(self): role = models.ManagedIAMRole( resource_name='resource_name', role_name='myrole', trust_policy={}, policy=models.FileBasedIAMPolicy( filename='foo.json', document={'iam': 'policy'}), ) self.remote_state.declare_resource_exists(role, role_arn='myrole:arn') plan = self.determine_plan(role) assert plan[0] == models.StoreValue( name='myrole_role_arn', value='myrole:arn') self.assert_apicall_equals( plan[1], models.APICall( method_name='put_role_policy', params={'role_name': 'myrole', 'policy_name': 'myrole', 'policy_document': {'iam': 'policy'}}, ) ) def test_no_update_for_non_managed_role(self): role = models.PreCreatedIAMRole(role_arn='role:arn') plan = self.determine_plan(role) assert plan == [] class TestPlanCreateUpdateAPIMapping(BasePlannerTests): def test_can_create_api_mapping(self, lambda_function): rest_api = models.RestAPI( resource_name='rest_api', swagger_doc={'swagger': '2.0'}, minimum_compression='', api_gateway_stage='api', endpoint_type='EDGE', lambda_function=lambda_function, domain_name=create_http_domain_name() ) self.remote_state.declare_no_resources_exists() plan = self.determine_plan(rest_api) params = { 'domain_name': rest_api.domain_name.domain_name, 'path_key': '(none)', 'stage': 'dev', 'api_id': Variable('rest_api_id') } expected = [ models.APICall( method_name='create_base_path_mapping', params=params, output_var='base_path_mapping' ), ] # Create api mapping. self.assert_apicall_equals(plan[-3], expected[0]) msg = 'Creating api mapping: /\n' assert list(self.last_plan.messages.values())[-1] == msg def test_can_create_websocket_api_mapping_with_path(self): domain_name = create_websocket_domain_name() domain_name.api_mapping.mount_path = 'path-key' connect_function = create_function_resource( 'function_name_connect') message_function = create_function_resource( 'function_name_message') disconnect_function = create_function_resource( 'function_name_disconnect') websocket_api = models.WebsocketAPI( resource_name='websocket_api', name='app-dev-websocket-api', api_gateway_stage='api', routes=['$connect', '$default', '$disconnect'], connect_function=connect_function, message_function=message_function, disconnect_function=disconnect_function, domain_name=domain_name ) self.remote_state.declare_no_resources_exists() plan = self.determine_plan(websocket_api) params = { 'domain_name': domain_name.domain_name, 'path_key': 'path-key', 'stage': 'dev', 'api_id': Variable('websocket_api_id') } expected = [ models.APICall( method_name='create_api_mapping', params=params, output_var='api_mapping' ), ] # create api mapping self.assert_apicall_equals(plan[-3], expected[0]) msg = 'Creating api mapping: /path-key\n' assert list(self.last_plan.messages.values())[-1] == msg def test_store_api_mapping_if_already_exists(self, lambda_function): domain_name = create_http_domain_name() domain_name.api_mapping.mount_path = 'test-path' rest_api = models.RestAPI( resource_name='rest_api', swagger_doc={'swagger': '2.0'}, minimum_compression='', api_gateway_stage='api', endpoint_type='EDGE', lambda_function=lambda_function, domain_name=domain_name ) deployed_value = { 'name': 'api_gateway_custom_domain', 'resource_type': 'domain_name', 'hosted_zone_id': 'hosted_zone_id', 'certificate_arn': 'certificate_arn', 'security_policy': 'TLS_1_0', 'domain_name': 'example.com', 'api_mapping': [ { 'key': '/test-path' }, { 'key': '/test-path-2' } ] } self.remote_state.declare_resource_exists(domain_name, **deployed_value) plan = self.determine_plan(rest_api) expected = [ models.StoreMultipleValue( name='rest_api_mapping', value=[{ 'key': '/test-path' }] ) ] assert plan[-2].name == expected[0].name assert plan[-2].value == expected[0].value assert isinstance(expected[0], models.StoreMultipleValue) assert isinstance(plan[-2], models.StoreMultipleValue) def test_store_api_mapping_none_if_already_exists(self, lambda_function): domain_name = create_http_domain_name() domain_name.api_mapping.mount_path = '(none)' rest_api = models.RestAPI( resource_name='rest_api', swagger_doc={'swagger': '2.0'}, minimum_compression='', api_gateway_stage='api', endpoint_type='EDGE', lambda_function=lambda_function, domain_name=domain_name ) deployed_value = { 'name': 'api_gateway_custom_domain', 'resource_type': 'domain_name', 'hosted_zone_id': 'hosted_zone_id', 'certificate_arn': 'certificate_arn', 'security_policy': 'TLS_1_0', 'domain_name': 'example.com', 'api_mapping': [ { 'key': '/' }, ] } self.remote_state.declare_resource_exists(domain_name, **deployed_value) plan = self.determine_plan(rest_api) expected = [ models.StoreMultipleValue( name='rest_api_mapping', value=[{ 'key': '/' }] ) ] assert plan[-2].name == expected[0].name assert plan[-2].value == expected[0].value assert isinstance(expected[0], models.StoreMultipleValue) assert isinstance(plan[-2], models.StoreMultipleValue) class TestPlanCreateUpdateDomainName(BasePlannerTests): def test_can_create_domain_name(self, lambda_function): domain_name = create_http_domain_name() rest_api = models.RestAPI( resource_name='rest_api', swagger_doc={'swagger': '2.0'}, minimum_compression='', api_gateway_stage='api', endpoint_type='EDGE', lambda_function=lambda_function, domain_name=domain_name ) params = { 'protocol': domain_name.protocol.value, 'domain_name': domain_name.domain_name, 'security_policy': domain_name.tls_version.value, 'certificate_arn': domain_name.certificate_arn, 'endpoint_type': 'EDGE', 'tags': None } self.remote_state.declare_no_resources_exists() plan = self.determine_plan(rest_api) expected = [ models.APICall( method_name='create_domain_name', params=params, output_var=domain_name.resource_name ) ] # create domain name self.assert_apicall_equals(plan[13], expected[0]) msg = 'Creating custom domain name: example.com\n' assert list(self.last_plan.messages.values())[-2] == msg def test_can_update_domain_name(self): deployed_value = { 'name': 'rest_api_domain_name', 'resource_type': 'domain_name', 'hosted_zone_id': 'hosted_zone_id', 'certificate_arn': 'certificate_arn', 'security_policy': 'TLS_1_0', 'domain_name': 'example.com', } domain_name = create_http_domain_name() domain_name.security_policy = 'TLS_1_2' domain_name.certificate_arn = 'certificate_arn_1' domain_name.hosted_zone_id = ' hosted_zone_1' params = { 'protocol': domain_name.protocol.value, 'domain_name': domain_name.domain_name, 'security_policy': domain_name.tls_version.value, 'certificate_arn': domain_name.certificate_arn, 'endpoint_type': 'EDGE', 'tags': None } self.remote_state.declare_resource_exists( domain_name, **deployed_value ) planner = PlanStage(self.remote_state, self.osutils) plan = planner._add_domainname_plan(domain_name, 'EDGE') expected = [ models.APICall( method_name='update_domain_name', params=params, output_var=domain_name.resource_name ) ] # update domain name self.assert_apicall_equals(plan[0][0], expected[0]) assert plan[0][1] == 'Updating custom domain name: example.com\n' class TestPlanLambdaFunction(BasePlannerTests): def test_can_create_layer(self): layer = models.LambdaLayer( resource_name='layer', layer_name='bar', runtime='python2.7', deployment_package=models.DeploymentPackage( filename='foo') ) plan = self.determine_plan(layer) expected = [models.APICall( method_name='publish_layer', params={ 'layer_name': 'bar', 'zip_contents': mock.ANY, 'runtime': 'python2.7'}) ] self.assert_apicall_equals(plan[0], expected[0]) assert list(self.last_plan.messages.values()) == [ 'Creating lambda layer: bar\n', ] def test_can_update_layer(self): layer = models.LambdaLayer( resource_name='layer', layer_name='bar', runtime='python2.7', deployment_package=models.DeploymentPackage( filename='foo') ) copy_of_layer = replace(layer) self.remote_state.declare_resource_exists( copy_of_layer, layer_version_arn='arn:bar:4' ) plan = self.determine_plan(layer) expected = [ models.APICall( method_name='delete_layer_version', params={'layer_version_arn': 'arn:bar:4'}), models.APICall( method_name='publish_layer', params={ 'layer_name': 'bar', 'zip_contents': mock.ANY, 'runtime': 'python2.7'}), models.RecordResourceVariable( resource_type='lambda_layer', resource_name='layer', name='layer_version_arn', variable_name='layer_version_arn') ] assert len(plan) == 3 assert plan[0] == expected[0] assert plan[2] == expected[2] self.assert_apicall_equals(plan[1], expected[1]) assert list(self.last_plan.messages.values()) == [ 'Updating lambda layer: bar\n', ] def test_can_create_function(self): function = create_function_resource('function_name') self.remote_state.declare_no_resources_exists() plan = self.determine_plan(function) expected = [models.APICall( method_name='create_function', params={ 'function_name': 'appname-dev-function_name', 'role_arn': 'role:arn', 'zip_contents': mock.ANY, 'runtime': 'python2.7', 'handler': 'app.app', 'environment_variables': {}, 'tags': {}, 'xray': None, 'timeout': 60, 'memory_size': 128, 'security_group_ids': [], 'subnet_ids': [], 'layers': [], }, ), models.APICall( method_name='delete_function_concurrency', params={ 'function_name': 'appname-dev-function_name', }, output_var='reserved_concurrency_result', )] # create_function self.assert_apicall_equals(plan[0], expected[0]) # delete_function_concurrency self.assert_apicall_equals(plan[2], expected[1]) assert list(self.last_plan.messages.values()) == [ 'Creating lambda function: appname-dev-function_name\n', ] def test_create_function_with_layers(self): layers = ['arn:aws:lambda:us-east-1:111:layer:test_layer:1'] function = create_function_resource( 'function_name', layers=layers, managed_layer=create_managed_layer() ) self.remote_state.declare_no_resources_exists() plan = self.filter_api_calls( self.determine_plan(function.managed_layer)) plan.extend(self.filter_api_calls(self.determine_plan(function))) expected = [models.APICall( method_name='publish_layer', params={ 'layer_name': 'bar', 'zip_contents': mock.ANY, 'runtime': 'python2.7'} ), models.APICall( method_name='create_function', params={ 'function_name': 'appname-dev-function_name', 'role_arn': 'role:arn', 'zip_contents': mock.ANY, 'runtime': 'python2.7', 'handler': 'app.app', 'environment_variables': {}, 'tags': {}, 'timeout': 60, 'xray': None, 'memory_size': 128, 'security_group_ids': [], 'subnet_ids': [], 'layers': [Variable('layer_version_arn')] + layers }, ), models.APICall( method_name='delete_function_concurrency', params={ 'function_name': 'appname-dev-function_name', }, output_var='reserved_concurrency_result', )] # create_function self.assert_apicall_equals(plan[0], expected[0]) # delete_function_concurrency self.assert_apicall_equals(plan[1], expected[1]) def test_can_update_lambda_function_code(self): function = create_function_resource('function_name') copy_of_function = replace(function) self.remote_state.declare_resource_exists(copy_of_function) # Now let's change the memory size and ensure we # get an update. function.memory_size = 256 plan = self.determine_plan(function) existing_params = { 'function_name': 'appname-dev-function_name', 'role_arn': 'role:arn', 'zip_contents': mock.ANY, 'runtime': 'python2.7', 'environment_variables': {}, 'xray': None, 'tags': {}, 'timeout': 60, 'security_group_ids': [], 'subnet_ids': [], 'layers': [], } expected_params = dict(memory_size=256, **existing_params) expected = [models.APICall( method_name='update_function', params=expected_params, ), models.APICall( method_name='delete_function_concurrency', params={ 'function_name': 'appname-dev-function_name', }, output_var='reserved_concurrency_result', )] # update_function self.assert_apicall_equals(plan[0], expected[0]) # delete_function_concurrency self.assert_apicall_equals(plan[3], expected[1]) assert list(self.last_plan.messages.values()) == [ 'Updating lambda function: appname-dev-function_name\n', ] def test_can_update_lambda_function_with_managed_layer(self): function = create_function_resource( 'function_name', managed_layer=create_managed_layer(), ) copy_of_function = replace(function) self.remote_state.declare_resource_exists(copy_of_function) copy_of_layer = replace(function.managed_layer) self.remote_state.declare_resource_exists( copy_of_layer, layer_version_arn='arn:bar:4' ) plan = self.determine_plan(function.managed_layer) plan.extend(self.determine_plan(function)) self.assert_apicall_equals(plan[0], models.APICall( method_name='delete_layer_version', params={'layer_version_arn': 'arn:bar:4'}, )) assert plan[3].method_name == 'update_function' assert plan[3].params['layers'] == [Variable('layer_version_arn')] def test_can_create_function_with_reserved_concurrency(self): function = create_function_resource('function_name') function.reserved_concurrency = 5 self.remote_state.declare_no_resources_exists() plan = self.determine_plan(function) expected = [models.APICall( method_name='create_function', params={ 'function_name': 'appname-dev-function_name', 'role_arn': 'role:arn', 'zip_contents': mock.ANY, 'runtime': 'python2.7', 'handler': 'app.app', 'environment_variables': {}, 'tags': {}, 'xray': None, 'timeout': 60, 'memory_size': 128, 'security_group_ids': [], 'subnet_ids': [], 'layers': [], }, ), models.APICall( method_name='put_function_concurrency', params={ 'function_name': 'appname-dev-function_name', 'reserved_concurrent_executions': 5 }, output_var='reserved_concurrency_result', )] # create_function self.assert_apicall_equals(plan[0], expected[0]) # put_function_concurrency self.assert_apicall_equals(plan[2], expected[1]) assert list(self.last_plan.messages.values()) == [ 'Creating lambda function: appname-dev-function_name\n', 'Updating lambda function concurrency limit:' ' appname-dev-function_name\n', ] def test_can_set_variables_when_needed(self): function = create_function_resource('function_name') self.remote_state.declare_no_resources_exists() function.role = models.ManagedIAMRole( resource_name='myrole', role_name='myrole-dev', trust_policy={'trust': 'policy'}, policy=models.FileBasedIAMPolicy( filename='foo.json', document={'iam': 'role'}), ) plan = self.determine_plan(function) call = plan[0] assert call.method_name == 'create_function' # The params are verified in test_can_create_function, # we just care about how the role_arn Variable is constructed. role_arn = call.params['role_arn'] assert isinstance(role_arn, Variable) assert role_arn.name == 'myrole-dev_role_arn' class TestPlanS3Events(BasePlannerTests): def test_can_plan_s3_event(self): function = create_function_resource('function_name') bucket_event = models.S3BucketNotification( resource_name='function_name-s3event', bucket='mybucket', events=['s3:ObjectCreated:*'], prefix=None, suffix=None, lambda_function=function, ) full_plan = self.determine_plan(bucket_event) setup_plan, plan = full_plan[:4], full_plan[4:] assert setup_plan[0:4] == [ models.BuiltinFunction( 'parse_arn', [Variable("function_name_lambda_arn")], output_var='parsed_lambda_arn', ), models.JPSearch('account_id', input_var='parsed_lambda_arn', output_var='account_id'), models.JPSearch('region', input_var='parsed_lambda_arn', output_var='region_name'), models.JPSearch('partition', input_var='parsed_lambda_arn', output_var='partition') ] self.assert_apicall_equals( plan[0], models.APICall( method_name='add_permission_for_s3_event', params={ 'bucket': 'mybucket', 'function_arn': Variable('function_name_lambda_arn'), 'account_id': Variable('account_id'), }, ) ) self.assert_apicall_equals( plan[1], models.APICall( method_name='connect_s3_bucket_to_lambda', params={ 'bucket': 'mybucket', 'function_arn': Variable('function_name_lambda_arn'), 'events': ['s3:ObjectCreated:*'], 'prefix': None, 'suffix': None, }, ) ) assert plan[2] == models.RecordResourceValue( resource_type='s3_event', resource_name='function_name-s3event', name='bucket', value='mybucket', ) assert plan[3] == models.RecordResourceVariable( resource_type='s3_event', resource_name='function_name-s3event', name='lambda_arn', variable_name='function_name_lambda_arn', ) class TestPlanCloudWatchEvent(BasePlannerTests): def test_can_plan_cloudwatch_event(self): function = create_function_resource('function_name') event = models.CloudWatchEvent( resource_name='bar', rule_name='myrulename', event_pattern='"source": ["aws.ec2"]', lambda_function=function, ) plan = self.determine_plan(event) assert len(plan) == 4 self.assert_apicall_equals( plan[0], models.APICall( method_name='get_or_create_rule_arn', params={ 'rule_name': 'myrulename', 'event_pattern': '"source": ["aws.ec2"]' }, output_var='rule-arn', ) ) self.assert_apicall_equals( plan[1], models.APICall( method_name='connect_rule_to_lambda', params={'rule_name': 'myrulename', 'function_arn': Variable('function_name_lambda_arn')} ) ) self.assert_apicall_equals( plan[2], models.APICall( method_name='add_permission_for_cloudwatch_event', params={ 'rule_arn': Variable('rule-arn'), 'function_arn': Variable('function_name_lambda_arn'), }, ) ) assert plan[3] == models.RecordResourceValue( resource_type='cloudwatch_event', resource_name='bar', name='rule_name', value='myrulename', ) class TestPlanScheduledEvent(BasePlannerTests): def test_can_plan_scheduled_event(self): function = create_function_resource('function_name') event = models.ScheduledEvent( resource_name='bar', rule_name='myrulename', rule_description="my rule description", schedule_expression='rate(5 minutes)', lambda_function=function, ) plan = self.determine_plan(event) assert len(plan) == 4 self.assert_apicall_equals( plan[0], models.APICall( method_name='get_or_create_rule_arn', params={ 'rule_name': 'myrulename', 'rule_description': 'my rule description', 'schedule_expression': 'rate(5 minutes)', }, output_var='rule-arn', ) ) self.assert_apicall_equals( plan[1], models.APICall( method_name='connect_rule_to_lambda', params={'rule_name': 'myrulename', 'function_arn': Variable('function_name_lambda_arn')} ) ) self.assert_apicall_equals( plan[2], models.APICall( method_name='add_permission_for_cloudwatch_event', params={ 'rule_arn': Variable('rule-arn'), 'function_arn': Variable('function_name_lambda_arn'), }, ) ) assert plan[3] == models.RecordResourceValue( resource_type='cloudwatch_event', resource_name='bar', name='rule_name', value='myrulename', ) def test_can_plan_scheduled_event_can_omit_description(self): function = create_function_resource('function_name') event = models.ScheduledEvent( resource_name='bar', rule_name='myrulename', schedule_expression='rate(5 minutes)', lambda_function=function, ) plan = self.determine_plan(event) self.assert_apicall_equals( plan[0], models.APICall( method_name='get_or_create_rule_arn', params={ 'rule_name': 'myrulename', 'schedule_expression': 'rate(5 minutes)', }, output_var='rule-arn', ) ) class TestPlanWebsocketAPI(BasePlannerTests): def assert_loads_needed_variables(self, plan): # Parse arn and store region/account id for future # API calls. assert plan[0:5] == [ models.BuiltinFunction( 'parse_arn', [Variable('function_name_connect_lambda_arn')], output_var='parsed_lambda_arn', ), models.JPSearch('account_id', input_var='parsed_lambda_arn', output_var='account_id'), models.JPSearch('region', input_var='parsed_lambda_arn', output_var='region_name'), models.JPSearch('partition', input_var='parsed_lambda_arn', output_var='partition'), models.JPSearch('dns_suffix', input_var='parsed_lambda_arn', output_var='dns_suffix'), ] def test_can_plan_websocket_api(self): connect_function = create_function_resource( 'function_name_connect') message_function = create_function_resource( 'function_name_message') disconnect_function = create_function_resource( 'function_name_disconnect') websocket_api = models.WebsocketAPI( resource_name='websocket_api', name='app-dev-websocket-api', api_gateway_stage='api', routes=['$connect', '$default', '$disconnect'], connect_function=connect_function, message_function=message_function, disconnect_function=disconnect_function, ) plan = self.determine_plan(websocket_api) self.assert_loads_needed_variables(plan) assert plan[5:] == [ models.APICall( method_name='create_websocket_api', params={'name': 'app-dev-websocket-api'}, output_var='websocket_api_id', ), models.StoreValue( name='routes', value=[], ), models.StoreValue( name='websocket-connect-integration-lambda-path', value=StringFormat( 'arn:{partition}:apigateway:{region_name}:lambda:path/' '2015-03-31/functions/arn:{partition}:lambda' ':{region_name}:{account_id}:function:%s/' 'invocations' % 'appname-dev-function_name_connect', ['partition', 'region_name', 'account_id'], ), ), models.APICall( method_name='create_websocket_integration', params={ 'api_id': Variable('websocket_api_id'), 'lambda_function': Variable( 'websocket-connect-integration-lambda-path'), 'handler_type': 'connect', }, output_var='connect-integration-id', ), models.StoreValue( name='websocket-message-integration-lambda-path', value=StringFormat( 'arn:{partition}:apigateway:{region_name}:lambda:path/' '2015-03-31/functions/arn:{partition}:lambda' ':{region_name}:{account_id}:function:%s/' 'invocations' % 'appname-dev-function_name_message', ['partition', 'region_name', 'account_id'], ), ), models.APICall( method_name='create_websocket_integration', params={ 'api_id': Variable('websocket_api_id'), 'lambda_function': Variable( 'websocket-message-integration-lambda-path'), 'handler_type': 'message', }, output_var='message-integration-id', ), models.StoreValue( name='websocket-disconnect-integration-lambda-path', value=StringFormat( 'arn:{partition}:apigateway:{region_name}:lambda:path/' '2015-03-31/functions/arn:{partition}:lambda' ':{region_name}:{account_id}:function:%s/' 'invocations' % 'appname-dev-function_name_disconnect', ['partition', 'region_name', 'account_id'], ), ), models.APICall( method_name='create_websocket_integration', params={ 'api_id': Variable('websocket_api_id'), 'lambda_function': Variable( 'websocket-disconnect-integration-lambda-path'), 'handler_type': 'disconnect', }, output_var='disconnect-integration-id', ), models.APICall( method_name='create_websocket_route', params={ 'api_id': Variable('websocket_api_id'), 'route_key': '$connect', 'integration_id': Variable('connect-integration-id'), }, ), models.APICall( method_name='create_websocket_route', params={ 'api_id': Variable('websocket_api_id'), 'route_key': '$default', 'integration_id': Variable('message-integration-id'), }, ), models.APICall( method_name='create_websocket_route', params={ 'api_id': Variable('websocket_api_id'), 'route_key': '$disconnect', 'integration_id': Variable('disconnect-integration-id'), }, ), models.APICall( method_name='deploy_websocket_api', params={ 'api_id': Variable('websocket_api_id'), }, output_var='deployment-id', ), models.APICall( method_name='create_stage', params={ 'api_id': Variable('websocket_api_id'), 'stage_name': 'api', 'deployment_id': Variable('deployment-id'), } ), models.StoreValue( name='websocket_api_url', value=StringFormat( 'wss://{websocket_api_id}.execute-api.{region_name}' '.{dns_suffix}/%s/' % 'api', ['websocket_api_id', 'region_name', 'dns_suffix'], ), ), models.RecordResourceVariable( resource_type='websocket_api', resource_name='websocket_api', name='websocket_api_url', variable_name='websocket_api_url', ), models.RecordResourceVariable( resource_type='websocket_api', resource_name='websocket_api', name='websocket_api_id', variable_name='websocket_api_id', ), models.APICall( method_name='add_permission_for_apigateway_v2', params={'function_name': 'appname-dev-function_name_connect', 'region_name': Variable('region_name'), 'account_id': Variable('account_id'), 'api_id': Variable('websocket_api_id')}, ), models.APICall( method_name='add_permission_for_apigateway_v2', params={'function_name': 'appname-dev-function_name_message', 'region_name': Variable('region_name'), 'account_id': Variable('account_id'), 'api_id': Variable('websocket_api_id')}, ), models.APICall( method_name='add_permission_for_apigateway_v2', params={ 'function_name': 'appname-dev-function_name_disconnect', 'region_name': Variable('region_name'), 'account_id': Variable('account_id'), 'api_id': Variable('websocket_api_id')}, ), ] def test_can_update_websocket_api(self): connect_function = create_function_resource( 'function_name_connect') message_function = create_function_resource( 'function_name_message') disconnect_function = create_function_resource( 'function_name_disconnect') websocket_api = models.WebsocketAPI( resource_name='websocket_api', name='app-dev-websocket-api', api_gateway_stage='api', routes=['$connect', '$default', '$disconnect'], connect_function=connect_function, message_function=message_function, disconnect_function=disconnect_function, ) self.remote_state.declare_resource_exists(websocket_api) self.remote_state.deployed_values['websocket_api'] = { 'websocket_api_id': 'my_websocket_api_id', } plan = self.determine_plan(websocket_api) self.assert_loads_needed_variables(plan) assert plan[5:] == [ models.StoreValue( name='websocket_api_id', value='my_websocket_api_id', ), models.APICall( method_name='get_websocket_routes', params={'api_id': Variable('websocket_api_id')}, output_var='routes', ), models.APICall( method_name='delete_websocket_routes', params={'api_id': Variable('websocket_api_id'), 'routes': Variable('routes')}, ), models.APICall( method_name='get_websocket_integrations', params={'api_id': Variable('websocket_api_id')}, output_var='integrations', ), models.APICall( method_name='delete_websocket_integrations', params={'api_id': Variable('websocket_api_id'), 'integrations': Variable('integrations')}, ), models.StoreValue( name='websocket-connect-integration-lambda-path', value=StringFormat( 'arn:{partition}:apigateway:{region_name}:lambda:path/' '2015-03-31/functions/arn:{partition}:lambda' ':{region_name}:{account_id}:function:%s/' 'invocations' % 'appname-dev-function_name_connect', ['partition', 'region_name', 'account_id'], ), ), models.APICall( method_name='create_websocket_integration', params={ 'api_id': Variable('websocket_api_id'), 'lambda_function': Variable( 'websocket-connect-integration-lambda-path'), 'handler_type': 'connect', }, output_var='connect-integration-id', ), models.StoreValue( name='websocket-message-integration-lambda-path', value=StringFormat( 'arn:{partition}:apigateway:{region_name}:lambda:path/' '2015-03-31/functions/arn:{partition}:lambda' ':{region_name}:{account_id}:function:%s/' 'invocations' % 'appname-dev-function_name_message', ['partition', 'region_name', 'account_id'], ), ), models.APICall( method_name='create_websocket_integration', params={ 'api_id': Variable('websocket_api_id'), 'lambda_function': Variable( 'websocket-message-integration-lambda-path'), 'handler_type': 'message', }, output_var='message-integration-id', ), models.StoreValue( name='websocket-disconnect-integration-lambda-path', value=StringFormat( 'arn:{partition}:apigateway:{region_name}:lambda:path/' '2015-03-31/functions/arn:{partition}:lambda' ':{region_name}:{account_id}:function:%s/' 'invocations' % 'appname-dev-function_name_disconnect', ['partition', 'region_name', 'account_id'], ), ), models.APICall( method_name='create_websocket_integration', params={ 'api_id': Variable('websocket_api_id'), 'lambda_function': Variable( 'websocket-disconnect-integration-lambda-path'), 'handler_type': 'disconnect', }, output_var='disconnect-integration-id', ), models.APICall( method_name='create_websocket_route', params={ 'api_id': Variable('websocket_api_id'), 'route_key': '$connect', 'integration_id': Variable('connect-integration-id'), }, ), models.APICall( method_name='create_websocket_route', params={ 'api_id': Variable('websocket_api_id'), 'route_key': '$default', 'integration_id': Variable('message-integration-id'), }, ), models.APICall( method_name='create_websocket_route', params={ 'api_id': Variable('websocket_api_id'), 'route_key': '$disconnect', 'integration_id': Variable('disconnect-integration-id'), }, ), models.StoreValue( name='websocket_api_url', value=StringFormat( 'wss://{websocket_api_id}.execute-api.{region_name}' '.{dns_suffix}/%s/' % 'api', ['websocket_api_id', 'region_name', 'dns_suffix'], ), ), models.RecordResourceVariable( resource_type='websocket_api', resource_name='websocket_api', name='websocket_api_url', variable_name='websocket_api_url', ), models.RecordResourceVariable( resource_type='websocket_api', resource_name='websocket_api', name='websocket_api_id', variable_name='websocket_api_id', ), models.APICall( method_name='add_permission_for_apigateway_v2', params={'function_name': 'appname-dev-function_name_connect', 'region_name': Variable('region_name'), 'account_id': Variable('account_id'), 'api_id': Variable('websocket_api_id')}, ), models.APICall( method_name='add_permission_for_apigateway_v2', params={'function_name': 'appname-dev-function_name_message', 'region_name': Variable('region_name'), 'account_id': Variable('account_id'), 'api_id': Variable('websocket_api_id')}, ), models.APICall( method_name='add_permission_for_apigateway_v2', params={ 'function_name': 'appname-dev-function_name_disconnect', 'region_name': Variable('region_name'), 'account_id': Variable('account_id'), 'api_id': Variable('websocket_api_id'), }, ), ] class TestPlanRestAPI(BasePlannerTests): def assert_loads_needed_variables(self, plan): # Parse arn and store region/account id for future # API calls. assert plan[0:6] == [ models.BuiltinFunction( 'parse_arn', [Variable('function_name_lambda_arn')], output_var='parsed_lambda_arn', ), models.JPSearch('account_id', input_var='parsed_lambda_arn', output_var='account_id'), models.JPSearch('region', input_var='parsed_lambda_arn', output_var='region_name'), models.JPSearch('partition', input_var='parsed_lambda_arn', output_var='partition'), models.JPSearch('dns_suffix', input_var='parsed_lambda_arn', output_var='dns_suffix'), # Verify we copy the function arn as needed. models.CopyVariable( from_var='function_name_lambda_arn', to_var='api_handler_lambda_arn'), ] def test_can_plan_rest_api(self): function = create_function_resource('function_name') rest_api = models.RestAPI( resource_name='rest_api', swagger_doc={'swagger': '2.0'}, endpoint_type='EDGE', minimum_compression='100', api_gateway_stage='api', xray=False, lambda_function=function, ) plan = self.determine_plan(rest_api) self.assert_loads_needed_variables(plan) assert plan[6:] == [ models.APICall( method_name='import_rest_api', params={'swagger_document': {'swagger': '2.0'}, 'endpoint_type': 'EDGE'}, output_var='rest_api_id', ), models.RecordResourceVariable( resource_type='rest_api', resource_name='rest_api', name='rest_api_id', variable_name='rest_api_id', ), models.APICall( method_name='update_rest_api', params={ 'rest_api_id': Variable('rest_api_id'), 'patch_operations': [{ 'op': 'replace', 'path': '/minimumCompressionSize', 'value': '100', }], } ), models.APICall( method_name='add_permission_for_apigateway', params={ 'function_name': 'appname-dev-function_name', 'region_name': Variable('region_name'), 'account_id': Variable('account_id'), 'rest_api_id': Variable('rest_api_id'), } ), models.APICall(method_name='deploy_rest_api', params={'rest_api_id': Variable('rest_api_id'), 'xray': False, 'api_gateway_stage': 'api'}), models.StoreValue( name='rest_api_url', value=StringFormat( 'https://{rest_api_id}.execute-api.{region_name}' '.{dns_suffix}/api/', ['rest_api_id', 'region_name', 'dns_suffix'], ), ), models.RecordResourceVariable( resource_type='rest_api', resource_name='rest_api', name='rest_api_url', variable_name='rest_api_url' ), ] assert list(self.last_plan.messages.values()) == [ 'Creating Rest API\n' ] def test_can_update_rest_api_with_policy(self): function = create_function_resource('function_name') rest_api = models.RestAPI( resource_name='rest_api', swagger_doc={'swagger': '2.0'}, minimum_compression='', api_gateway_stage='api', endpoint_type='EDGE', policy="{'Statement': []}", lambda_function=function, ) self.remote_state.declare_resource_exists(rest_api) self.remote_state.deployed_values['rest_api'] = { 'rest_api_id': 'my_rest_api_id', } plan = self.determine_plan(rest_api) assert plan[10].params == { 'patch_operations': [ {'op': 'replace', 'path': '/minimumCompressionSize', 'value': ''}, {'op': 'replace', 'path': StringFormat( ("/endpointConfiguration/types/" "{rest_api[endpointConfiguration][types][0]}"), ['rest_api']), 'value': 'EDGE'} ], 'rest_api_id': Variable("rest_api_id") } def test_can_update_rest_api(self): function = create_function_resource('function_name') rest_api = models.RestAPI( resource_name='rest_api', swagger_doc={'swagger': '2.0'}, minimum_compression='', api_gateway_stage='api', endpoint_type='REGIONAL', xray=False, lambda_function=function, ) self.remote_state.declare_resource_exists(rest_api) self.remote_state.deployed_values['rest_api'] = { 'rest_api_id': 'my_rest_api_id', } plan = self.determine_plan(rest_api) self.assert_loads_needed_variables(plan) assert plan[6:] == [ models.StoreValue(name='rest_api_id', value='my_rest_api_id'), models.RecordResourceVariable( resource_type='rest_api', resource_name='rest_api', name='rest_api_id', variable_name='rest_api_id', ), models.APICall( method_name='update_api_from_swagger', params={ 'rest_api_id': Variable('rest_api_id'), 'swagger_document': {'swagger': '2.0'}, }, ), models.APICall( method_name='get_rest_api', params={'rest_api_id': Variable('rest_api_id')}, output_var='rest_api' ), models.APICall( method_name='update_rest_api', params={ 'rest_api_id': Variable('rest_api_id'), 'patch_operations': [{ 'op': 'replace', 'path': '/minimumCompressionSize', 'value': ''}, {'op': 'replace', 'value': 'REGIONAL', 'path': StringFormat( '/endpointConfiguration/types/%s' % ( '{rest_api[endpointConfiguration][types][0]}'), ['rest_api'])}, ], }, ), models.APICall( method_name='add_permission_for_apigateway', params={'rest_api_id': Variable("rest_api_id"), 'region_name': Variable("region_name"), 'account_id': Variable("account_id"), 'function_name': 'appname-dev-function_name'}, output_var=None), models.APICall( method_name='deploy_rest_api', params={'rest_api_id': Variable('rest_api_id'), 'xray': False, 'api_gateway_stage': 'api'}, ), models.StoreValue( name='rest_api_url', value=StringFormat( 'https://{rest_api_id}.execute-api.{region_name}' '.{dns_suffix}/api/', ['rest_api_id', 'region_name', 'dns_suffix'], ), ), models.RecordResourceVariable( resource_type='rest_api', resource_name='rest_api', name='rest_api_url', variable_name='rest_api_url' ), ] class TestPlanSNSSubscription(BasePlannerTests): def test_can_plan_sns_subscription(self): function = create_function_resource('function_name') sns_subscription = models.SNSLambdaSubscription( resource_name='function_name-sns-subscription', topic='mytopic', lambda_function=function ) plan = self.determine_plan(sns_subscription) plan_parse_arn = plan[:5] assert plan_parse_arn == [ models.BuiltinFunction( function_name='parse_arn', args=[Variable("function_name_lambda_arn")], output_var='parsed_lambda_arn'), models.JPSearch( expression='account_id', input_var='parsed_lambda_arn', output_var='account_id'), models.JPSearch( expression='region', input_var='parsed_lambda_arn', output_var='region_name'), models.JPSearch( expression='partition', input_var='parsed_lambda_arn', output_var='partition'), models.StoreValue( name='function_name-sns-subscription_topic_arn', value=StringFormat( "arn:{partition}:sns:{region_name}:{account_id}:mytopic", variables=['partition', 'region_name', 'account_id'], ) ), ] topic_arn_var = Variable("function_name-sns-subscription_topic_arn") assert plan[5:7] == [ models.APICall( method_name='add_permission_for_sns_topic', params={ 'function_arn': Variable("function_name_lambda_arn"), 'topic_arn': topic_arn_var, }, output_var=None ), models.APICall( method_name='subscribe_function_to_topic', params={ 'function_arn': Variable("function_name_lambda_arn"), 'topic_arn': topic_arn_var, }, output_var='function_name-sns-subscription_subscription_arn' ), ] self.assert_recorded_values( plan, 'sns_event', 'function_name-sns-subscription', { 'topic': 'mytopic', 'lambda_arn': Variable('function_name_lambda_arn'), 'subscription_arn': Variable( 'function_name-sns-subscription_subscription_arn'), 'topic_arn': Variable( 'function_name-sns-subscription_topic_arn'), } ) def test_can_plan_sns_arn_subscription(self): function = create_function_resource('function_name') topic_arn = 'arn:aws:sns:mars-west-2:123456789:mytopic' sns_subscription = models.SNSLambdaSubscription( resource_name='function_name-sns-subscription', topic=topic_arn, lambda_function=function ) plan = self.determine_plan(sns_subscription) plan_parse_arn = plan[0] assert plan_parse_arn == models.StoreValue( name='function_name-sns-subscription_topic_arn', value=topic_arn, ) topic_arn_var = Variable("function_name-sns-subscription_topic_arn") assert plan[1:3] == [ models.APICall( method_name='add_permission_for_sns_topic', params={ 'function_arn': Variable("function_name_lambda_arn"), 'topic_arn': topic_arn_var, }, output_var=None ), models.APICall( method_name='subscribe_function_to_topic', params={ 'function_arn': Variable("function_name_lambda_arn"), 'topic_arn': topic_arn_var, }, output_var='function_name-sns-subscription_subscription_arn' ), ] self.assert_recorded_values( plan, 'sns_event', 'function_name-sns-subscription', { 'topic': topic_arn, 'lambda_arn': Variable('function_name_lambda_arn'), 'subscription_arn': Variable( 'function_name-sns-subscription_subscription_arn'), 'topic_arn': Variable( 'function_name-sns-subscription_topic_arn'), } ) def test_sns_subscription_exists_is_noop_for_planner(self): function = create_function_resource('function_name') sns_subscription = models.SNSLambdaSubscription( resource_name='function_name-sns-subscription', topic='mytopic', lambda_function=function ) self.remote_state.declare_resource_exists( sns_subscription, topic='mytopic', resource_type='sns_event', lambda_arn='arn:lambda', subscription_arn='arn:aws:subscribe', ) plan = self.determine_plan(sns_subscription) plan_parse_arn = plan[:5] assert plan_parse_arn == [ models.BuiltinFunction( function_name='parse_arn', args=[Variable("function_name_lambda_arn")], output_var='parsed_lambda_arn'), models.JPSearch( expression='account_id', input_var='parsed_lambda_arn', output_var='account_id'), models.JPSearch( expression='region', input_var='parsed_lambda_arn', output_var='region_name'), models.JPSearch( expression='partition', input_var='parsed_lambda_arn', output_var='partition'), models.StoreValue( name='function_name-sns-subscription_topic_arn', value=StringFormat( "arn:{partition}:sns:{region_name}:{account_id}:mytopic", variables=['partition', 'region_name', 'account_id'], ) ), ] self.assert_recorded_values( plan, 'sns_event', 'function_name-sns-subscription', { 'topic': 'mytopic', 'lambda_arn': Variable('function_name_lambda_arn'), 'subscription_arn': 'arn:aws:subscribe', 'topic_arn': Variable( 'function_name-sns-subscription_topic_arn'), } ) class TestPlanSQSSubscription(BasePlannerTests): def test_can_plan_sqs_event_source(self): function = create_function_resource('function_name') sqs_event_source = models.SQSEventSource( resource_name='function_name-sqs-event-source', queue='myqueue', batch_size=10, lambda_function=function, maximum_batching_window_in_seconds=60 ) plan = self.determine_plan(sqs_event_source) plan_parse_arn = plan[:5] assert plan_parse_arn == [ models.BuiltinFunction( function_name='parse_arn', args=[Variable("function_name_lambda_arn")], output_var='parsed_lambda_arn' ), models.JPSearch( expression='account_id', input_var='parsed_lambda_arn', output_var='account_id' ), models.JPSearch( expression='region', input_var='parsed_lambda_arn', output_var='region_name' ), models.JPSearch( expression='partition', input_var='parsed_lambda_arn', output_var='partition' ), models.StoreValue( name='function_name-sqs-event-source_queue_arn', value=StringFormat( "arn:{partition}:sqs:{region_name}:{account_id}:myqueue", variables=['partition', 'region_name', 'account_id'], ), ) ] assert plan[5] == models.APICall( method_name='create_lambda_event_source', params={ 'event_source_arn': Variable( "function_name-sqs-event-source_queue_arn" ), 'batch_size': 10, 'maximum_batching_window_in_seconds': 60, 'function_name': Variable("function_name_lambda_arn"), 'maximum_concurrency': None }, output_var='function_name-sqs-event-source_uuid' ) self.assert_recorded_values( plan, 'sqs_event', 'function_name-sqs-event-source', { 'queue_arn': Variable( 'function_name-sqs-event-source_queue_arn'), 'event_uuid': Variable( 'function_name-sqs-event-source_uuid'), 'queue': 'myqueue', 'lambda_arn': Variable( 'function_name_lambda_arn') } ) def test_sqs_event_supports_queue_arn(self): function = create_function_resource('function_name') sqs_event_source = models.SQSEventSource( resource_name='function_name-sqs-event-source', queue=models.QueueARN(arn='arn:us-west-2:myqueue'), batch_size=10, lambda_function=function, maximum_batching_window_in_seconds=0 ) plan = self.determine_plan(sqs_event_source) assert plan[0] == models.StoreValue( name='function_name-sqs-event-source_queue_arn', value='arn:us-west-2:myqueue', ) assert plan[1] == models.APICall( method_name='create_lambda_event_source', params={ 'event_source_arn': Variable( "function_name-sqs-event-source_queue_arn" ), 'batch_size': 10, 'maximum_batching_window_in_seconds': 0, 'function_name': Variable("function_name_lambda_arn"), 'maximum_concurrency': None, }, output_var='function_name-sqs-event-source_uuid' ) self.assert_recorded_values( plan, 'sqs_event', 'function_name-sqs-event-source', { 'queue_arn': Variable( 'function_name-sqs-event-source_queue_arn'), 'event_uuid': Variable( 'function_name-sqs-event-source_uuid'), 'queue': 'myqueue', 'lambda_arn': Variable( 'function_name_lambda_arn') } ) def test_can_update_sqs_event_with_queue_arn(self): function = create_function_resource('function_name') sqs_event_source = models.SQSEventSource( resource_name='function_name-sqs-event-source', queue=models.QueueARN(arn='arn:sqs:myqueue'), batch_size=10, lambda_function=function, maximum_batching_window_in_seconds=0 ) self.remote_state.declare_resource_exists( sqs_event_source, queue='myqueue', queue_arn='arn:sqs:myqueue', resource_type='sqs_event', lambda_arn='arn:lambda', event_uuid='my-uuid', ) plan = self.determine_plan(sqs_event_source) assert plan[0] == models.StoreValue( name='function_name-sqs-event-source_queue_arn', value='arn:sqs:myqueue', ) assert plan[1] == models.APICall( method_name='update_lambda_event_source', params={ 'event_uuid': 'my-uuid', 'batch_size': 10, 'maximum_batching_window_in_seconds': 0, 'maximum_concurrency': None, }, ) self.assert_recorded_values( plan, 'sqs_event', 'function_name-sqs-event-source', { 'queue_arn': 'arn:sqs:myqueue', 'event_uuid': 'my-uuid', 'queue': 'myqueue', 'lambda_arn': 'arn:lambda' } ) def test_sqs_event_source_exists_updates_batch_size(self): function = create_function_resource('function_name') sqs_event_source = models.SQSEventSource( resource_name='function_name-sqs-event-source', queue='myqueue', batch_size=10, lambda_function=function, maximum_batching_window_in_seconds=0 ) self.remote_state.declare_resource_exists( sqs_event_source, queue='myqueue', queue_arn='arn:sqs:myqueue', resource_type='sqs_event', lambda_arn='arn:lambda', event_uuid='my-uuid', ) plan = self.determine_plan(sqs_event_source) plan_parse_arn = plan[:5] assert plan_parse_arn == [ models.BuiltinFunction( function_name='parse_arn', args=[Variable("function_name_lambda_arn")], output_var='parsed_lambda_arn'), models.JPSearch( expression='account_id', input_var='parsed_lambda_arn', output_var='account_id'), models.JPSearch( expression='region', input_var='parsed_lambda_arn', output_var='region_name'), models.JPSearch( expression='partition', input_var='parsed_lambda_arn', output_var='partition' ), models.StoreValue( name='function_name-sqs-event-source_queue_arn', value=StringFormat( "arn:{partition}:sqs:{region_name}:{account_id}:myqueue", variables=['partition', 'region_name', 'account_id'], ), ) ] assert plan[5] == models.APICall( method_name='update_lambda_event_source', params={ 'event_uuid': 'my-uuid', 'batch_size': 10, 'maximum_batching_window_in_seconds': 0, 'maximum_concurrency': None, }, ) self.assert_recorded_values( plan, 'sqs_event', 'function_name-sqs-event-source', { 'queue_arn': 'arn:sqs:myqueue', 'event_uuid': 'my-uuid', 'queue': 'myqueue', 'lambda_arn': 'arn:lambda' } ) def test_sqs_event_supports_maximum_concurrency(self): function = create_function_resource('function_name') sqs_event_source = models.SQSEventSource( resource_name='function_name-sqs-event-source', queue=models.QueueARN(arn='arn:us-west-2:myqueue'), batch_size=10, lambda_function=function, maximum_batching_window_in_seconds=0, maximum_concurrency=2 ) plan = self.determine_plan(sqs_event_source) assert plan[1] == models.APICall( method_name='create_lambda_event_source', params={ 'event_source_arn': Variable( "function_name-sqs-event-source_queue_arn" ), 'batch_size': 10, 'maximum_batching_window_in_seconds': 0, 'function_name': Variable("function_name_lambda_arn"), 'maximum_concurrency': 2, }, output_var='function_name-sqs-event-source_uuid' ) self.assert_recorded_values( plan, 'sqs_event', 'function_name-sqs-event-source', { 'queue_arn': Variable( 'function_name-sqs-event-source_queue_arn'), 'event_uuid': Variable( 'function_name-sqs-event-source_uuid'), 'queue': 'myqueue', 'lambda_arn': Variable( 'function_name_lambda_arn') } ) def test_sqs_event_source_exists_updates_maximum_concurrency(self): function = create_function_resource('function_name') sqs_event_source = models.SQSEventSource( resource_name='function_name-sqs-event-source', queue='myqueue', batch_size=10, lambda_function=function, maximum_batching_window_in_seconds=0, maximum_concurrency=2 ) self.remote_state.declare_resource_exists( sqs_event_source, queue='myqueue', queue_arn='arn:sqs:myqueue', resource_type='sqs_event', lambda_arn='arn:lambda', event_uuid='my-uuid', ) plan = self.determine_plan(sqs_event_source) assert plan[5] == models.APICall( method_name='update_lambda_event_source', params={ 'event_uuid': 'my-uuid', 'batch_size': 10, 'maximum_batching_window_in_seconds': 0, 'maximum_concurrency': 2, }, ) self.assert_recorded_values( plan, 'sqs_event', 'function_name-sqs-event-source', { 'queue_arn': 'arn:sqs:myqueue', 'event_uuid': 'my-uuid', 'queue': 'myqueue', 'lambda_arn': 'arn:lambda' } ) @pytest.mark.parametrize('functions,integration_injected', [ ( (create_function_resource('connect'), None, None), 'connect' ), ( (None, create_function_resource('message'), None), 'message' ), ( (None, None, create_function_resource('disconnect')), 'disconnect' ), ]) def test_websocket_api_plan_omits_unused_lambdas( self, functions, integration_injected): websocket_api = models.WebsocketAPI( resource_name='websocket_api', name='app-dev-websocket-api', api_gateway_stage='api', routes=['$connect', '$default', '$disconnect'], connect_function=functions[0], message_function=functions[1], disconnect_function=functions[2], ) plan = self.determine_plan(websocket_api) integrations = [ code.params['handler_type'] for code in plan if isinstance(code, APICall) and code.method_name == 'create_websocket_integration' ] assert len(integrations) == 1 assert integrations[0] == integration_injected class TestPlanKinesisSubscription(BasePlannerTests): def test_can_plan_kinesis_event_source(self): function = create_function_resource('function_name') kinesis_event_source = models.KinesisEventSource( resource_name='function_name-kinesis-event-source', stream='mystream', batch_size=10, starting_position='LATEST', maximum_batching_window_in_seconds=0, lambda_function=function ) plan = self.determine_plan(kinesis_event_source) plan_parse_arn = plan[:5] assert plan_parse_arn == [ models.BuiltinFunction( function_name='parse_arn', args=[Variable("function_name_lambda_arn")], output_var='parsed_lambda_arn' ), models.JPSearch( expression='account_id', input_var='parsed_lambda_arn', output_var='account_id' ), models.JPSearch( expression='region', input_var='parsed_lambda_arn', output_var='region_name' ), models.JPSearch( expression='partition', input_var='parsed_lambda_arn', output_var='partition' ), models.StoreValue( name='function_name-kinesis-event-source_stream_arn', value=StringFormat( ("arn:{partition}:kinesis:{region_name}:{account_id}:" "stream/mystream"), variables=['partition', 'region_name', 'account_id'], ), ) ] assert plan[5] == models.APICall( method_name='create_lambda_event_source', params={ 'event_source_arn': Variable( "function_name-kinesis-event-source_stream_arn" ), 'batch_size': 10, 'starting_position': 'LATEST', 'maximum_batching_window_in_seconds': 0, 'function_name': Variable("function_name_lambda_arn") }, output_var='function_name-kinesis-event-source_uuid' ) self.assert_recorded_values( plan, 'kinesis_event', 'function_name-kinesis-event-source', { 'kinesis_arn': Variable( 'function_name-kinesis-event-source_stream_arn'), 'event_uuid': Variable( 'function_name-kinesis-event-source_uuid'), 'stream': 'mystream', 'lambda_arn': Variable( 'function_name_lambda_arn') } ) def test_can_update_kinesis_event_source(self): function = create_function_resource('function_name') kinesis_event_source = models.KinesisEventSource( resource_name='function_name-kinesis-event-source', stream='mystream', batch_size=10, starting_position='LATEST', maximum_batching_window_in_seconds=60, lambda_function=function ) self.remote_state.declare_resource_exists( kinesis_event_source, stream='mystream', kinesis_arn='arn:aws:kinesis:stream', resource_type='kinesis_event', lambda_arn='arn:lambda', event_uuid='my-uuid', ) plan = self.determine_plan(kinesis_event_source) assert plan[5] == models.APICall( method_name='update_lambda_event_source', params={ 'event_uuid': 'my-uuid', 'batch_size': 10, 'maximum_batching_window_in_seconds': 60, } ) class TestPlanDynamoDBSubscription(BasePlannerTests): def test_can_plan_dynamodb_event_source(self): function = create_function_resource('function_name') event_source = models.DynamoDBEventSource( resource_name='handler-dynamodb-event-source', stream_arn='arn:stream', batch_size=100, maximum_batching_window_in_seconds=0, starting_position='LATEST', lambda_function=function) plan = self.determine_plan(event_source) assert plan[0] == models.APICall( method_name='create_lambda_event_source', params={ 'event_source_arn': 'arn:stream', 'batch_size': 100, 'function_name': Variable('function_name_lambda_arn'), 'starting_position': 'LATEST', 'maximum_batching_window_in_seconds': 0, }, output_var='handler-dynamodb-event-source_uuid', ) def test_can_plan_dynamodb_event_source_update(self): function = create_function_resource('function_name') event_source = models.DynamoDBEventSource( resource_name='handler-dynamodb-event-source', stream_arn='arn:stream', batch_size=100, maximum_batching_window_in_seconds=60, starting_position='LATEST', lambda_function=function) self.remote_state.declare_resource_exists( event_source, stream_arn='arn:stream', resource_type='dynamodb_event', lambda_arn='arn:lambda', event_uuid='my-uuid', ) plan = self.determine_plan(event_source) assert plan[0] == models.APICall( method_name='update_lambda_event_source', params={ 'event_uuid': 'my-uuid', 'batch_size': 100, 'maximum_batching_window_in_seconds': 60 }, ) class TestRemoteState(object): def setup_method(self): self.client = mock.Mock(spec=TypedAWSClient) self.config = FakeConfig({'resources': []}) self.remote_state = RemoteState( self.client, self.config.deployed_resources('dev'), ) def create_rest_api_model(self): rest_api = models.RestAPI( resource_name='rest_api', swagger_doc={'swagger': '2.0'}, minimum_compression='', endpoint_type='EDGE', api_gateway_stage='api', xray=False, lambda_function=None, ) return rest_api def create_api_mapping(self): api_mapping = models.APIMapping( resource_name='api_mapping', mount_path='(none)', api_gateway_stage='dev' ) return api_mapping def create_domain_name(self): domain_name = models.DomainName( protocol=models.APIType.HTTP, resource_name='api_gateway_custom_domain', domain_name='example.com', tls_version=models.TLSVersion.TLS_1_0, certificate_arn='certificate_arn', api_mapping=self.create_api_mapping() ) return domain_name def create_websocket_api_model(self): websocket_api = models.WebsocketAPI( resource_name='websocket_api', name='app-stage-websocket-api', api_gateway_stage='api', routes=[], connect_function=None, message_function=None, disconnect_function=None, ) return websocket_api def test_role_exists(self): self.client.get_role_arn_for_name.return_value = 'role:arn' role = models.ManagedIAMRole('my_role', role_name='app-dev', trust_policy={}, policy=None) assert self.remote_state.resource_exists(role) self.client.get_role_arn_for_name.assert_called_with('app-dev') def test_role_does_not_exist(self): client = self.client client.get_role_arn_for_name.side_effect = ResourceDoesNotExistError() role = models.ManagedIAMRole('my_role', role_name='app-dev', trust_policy={}, policy=None) assert not self.remote_state.resource_exists(role) self.client.get_role_arn_for_name.assert_called_with('app-dev') def test_lambda_layer_not_exists(self): layer = models.LambdaLayer( resource_name='layer', layer_name='bar', runtime='python2.7', deployment_package=models.DeploymentPackage( filename='foo') ) assert not self.remote_state.resource_exists(layer) def test_lambda_layer_exists(self): layer = models.LambdaLayer( resource_name='layer', layer_name='bar', runtime='python2.7', deployment_package=models.DeploymentPackage( filename='foo') ) deployed_resources = { 'resources': [{ 'name': 'layer', 'resource_type': 'lambda_layer', 'layer_version_arn': 'arn:layer:4' }] } self.client.get_layer_version.return_value = { 'LayerVersionArn': 'arn:layer:4'} remote_state = RemoteState( self.client, DeployedResources(deployed_resources)) assert remote_state.resource_exists(layer) def test_lambda_function_exists(self): function = create_function_resource('function-name') self.client.lambda_function_exists.return_value = True assert self.remote_state.resource_exists(function) self.client.lambda_function_exists.assert_called_with( function.function_name) def test_lambda_function_does_not_exist(self): function = create_function_resource('function-name') self.client.lambda_function_exists.return_value = False assert not self.remote_state.resource_exists(function) self.client.lambda_function_exists.assert_called_with( function.function_name) def test_api_gateway_domain_name_exists(self): domain_name = self.create_domain_name() self.client.domain_name_exists.return_value = True assert self.remote_state.resource_exists(domain_name) def test_websocket_domain_name_exists(self): domain_name = self.create_domain_name() domain_name.protocol = models.APIType.WEBSOCKET domain_name.resource_name = 'websocket_api_custom_domain' self.client.domain_name_exists_v2.return_value = True assert self.remote_state.resource_exists(domain_name) def test_none_api_mapping_exists(self): api_mapping = self.create_api_mapping() self.client.api_mapping_exists.return_value = True assert self.remote_state.resource_exists(api_mapping, 'domain_name') def test_path_api_mapping_exists_with_slash(self): api_mapping = self.create_api_mapping() api_mapping.mount_path = '/path' self.client.api_mapping_exists.return_value = True assert self.remote_state.resource_exists(api_mapping, 'domain_name') def test_path_api_mapping_exists(self): api_mapping = self.create_api_mapping() api_mapping.mount_path = 'path' self.client.api_mapping_exists.return_value = True assert self.remote_state.resource_exists(api_mapping, 'domain_name') def test_domain_name_does_not_exist(self): domain_name = self.create_domain_name() self.client.domain_name_exists.return_value = False assert not self.remote_state.resource_exists(domain_name) domain_name.protocol = models.APIType.WEBSOCKET domain_name.resource_name = 'websocket_api_custom_domain' self.client.domain_name_exists_v2.return_value = False assert not self.remote_state.resource_exists(domain_name) def test_exists_check_is_cached(self): function = create_function_resource('function-name') self.client.lambda_function_exists.return_value = True assert self.remote_state.resource_exists(function) # Now if we call this method repeatedly we should only invoke # the underlying client method once. Subsequent calls are cached. assert self.remote_state.resource_exists(function) assert self.remote_state.resource_exists(function) assert self.client.lambda_function_exists.call_count == 1 def test_exists_check_is_cached_api_mapping(self): api_mapping = models.APIMapping( resource_name='api_mapping', mount_path='(none)', api_gateway_stage='dev' ) self.client.api_mapping_exists.return_value = True assert self.remote_state.resource_exists(api_mapping, 'domain_name') assert self.remote_state.resource_exists(api_mapping, 'domain_name') assert self.remote_state.resource_exists(api_mapping, 'domain_name') def test_rest_api_exists_no_deploy(self, no_deployed_values): rest_api = self.create_rest_api_model() remote_state = RemoteState( self.client, no_deployed_values) assert not remote_state.resource_exists(rest_api) assert not self.client.get_rest_api.called def test_rest_api_exists_with_existing_deploy(self): rest_api = self.create_rest_api_model() deployed_resources = { 'resources': [{ 'name': 'rest_api', 'resource_type': 'rest_api', 'rest_api_id': 'my_rest_api_id', }] } self.client.get_rest_api.return_value = {'apiId': 'my_rest_api_id'} remote_state = RemoteState( self.client, DeployedResources(deployed_resources)) assert remote_state.resource_exists(rest_api) self.client.get_rest_api.assert_called_with('my_rest_api_id') def test_rest_api_not_exists_with_preexisting_deploy(self): rest_api = self.create_rest_api_model() deployed_resources = { 'resources': [{ 'name': 'rest_api', 'resource_type': 'rest_api', 'rest_api_id': 'my_rest_api_id', }] } self.client.get_rest_api.return_value = {} remote_state = RemoteState( self.client, DeployedResources(deployed_resources)) assert not remote_state.resource_exists(rest_api) self.client.get_rest_api.assert_called_with('my_rest_api_id') def test_websocket_api_exists_no_deploy(self, no_deployed_values): rest_api = self.create_websocket_api_model() remote_state = RemoteState( self.client, no_deployed_values) assert not remote_state.resource_exists(rest_api) assert not self.client.websocket_api_exists.called def test_websocket_api_exists_with_existing_deploy(self): websocket_api = self.create_websocket_api_model() deployed_resources = { 'resources': [{ 'name': 'websocket_api', 'resource_type': 'websocket_api', 'websocket_api_id': 'my_websocket_api_id', }] } self.client.websocket_api_exists.return_value = True remote_state = RemoteState( self.client, DeployedResources(deployed_resources)) assert remote_state.resource_exists(websocket_api) self.client.websocket_api_exists.assert_called_with( 'my_websocket_api_id') def test_websocket_api_not_exists_with_preexisting_deploy(self): websocket_api = self.create_websocket_api_model() deployed_resources = { 'resources': [{ 'name': 'websocket_api', 'resource_type': 'websocket_api', 'websocket_api_id': 'my_websocket_api_id', }] } self.client.websocket_api_exists.return_value = False remote_state = RemoteState( self.client, DeployedResources(deployed_resources)) assert not remote_state.resource_exists(websocket_api) self.client.websocket_api_exists.assert_called_with( 'my_websocket_api_id') def test_can_get_deployed_values(self): remote_state = RemoteState( self.client, DeployedResources({'resources': [ {'name': 'rest_api', 'rest_api_id': 'foo'}]}) ) rest_api = self.create_rest_api_model() values = remote_state.resource_deployed_values(rest_api) assert values == {'name': 'rest_api', 'rest_api_id': 'foo'} def test_value_error_raised_on_no_deployed_values(self, no_deployed_values): remote_state = RemoteState(self.client, deployed_resources=no_deployed_values) rest_api = self.create_rest_api_model() with pytest.raises(ValueError): remote_state.resource_deployed_values(rest_api) def test_value_error_raised_for_unknown_resource_name(self): remote_state = RemoteState( self.client, DeployedResources({'resources': [ {'name': 'not_rest_api', 'rest_api_id': 'foo'}]}) ) rest_api = self.create_rest_api_model() with pytest.raises(ValueError): remote_state.resource_deployed_values(rest_api) def test_dynamically_lookup_iam_role(self): remote_state = RemoteState( self.client, DeployedResources({'resources': [ {'name': 'rest_api', 'rest_api_id': 'foo'}]}) ) resource = models.ManagedIAMRole( resource_name='default-role', role_name='myrole', trust_policy={'trust': 'policy'}, policy=models.AutoGenIAMPolicy(document={'iam': 'policy'}), ) self.client.get_role_arn_for_name.return_value = 'my-role-arn' values = remote_state.resource_deployed_values(resource) assert values == { 'name': 'default-role', 'resource_type': 'iam_role', 'role_arn': 'my-role-arn', 'role_name': 'myrole' } def test_unknown_model_type_raises_error(self): @dataclass class Foo(models.ManagedModel): resource_type = 'foo' foo = Foo(resource_name='myfoo') with pytest.raises(ValueError): self.remote_state.resource_exists(foo) @pytest.mark.parametrize( 'resource_topic,deployed_topic,is_current,expected_result', [ ('mytopic', 'mytopic', True, True), ('mytopic-new', 'mytopic-old', False, False), ] ) def test_sns_subscription_exists(self, resource_topic, deployed_topic, is_current, expected_result): sns_subscription = models.SNSLambdaSubscription( topic=resource_topic, resource_name='handler-sns-subscription', lambda_function=None ) deployed_resources = { 'resources': [{ 'name': 'handler-sns-subscription', 'topic': deployed_topic, 'resource_type': 'sns_event', 'lambda_arn': 'arn:lambda', 'subscription_arn': 'arn:aws:subscribe', }] } self.client.verify_sns_subscription_current.return_value = \ is_current remote_state = RemoteState( self.client, DeployedResources(deployed_resources)) assert ( remote_state.resource_exists(sns_subscription) == expected_result ) self.client.verify_sns_subscription_current.assert_called_with( 'arn:aws:subscribe', topic_name=resource_topic, function_arn='arn:lambda', ) def test_sns_subscription_not_in_deployed_values(self): sns_subscription = models.SNSLambdaSubscription( topic='mytopic', resource_name='handler-sns-subscription', lambda_function=None ) deployed_resources = {'resources': []} remote_state = RemoteState( self.client, DeployedResources(deployed_resources)) assert not remote_state.resource_exists(sns_subscription) assert not self.client.verify_sns_subscription_current.called @pytest.mark.parametrize( 'new_queue,deployed_queue,expected_result', [ ('queue', 'queue', True), ('new-queue', 'queue', False), ('new-queue', None, False), ] ) def test_sqs_event_source_exists(self, new_queue, deployed_queue, expected_result): event_source = models.SQSEventSource( resource_name='handler-sqs-event-source', maximum_batching_window_in_seconds=0, queue=new_queue, batch_size=100, lambda_function=None ) if deployed_queue is not None: deployed_resources = { 'resources': [{ 'queue': deployed_queue, 'queue_arn': 'arn:aws:sqs:us-west-2:123:myqueue', 'name': 'handler-sqs-event-source', 'lambda_arn': 'arn:aws:lambda:handler', 'event_uuid': 'event-uid-123', 'resource_type': 'sqs_event' }] } else: deployed_resources = {'resources': []} self.client.verify_event_source_current.return_value = \ new_queue == deployed_queue remote_state = RemoteState( self.client, DeployedResources(deployed_resources), ) assert remote_state.resource_exists(event_source) == expected_result if deployed_queue is not None: self.client.verify_event_source_current.assert_called_with( event_uuid='event-uid-123', resource_name=new_queue, service_name='sqs', function_arn='arn:aws:lambda:handler', ) def test_kinesis_event_source_not_exists(self): event_source = models.KinesisEventSource( resource_name='handler-kinesis-event-source', stream='mystream', batch_size=100, starting_position='LATEST', maximum_batching_window_in_seconds=0, lambda_function=None) deployed_resources = {'resources': []} remote_state = RemoteState( self.client, DeployedResources(deployed_resources), ) assert not remote_state.resource_exists(event_source) def test_kinesis_event_source_exists(self): event_source = models.KinesisEventSource( resource_name='handler-kinesis-event-source', stream='mystream', batch_size=100, starting_position='LATEST', maximum_batching_window_in_seconds=0, lambda_function=None) deployed_resources = { 'resources': [{ 'name': 'handler-kinesis-event-source', 'resource_type': 'kinesis_event', 'kinesis_arn': 'arn:aws:kinesis:...:stream/mystream', 'event_uuid': 'abcd', 'stream': 'mystream', 'lambda_arn': 'arn:aws:lambda:function:test-dev-index' }] } remote_state = RemoteState( self.client, DeployedResources(deployed_resources), ) self.client.verify_event_source_current.return_value = True assert remote_state.resource_exists(event_source) def test_ddb_event_source_not_exists(self): event_source = models.DynamoDBEventSource( resource_name='handler-dynamodb-event-source', stream_arn='arn:stream', batch_size=100, maximum_batching_window_in_seconds=0, starting_position='LATEST', lambda_function=None) deployed_resources = {'resources': []} remote_state = RemoteState( self.client, DeployedResources(deployed_resources), ) assert not remote_state.resource_exists(event_source) def test_ddb_event_source_exists(self): event_source = models.KinesisEventSource( resource_name='handler-kinesis-event-source', stream='mystream', batch_size=100, starting_position='LATEST', maximum_batching_window_in_seconds=0, lambda_function=None) deployed_resources = { 'resources': [{ 'name': 'handler-kinesis-event-source', 'resource_type': 'kinesis_event', 'stream_arn': 'arn:aws:kinesis:...:stream/mystream', 'event_uuid': 'abcd', 'stream': 'mystream', 'lambda_arn': 'arn:aws:lambda:function:test-dev-index' }] } remote_state = RemoteState( self.client, DeployedResources(deployed_resources), ) self.client.verify_event_source_arn_current.return_value = True assert remote_state.resource_exists(event_source) class TestUnreferencedResourcePlanner(BasePlannerTests): def setup_method(self): super(TestUnreferencedResourcePlanner, self).setup_method() self.sweeper = ResourceSweeper() def execute(self, plan, config): self.sweeper.execute(models.Plan(plan, messages={}), config) @pytest.fixture def function_resource(self): return create_function_resource('myfunction') def one_deployed_lambda_function(self, name='myfunction', arn='arn'): return { 'resources': [{ 'name': name, 'resource_type': 'lambda_function', 'lambda_arn': arn, }] } def test_noop_when_all_resources_accounted_for(self, function_resource): plan = [ models.RecordResource( resource_type='lambda_function', resource_name='myfunction', name='foo', ) ] original_plan = plan[:] deployed = self.one_deployed_lambda_function(name='myfunction') config = FakeConfig(deployed) self.execute(plan, config) # We shouldn't add anything to the list. assert plan == original_plan def test_will_delete_unreferenced_resource(self): plan = [] deployed = self.one_deployed_lambda_function() config = FakeConfig(deployed) self.execute(plan, config) assert len(plan) == 1 assert plan[0].method_name == 'delete_function' assert plan[0].params == {'function_name': 'arn'} def test_will_delete_log_group(self): plan = [] deployed = { 'resources': [{ 'resource_type': 'log_group', 'name': 'my-log-group', 'log_group_name': '/aws/lambda/mygroup', }], } config = FakeConfig(deployed) self.execute(plan, config) assert len(plan) == 1 assert plan[0].method_name == 'delete_retention_policy' assert plan[0].params == {'log_group_name': '/aws/lambda/mygroup'} def test_supports_multiple_unreferenced_and_unchanged(self): first = create_function_resource('first') second = create_function_resource('second') third = create_function_resource('third') plan = [ models.RecordResource( resource_type='lambda_function', resource_name=first.resource_name, name='foo', ), models.RecordResource( resource_type='asdf', resource_name=second.resource_name, name='foo', ) ] deployed = { 'resources': [{ 'name': second.resource_name, 'resource_type': 'lambda_function', 'lambda_arn': 'second_arn', }, { 'name': third.resource_name, 'resource_type': 'lambda_function', 'lambda_arn': 'third_arn', }] } config = FakeConfig(deployed) self.execute(plan, config) assert len(plan) == 3 assert plan[2].method_name == 'delete_function' assert plan[2].params == {'function_name': 'third_arn'} def test_can_delete_iam_role(self): plan = [] deployed = { 'resources': [{ 'name': 'myrole', 'resource_type': 'iam_role', 'role_name': 'myrole', 'role_arn': 'arn:role/myrole', }] } config = FakeConfig(deployed) self.execute(plan, config) assert len(plan) == 1 assert plan[0].method_name == 'delete_role' assert plan[0].params == {'name': 'myrole'} def test_correct_deletion_order_for_dependencies(self): plan = [] deployed = { # This is the order they were deployed. While not # strictly required for IAM Roles, we typically # want to delete resources in the reverse order they # were created. 'resources': [ { 'name': 'myrole', 'resource_type': 'iam_role', 'role_name': 'myrole', 'role_arn': 'arn:role/myrole', }, { 'name': 'myrole2', 'resource_type': 'iam_role', 'role_name': 'myrole2', 'role_arn': 'arn:role/myrole2', }, { 'name': 'myfunction', 'resource_type': 'lambda_function', 'lambda_arn': 'my:arn', } ] } config = FakeConfig(deployed) self.execute(plan, config) assert len(plan) == 3 expected_api_calls = [p.method_name for p in plan] assert expected_api_calls == ['delete_function', 'delete_role', 'delete_role'] expected_api_args = [p.params for p in plan] assert expected_api_args == [ {'function_name': 'my:arn'}, {'name': 'myrole2'}, {'name': 'myrole'}, ] def test_can_delete_lambda_layer(self): plan = [] deployed = { 'resources': [{ 'name': 'layer', 'resource_type': 'lambda_layer', 'layer_version_arn': 'arn'}]} config = FakeConfig(deployed) self.execute(plan, config) assert plan == [ models.APICall( method_name='delete_layer_version', params={'layer_version_arn': 'arn'})] def test_can_delete_scheduled_event(self): plan = [] deployed = { 'resources': [{ 'name': 'index-event', 'resource_type': 'cloudwatch_event', 'rule_name': 'app-dev-index-event', }] } config = FakeConfig(deployed) self.execute(plan, config) assert plan == [ models.APICall( method_name='delete_rule', params={'rule_name': 'app-dev-index-event'}, ) ] def test_can_delete_s3_event(self): plan = [] deployed = { 'resources': [{ 'name': 'test-s3-event', 'resource_type': 's3_event', 'bucket': 'mybucket', 'lambda_arn': 'lambda_arn', }] } config = FakeConfig(deployed) self.execute(plan, config) assert plan == [ models.BuiltinFunction( function_name='parse_arn', args=['lambda_arn'], output_var='parsed_lambda_arn'), models.JPSearch(expression='account_id', input_var='parsed_lambda_arn', output_var='account_id'), models.APICall( method_name='disconnect_s3_bucket_from_lambda', params={'bucket': 'mybucket', 'function_arn': 'lambda_arn'}, ), models.APICall( method_name='remove_permission_for_s3_event', params={'bucket': 'mybucket', 'function_arn': 'lambda_arn', 'account_id': Variable("account_id")} ) ] def test_can_delete_rest_api(self): plan = [] deployed = { 'resources': [{ 'name': 'rest_api', 'rest_api_id': 'my_rest_api_id', 'resource_type': 'rest_api', }] } config = FakeConfig(deployed) self.execute(plan, config) assert plan == [ models.APICall( method_name='delete_rest_api', params={'rest_api_id': 'my_rest_api_id'}, ) ] def test_can_delete_websocket_api(self): plan = [] deployed = { 'resources': [{ 'name': 'websocket_api', 'websocket_api_id': 'my_websocket_api_id', 'resource_type': 'websocket_api', }] } config = FakeConfig(deployed) self.execute(plan, config) assert plan == [ models.APICall( method_name='delete_websocket_api', params={'api_id': 'my_websocket_api_id'}, ) ] def test_can_handle_when_resource_changes_values(self): plan = self.determine_plan( models.S3BucketNotification( resource_name='test-s3-event', bucket='NEWBUCKET', events=['s3:ObjectCreated:*'], prefix=None, suffix=None, lambda_function=create_function_resource('function_name'), ) ) deployed = { 'resources': [{ 'name': 'test-s3-event', 'resource_type': 's3_event', 'bucket': 'OLDBUCKET', 'lambda_arn': 'lambda_arn', }] } config = FakeConfig(deployed) self.execute(plan, config) assert plan[-2:] == [ models.APICall( method_name='disconnect_s3_bucket_from_lambda', params={'bucket': 'OLDBUCKET', 'function_arn': 'lambda_arn'}, ), models.APICall( method_name='remove_permission_for_s3_event', params={'bucket': 'OLDBUCKET', 'function_arn': 'lambda_arn', 'account_id': Variable('account_id')}, ), ] def test_no_sweeping_when_resource_value_unchanged(self): plan = self.determine_plan( models.S3BucketNotification( resource_name='test-s3-event', bucket='EXISTING-BUCKET', events=['s3:ObjectCreated:*'], prefix=None, suffix=None, lambda_function=create_function_resource('function_name'), ) ) deployed = { 'resources': [{ 'name': 'test-s3-event', 'resource_type': 's3_event', 'bucket': 'EXISTING-BUCKET', 'lambda_arn': 'lambda_arn', }] } config = FakeConfig(deployed) original_plan = plan[:] self.execute(plan, config) assert plan == original_plan def test_can_delete_sns_subscription(self): plan = [] deployed = { 'resources': [{ 'name': 'handler-sns-subscription', 'topic': 'mytopic', 'topic_arn': 'arn:mytopic', 'resource_type': 'sns_event', 'lambda_arn': 'arn:lambda', 'subscription_arn': 'arn:aws:subscribe', }] } config = FakeConfig(deployed) self.execute(plan, config) assert plan == [ models.APICall( method_name='unsubscribe_from_topic', params={'subscription_arn': 'arn:aws:subscribe'}, ), models.APICall( method_name='remove_permission_for_sns_topic', params={ 'topic_arn': 'arn:mytopic', 'function_arn': 'arn:lambda', }, ) ] def test_no_deletion_when_no_changes(self): plan = self.determine_plan( models.SNSLambdaSubscription( resource_name='handler-sns-subscription', topic='mytopic', lambda_function=create_function_resource('function_name') ) ) deployed = { 'resources': [{ 'name': 'handler-sns-subscription', 'topic': 'mytopic', 'resource_type': 'sns_event', 'lambda_arn': 'arn:lambda', 'subscription_arn': 'arn:aws:subscribe', }] } config = FakeConfig(deployed) original_plan = plan[:] self.execute(plan, config) # We shouldn't have added anything to the plan. assert plan == original_plan def test_handles_when_topic_name_change(self): # So let's say we subscribed to a topic 'old-topic' # and deployed our app: deployed = { 'resources': [{ 'name': 'handler-sns-subscription', 'topic': 'old-topic', 'topic_arn': 'arn:old-topic', 'resource_type': 'sns_event', 'lambda_arn': 'arn:lambda', 'subscription_arn': 'arn:aws:subscribe', }] } # Now we update our app and change the topic param # to 'new-topic' plan = self.determine_plan( models.SNSLambdaSubscription( resource_name='handler-sns-subscription', topic='new-topic', lambda_function=create_function_resource('function_name') ) ) config = FakeConfig(deployed) self.execute(plan, config) # Then we should unsubscribe from the old-topic because it's # no longer referenced in our app. assert plan[-2:] == [ models.APICall( method_name='unsubscribe_from_topic', params={'subscription_arn': 'arn:aws:subscribe'}, ), models.APICall( method_name='remove_permission_for_sns_topic', params={ 'topic_arn': 'arn:old-topic', 'function_arn': 'arn:lambda', }, ), ] def test_no_sqs_deletion_when_no_changes(self): plan = self.determine_plan( models.SQSEventSource( resource_name='handler-sqs-event-source', queue='my-queue', batch_size=10, lambda_function=create_function_resource('function_name'), maximum_batching_window_in_seconds=0 ) ) deployed = { 'resources': [{ 'name': 'handler-sqs-event-source', 'queue': 'my-queue', 'resource_type': 'sqs_event', 'lambda_arn': 'arn:lambda', 'event_uuid': 'event-uuid', }] } config = FakeConfig(deployed) original_plan = plan[:] self.execute(plan, config) assert plan == original_plan def test_can_delete_sqs_subscription(self): plan = [] deployed = { 'resources': [{ 'name': 'handler-sqs-event-source', 'queue': 'my-queue', 'resource_type': 'sqs_event', 'lambda_arn': 'arn:lambda', 'event_uuid': 'event-uuid', }] } config = FakeConfig(deployed) self.execute(plan, config) assert plan == [ models.APICall( method_name='remove_lambda_event_source', params={'event_uuid': 'event-uuid'}, ), ] def test_handles_when_queue_name_change(self): deployed = { 'resources': [{ 'name': 'handler-sqs-event-source', 'queue': 'my-queue', 'resource_type': 'sqs_event', 'lambda_arn': 'arn:lambda', 'event_uuid': 'event-uuid', }] } plan = self.determine_plan( models.SQSEventSource( resource_name='handler-sqs-event-source', queue='my-new-queue', batch_size=10, lambda_function=create_function_resource('function_name'), maximum_batching_window_in_seconds=0 ) ) config = FakeConfig(deployed) self.execute(plan, config) assert plan[-1:] == [ models.APICall( method_name='remove_lambda_event_source', params={'event_uuid': 'event-uuid'}, ), ] def test_can_delete_domain_name(self): deployed = { 'resources': [{ 'name': 'api_gateway_custom_domain', 'resource_type': 'domain_name', 'domain_name': 'example.com' }] } plan = [] config = FakeConfig(deployed) self.execute(plan, config) assert plan[-1:] == [ models.APICall( method_name='delete_domain_name', params={'domain_name': 'example.com'}, ), ] def test_can_handle_domain_name_without_api_mapping(self): deployed = { 'resources': [{ 'name': 'api_gateway_custom_domain', 'resource_type': 'domain_name', 'domain_name': 'example.com', }] } function = create_function_resource('function_name') domain_name = create_http_domain_name() rest_api = models.RestAPI( resource_name='rest_api', swagger_doc={'swagger': '2.0'}, endpoint_type='EDGE', minimum_compression='100', api_gateway_stage='api', lambda_function=function, domain_name=domain_name ) plan = self.determine_plan( rest_api ) config = FakeConfig(deployed) self.execute(plan, config) assert plan[-1] == models.RecordResourceVariable( resource_type='domain_name', resource_name='api_gateway_custom_domain', name='api_mapping', variable_name='rest_api_mapping' ) def test_can_delete_api_mapping(self): deployed = { 'resources': [{ 'name': 'api_gateway_custom_domain', 'resource_type': 'domain_name', 'domain_name': 'example.com', 'api_mapping': [ {'key': '/path_key'} ] }] } domain_name = create_http_domain_name() plan = [ models.RecordResourceVariable( resource_type='domain_name', resource_name=domain_name.resource_name, name='api_mapping', variable_name='rest_api_mapping' ) ] config = FakeConfig(deployed) self.execute(plan, config) assert self.sweeper.plan.instructions[0] == models.APICall( method_name='delete_api_mapping', params={'domain_name': 'example.com', 'path_key': 'path_key'}, output_var=None ) def test_can_delete_api_mapping_none(self): deployed = { 'resources': [{ 'name': 'api_gateway_custom_domain', 'resource_type': 'domain_name', 'domain_name': 'example.com', 'api_mapping': [ {'key': '/'} ] }] } domain_name = create_http_domain_name() plan = [ models.RecordResourceVariable( resource_type='domain_name', resource_name=domain_name.resource_name, name='api_mapping', variable_name='rest_api_mapping' ) ] config = FakeConfig(deployed) self.execute(plan, config) assert self.sweeper.plan.instructions[0] == models.APICall( method_name='delete_api_mapping', params={'domain_name': 'example.com', 'path_key': '(none)'}, output_var=None ) def test_raise_error_not_existed_resource_delete(self): deployed = { 'resources': [{ 'name': 'name', 'resource_type': 'not_existed', }] } config = FakeConfig(deployed) with pytest.raises(RuntimeError): self.execute([], config) def test_update_plan_with_insert_without_message(self): instructions = ( models.APICall( method_name='unsubscribe_from_topic', params={'subscription_arn': 'subscription_arn'}, ), models.APICall( method_name='remove_permission_for_sns_topic', params={ 'topic_arn': 'topic_arn', 'function_arn': 'lambda_arn', }, ), ) # type: Tuple[models.Instruction] self.sweeper._update_plan(instructions, insert=True) assert len(self.sweeper.plan.instructions) == 2 class TestKeyVariable(object): def test_key_variable_str(self): key_var = KeyDataVariable('name', 'key') assert str(key_var) == 'KeyDataVariable("name", "key")' def test_key_variables_equal(self): key_var = KeyDataVariable('name', 'key') key_var_1 = KeyDataVariable('name', 'key_1') assert not key_var == key_var_1 key_var_2 = KeyDataVariable('name', 'key') assert key_var == key_var_2 class TestPlanLogGroup(BasePlannerTests): def test_can_create_log_group(self): self.remote_state.declare_no_resources_exists() resource = models.LogGroup( resource_name='default-log-group', log_group_name='/aws/lambda/func-name', retention_in_days=14, ) plan = self.determine_plan(resource) assert plan == [ models.APICall( method_name='create_log_group', params={'log_group_name': '/aws/lambda/func-name'} ), models.APICall( method_name='put_retention_policy', params={'name': '/aws/lambda/func-name', 'retention_in_days': 14}, ), models.RecordResourceValue( resource_type='log_group', resource_name='default-log-group', name='log_group_name', value='/aws/lambda/func-name'), ] def test_can_update_log_group(self): resource = models.LogGroup( resource_name='default-log-group', log_group_name='/aws/lambda/func-name', retention_in_days=14, ) self.remote_state.declare_resource_exists(resource) plan = self.determine_plan(resource) assert plan == [ models.APICall( method_name='put_retention_policy', params={'name': '/aws/lambda/func-name', 'retention_in_days': 14}, ), models.RecordResourceValue( resource_type='log_group', resource_name='default-log-group', name='log_group_name', value='/aws/lambda/func-name'), ] ================================================ FILE: tests/unit/deploy/test_swagger.py ================================================ from unittest import mock from chalice.deploy.swagger import ( SwaggerGenerator, CFNSwaggerGenerator, TerraformSwaggerGenerator) from chalice import CORSConfig from chalice.app import CustomAuthorizer, CognitoUserPoolAuthorizer from chalice.app import IAMAuthorizer, Chalice from chalice.deploy.models import RestAPI, IAMPolicy from pytest import fixture @fixture def swagger_gen(): return SwaggerGenerator( region='us-west-2', deployed_resources={ 'api_handler_arn': 'arn:aws:lambda:mars-west-1:123456789' ':function:lambda_arn' }) def test_can_add_binary_media_types(swagger_gen): app = Chalice('test-binary') doc = swagger_gen.generate_swagger(app) media_types = doc.get('x-amazon-apigateway-binary-media-types') assert sorted(media_types) == sorted(app.api.binary_types) def test_can_produce_swagger_top_level_keys(sample_app, swagger_gen): swagger_doc = swagger_gen.generate_swagger(sample_app) assert swagger_doc['swagger'] == '2.0' assert swagger_doc['info']['title'] == 'sample' assert swagger_doc['schemes'] == ['https'] assert '/' in swagger_doc['paths'], swagger_doc['paths'] index_config = swagger_doc['paths']['/'] assert 'get' in index_config def test_can_produce_doc_for_method(sample_app, swagger_gen): doc = swagger_gen.generate_swagger(sample_app) single_method = doc['paths']['/']['get'] assert single_method['consumes'] == ['application/json'] assert single_method['produces'] == ['application/json'] # 'responses' is validated in a separate test, # it's all boilerplate anyways. # Same for x-amazon-apigateway-integration. def test_can_produce_doc_for_no_docstring(sample_app, swagger_gen): @sample_app.route('/method') def method(): pass doc = swagger_gen.generate_swagger(sample_app) view_config = doc['paths']['/method']['get'] assert 'summary' not in view_config assert 'description' not in view_config def test_can_produce_doc_for_single_docstring(sample_app, swagger_gen): @sample_app.route('/method1') def method1(): """Single line method summary""" pass @sample_app.route('/method2') def method2(): '''Single line method summary''' pass @sample_app.route('/method3') def method3(): """ Single line method summary """ pass doc = swagger_gen.generate_swagger(sample_app) for method in [1, 2, 3]: view_config = doc['paths']['/method' + str(method)]['get'] assert 'summary' in view_config assert 'description' not in view_config assert view_config['summary'] == 'Single line method summary' def test_can_produce_doc_for_multi_docstring(sample_app, swagger_gen): @sample_app.route('/method1') def method1(): """Multiline method summary And here is a more detailed description that can span multiple lines. It can also handle indenting for things like method arguments like so: param1 - description param2 - description """ pass @sample_app.route('/method2') def method2(): """Multiline method summary And here is a more detailed description that can span multiple lines. It can also handle indenting for things like method arguments like so: param1 - description param2 - description """ pass @sample_app.route('/method3') def method3(): """Multiline method summary And here is a more detailed description that can span multiple lines. It can also handle indenting for things like method arguments like so: param1 - description param2 - description """ pass doc = swagger_gen.generate_swagger(sample_app) for method in [1, 2, 3]: view_config = doc['paths']['/method' + str(method)]['get'] assert 'summary' in view_config assert 'description' in view_config assert view_config['summary'] == 'Multiline method summary' assert view_config['description'] == ( 'And here is a more detailed description that can span multiple\n' 'lines. It can also handle indenting for things like method\n' 'arguments like so:\n' ' param1 - description\n' ' param2 - description' ) def test_apigateway_integration_generation(sample_app, swagger_gen): doc = swagger_gen.generate_swagger(sample_app) single_method = doc['paths']['/']['get'] apig_integ = single_method['x-amazon-apigateway-integration'] assert apig_integ['passthroughBehavior'] == 'when_no_match' assert apig_integ['httpMethod'] == 'POST' assert apig_integ['type'] == 'aws_proxy' assert apig_integ['uri'] == ( "arn:aws:apigateway:us-west-2:lambda:path" "/2015-03-31/functions/" "arn:aws:lambda:mars-west-1:123456789:function:lambda_arn/invocations" ) assert 'responses' in apig_integ responses = apig_integ['responses'] assert responses['default'] == {'statusCode': '200'} def test_can_add_url_captures_to_params(sample_app, swagger_gen): @sample_app.route('/path/{capture}') def foo(name): return {} doc = swagger_gen.generate_swagger(sample_app) single_method = doc['paths']['/path/{capture}']['get'] assert 'parameters' in single_method assert single_method['parameters'] == [ {'name': "capture", "in": "path", "required": True, "type": "string"} ] def test_can_add_multiple_http_methods(sample_app, swagger_gen): @sample_app.route('/multimethod', methods=['GET', 'POST']) def multiple_methods(): pass doc = swagger_gen.generate_swagger(sample_app) view_config = doc['paths']['/multimethod'] assert 'get' in view_config assert 'post' in view_config assert view_config['get'] == view_config['post'] def test_can_use_same_route_with_diff_http_methods(sample_app, swagger_gen): @sample_app.route('/multimethod', methods=['GET']) def multiple_methods_get(): pass @sample_app.route('/multimethod', methods=['POST']) def multiple_methods_post(): pass doc = swagger_gen.generate_swagger(sample_app) view_config = doc['paths']['/multimethod'] assert 'get' in view_config assert 'post' in view_config assert view_config['get'] == view_config['post'] class TestPreflightCORS(object): def get_access_control_methods(self, view_config): return view_config['options'][ 'x-amazon-apigateway-integration']['responses']['default'][ 'responseParameters'][ 'method.response.header.Access-Control-Allow-Methods'] def test_can_add_preflight_cors(self, sample_app, swagger_gen): @sample_app.route('/cors', methods=['GET', 'POST'], cors=CORSConfig( allow_origin='http://foo.com', allow_headers=['X-ZZ-Top', 'X-Special-Header'], expose_headers=['X-Exposed', 'X-Special'], max_age=600, allow_credentials=True)) def cors_request(): pass doc = swagger_gen.generate_swagger(sample_app) view_config = doc['paths']['/cors'] # We should add an OPTIONS preflight request automatically. assert 'options' in view_config, ( 'Preflight OPTIONS method not added to CORS view') options = view_config['options'] expected_response_params = { 'method.response.header.Access-Control-Allow-Methods': mock.ANY, 'method.response.header.Access-Control-Allow-Headers': ( "'Authorization,Content-Type,X-Amz-Date,X-Amz-Security-Token," "X-Api-Key,X-Special-Header,X-ZZ-Top'"), 'method.response.header.Access-Control-Allow-Origin': ( "'http://foo.com'"), 'method.response.header.Access-Control-Expose-Headers': ( "'X-Exposed,X-Special'"), 'method.response.header.Access-Control-Max-Age': ( "'600'"), 'method.response.header.Access-Control-Allow-Credentials': ( "'true'"), } assert options == { 'consumes': ['application/json'], 'produces': ['application/json'], 'responses': { '200': { 'description': '200 response', 'schema': { '$ref': '#/definitions/Empty' }, 'headers': { 'Access-Control-Allow-Origin': {'type': 'string'}, 'Access-Control-Allow-Methods': {'type': 'string'}, 'Access-Control-Allow-Headers': {'type': 'string'}, 'Access-Control-Expose-Headers': {'type': 'string'}, 'Access-Control-Max-Age': {'type': 'string'}, 'Access-Control-Allow-Credentials': {'type': 'string'}, } } }, 'x-amazon-apigateway-integration': { 'responses': { 'default': { 'statusCode': '200', 'responseParameters': expected_response_params, } }, 'requestTemplates': { 'application/json': '{"statusCode": 200}' }, 'passthroughBehavior': 'when_no_match', 'type': 'mock', 'contentHandling': 'CONVERT_TO_TEXT' }, } allow_methods = self.get_access_control_methods(view_config) # Typically the header will follow the form of: # "METHOD,METHOD,...OPTIONS" # The individual assertions is needed because there is no guarantee # on the order of these methods in the string because the order is # derived from iterating through a dictionary, which is not ordered # in python 2.7. So instead assert the correct methods are present in # the string. assert 'GET' in allow_methods assert 'POST' in allow_methods assert 'OPTIONS' in allow_methods def test_can_add_preflight_custom_cors(self, sample_app, swagger_gen): @sample_app.route('/cors', methods=['GET', 'POST'], cors=True) def cors_request(): pass doc = swagger_gen.generate_swagger(sample_app) view_config = doc['paths']['/cors'] # We should add an OPTIONS preflight request automatically. assert 'options' in view_config, ( 'Preflight OPTIONS method not added to CORS view') options = view_config['options'] expected_response_params = { 'method.response.header.Access-Control-Allow-Methods': mock.ANY, 'method.response.header.Access-Control-Allow-Headers': ( "'Authorization,Content-Type,X-Amz-Date,X-Amz-Security-Token," "X-Api-Key'"), 'method.response.header.Access-Control-Allow-Origin': "'*'", } assert options == { 'consumes': ['application/json'], 'produces': ['application/json'], 'responses': { '200': { 'description': '200 response', 'schema': { '$ref': '#/definitions/Empty' }, 'headers': { 'Access-Control-Allow-Origin': {'type': 'string'}, 'Access-Control-Allow-Methods': {'type': 'string'}, 'Access-Control-Allow-Headers': {'type': 'string'}, } } }, 'x-amazon-apigateway-integration': { 'responses': { 'default': { 'statusCode': '200', 'responseParameters': expected_response_params, } }, 'requestTemplates': { 'application/json': '{"statusCode": 200}' }, 'passthroughBehavior': 'when_no_match', 'type': 'mock', 'contentHandling': 'CONVERT_TO_TEXT' }, } allow_methods = self.get_access_control_methods(view_config) # Typically the header will follow the form of: # "METHOD,METHOD,...OPTIONS" # The individual assertions is needed because there is no guarantee # on the order of these methods in the string because the order is # derived from iterating through a dictionary, which is not ordered # in python 2.7. So instead assert the correct methods are present in # the string. assert 'GET' in allow_methods assert 'POST' in allow_methods assert 'OPTIONS' in allow_methods def test_can_add_preflight_cors_for_shared_routes( self, sample_app, swagger_gen): @sample_app.route('/cors', methods=['GET'], cors=True) def cors_request(): pass @sample_app.route('/cors', methods=['PUT']) def non_cors_request(): pass doc = swagger_gen.generate_swagger(sample_app) view_config = doc['paths']['/cors'] # We should add an OPTIONS preflight request automatically. assert 'options' in view_config, ( 'Preflight OPTIONS method not added to CORS view') allow_methods = self.get_access_control_methods(view_config) # PUT should not be included in allowed methods as it was not enabled # for CORS. assert allow_methods == "'GET,OPTIONS'" def test_can_add_api_key(sample_app, swagger_gen): @sample_app.route('/api-key-required', api_key_required=True) def foo(name): return {} doc = swagger_gen.generate_swagger(sample_app) single_method = doc['paths']['/api-key-required']['get'] assert 'security' in single_method assert single_method['security'] == [{ 'api_key': [] }] # Also need to add in the api_key definition in the top level # security definitions. assert 'securityDefinitions' in doc assert 'api_key' in doc['securityDefinitions'] assert doc['securityDefinitions']['api_key'] == { 'type': 'apiKey', 'name': 'x-api-key', 'in': 'header' } def test_can_use_authorizer_object(sample_app, swagger_gen): authorizer = CustomAuthorizer( 'MyAuth', authorizer_uri='auth-uri', header='Authorization' ) @sample_app.route('/auth', authorizer=authorizer) def auth(): return {'foo': 'bar'} doc = swagger_gen.generate_swagger(sample_app) single_method = doc['paths']['/auth']['get'] assert single_method.get('security') == [{'MyAuth': []}] security_definitions = doc['securityDefinitions'] assert 'MyAuth' in security_definitions my_auth = security_definitions['MyAuth'] # authorizerCredentials should not be in this dict because it's None. assert my_auth['x-amazon-apigateway-authorizer'] == { 'authorizerUri': 'auth-uri', 'type': 'token', 'authorizerResultTtlInSeconds': 300, } def test_can_use_authorizer_object_with_role_arn(sample_app, swagger_gen): authorizer = CustomAuthorizer( 'MyAuth', authorizer_uri='auth-uri', header='Authorization', invoke_role_arn='role-arn') @sample_app.route('/auth', authorizer=authorizer) def auth(): return {'foo': 'bar'} doc = swagger_gen.generate_swagger(sample_app) single_method = doc['paths']['/auth']['get'] assert single_method.get('security') == [{'MyAuth': []}] security_definitions = doc['securityDefinitions'] assert 'MyAuth' in security_definitions assert security_definitions['MyAuth'] == { 'type': 'apiKey', 'name': 'Authorization', 'in': 'header', 'x-amazon-apigateway-authtype': 'custom', 'x-amazon-apigateway-authorizer': { 'authorizerUri': 'auth-uri', 'type': 'token', 'authorizerResultTtlInSeconds': 300, 'authorizerCredentials': 'role-arn' } } def test_can_use_authorizer_object_scopes(sample_app, swagger_gen): authorizer = CustomAuthorizer( 'MyAuth', authorizer_uri='auth-uri', header='Authorization', invoke_role_arn='role-arn', scopes=["write:test", "read:test"]) @sample_app.route('/auth', authorizer=authorizer) def auth(): return {'foo': 'bar'} doc = swagger_gen.generate_swagger(sample_app) single_method = doc['paths']['/auth']['get'] assert single_method.get('security') == [{ 'MyAuth': ["write:test", "read:test"] }] security_definitions = doc['securityDefinitions'] assert 'MyAuth' in security_definitions assert security_definitions['MyAuth'] == { 'type': 'apiKey', 'name': 'Authorization', 'in': 'header', 'x-amazon-apigateway-authtype': 'custom', 'x-amazon-apigateway-authorizer': { 'authorizerUri': 'auth-uri', 'type': 'token', 'authorizerResultTtlInSeconds': 300, 'authorizerCredentials': 'role-arn' } } def test_can_use_authorizer_object_with_scopes(sample_app, swagger_gen): authorizer = CustomAuthorizer( 'MyAuth', authorizer_uri='auth-uri', header='Authorization', invoke_role_arn='role-arn') @sample_app.route( '/auth', authorizer=authorizer.with_scopes(["write:test", "read:test"]) ) def auth(): return {'foo': 'bar'} doc = swagger_gen.generate_swagger(sample_app) single_method = doc['paths']['/auth']['get'] assert single_method.get('security') == [{ 'MyAuth': ["write:test", "read:test"] }] security_definitions = doc['securityDefinitions'] assert 'MyAuth' in security_definitions assert security_definitions['MyAuth'] == { 'type': 'apiKey', 'name': 'Authorization', 'in': 'header', 'x-amazon-apigateway-authtype': 'custom', 'x-amazon-apigateway-authorizer': { 'authorizerUri': 'auth-uri', 'type': 'token', 'authorizerResultTtlInSeconds': 300, 'authorizerCredentials': 'role-arn' } } def test_can_use_api_key_and_authorizers(sample_app, swagger_gen): authorizer = CustomAuthorizer( 'MyAuth', authorizer_uri='auth-uri', header='Authorization') @sample_app.route('/auth', authorizer=authorizer, api_key_required=True) def auth(): return {'foo': 'bar'} doc = swagger_gen.generate_swagger(sample_app) single_method = doc['paths']['/auth']['get'] assert single_method.get('security') == [ {'api_key': []}, {'MyAuth': []}, ] def test_can_use_api_key_and_authorizers_with_scopes(sample_app, swagger_gen): authorizer = CustomAuthorizer( 'MyAuth', authorizer_uri='auth-uri', header='Authorization') @sample_app.route( '/auth', authorizer=authorizer.with_scopes(["write:test", "read:test"]), api_key_required=True ) def auth(): return {'foo': 'bar'} doc = swagger_gen.generate_swagger(sample_app) single_method = doc['paths']['/auth']['get'] assert single_method.get('security') == [ {'api_key': []}, {'MyAuth': ["write:test", "read:test"]}, ] def test_can_use_iam_authorizer_object(sample_app, swagger_gen): authorizer = IAMAuthorizer() @sample_app.route('/auth', authorizer=authorizer) def auth(): return {'foo': 'bar'} doc = swagger_gen.generate_swagger(sample_app) single_method = doc['paths']['/auth']['get'] assert single_method.get('security') == [{'sigv4': []}] security_definitions = doc['securityDefinitions'] assert 'sigv4' in security_definitions assert security_definitions['sigv4'] == { "in": "header", "type": "apiKey", "name": "Authorization", "x-amazon-apigateway-authtype": "awsSigv4" } def test_can_use_cognito_auth_object(sample_app, swagger_gen): authorizer = CognitoUserPoolAuthorizer('MyUserPool', header='Authorization', provider_arns=['myarn']) @sample_app.route('/api-key-required', authorizer=authorizer) def foo(): return {} doc = swagger_gen.generate_swagger(sample_app) single_method = doc['paths']['/api-key-required']['get'] assert single_method.get('security') == [{'MyUserPool': []}] assert 'securityDefinitions' in doc assert doc['securityDefinitions'].get('MyUserPool') == { 'in': 'header', 'type': 'apiKey', 'name': 'Authorization', 'x-amazon-apigateway-authtype': 'cognito_user_pools', 'x-amazon-apigateway-authorizer': { 'type': 'cognito_user_pools', 'providerARNs': ['myarn'] } } def test_can_use_cognito_auth_object_with_scopes(sample_app, swagger_gen): authorizer = CognitoUserPoolAuthorizer('MyUserPool', header='Authorization', provider_arns=['myarn']) @sample_app.route( '/api-key-required', authorizer=authorizer.with_scopes(["write:test", "read:test"]) ) def foo(): return {} doc = swagger_gen.generate_swagger(sample_app) single_method = doc['paths']['/api-key-required']['get'] assert single_method.get('security') == [{ 'MyUserPool': ["write:test", "read:test"] }] assert 'securityDefinitions' in doc assert doc['securityDefinitions'].get('MyUserPool') == { 'in': 'header', 'type': 'apiKey', 'name': 'Authorization', 'x-amazon-apigateway-authtype': 'cognito_user_pools', 'x-amazon-apigateway-authorizer': { 'type': 'cognito_user_pools', 'providerARNs': ['myarn'] } } def test_auth_defined_for_multiple_methods(sample_app, swagger_gen): authorizer = CognitoUserPoolAuthorizer('MyUserPool', header='Authorization', provider_arns=['myarn']) @sample_app.route('/pool1', authorizer=authorizer) def foo(): return {} @sample_app.route('/pool2', authorizer=authorizer) def bar(): return {} doc = swagger_gen.generate_swagger(sample_app) assert 'securityDefinitions' in doc assert len(doc['securityDefinitions']) == 1 def test_builtin_auth(sample_app): swagger_gen = SwaggerGenerator( region='us-west-2', deployed_resources={ 'api_handler_arn': 'arn:aws:lambda:mars-west-1:123456789' ':function:lambda_arn', 'api_handler_name': 'api-dev', 'lambda_functions': { 'api-dev-myauth': { 'arn': 'arn:aws:lambda:mars-west-1:123456789' ':function:auth_arn', 'type': 'authorizer', } } } ) @sample_app.authorizer(name='myauth', ttl_seconds=10, execution_role='arn:role') def auth(auth_request): pass @sample_app.route('/auth', authorizer=auth) def foo(): pass doc = swagger_gen.generate_swagger(sample_app) single_method = doc['paths']['/auth']['get'] assert single_method.get('security') == [{'myauth': []}] assert 'securityDefinitions' in doc assert doc['securityDefinitions']['myauth'] == { 'in': 'header', 'name': 'Authorization', 'type': 'apiKey', 'x-amazon-apigateway-authtype': 'custom', 'x-amazon-apigateway-authorizer': { 'type': 'token', 'authorizerCredentials': 'arn:role', 'authorizerResultTtlInSeconds': 10, 'authorizerUri': ('arn:aws:apigateway:us-west-2:lambda:path' '/2015-03-31/functions/arn:aws:lambda:' 'mars-west-1:123456789:function:auth_arn' '/invocations'), } } def test_builtin_auth_with_custom_header(sample_app): swagger_gen = SwaggerGenerator( region='us-west-2', deployed_resources={ 'api_handler_arn': 'arn:aws:lambda:mars-west-1:123456789' ':function:lambda_arn', 'api_handler_name': 'api-dev', 'lambda_functions': { 'api-dev-myauth': { 'arn': 'arn:aws:lambda:mars-west-1:123456789' ':function:auth_arn', 'type': 'authorizer', } } } ) @sample_app.authorizer(name='myauth', ttl_seconds=10, execution_role='arn:role', header='FOO') def auth(auth_request): pass @sample_app.route('/auth', authorizer=auth) def foo(): pass doc = swagger_gen.generate_swagger(sample_app) single_method = doc['paths']['/auth']['get'] assert single_method.get('security') == [{'myauth': []}] assert 'securityDefinitions' in doc assert doc['securityDefinitions']['myauth'] == { 'in': 'header', 'name': 'FOO', 'type': 'apiKey', 'x-amazon-apigateway-authtype': 'custom', 'x-amazon-apigateway-authorizer': { 'type': 'token', 'authorizerCredentials': 'arn:role', 'authorizerResultTtlInSeconds': 10, 'authorizerUri': ('arn:aws:apigateway:us-west-2:lambda:path' '/2015-03-31/functions/arn:aws:lambda:' 'mars-west-1:123456789:function:auth_arn' '/invocations'), } } def test_builtin_auth_with_scopes(sample_app): swagger_gen = SwaggerGenerator( region='us-west-2', deployed_resources={ 'api_handler_arn': 'arn:aws:lambda:mars-west-1:123456789' ':function:lambda_arn', 'api_handler_name': 'api-dev', 'lambda_functions': { 'api-dev-myauth': { 'arn': 'arn:aws:lambda:mars-west-1:123456789' ':function:auth_arn', 'type': 'authorizer', } } } ) @sample_app.authorizer(name='myauth', ttl_seconds=10, execution_role='arn:role') def auth(auth_request): pass @sample_app.route( '/auth', authorizer=auth.with_scopes(["write:test", "read:test"]) ) def foo(): pass doc = swagger_gen.generate_swagger(sample_app) single_method = doc['paths']['/auth']['get'] assert single_method.get('security') == [{ 'myauth': ["write:test", "read:test"] }] assert 'securityDefinitions' in doc assert doc['securityDefinitions']['myauth'] == { 'in': 'header', 'name': 'Authorization', 'type': 'apiKey', 'x-amazon-apigateway-authtype': 'custom', 'x-amazon-apigateway-authorizer': { 'type': 'token', 'authorizerCredentials': 'arn:role', 'authorizerResultTtlInSeconds': 10, 'authorizerUri': ('arn:aws:apigateway:us-west-2:' 'lambda:path/2015-03-31/functions' '/arn:aws:lambda:mars-west-1:123456789:' 'function:auth_arn/invocations'), } } def test_will_default_to_function_name_for_auth(sample_app): swagger_gen = SwaggerGenerator( region='us-west-2', deployed_resources={ 'api_handler_arn': 'arn:aws:lambda:mars-west-1:123456789' ':function:lambda_arn', 'api_handler_name': 'api-dev', 'lambda_functions': { 'api-dev-auth': { 'arn': 'arn:aws:lambda:mars-west-1:123456789' ':function:auth_arn', 'type': 'authorizer', } } } ) # No "name=" kwarg provided should default # to a name of "auth". @sample_app.authorizer(ttl_seconds=10, execution_role='arn:role') def auth(auth_request): pass @sample_app.route('/auth', authorizer=auth) def foo(): pass doc = swagger_gen.generate_swagger(sample_app) single_method = doc['paths']['/auth']['get'] assert single_method.get('security') == [{'auth': []}] assert 'securityDefinitions' in doc assert doc['securityDefinitions']['auth'] == { 'in': 'header', 'name': 'Authorization', 'type': 'apiKey', 'x-amazon-apigateway-authtype': 'custom', 'x-amazon-apigateway-authorizer': { 'type': 'token', 'authorizerCredentials': 'arn:role', 'authorizerResultTtlInSeconds': 10, 'authorizerUri': ('arn:aws:apigateway:us-west-2:lambda:path' '/2015-03-31/functions/' 'arn:aws:lambda:mars-west-1:123456789:function' ':auth_arn/invocations'), } } def test_can_custom_resource_policy(sample_app, swagger_gen): rest_api = RestAPI( resource_name='dev', swagger_doc={}, lambda_function=None, minimum_compression="", api_gateway_stage="xyz", endpoint_type="PRIVATE", policy=IAMPolicy({ 'Statement': [{ "Effect": "Allow", "Principal": "*", "Action": "execute-api:Invoke", "Resource": [ "arn:aws:execute-api:*:*:*", "arn:aws:exceute-api:*:*:*/*" ], "Condition": { "StringEquals": { "aws:SourceVpce": "vpce-abc123" } } }] }) ) doc = swagger_gen.generate_swagger(sample_app, rest_api) assert doc['x-amazon-apigateway-policy'] == { 'Statement': [{ 'Action': 'execute-api:Invoke', 'Condition': {'StringEquals': { 'aws:SourceVpce': 'vpce-abc123'}}, 'Effect': 'Allow', 'Principal': '*', 'Resource': [ 'arn:aws:execute-api:*:*:*', "arn:aws:exceute-api:*:*:*/*"] }] } def test_can_vpce(sample_app, swagger_gen): rest_api = RestAPI( resource_name='dev', swagger_doc={}, lambda_function=None, minimum_compression="", api_gateway_stage="xyz", endpoint_type="PRIVATE", vpce_ids=["vpce-12346", "vpce-abc123"], ) doc = swagger_gen.generate_swagger(sample_app, rest_api) assert doc['x-amazon-apigateway-endpoint-configuration'] == { "vpcEndpointIds": ["vpce-12346", "vpce-abc123"] } def test_can_auto_resource_policy_with_cfn(sample_app): swagger_gen = CFNSwaggerGenerator() rest_api = RestAPI( resource_name='dev', swagger_doc={}, lambda_function=None, minimum_compression="", api_gateway_stage="xyz", endpoint_type="PRIVATE", policy=IAMPolicy({ 'Statement': [{ "Effect": "Allow", "Principal": "*", "Action": "execute-api:Invoke", "Resource": "arn:aws:execute-api:*:*:*/*", "Condition": { "StringEquals": { "aws:SourceVpce": "vpce-abc123" } } }] }) ) doc = swagger_gen.generate_swagger(sample_app, rest_api) assert doc['x-amazon-apigateway-policy'] == { 'Statement': [{ 'Action': 'execute-api:Invoke', 'Condition': {'StringEquals': { 'aws:SourceVpce': 'vpce-abc123'}}, 'Effect': 'Allow', 'Principal': '*', 'Resource': 'arn:aws:execute-api:*:*:*/*', }] } def test_will_custom_auth_with_cfn(sample_app): swagger_gen = CFNSwaggerGenerator() # No "name=" kwarg provided should default # to a name of "auth". @sample_app.authorizer(ttl_seconds=10, execution_role='arn:role') def auth(auth_request): pass @sample_app.route('/auth', authorizer=auth) def foo(): pass doc = swagger_gen.generate_swagger(sample_app) assert 'securityDefinitions' in doc assert doc['securityDefinitions']['auth'] == { 'in': 'header', 'name': 'Authorization', 'type': 'apiKey', 'x-amazon-apigateway-authtype': 'custom', 'x-amazon-apigateway-authorizer': { 'type': 'token', 'authorizerCredentials': 'arn:role', 'authorizerResultTtlInSeconds': 10, 'authorizerUri': { 'Fn::Sub': ( 'arn:${AWS::Partition}:apigateway:${AWS::Region}' ':lambda:path/2015-03-31/functions/' '${Auth.Arn}/invocations' ) } } } def test_custom_auth_with_tf(sample_app): swagger_gen = TerraformSwaggerGenerator() # No "name=" kwarg provided should default # to a name of "auth". @sample_app.authorizer(ttl_seconds=10, execution_role='arn:role') def auth(auth_request): pass @sample_app.route('/auth', authorizer=auth) def foo(): pass doc = swagger_gen.generate_swagger(sample_app) assert 'securityDefinitions' in doc assert doc['securityDefinitions']['auth'] == { 'in': 'header', 'name': 'Authorization', 'type': 'apiKey', 'x-amazon-apigateway-authtype': 'custom', 'x-amazon-apigateway-authorizer': { 'type': 'token', 'authorizerCredentials': 'arn:role', 'authorizerResultTtlInSeconds': 10, 'authorizerUri': '${aws_lambda_function.auth.invoke_arn}' } } ================================================ FILE: tests/unit/deploy/test_validate.py ================================================ import pytest import warnings from unittest import mock from chalice.app import Chalice from chalice.config import Config from chalice import CORSConfig from chalice.constants import MIN_COMPRESSION_SIZE from chalice.constants import MAX_COMPRESSION_SIZE from chalice.deploy.validate import validate_configuration from chalice.deploy.validate import validate_routes from chalice.deploy.validate import validate_python_version from chalice.deploy.validate import validate_route_content_types from chalice.deploy.validate import validate_unique_function_names from chalice.deploy.validate import validate_feature_flags from chalice.deploy.validate import validate_endpoint_type from chalice.deploy.validate import validate_resource_policy from chalice.deploy.validate import ExperimentalFeatureError def test_trailing_slash_routes_result_in_error(): app = Chalice('appname') app.routes = {'/trailing-slash/': None} config = Config.create(chalice_app=app) with pytest.raises(ValueError): validate_configuration(config) def test_empty_route_results_in_error(): app = Chalice('appname') app.routes = {'': {}} config = Config.create(chalice_app=app) with pytest.raises(ValueError): validate_configuration(config) def test_validate_python_version_invalid(): config = mock.Mock(spec=Config) config.lambda_python_version = 'python1.0' with pytest.warns(UserWarning): validate_python_version(config) def test_python_version_invalid_from_real_config(): config = Config.create() with pytest.warns(UserWarning): validate_python_version(config, 'python1.0') def test_python_version_is_valid(): config = Config.create() with warnings.catch_warnings(): warnings.simplefilter('error') validate_python_version(config, config.lambda_python_version) def test_manage_iam_role_false_requires_role_arn(sample_app): config = Config.create(chalice_app=sample_app, manage_iam_role=False, iam_role_arn='arn:::foo') assert validate_configuration(config) is None def test_validation_error_if_no_role_provided_when_manage_false(sample_app): # We're indicating that we should not be managing the # IAM role, but we're not giving a role ARN to use. # This is a validation error. config = Config.create(chalice_app=sample_app, manage_iam_role=False) with pytest.raises(ValueError): validate_configuration(config) def test_validate_unique_lambda_function_names(sample_app): @sample_app.lambda_function() def foo(event, context): pass # This will cause a validation error because # 'foo' is already registered as a lambda function. @sample_app.lambda_function(name='foo') def bar(event, context): pass config = Config.create(chalice_app=sample_app, manage_iam_role=False) with pytest.raises(ValueError): validate_unique_function_names(config) def test_validate_names_across_function_types(sample_app): @sample_app.lambda_function() def foo(event, context): pass @sample_app.schedule('rate(1 hour)', name='foo') def bar(event): pass config = Config.create(chalice_app=sample_app, manage_iam_role=False) with pytest.raises(ValueError): validate_unique_function_names(config) def test_validate_names_using_name_kwarg(sample_app): @sample_app.authorizer(name='duplicate') def foo(auth_request): pass @sample_app.lambda_function(name='duplicate') def bar(event): pass config = Config.create(chalice_app=sample_app, manage_iam_role=False) with pytest.raises(ValueError): validate_unique_function_names(config) class TestValidateCORS(object): def test_cant_have_options_with_cors(self, sample_app): @sample_app.route('/badcors', methods=['GET', 'OPTIONS'], cors=True) def badview(): pass with pytest.raises(ValueError): validate_routes(sample_app.routes) def test_cant_have_differing_cors_configurations(self, sample_app): custom_cors = CORSConfig( allow_origin='https://foo.example.com', allow_headers=['X-Special-Header'], max_age=600, expose_headers=['X-Special-Header'], allow_credentials=True ) @sample_app.route('/cors', methods=['GET'], cors=True) def cors(): pass @sample_app.route('/cors', methods=['PUT'], cors=custom_cors) def different_cors(): pass with pytest.raises(ValueError): validate_routes(sample_app.routes) def test_can_have_same_cors_configurations(self, sample_app): @sample_app.route('/cors', methods=['GET'], cors=True) def cors(): pass @sample_app.route('/cors', methods=['PUT'], cors=True) def same_cors(): pass try: validate_routes(sample_app.routes) except ValueError: pytest.fail( 'A ValueError was unexpectedly thrown. Applications ' 'may have multiple view functions that share the same ' 'route and CORS configuration.' ) def test_can_have_same_custom_cors_configurations(self, sample_app): custom_cors = CORSConfig( allow_origin='https://foo.example.com', allow_headers=['X-Special-Header'], max_age=600, expose_headers=['X-Special-Header'], allow_credentials=True ) @sample_app.route('/cors', methods=['GET'], cors=custom_cors) def cors(): pass same_custom_cors = CORSConfig( allow_origin='https://foo.example.com', allow_headers=['X-Special-Header'], max_age=600, expose_headers=['X-Special-Header'], allow_credentials=True ) @sample_app.route('/cors', methods=['PUT'], cors=same_custom_cors) def same_cors(): pass try: validate_routes(sample_app.routes) except ValueError: pytest.fail( 'A ValueError was unexpectedly thrown. Applications ' 'may have multiple view functions that share the same ' 'route and CORS configuration.' ) def test_can_have_one_cors_configured_and_others_not(self, sample_app): @sample_app.route('/cors', methods=['GET'], cors=True) def cors(): pass @sample_app.route('/cors', methods=['PUT']) def no_cors(): pass try: validate_routes(sample_app.routes) except ValueError: pytest.fail( 'A ValueError was unexpectedly thrown. Applications ' 'may have multiple view functions that share the same ' 'route but only one is configured for CORS.' ) def test_cant_have_mixed_content_types(sample_app): @sample_app.route('/index', content_types=['application/octet-stream', 'text/plain']) def index(): return {'hello': 'world'} with pytest.raises(ValueError): validate_route_content_types(sample_app.routes, sample_app.api.binary_types) def test_can_validate_updated_custom_binary_types(sample_app): sample_app.api.binary_types.extend(['text/plain']) @sample_app.route('/index', content_types=['application/octet-stream', 'text/plain']) def index(): return {'hello': 'world'} assert validate_route_content_types(sample_app.routes, sample_app.api.binary_types) is None def test_can_validate_resource_policy(sample_app): config = Config.create( chalice_app=sample_app, api_gateway_endpoint_type='PRIVATE') with pytest.raises(ValueError): validate_resource_policy(config) config = Config.create( chalice_app=sample_app, api_gateway_endpoint_vpce='vpce-abc123', api_gateway_endpoint_type='PRIVATE') validate_resource_policy(config) config = Config.create( chalice_app=sample_app, api_gateway_endpoint_vpce='vpce-abc123', api_gateway_endpoint_type='REGIONAL') with pytest.raises(ValueError): validate_resource_policy(config) config = Config.create( chalice_app=sample_app, api_gateway_policy_file='xyz.json', api_gateway_endpoint_type='PRIVATE') validate_resource_policy(config) config = Config.create( chalice_app=sample_app, api_gateway_endpoint_vpce=['vpce-abc123', 'vpce-bdef'], api_gateway_policy_file='bar.json', api_gateway_endpoint_type='PRIVATE') with pytest.raises(ValueError): validate_resource_policy(config) def test_can_validate_endpoint_type(sample_app): config = Config.create( chalice_app=sample_app, api_gateway_endpoint_type='EDGE2') with pytest.raises(ValueError): validate_endpoint_type(config) config = Config.create( chalice_app=sample_app, api_gateway_endpoint_type='REGIONAL') validate_endpoint_type(config) def test_can_validate_feature_flags(sample_app): # The _features_used is marked internal because we don't want # chalice users to access it, but this attribute is intended to be # accessed by anything within the chalice codebase. sample_app._features_used.add('SOME_NEW_FEATURE') with pytest.raises(ExperimentalFeatureError): validate_feature_flags(sample_app) # Now if we opt in, validation is fine. sample_app.experimental_feature_flags.add('SOME_NEW_FEATURE') try: validate_feature_flags(sample_app) except ExperimentalFeatureError: raise AssertionError("App was not suppose to raise an error when " "opting in to features via a feature flag.") def test_validation_error_if_minimum_compression_size_not_int(sample_app): config = Config.create(chalice_app=sample_app, minimum_compression_size='not int') with pytest.raises(ValueError): validate_configuration(config) def test_validation_error_if_minimum_compression_size_invalid_int(sample_app): config = Config.create(chalice_app=sample_app, minimum_compression_size=MIN_COMPRESSION_SIZE-1) with pytest.raises(ValueError): validate_configuration(config) config = Config.create(chalice_app=sample_app, minimum_compression_size=MAX_COMPRESSION_SIZE+1) with pytest.raises(ValueError): validate_configuration(config) def test_valid_minimum_compression_size(sample_app): config = Config.create(chalice_app=sample_app, minimum_compression_size=1) assert validate_configuration(config) is None def test_validate_sqs_queue_name(sample_app): @sample_app.on_sqs_message( queue='https://sqs.us-west-2.amazonaws.com/12345/myqueue') def handler(event): pass config = Config.create(chalice_app=sample_app) with pytest.raises(ValueError): validate_configuration(config) def test_can_use_queue_arn(sample_app): @sample_app.on_sqs_message(queue_arn='arn:sqs:...:myqueue') def handler(event): pass config = Config.create(chalice_app=sample_app) assert validate_configuration(config) is None def test_queue_arn_must_be_arn(sample_app): @sample_app.on_sqs_message( queue_arn='https://sqs.us-west-2.amazonaws.com/12345/myqueue') def handler(event): pass config = Config.create(chalice_app=sample_app) with pytest.raises(ValueError): validate_configuration(config) def test_validate_environment_variables_value_type_not_str(sample_app): config = Config.create(chalice_app=sample_app, environment_variables={"ENV_KEY": 1}) with pytest.raises(ValueError): validate_configuration(config) def test_validate_unicode_is_valid_env_var(sample_app): config = Config.create(chalice_app=sample_app, environment_variables={"ENV_KEY": u'unicode-val'}) assert validate_configuration(config) is None def test_validate_env_var_is_string_for_lambda_functions(sample_app): @sample_app.lambda_function() def foo(event, context): pass config = Config( chalice_stage='dev', config_from_disk={ 'stages': { 'dev': { 'lambda_functions': { 'foo': {'environment_variables': {'BAR': 2}}} } } }, user_provided_params={'chalice_app': sample_app} ) with pytest.raises(ValueError): validate_configuration(config) ================================================ FILE: tests/unit/test_analyzer.py ================================================ import sys import pytest from textwrap import dedent from chalice import analyzer from chalice.analyzer import Boto3ModuleType, Boto3CreateClientType from chalice.analyzer import Boto3ClientType, Boto3ClientMethodType from chalice.analyzer import Boto3ClientMethodCallType from chalice.analyzer import FunctionType def aws_calls(source_code): real_source_code = dedent(source_code) calls = analyzer.get_client_calls(real_source_code) return calls def chalice_aws_calls(source_code): real_source_code = dedent(source_code) calls = analyzer.get_client_calls_for_app(real_source_code) return calls def known_types_for_module(source_code): real_source_code = dedent(source_code) compiled = analyzer.parse_code(real_source_code) t = analyzer.SymbolTableTypeInfer(compiled) t.bind_types() known = t.known_types() return known def known_types_for_function(source_code, name): real_source_code = dedent(source_code) compiled = analyzer.parse_code(real_source_code) t = analyzer.SymbolTableTypeInfer(compiled) t.bind_types() known = t.known_types(scope_name=name) return known def test_can_analyze_chalice_app(): assert chalice_aws_calls("""\ from chalice import Chalice import boto3 app = Chalice(app_name='james1') ec2 = boto3.client('ec2') @app.route('/') def index(): ec2.describe_instances() return {} """) == {'ec2': set(['describe_instances'])} def test_inferred_module_type(): assert known_types_for_module("""\ import boto3 import os a = 1 """) == {'boto3': Boto3ModuleType()} def test_recursive_function_none(): assert aws_calls("""\ def recursive_function(): recursive_function() recursive_function() """) == {} def test_recursive_comprehension_none(): assert aws_calls("""\ xs = [] def recursive_function(): [recursive_function() for x in xs] recursive_function() """) == {} def test_recursive_function_client_calls(): assert aws_calls("""\ import boto3 def recursive_function(): recursive_function() boto3.client('ec2').describe_instances() recursive_function() """) == {'ec2': set(['describe_instances'])} def test_mutual_recursion(): assert aws_calls("""\ import boto3 ec2 = boto3.client('ec2') def a(): b() ec2.run_instances() def b(): ec2.describe_instances() a() a() """) == {'ec2': set(['describe_instances', 'run_instances'])} def test_inferred_module_type_tracks_assignment(): assert known_types_for_module("""\ import boto3 a = boto3 """) == {'boto3': Boto3ModuleType(), 'a': Boto3ModuleType()} def test_inferred_module_type_tracks_multi_assignment(): assert known_types_for_module("""\ import boto3 a = b = c = boto3 """) == {'boto3': Boto3ModuleType(), 'a': Boto3ModuleType(), 'b': Boto3ModuleType(), 'c': Boto3ModuleType()} def test_inferred_client_create_type(): assert known_types_for_module("""\ import boto3 a = boto3.client """) == {'boto3': Boto3ModuleType(), 'a': Boto3CreateClientType()} def test_inferred_client_type(): assert known_types_for_module("""\ import boto3 a = boto3.client('ec2') """) == {'boto3': Boto3ModuleType(), 'a': Boto3ClientType('ec2')} def test_inferred_client_type_each_part(): assert known_types_for_module("""\ import boto3 a = boto3.client b = a('ec2') """) == {'boto3': Boto3ModuleType(), 'a': Boto3CreateClientType(), 'b': Boto3ClientType('ec2')} def test_infer_client_method(): assert known_types_for_module("""\ import boto3 a = boto3.client('ec2').describe_instances """) == {'boto3': Boto3ModuleType(), 'a': Boto3ClientMethodType('ec2', 'describe_instances')} def test_infer_client_method_called(): assert known_types_for_module("""\ import boto3 a = boto3.client('ec2').describe_instances() """) == {'boto3': Boto3ModuleType(), 'a': Boto3ClientMethodCallType('ec2', 'describe_instances')} def test_infer_type_on_function_scope(): assert known_types_for_function("""\ import boto3 def foo(): d = boto3.client('dynamodb') e = d.list_tables() foo() """, name='foo') == { 'd': Boto3ClientType('dynamodb'), 'e': Boto3ClientMethodCallType('dynamodb', 'list_tables') } def test_can_understand_return_types(): assert known_types_for_module("""\ import boto3 def create_client(): d = boto3.client('dynamodb') return d e = create_client() """) == { 'boto3': Boto3ModuleType(), 'create_client': FunctionType(Boto3ClientType('dynamodb')), 'e': Boto3ClientType('dynamodb'), } def test_type_equality(): assert Boto3ModuleType() == Boto3ModuleType() assert Boto3CreateClientType() == Boto3CreateClientType() assert Boto3ModuleType() != Boto3CreateClientType() assert Boto3ClientType('s3') == Boto3ClientType('s3') assert Boto3ClientType('s3') != Boto3ClientType('ec2') assert Boto3ClientType('s3') == Boto3ClientType('s3') assert (Boto3ClientMethodType('s3', 'list_objects') == Boto3ClientMethodType('s3', 'list_objects')) assert (Boto3ClientMethodType('ec2', 'describe_instances') != Boto3ClientMethodType('s3', 'list_object')) assert (Boto3ClientMethodType('ec2', 'describe_instances') != Boto3CreateClientType()) def test_single_call(): assert aws_calls("""\ import boto3 d = boto3.client('dynamodb') d.list_tables() """) == {'dynamodb': set(['list_tables'])} def test_multiple_calls(): assert aws_calls("""\ import boto3 d = boto3.client('dynamodb') d.list_tables() d.create_table(TableName='foobar') """) == {'dynamodb': set(['list_tables', 'create_table'])} def test_multiple_services(): assert aws_calls("""\ import boto3 d = boto3.client('dynamodb') asdf = boto3.client('s3') d.list_tables() asdf.get_object(Bucket='foo', Key='bar') d.create_table(TableName='foobar') """) == {'dynamodb': set(['list_tables', 'create_table']), 's3': set(['get_object'])} def test_basic_aliasing(): assert aws_calls("""\ import boto3 d = boto3.client('dynamodb') alias = d alias.list_tables() """) == {'dynamodb': set(['list_tables'])} def test_multiple_aliasing(): assert aws_calls("""\ import boto3 d = boto3.client('dynamodb') alias = d alias2 = alias alias3 = alias2 alias3.list_tables() """) == {'dynamodb': set(['list_tables'])} def test_multiple_aliasing_non_chained(): assert aws_calls("""\ import boto3 d = boto3.client('dynamodb') alias = d alias2 = alias alias3 = alias alias3.list_tables() """) == {'dynamodb': set(['list_tables'])} def test_no_calls_found(): assert aws_calls("""\ import boto3 """) == {} def test_original_name_replaced(): assert aws_calls("""\ import boto3 import some_other_thing d = boto3.client('dynamodb') d.list_tables() d = some_other_thing d.create_table() """) == {'dynamodb': set(['list_tables'])} def test_multiple_targets(): assert aws_calls("""\ import boto3 a = b = boto3.client('dynamodb') b.list_tables() a.create_table() """) == {'dynamodb': set(['create_table', 'list_tables'])} def test_in_function(): assert aws_calls("""\ import boto3 def foo(): d = boto3.client('dynamodb') d.list_tables() foo() """) == {'dynamodb': set(['list_tables'])} def test_ignores_built_in_scope(): assert aws_calls("""\ import boto3 a = boto3.client('dynamodb') def foo(): if a is not None: try: a.list_tables() except Exception as e: a.create_table() foo() """) == {'dynamodb': set(['create_table', 'list_tables'])} def test_understands_scopes(): assert aws_calls("""\ import boto3, mock d = mock.Mock() def foo(): d = boto3.client('dynamodb') d.list_tables() """) == {} def test_function_return_types(): assert aws_calls("""\ import boto3 def create_client(): return boto3.client('dynamodb') create_client().list_tables() """) == {'dynamodb': set(['list_tables'])} def test_propagates_return_types(): assert aws_calls("""\ import boto3 def create_client1(): return create_client2() def create_client2(): return create_client3() def create_client3(): return boto3.client('dynamodb') create_client1().list_tables() """) == {'dynamodb': set(['list_tables'])} def test_decorator_list_is_ignored(): assert known_types_for_function("""\ import boto3 import decorators @decorators.retry(10) def foo(): d = boto3.client('dynamodb') e = d.list_tables() foo() """, name='foo') == { 'd': Boto3ClientType('dynamodb'), 'e': Boto3ClientMethodCallType('dynamodb', 'list_tables') } def test_can_map_function_params(): assert aws_calls("""\ import boto3 d = boto3.client('dynamodb') def make_call(client): a = 1 return client.list_tables() make_call(d) """) == {'dynamodb': set(['list_tables'])} def test_can_understand_shadowed_vars_from_func_arg(): assert aws_calls("""\ import boto3 d = boto3.client('dynamodb') def make_call(d): return d.list_tables() make_call('foo') """) == {} def test_can_understand_shadowed_vars_from_local_scope(): assert aws_calls("""\ import boto3, mock d = boto3.client('dynamodb') def make_call(e): d = mock.Mock() return d.list_tables() make_call(d) """) == {} def test_can_map_function_with_multiple_args(): assert aws_calls("""\ import boto3, mock m = mock.Mock() d = boto3.client('dynamodb') def make_call(other, client): a = 1 other.create_table() return client.list_tables() make_call(m, d) """) == {'dynamodb': set(['list_tables'])} def test_multiple_function_calls(): assert aws_calls("""\ import boto3, mock m = mock.Mock() d = boto3.client('dynamodb') def make_call(other, client): a = 1 other.create_table() return other_call(a, 2, 3, client) def other_call(a, b, c, client): return client.list_tables() make_call(m, d) """) == {'dynamodb': set(['list_tables'])} def test_can_lookup_var_names_to_functions(): assert aws_calls("""\ import boto3 service_name = 'dynamodb' d = boto3.client(service_name) d.list_tables() """) == {'dynamodb': set(['list_tables'])} def test_map_string_literals_across_scopes(): assert aws_calls("""\ import boto3 service_name = 'dynamodb' def foo(): service_name = 's3' d = boto3.client(service_name) d.list_buckets() d = boto3.client(service_name) d.list_tables() foo() """) == {'s3': set(['list_buckets']), 'dynamodb': set(['list_tables'])} def test_can_handle_lambda_keyword(): assert aws_calls("""\ def foo(a): return sorted(bar.values(), key=lambda x: x.baz[a - 1], reverse=True) bar = {} foo(12) """) == {} def test_dict_comp_with_no_client_calls(): assert aws_calls("""\ import boto3 foo = {i: i for i in range(10)} """) == {} def test_can_handle_gen_expr(): assert aws_calls("""\ import boto3 ('a' for y in [1,2,3]) """) == {} def test_can_detect_calls_in_gen_expr(): assert aws_calls("""\ import boto3 service_name = 'dynamodb' d = boto3.client('dynamodb') (d.list_tables() for i in [1,2,3]) """) == {'dynamodb': set(['list_tables'])} def test_can_handle_gen_from_call(): assert aws_calls("""\ import boto3 service_name = 'dynamodb' d = boto3.client('dynamodb') (i for i in d.list_tables()) """) == {'dynamodb': set(['list_tables'])} def test_can_detect_calls_in_multiple_gen_exprs(): assert aws_calls("""\ import boto3 d = boto3.client('dynamodb') (d for i in [1,2,3]) (d.list_tables() for j in [1,2,3]) """) == {'dynamodb': set(['list_tables'])} def test_multiple_gen_exprs(): assert aws_calls("""\ (i for i in [1,2,3]) (j for j in [1,2,3]) """) == {} def test_can_handle_list_expr_with_api_calls(): assert aws_calls("""\ import boto3 d = boto3.client('dynamodb') [d.list_tables() for y in [1,2,3]] """) == {'dynamodb': set(['list_tables'])} def test_can_handle_multiple_listcomps(): assert aws_calls("""\ bar_key = 'bar' baz_key = 'baz' items = [{'foo': 'sun', 'bar': 'moon', 'baz': 'stars'}] foos = [i['foo'] for i in items] bars = [j[bar_key] for j in items] bazs = [k[baz_key] for k in items] """) == {} def test_can_analyze_lambda_function(): assert chalice_aws_calls("""\ from chalice import Chalice import boto3 app = Chalice(app_name='james1') ec2 = boto3.client('ec2') @app.lambda_function(name='lambda1') def index(): ec2.describe_instances() return {} """) == {'ec2': set(['describe_instances'])} def test_can_analyze_schedule(): assert chalice_aws_calls("""\ from chalice import Chalice import boto3 app = Chalice(app_name='james1') s3cli = boto3.client('s3') @app.schedule('rate(1 hour)') def index(): s3cli.list_buckets() return {} """) == {'s3': set(['list_buckets'])} def test_can_analyze_combination(): assert chalice_aws_calls("""\ from chalice import Chalice import boto3 app = Chalice(app_name='james1') s3 = boto3.client('s3') ec = boto3.client('ec2') @app.route('/') def index(): ec2.describe_instances() return {} @app.schedule('rate(1 hour)') def index_sc(): s3.list_buckets() return {} @app.lambda_function(name='lambda1') def index_lm(): ec.describe_instances() return {} @random def foo(): return {} """) == {'s3': set(['list_buckets']), 'ec2': set(['describe_instances'])} def test_can_handle_dict_comp(): assert aws_calls("""\ import boto3 ddb = boto3.client('dynamodb') tables = {t: t for t in ddb.list_tables()} """) == {'dynamodb': set(['list_tables'])} def test_can_handle_dict_comp_if(): assert aws_calls("""\ import boto3 ddb = boto3.client('dynamodb') tables = {t: t for t in [1] if ddb.list_tables()} """) == {'dynamodb': set(['list_tables'])} def test_can_handle_comp_ifs(): assert aws_calls("""\ [(x,y) for x in [1,2,3,4] for y in [1,2,3,4] if x % 2 == 0] """) == {} def test_can_handle_dict_comp_ifs(): assert aws_calls("""\ import boto3 d = boto3.client('dynamodb') {x: y for x in d.create_table()\ for y in d.update_table()\ if d.list_tables()} {x: y for x in d.create_table()\ for y in d.update_table()\ if d.list_tables()} """) == {'dynamodb': set(['list_tables', 'create_table', 'update_table'])} @pytest.mark.skipif(sys.version[0] == '2', reason=( 'Async await syntax is not in Python 2' )) def test_can_handle_async_await(): assert aws_calls("""\ import boto3 import asyncio async def test(): d = boto3.client('dynamodb') d.list_tables() await asyncio.sleep(1) test() """) == {'dynamodb': set(['list_tables'])} def test_can_analyze_custom_auth(): assert chalice_aws_calls("""\ from chalice import Chalice import boto3 ec2 = boto3.client('ec2') app = Chalice(app_name='custom-auth') @app.authorizer() def index(auth_request): ec2.describe_instances() return {} """) == {'ec2': set(['describe_instances'])} def test_can_analyze_s3_events(): assert chalice_aws_calls("""\ from chalice import Chalice import boto3 s3 = boto3.client('s3') app = Chalice(app_name='s3-event') @app.on_s3_event(bucket='mybucket') def index(event): s3.list_buckets() return {} """) == {'s3': set(['list_buckets'])} def test_can_analyze_sns_events(): assert chalice_aws_calls("""\ from chalice import Chalice import boto3 s3 = boto3.client('s3') app = Chalice(app_name='sns-event') @app.on_sns_message(topic='mytopic') def index(event): s3.list_buckets() return {} """) == {'s3': set(['list_buckets'])} def test_can_analyze_sqs_events(): assert chalice_aws_calls("""\ from chalice import Chalice import boto3 s3 = boto3.client('s3') app = Chalice(app_name='sqs-event') @app.on_sqs_message(queue='myqueue') def index(event): s3.list_buckets() return {} """) == {'s3': set(['list_buckets'])} def test_can_analyze_transfer_manager_methods(): assert chalice_aws_calls("""\ from chalice import Chalice import boto3 s3 = boto3.client('s3') app = Chalice(app_name='sqs-event') @app.on_s3_event(bucket='mybucket') def index(event): s3.download_file(event.bucket, event.key, 'foo') return {} """) == {'s3': set(['download_file'])} def test_can_handle_replacing_function_name(): assert chalice_aws_calls("""\ from chalice import Chalice import boto3 app = Chalice(app_name='sqs-event') def index(): pass @app.on_sqs_message(queue='myqueue') def index(event): foo = boto3.client('s3').list_buckets() """) == {'s3': set(['list_buckets'])} def test_can_handle_multiple_shadowing(): assert chalice_aws_calls("""\ from chalice import Chalice import boto3 app = Chalice(app_name='sqs-event') def index(): pass @app.on_sqs_message(queue='myqueue') def index(event): foo = boto3.client('s3').list_buckets() @app.on_s3_event(bucket='mybucket') def index(event): bar = boto3.client('s3').head_bucket(Bucket='foo') """) == {'s3': set(['list_buckets', 'head_bucket'])} def test_can_handle_forward_declaration(): assert chalice_aws_calls("""\ from chalice import Chalice import boto3 app = Chalice(app_name='forward-declaration') def get_regions(): return boto3.client('s3').list_buckets() @app.route('/') def index(): return get_regions() """) == {'s3': set(['list_buckets'])} def test_can_handle_post_declaration(): assert chalice_aws_calls("""\ from chalice import Chalice import boto3 app = Chalice(app_name='post-declaration') @app.route('/') def index(): return get_regions() def get_regions(): return boto3.client('s3').list_buckets() """) == {'s3': set(['list_buckets'])} def test_can_handle_shadowed_declaration(): assert chalice_aws_calls("""\ from chalice import Chalice import boto3 app = Chalice(app_name='shadowed-declaration') def get_regions(): return boto3.client('s3').list_buckets() @app.route('/') def index(): return get_regions() def get_regions(): return boto3.client('s3').head_bucket(Bucket='foo') """) == {'s3': set(['head_bucket'])} # def test_tuple_assignment(): # assert aws_calls("""\ # import boto3 # import some_other_thing # a, d = (1, boto3.client('dynamodb')) # d.list_tables() # d.create_table() # """) == {'dynamodb': set(['list_tables'])} # def test_multiple_client_assignment(): # assert aws_calls("""\ # import boto3 # import some_other_thing # s3, db = (boto3.client('s3'), boto3.client('dynamodb')) # db.list_tables() # s3.get_object(Bucket='a', Key='b') # """) == {'dynamodb': set(['list_tables']) # 's3': set(['get_object'])} # def test_understands_instance_methods(): # assert aws_calls("""\ # import boto3, mock # class Foo(object): # def make_call(self, client): # return client.list_tables() # # d = boto3.client('dynamodb') # instance = Foo() # instance.make_call(d) # """) == {'dynamodb': set(['list_tables'])} # def test_understands_function_and_methods(): # assert aws_calls("""\ # import boto3, mock # class Foo(object): # def make_call(self, client): # return foo_call(1, client) # # def foo_call(a, client): # return client.list_tables() # # d = boto3.client('dynamodb') # instance = Foo() # instance.make_call(d) # """) == {'dynamodb': set(['list_tables'])} # def test_can_track_across_classes(): # assert aws_calls("""\ # import boto3 # ddb = boto3.client('dynamodb') # class Helper(object): # def __init__(self, client): # self.client = client # def foo(self): # return self.client.list_tables() # h = Helper(ddb) # h.foo() # """) == {'dynamodb': set(['list_tables'])} ================================================ FILE: tests/unit/test_app.py ================================================ import sys import base64 import logging import json import gzip import inspect import collections from copy import deepcopy from datetime import datetime import pytest from pytest import fixture import hypothesis.strategies as st from hypothesis import given, assume import six from chalice import app from chalice import NotFoundError from chalice.test import Client from chalice.app import ( APIGateway, Request, Response, handle_extra_types, MultiDict, WebsocketEvent, BadRequestError, WebsocketDisconnectedError, WebsocketEventSourceHandler, ConvertToMiddleware, WebsocketAPI, ChaliceUnhandledError, ) from chalice import __version__ as chalice_version from chalice.deploy.validate import ExperimentalFeatureError from chalice.deploy.validate import validate_feature_flags # These are used to generate sample data for hypothesis tests. STR_MAP = st.dictionaries(st.text(), st.text()) STR_TO_LIST_MAP = st.dictionaries( st.text(), st.lists(elements=st.text(), min_size=1, max_size=5) ) HTTP_METHOD = st.sampled_from(['GET', 'POST', 'PUT', 'PATCH', 'OPTIONS', 'HEAD', 'DELETE']) PATHS = st.sampled_from(['/', '/foo/bar']) HTTP_BODY = st.none() | st.text() HTTP_REQUEST = st.fixed_dictionaries({ 'query_params': STR_TO_LIST_MAP, 'headers': STR_MAP, 'uri_params': STR_MAP, 'method': HTTP_METHOD, 'body': HTTP_BODY, 'context': STR_MAP, 'stage_vars': STR_MAP, 'is_base64_encoded': st.booleans(), 'path': PATHS, }) HTTP_REQUEST = st.fixed_dictionaries({ 'multiValueQueryStringParameters': st.fixed_dictionaries({}), 'headers': STR_MAP, 'pathParameters': STR_MAP, 'requestContext': st.fixed_dictionaries({ 'httpMethod': HTTP_METHOD, 'resourcePath': PATHS, }), 'body': HTTP_BODY, 'stageVariables': STR_MAP, 'isBase64Encoded': st.booleans(), }) BINARY_TYPES = APIGateway().binary_types class FakeLambdaContextIdentity(object): def __init__(self, cognito_identity_id, cognito_identity_pool_id): self.cognito_identity_id = cognito_identity_id self.cognito_identity_pool_id = cognito_identity_pool_id class FakeLambdaContext(object): def __init__(self): self.function_name = 'test_name' self.function_version = 'version' self.invoked_function_arn = 'arn' self.memory_limit_in_mb = 256 self.aws_request_id = 'id' self.log_group_name = 'log_group_name' self.log_stream_name = 'log_stream_name' self.identity = FakeLambdaContextIdentity('id', 'id_pool') # client_context is set by the mobile SDK and wont be set for chalice self.client_context = None def get_remaining_time_in_millis(self): return 500 def serialize(self): serialized = {} serialized.update(vars(self)) serialized['identity'] = vars(self.identity) return serialized class FakeGoneException(Exception): pass class FakeExceptionFactory(object): def __init__(self): self.GoneException = FakeGoneException class FakeClient(object): def __init__(self, errors=None, infos=None): if errors is None: errors = [] if infos is None: infos = [] self._errors = errors self._infos = infos self.calls = collections.defaultdict(lambda: []) self.exceptions = FakeExceptionFactory() def post_to_connection(self, ConnectionId, Data): self._call('post_to_connection', ConnectionId, Data) def delete_connection(self, ConnectionId): self._call('close', ConnectionId) def get_connection(self, ConnectionId): self._call('info', ConnectionId) if self._infos is not None: info = self._infos.pop() return info def _call(self, name, *args): self.calls[name].append(tuple(args)) if self._errors: error = self._errors.pop() raise error class FakeSession(object): def __init__(self, client=None, region_name='us-west-2'): self.calls = [] self._client = client self.region_name = region_name def client(self, name, endpoint_url=None): self.calls.append((name, endpoint_url)) return self._client @pytest.fixture def view_function(): def _func(): return {"hello": "world"} def create_request_with_content_type(content_type): body = '{"json": "body"}' event = { 'multiValueQueryStringParameters': '', 'headers': {'Content-Type': content_type}, 'pathParameters': {}, 'requestContext': { 'httpMethod': 'GET', 'resourcePath': '/', }, 'body': body, 'stageVariables': {}, 'isBase64Encoded': False, } return app.Request(event, FakeLambdaContext()) def assert_response_body_is(response, body): assert json.loads(response['body']) == body def json_response_body(response): return json.loads(response['body']) def assert_requires_opt_in(app, flag): with pytest.raises(ExperimentalFeatureError): validate_feature_flags(app) # Now ensure if we opt in to the feature, we don't # raise an exception. app.experimental_feature_flags.add(flag) try: validate_feature_flags(app) except ExperimentalFeatureError: raise AssertionError( "Opting in to feature %s still raises an " "ExperimentalFeatureError." % flag ) def websocket_handler_for_route(route, app): fn = app.websocket_handlers[route].handler_function handler = WebsocketEventSourceHandler( fn, WebsocketEvent, app.websocket_api) return handler @fixture def sample_app(): demo = app.Chalice('demo-app') @demo.route('/index', methods=['GET']) def index(): return {'hello': 'world'} @demo.route('/name/{name}', methods=['GET']) def name(name): return {'provided-name': name} return demo @fixture def sample_app_with_cors(): demo = app.Chalice('demo-app') @demo.route('/image', methods=['POST'], cors=True, content_types=['image/gif']) def image(): return {'image': True} return demo @fixture def sample_app_with_default_cors(): demo = app.Chalice('demo-app') demo.api.cors = True @demo.route('/on', methods=['POST'], content_types=['image/gif']) def on(): return {'image': True} @demo.route('/off', methods=['POST'], cors=False, content_types=['image/gif']) def off(): return {'image': True} @demo.route('/default', methods=['POST'], cors=None, content_types=['image/gif']) def default(): return {'image': True} return demo @fixture def sample_websocket_app(): demo = app.Chalice('app-name') demo.websocket_api.session = FakeSession() calls = [] @demo.on_ws_connect() def connect(event): demo.websocket_api.send(event.connection_id, 'connected') calls.append(('connect', event)) @demo.on_ws_disconnect() def disconnect(event): demo.websocket_api.send(event.connection_id, 'message') calls.append(('disconnect', event)) @demo.on_ws_message() def message(event): demo.websocket_api.send(event.connection_id, 'disconnected') calls.append(('default', event)) return demo, calls @fixture def sample_middleware_app(): demo = app.Chalice('app-name') demo.calls = [] @demo.middleware('all') def mymiddleware(event, get_response): demo.calls.append({'type': 'all', 'event': event.__class__.__name__}) return get_response(event) @demo.middleware('s3') def mymiddleware_s3(event, get_response): demo.calls.append({'type': 's3', 'event': event.__class__.__name__}) return get_response(event) @demo.middleware('sns') def mymiddleware_sns(event, get_response): demo.calls.append({'type': 'sns', 'event': event.__class__.__name__}) return get_response(event) @demo.middleware('http') def mymiddleware_http(event, get_response): demo.calls.append({'type': 'http', 'event': event.__class__.__name__}) return get_response(event) @demo.middleware('websocket') def mymiddleware_websocket(event, get_response): demo.calls.append({'type': 'websocket', 'event': event.__class__.__name__}) return get_response(event) @demo.middleware('pure_lambda') def mymiddleware_pure_lambda(event, get_response): demo.calls.append({'type': 'pure_lambda', 'event': event.__class__.__name__}) return get_response(event) @demo.route('/') def index(): return {} @demo.on_s3_event(bucket='foo') def s3_handler(event): pass @demo.on_sns_message(topic='foo') def sns_handler(event): pass @demo.on_sqs_message(queue='foo') def sqs_handler(event): pass @demo.lambda_function() def lambda_handler(event, context): pass @demo.on_ws_message() def ws_handler(event): pass return demo @fixture def auth_request(): method_arn = ( "arn:aws:execute-api:us-west-2:123:rest-api-id/dev/GET/needs/auth") request = app.AuthRequest('TOKEN', 'authtoken', method_arn) return request @pytest.mark.skipif(sys.version[0] == '2', reason=('Test is irrelevant under python 2, since str and ' 'bytes are interchangeable.')) def test_invalid_binary_response_body_throws_value_error(sample_app): response = app.Response( status_code=200, body={'foo': 'bar'}, headers={'Content-Type': 'application/octet-stream'} ) with pytest.raises(ValueError): response.to_dict(sample_app.api.binary_types) def test_invalid_JSON_response_body_throws_type_error(sample_app): response = app.Response( status_code=200, body={'foo': object()}, headers={'Content-Type': 'application/json'} ) with pytest.raises(TypeError): response.to_dict() def test_can_encode_binary_body_as_base64(sample_app): response = app.Response( status_code=200, body=b'foobar', headers={'Content-Type': 'application/octet-stream'} ) encoded_response = response.to_dict(sample_app.api.binary_types) assert encoded_response['body'] == 'Zm9vYmFy' def test_can_return_unicode_body(sample_app): unicode_data = u'\u2713' response = app.Response( status_code=200, body=unicode_data ) encoded_response = response.to_dict() assert encoded_response['body'] == unicode_data def test_can_encode_binary_body_with_header_charset(sample_app): response = app.Response( status_code=200, body=b'foobar', headers={'Content-Type': 'application/octet-stream; charset=binary'} ) encoded_response = response.to_dict(sample_app.api.binary_types) assert encoded_response['body'] == 'Zm9vYmFy' def test_can_encode_binary_json(sample_app): sample_app.api.binary_types.extend(['application/json']) response = app.Response( status_code=200, body={'foo': 'bar'}, headers={'Content-Type': 'application/json'} ) encoded_response = response.to_dict(sample_app.api.binary_types) assert encoded_response['body'] == 'eyJmb28iOiJiYXIifQ==' def test_wildcard_accepts_with_native_python_types_serializes_json( sample_app, create_event): sample_app.api.binary_types = ['*/*'] @sample_app.route('/py-dict') def py_dict(): return {'foo': 'bar'} event = create_event('/py-dict', 'GET', {}) event['headers']['Accept'] = '*/*' response = sample_app(event, context=None) # In this case, they've return a native python dict type, which should # be serialized to JSON and returned back to the user as JSON. Because # we also have ``*/*`` as a binary type, we'll return the response # as a binary response type. assert base64.b64decode(response['body']) == b'{"foo":"bar"}' assert response['isBase64Encoded'] def test_wildcard_accepts_with_response_class( sample_app, create_event): sample_app.api.binary_types = ['*/*'] @sample_app.route('/py-dict') def py_dict(): return Response(body=json.dumps({'foo': 'bar'}).encode('utf-8'), headers={'Content-Type': 'application/json'}, status_code=200) event = create_event('/py-dict', 'GET', {}) event['headers']['Accept'] = '*/*' response = sample_app(event, context=None) # Because our binary types is '*/*' we should be returning this # content as binary. assert base64.b64decode(response['body']) == b'{"foo": "bar"}' assert response['isBase64Encoded'] def test_can_parse_route_view_args(): entry = app.RouteEntry(lambda: {"foo": "bar"}, 'view-name', '/foo/{bar}/baz/{qux}', method='GET') assert entry.view_args == ['bar', 'qux'] def test_can_route_single_view(): demo = app.Chalice('app-name') @demo.route('/index') def index_view(): return {} assert demo.routes['/index']['GET'] == app.RouteEntry( index_view, 'index_view', '/index', 'GET', content_types=['application/json']) def test_can_handle_multiple_routes(): demo = app.Chalice('app-name') @demo.route('/index') def index_view(): return {} @demo.route('/other') def other_view(): return {} assert len(demo.routes) == 2, demo.routes assert '/index' in demo.routes, demo.routes assert '/other' in demo.routes, demo.routes assert demo.routes['/index']['GET'].view_function == index_view assert demo.routes['/other']['GET'].view_function == other_view def test_error_on_unknown_event(sample_app): bad_event = {'random': 'event'} raw_response = sample_app(bad_event, context=None) assert raw_response['statusCode'] == 500 assert json_response_body(raw_response)['Code'] == 'InternalServerError' def test_can_route_api_call_to_view_function(sample_app, create_event): event = create_event('/index', 'GET', {}) response = sample_app(event, context=None) assert_response_body_is(response, {'hello': 'world'}) def test_can_call_to_dict_on_current_request(sample_app, create_event): @sample_app.route('/todict') def todict(): return sample_app.current_request.to_dict() event = create_event('/todict', 'GET', {}) response = json_response_body(sample_app(event, context=None)) assert isinstance(response, dict) # The dict can change over time so we'll just pick # out a few keys as a basic sanity test. assert response['method'] == 'GET' # We also want to verify that to_dict() is always # JSON serializable so we check we can roundtrip # the data to/from JSON. assert isinstance(json.loads(json.dumps(response)), dict) def test_can_call_to_dict_on_request_with_querystring(sample_app, create_event): @sample_app.route('/todict') def todict(): return sample_app.current_request.to_dict() event = create_event('/todict', 'GET', {}) event['multiValueQueryStringParameters'] = { 'key': ['val1', 'val2'], 'key2': ['val'] } response = json_response_body(sample_app(event, context=None)) assert isinstance(response, dict) # The dict can change over time so we'll just pick # out a few keys as a basic sanity test. assert response['method'] == 'GET' assert response['query_params'] is not None assert response['query_params']['key'] == 'val2' assert response['query_params']['key2'] == 'val' # We also want to verify that to_dict() is always # JSON serializable so we check we can roundtrip # the data to/from JSON. assert isinstance(json.loads(json.dumps(response)), dict) def test_request_to_dict_does_not_contain_internal_attrs(sample_app, create_event): @sample_app.route('/todict') def todict(): return sample_app.current_request.to_dict() event = create_event('/todict', 'GET', {}) response = json_response_body(sample_app(event, context=None)) internal_attrs = [key for key in response if key.startswith('_')] assert not internal_attrs def test_will_pass_captured_params_to_view(sample_app, create_event): event = create_event('/name/{name}', 'GET', {'name': 'james'}) response = sample_app(event, context=None) response = json_response_body(response) assert response == {'provided-name': 'james'} def test_error_on_unsupported_method(sample_app, create_event): event = create_event('/name/{name}', 'POST', {'name': 'james'}) raw_response = sample_app(event, context=None) assert raw_response['statusCode'] == 405 assert raw_response['headers']['Allow'] == 'GET' assert json_response_body(raw_response)['Code'] == 'MethodNotAllowedError' def test_error_on_unsupported_method_gives_feedback_on_method(sample_app, create_event): method = 'POST' event = create_event('/name/{name}', method, {'name': 'james'}) raw_response = sample_app(event, context=None) assert 'POST' in json_response_body(raw_response)['Message'] def test_error_contains_cors_headers(sample_app_with_cors, create_event): event = create_event('/image', 'POST', {'not': 'image'}) raw_response = sample_app_with_cors(event, context=None) assert raw_response['statusCode'] == 415 assert 'Access-Control-Allow-Origin' in raw_response['headers'] class TestDefaultCORS(object): def test_cors_enabled(self, sample_app_with_default_cors, create_event): event = create_event('/on', 'POST', {'not': 'image'}) raw_response = sample_app_with_default_cors(event, context=None) assert raw_response['statusCode'] == 415 assert 'Access-Control-Allow-Origin' in raw_response['headers'] def test_cors_none(self, sample_app_with_default_cors, create_event): event = create_event('/default', 'POST', {'not': 'image'}) raw_response = sample_app_with_default_cors(event, context=None) assert raw_response['statusCode'] == 415 assert 'Access-Control-Allow-Origin' in raw_response['headers'] def test_cors_disabled(self, sample_app_with_default_cors, create_event): event = create_event('/off', 'POST', {'not': 'image'}) raw_response = sample_app_with_default_cors(event, context=None) assert raw_response['statusCode'] == 415 assert 'Access-Control-Allow-Origin' not in raw_response['headers'] def test_can_access_context(create_event): demo = app.Chalice('app-name') @demo.route('/index') def index_view(): serialized = demo.lambda_context.serialize() return serialized event = create_event('/index', 'GET', {}) lambda_context = FakeLambdaContext() result = demo(event, lambda_context) result = json_response_body(result) serialized_lambda_context = lambda_context.serialize() assert result == serialized_lambda_context def test_can_access_raw_body(create_event): demo = app.Chalice('app-name') @demo.route('/index') def index_view(): return {'rawbody': demo.current_request.raw_body.decode('utf-8')} event = create_event('/index', 'GET', {}) event['body'] = '{"hello": "world"}' result = demo(event, context=None) result = json_response_body(result) assert result == {'rawbody': '{"hello": "world"}'} def test_raw_body_cache_returns_same_result(create_event): demo = app.Chalice('app-name') @demo.route('/index') def index_view(): # The first raw_body decodes base64, # the second value should return the cached value. # Both should be the same value return {'rawbody': demo.current_request.raw_body.decode('utf-8'), 'rawbody2': demo.current_request.raw_body.decode('utf-8')} event = create_event('/index', 'GET', {}) event['base64-body'] = base64.b64encode( b'{"hello": "world"}').decode('ascii') result = demo(event, context=None) result = json_response_body(result) assert result['rawbody'] == result['rawbody2'] def test_can_have_views_of_same_route_but_different_methods(create_event): demo = app.Chalice('app-name') @demo.route('/index', methods=['GET']) def get_view(): return {'method': 'GET'} @demo.route('/index', methods=['PUT']) def put_view(): return {'method': 'PUT'} assert demo.routes['/index']['GET'].view_function == get_view assert demo.routes['/index']['PUT'].view_function == put_view event = create_event('/index', 'GET', {}) result = demo(event, context=None) assert json_response_body(result) == {'method': 'GET'} event = create_event('/index', 'PUT', {}) result = demo(event, context=None) assert json_response_body(result) == {'method': 'PUT'} def test_error_on_duplicate_route_methods(): demo = app.Chalice('app-name') @demo.route('/index', methods=['PUT']) def index_view(): return {'foo': 'bar'} with pytest.raises(ValueError): @demo.route('/index', methods=['PUT']) def index_view_dup(): return {'foo': 'bar'} def test_json_body_available_with_right_content_type(create_event): demo = app.Chalice('demo-app') @demo.route('/', methods=['POST']) def index(): return demo.current_request.json_body event = create_event('/', 'POST', {}) event['body'] = json.dumps({'foo': 'bar'}) result = demo(event, context=None) result = json_response_body(result) assert result == {'foo': 'bar'} def test_json_body_none_with_malformed_json(create_event): demo = app.Chalice('demo-app') @demo.route('/', methods=['POST']) def index(): return demo.current_request.json_body event = create_event('/', 'POST', {}) event['body'] = '{"foo": "bar"' result = demo(event, context=None) assert result['statusCode'] == 400 assert json_response_body(result)['Code'] == 'BadRequestError' def test_cant_access_json_body_with_wrong_content_type(create_event): demo = app.Chalice('demo-app') @demo.route('/', methods=['POST'], content_types=['application/xml']) def index(): return (demo.current_request.json_body, demo.current_request.raw_body.decode('utf-8')) event = create_event('/', 'POST', {}, content_type='application/xml') event['body'] = 'hello' response = json_response_body(demo(event, context=None)) json_body, raw_body = response assert json_body is None assert raw_body == 'hello' def test_json_body_available_on_multiple_content_types(create_event_with_body): demo = app.Chalice('demo-app') @demo.route('/', methods=['POST'], content_types=['application/xml', 'application/json']) def index(): return (demo.current_request.json_body, demo.current_request.raw_body.decode('utf-8')) event = create_event_with_body('hello', content_type='application/xml') response = json_response_body(demo(event, context=None)) json_body, raw_body = response assert json_body is None assert raw_body == 'hello' # Now if we create an event with JSON, we should be able # to access .json_body as well. event = create_event_with_body({'foo': 'bar'}, content_type='application/json') response = json_response_body(demo(event, context=None)) json_body, raw_body = response assert json_body == {'foo': 'bar'} assert raw_body == '{"foo": "bar"}' def test_json_body_available_with_lowercase_content_type_key( create_event_with_body): demo = app.Chalice('demo-app') @demo.route('/', methods=['POST']) def index(): return (demo.current_request.json_body, demo.current_request.raw_body.decode('utf-8')) event = create_event_with_body({'foo': 'bar'}) del event['headers']['Content-Type'] event['headers']['content-type'] = 'application/json' json_body, raw_body = json_response_body(demo(event, context=None)) assert json_body == {'foo': 'bar'} assert raw_body == '{"foo": "bar"}' def test_content_types_must_be_lists(): demo = app.Chalice('app-name') with pytest.raises(ValueError): @demo.route('/index', content_types='application/not-a-list') def index_post(): return {'foo': 'bar'} def test_content_type_validation_raises_error_on_unknown_types(create_event): demo = app.Chalice('demo-app') @demo.route('/', methods=['POST'], content_types=['application/xml']) def index(): return "success" bad_content_type = 'application/bad-xml' event = create_event('/', 'POST', {}, content_type=bad_content_type) event['body'] = 'Request body' json_response = json_response_body(demo(event, context=None)) assert json_response['Code'] == 'UnsupportedMediaType' assert 'application/bad-xml' in json_response['Message'] def test_content_type_with_charset(create_event): demo = app.Chalice('demo-app') @demo.route('/', content_types=['application/json']) def index(): return {'foo': 'bar'} event = create_event('/', 'GET', {}, 'application/json; charset=utf-8') response = json_response_body(demo(event, context=None)) assert response == {'foo': 'bar'} def test_can_return_response_object(create_event): demo = app.Chalice('app-name') @demo.route('/index') def index_view(): return app.Response( status_code=200, body={'foo': 'bar'}, headers={ 'Content-Type': 'application/json', 'Set-Cookie': ['key=value', 'foo=bar'], }, ) event = create_event('/index', 'GET', {}) response = demo(event, context=None) assert response == { 'statusCode': 200, 'body': '{"foo":"bar"}', 'headers': {'Content-Type': 'application/json'}, 'multiValueHeaders': {'Set-Cookie': ['key=value', 'foo=bar']}, } def test_headers_have_basic_validation(create_event): demo = app.Chalice('app-name') @demo.route('/index') def index_view(): return app.Response( status_code=200, body='{}', headers={'Invalid-Header': 'foo\nbar'}) event = create_event('/index', 'GET', {}) response = demo(event, context=None) assert response['statusCode'] == 500 assert 'Invalid-Header' not in response['headers'] assert json.loads(response['body'])['Code'] == 'InternalServerError' def test_empty_headers_have_basic_validation(create_empty_header_event): demo = app.Chalice('app-name') @demo.route('/index') def index_view(): return app.Response( status_code=200, body='{}', headers={}) event = create_empty_header_event('/index', 'GET', {}) response = demo(event, context=None) assert response['statusCode'] == 200 def test_no_content_type_is_still_allowed(create_event): # When the content type validation happens in API gateway, it appears # to assume a default of application/json, so the chalice handler needs # to emulate that behavior. demo = app.Chalice('demo-app') @demo.route('/', methods=['POST'], content_types=['application/json']) def index(): return {'success': True} event = create_event('/', 'POST', {}) del event['headers']['Content-Type'] json_response = json_response_body(demo(event, context=None)) assert json_response == {'success': True} @pytest.mark.parametrize('content_type,accept', [ ('application/octet-stream', 'application/octet-stream'), ( 'application/octet-stream', ( 'text/html,application/xhtml+xml,application/xml' ';q=0.9,image/webp,*/*;q=0.8' ) ), ('image/gif', 'text/html,image/gif'), ('image/gif', 'text/html ,image/gif'), ('image/gif', 'text/html, image/gif'), ('image/gif', 'text/html;q=0.8, image/gif ;q=0.5'), ('image/gif', 'text/html,image/png'), ('image/png', 'text/html,image/gif'), ]) def test_can_base64_encode_binary_multiple_media_types( create_event, content_type, accept): demo = app.Chalice('demo-app') @demo.route('/index') def index_view(): return app.Response( status_code=200, body=u'\u2713'.encode('utf-8'), headers={'Content-Type': content_type}) event = create_event('/index', 'GET', {}) event['headers']['Accept'] = accept response = demo(event, context=None) assert response['statusCode'] == 200 assert response['isBase64Encoded'] is True assert response['body'] == '4pyT' assert response['headers']['Content-Type'] == content_type def test_can_return_text_even_with_binary_content_type_configured( create_event): demo = app.Chalice('demo-app') @demo.route('/index') def index_view(): return app.Response( status_code=200, body='Plain text', headers={'Content-Type': 'text/plain'}) event = create_event('/index', 'GET', {}) event['headers']['Accept'] = 'application/octet-stream' response = demo(event, context=None) assert response['statusCode'] == 200 assert response['body'] == 'Plain text' assert response['headers']['Content-Type'] == 'text/plain' def test_route_equality(view_function): a = app.RouteEntry( view_function, view_name='myview', path='/', method='GET', api_key_required=True, content_types=['application/json'], ) b = app.RouteEntry( view_function, view_name='myview', path='/', method='GET', api_key_required=True, content_types=['application/json'], ) assert a == b def test_route_inequality(view_function): a = app.RouteEntry( view_function, view_name='myview', path='/', method='GET', api_key_required=True, content_types=['application/json'], ) b = app.RouteEntry( view_function, view_name='myview', path='/', method='GET', api_key_required=True, # Different content types content_types=['application/xml'], ) assert not a == b def test_exceptions_raised_as_chalice_errors(sample_app, create_event): @sample_app.route('/error') def raise_error(): raise TypeError("Raising arbitrary error, should never see.") event = create_event('/error', 'GET', {}) # This is intentional behavior. If we're not in debug mode # we don't want to surface internal errors that get raised. # We should reply with a general internal server error. raw_response = sample_app(event, context=None) response = json_response_body(raw_response) assert response['Code'] == 'InternalServerError' assert raw_response['statusCode'] == 500 def test_original_exception_raised_in_debug_mode(sample_app, create_event): sample_app.debug = True @sample_app.route('/error') def raise_error(): raise ValueError("You will see this error") event = create_event('/error', 'GET', {}) response = sample_app(event, context=None) # In debug mode, we let the original exception propagate. # This includes the original type as well as the message. assert response['statusCode'] == 500 assert 'ValueError' in response['body'] assert 'You will see this error' in response['body'] def test_chalice_view_errors_propagate_in_non_debug_mode(sample_app, create_event): @sample_app.route('/notfound') def notfound(): raise NotFoundError("resource not found") event = create_event('/notfound', 'GET', {}) raw_response = sample_app(event, context=None) assert raw_response['statusCode'] == 404 assert json_response_body(raw_response)['Code'] == 'NotFoundError' def test_chalice_view_errors_propagate_in_debug_mode(sample_app, create_event): @sample_app.route('/notfound') def notfound(): raise NotFoundError("resource not found") sample_app.debug = True event = create_event('/notfound', 'GET', {}) raw_response = sample_app(event, context=None) assert raw_response['statusCode'] == 404 assert json_response_body(raw_response)['Code'] == 'NotFoundError' def test_case_insensitive_mapping(): mapping = app.CaseInsensitiveMapping({'HEADER': 'Value'}) assert mapping['hEAdEr'] assert mapping.get('hEAdEr') assert 'hEAdEr' in mapping assert repr({'header': 'Value'}) in repr(mapping) def test_unknown_kwargs_raise_error(sample_app, create_event): with pytest.raises(TypeError): @sample_app.route('/foo', unknown_kwargs='foo') def badkwargs(): pass def test_name_kwargs_does_not_raise_error(sample_app): try: @sample_app.route('/foo', name='foo') def name_kwarg(): pass except TypeError: pytest.fail('route name kwarg should not raise TypeError.') def test_default_logging_handlers_created(): handlers_before = logging.getLogger('log_app').handlers[:] # configure_logs = True is the default, but we're # being explicit here. app.Chalice('log_app', configure_logs=True) handlers_after = logging.getLogger('log_app').handlers[:] new_handlers = set(handlers_after) - set(handlers_before) # Should have added a new handler assert len(new_handlers) == 1 def test_default_logging_only_added_once(): # And creating the same app object means we shouldn't # configure logging again. handlers_before = logging.getLogger('added_once').handlers[:] app.Chalice('added_once', configure_logs=True) # The same app name, we should still only configure logs # once. app.Chalice('added_once', configure_logs=True) handlers_after = logging.getLogger('added_once').handlers[:] new_handlers = set(handlers_after) - set(handlers_before) # Should have added a new handler assert len(new_handlers) == 1 def test_logs_can_be_disabled(): handlers_before = logging.getLogger('log_app').handlers[:] app.Chalice('log_app', configure_logs=False) handlers_after = logging.getLogger('log_app').handlers[:] new_handlers = set(handlers_after) - set(handlers_before) assert len(new_handlers) == 0 @pytest.mark.parametrize('content_type,is_json', [ ('application/json', True), ('application/json;charset=UTF-8', True), ('application/notjson', False), ]) def test_json_body_available_when_content_type_matches(content_type, is_json): request = create_request_with_content_type(content_type) if is_json: assert request.json_body == {'json': 'body'} else: assert request.json_body is None def test_can_receive_binary_data(create_event_with_body): content_type = 'application/octet-stream' demo = app.Chalice('demo-app') @demo.route('/bincat', methods=['POST'], content_types=[content_type]) def bincat(): raw_body = demo.current_request.raw_body return app.Response( raw_body, headers={'Content-Type': content_type}, status_code=200) body = 'L3UyNzEz' event = create_event_with_body(body, '/bincat', 'POST', content_type) event['headers']['Accept'] = content_type event['isBase64Encoded'] = True response = demo(event, context=None) assert response['statusCode'] == 200 assert response['body'] == body def test_cannot_receive_base64_string_with_binary_response( create_event_with_body): content_type = 'application/octet-stream' demo = app.Chalice('demo-app') @demo.route('/bincat', methods=['GET'], content_types=[content_type]) def bincat(): return app.Response( status_code=200, body=u'\u2713'.encode('utf-8'), headers={'Content-Type': content_type}) event = create_event_with_body('', '/bincat', 'GET', content_type) response = demo(event, context=None) assert response['statusCode'] == 400 def test_can_serialize_cognito_auth(): auth = app.CognitoUserPoolAuthorizer( 'Name', provider_arns=['Foo'], header='Authorization') assert auth.to_swagger() == { 'in': 'header', 'type': 'apiKey', 'name': 'Authorization', 'x-amazon-apigateway-authtype': 'cognito_user_pools', 'x-amazon-apigateway-authorizer': { 'type': 'cognito_user_pools', 'providerARNs': ['Foo'], } } def test_can_serialize_iam_auth(): auth = app.IAMAuthorizer() assert auth.to_swagger() == { 'in': 'header', 'type': 'apiKey', 'name': 'Authorization', 'x-amazon-apigateway-authtype': 'awsSigv4', } def test_typecheck_list_type(): with pytest.raises(TypeError): app.CognitoUserPoolAuthorizer('Name', 'Authorization', provider_arns='foo') def test_can_serialize_custom_authorizer(): auth = app.CustomAuthorizer( 'Name', 'myuri', ttl_seconds=10, header='NotAuth', invoke_role_arn='role-arn' ) assert auth.to_swagger() == { 'in': 'header', 'type': 'apiKey', 'name': 'NotAuth', 'x-amazon-apigateway-authtype': 'custom', 'x-amazon-apigateway-authorizer': { 'type': 'token', 'authorizerUri': 'myuri', 'authorizerResultTtlInSeconds': 10, 'authorizerCredentials': 'role-arn', } } class TestCORSConfig(object): def test_eq(self): cors_config = app.CORSConfig() other_cors_config = app.CORSConfig() assert cors_config == other_cors_config def test_not_eq_different_type(self): cors_config = app.CORSConfig() different_type_obj = object() assert not cors_config == different_type_obj def test_not_eq_differing_configurations(self): cors_config = app.CORSConfig() differing_cors_config = app.CORSConfig( allow_origin='https://foo.example.com') assert cors_config != differing_cors_config def test_eq_non_default_configurations(self): custom_cors = app.CORSConfig( allow_origin='https://foo.example.com', allow_headers=['X-Special-Header'], max_age=600, expose_headers=['X-Special-Header'], allow_credentials=True ) same_custom_cors = app.CORSConfig( allow_origin='https://foo.example.com', allow_headers=['X-Special-Header'], max_age=600, expose_headers=['X-Special-Header'], allow_credentials=True ) assert custom_cors == same_custom_cors def test_can_handle_builtin_auth(): demo = app.Chalice('builtin-auth') @demo.authorizer() def my_auth(auth_request): pass @demo.route('/', authorizer=my_auth) def index_view(): return {} assert len(demo.builtin_auth_handlers) == 1 authorizer = demo.builtin_auth_handlers[0] assert isinstance(authorizer, app.BuiltinAuthConfig) assert authorizer.name == 'my_auth' assert authorizer.handler_string == 'app.my_auth' def test_builtin_auth_can_transform_event(): event = { 'type': 'TOKEN', 'authorizationToken': 'authtoken', 'methodArn': 'arn:aws:execute-api:...:foo', } auth_app = app.Chalice('builtin-auth') request = [] @auth_app.authorizer() def builtin_auth(auth_request): request.append(auth_request) builtin_auth(event, None) assert len(request) == 1 transformed = request[0] assert transformed.auth_type == 'TOKEN' assert transformed.token == 'authtoken' assert transformed.method_arn == 'arn:aws:execute-api:...:foo' def test_can_return_auth_dict_directly(): # A user can bypass our AuthResponse and return the auth response # dict that API gateway expects. event = { 'type': 'TOKEN', 'authorizationToken': 'authtoken', 'methodArn': 'arn:aws:execute-api:...:foo', } auth_app = app.Chalice('builtin-auth') response = { 'context': {'foo': 'bar'}, 'principalId': 'user', 'policyDocument': { 'Version': '2012-10-17', 'Statement': [] } } @auth_app.authorizer() def builtin_auth(auth_request): return response actual = builtin_auth(event, None) assert actual == response def test_can_specify_extra_auth_attributes(): auth_app = app.Chalice('builtin-auth') @auth_app.authorizer(ttl_seconds=10, execution_role='arn:my-role') def builtin_auth(auth_request): pass handler = auth_app.builtin_auth_handlers[0] assert handler.ttl_seconds == 10 assert handler.execution_role == 'arn:my-role' def test_validation_raised_on_unknown_kwargs(): auth_app = app.Chalice('builtin-auth') with pytest.raises(TypeError): @auth_app.authorizer(this_is_an_unknown_kwarg=True) def builtin_auth(auth_request): pass def test_can_return_auth_response(): event = { 'type': 'TOKEN', 'authorizationToken': 'authtoken', 'methodArn': 'arn:aws:execute-api:us-west-2:1:id/dev/GET/a', } auth_app = app.Chalice('builtin-auth') response = { 'context': {}, 'principalId': 'principal', 'policyDocument': { 'Version': '2012-10-17', 'Statement': [ {'Action': 'execute-api:Invoke', 'Effect': 'Allow', 'Resource': [ 'arn:aws:execute-api:us-west-2:1:id/dev/*/a' ]} ] } } @auth_app.authorizer() def builtin_auth(auth_request): return app.AuthResponse(['/a'], 'principal') actual = builtin_auth(event, None) assert actual == response def test_auth_response_with_colon_chars(): event = { 'type': 'TOKEN', 'authorizationToken': 'authtoken', 'methodArn': 'arn:aws:execute-api:us-west-2:1:id/api/GET/foo/a:b:c:d', } auth_app = app.Chalice('builtin-auth') response = { 'context': {}, 'principalId': 'principal', 'policyDocument': { 'Version': '2012-10-17', 'Statement': [ {'Action': 'execute-api:Invoke', 'Effect': 'Allow', 'Resource': [ 'arn:aws:execute-api:us-west-2:1:id/api/*/foo/*' ]} ] } } @auth_app.authorizer() def builtin_auth(auth_request): return app.AuthResponse(['/foo/*'], 'principal') actual = builtin_auth(event, None) assert actual == response def test_auth_response_serialization(): method_arn = ( "arn:aws:execute-api:us-west-2:123:rest-api-id/dev/GET/needs/auth") request = app.AuthRequest('TOKEN', 'authtoken', method_arn) response = app.AuthResponse(routes=['/needs/auth'], principal_id='foo') response_dict = response.to_dict(request) expected = [method_arn.replace('GET', '*')] assert response_dict == { 'policyDocument': { 'Version': '2012-10-17', 'Statement': [ { 'Action': 'execute-api:Invoke', 'Resource': expected, 'Effect': 'Allow' } ] }, 'context': {}, 'principalId': 'foo', } def test_auth_response_can_include_context(auth_request): response = app.AuthResponse(['/foo'], 'principal', {'foo': 'bar'}) serialized = response.to_dict(auth_request) assert serialized['context'] == {'foo': 'bar'} def test_can_use_auth_routes_instead_of_strings(auth_request): expected = [ "arn:aws:execute-api:us-west-2:123:rest-api-id/dev/GET/a", "arn:aws:execute-api:us-west-2:123:rest-api-id/dev/GET/a/b", "arn:aws:execute-api:us-west-2:123:rest-api-id/dev/POST/a/b", ] response = app.AuthResponse( [app.AuthRoute('/a', ['GET']), app.AuthRoute('/a/b', ['GET', 'POST'])], 'principal') serialized = response.to_dict(auth_request) assert serialized['policyDocument'] == { 'Version': '2012-10-17', 'Statement': [{ 'Action': 'execute-api:Invoke', 'Effect': 'Allow', 'Resource': expected, }] } def test_auth_response_wildcard(auth_request): response = app.AuthResponse( routes=[app.AuthRoute(path='*', methods=['*'])], principal_id='user') serialized = response.to_dict(auth_request) assert serialized['policyDocument'] == { 'Statement': [ {'Action': 'execute-api:Invoke', 'Effect': 'Allow', 'Resource': [ 'arn:aws:execute-api:us-west-2:123:rest-api-id/dev/*/*']}], 'Version': '2012-10-17' } def test_auth_response_wildcard_string(auth_request): response = app.AuthResponse( routes=['*'], principal_id='user') serialized = response.to_dict(auth_request) assert serialized['policyDocument'] == { 'Statement': [ {'Action': 'execute-api:Invoke', 'Effect': 'Allow', 'Resource': [ 'arn:aws:execute-api:us-west-2:123:rest-api-id/dev/*/*']}], 'Version': '2012-10-17' } def test_can_mix_auth_routes_and_strings(auth_request): expected = [ 'arn:aws:execute-api:us-west-2:123:rest-api-id/dev/*/a', 'arn:aws:execute-api:us-west-2:123:rest-api-id/dev/GET/a/b', ] response = app.AuthResponse( ['/a', app.AuthRoute('/a/b', ['GET'])], 'principal') serialized = response.to_dict(auth_request) assert serialized['policyDocument'] == { 'Version': '2012-10-17', 'Statement': [{ 'Action': 'execute-api:Invoke', 'Effect': 'Allow', 'Resource': expected, }] } def test_root_resource(auth_request): auth_request.method_arn = ( "arn:aws:execute-api:us-west-2:123:rest-api-id/dev/GET/") expected = [ "arn:aws:execute-api:us-west-2:123:rest-api-id/dev/GET/" ] response = app.AuthResponse( [app.AuthRoute('/', ['GET'])], 'principal') serialized = response.to_dict(auth_request) assert serialized['policyDocument'] == { 'Version': '2012-10-17', 'Statement': [{ 'Action': 'execute-api:Invoke', 'Effect': 'Allow', 'Resource': expected, }] } def test_can_register_scheduled_event_with_str(sample_app): @sample_app.schedule('rate(1 minute)') def foo(event): pass assert len(sample_app.event_sources) == 1 event_source = sample_app.event_sources[0] assert event_source.name == 'foo' assert event_source.schedule_expression == 'rate(1 minute)' assert event_source.handler_string == 'app.foo' def test_can_register_scheduled_event_with_rate(sample_app): @sample_app.schedule(app.Rate(value=2, unit=app.Rate.HOURS)) def foo(event): pass # We don't convert the rate down to its string form until # we actually deploy. assert len(sample_app.event_sources) == 1 expression = sample_app.event_sources[0].schedule_expression # We already check the event source in the test above, so we're # only interested in the schedule expression here. assert expression.value == 2 assert expression.unit == app.Rate.HOURS def test_can_register_scheduled_event_with_event(sample_app): @sample_app.schedule(app.Cron(0, 10, '*', '*', '?', '*')) def foo(event): pass assert len(sample_app.event_sources) == 1 expression = sample_app.event_sources[0].schedule_expression assert expression.minutes == 0 assert expression.hours == 10 assert expression.day_of_month == '*' assert expression.month == '*' assert expression.day_of_week == '?' assert expression.year == '*' @pytest.mark.parametrize('value,unit,expected', [ (1, app.Rate.MINUTES, 'rate(1 minute)'), (2, app.Rate.MINUTES, 'rate(2 minutes)'), (1, app.Rate.HOURS, 'rate(1 hour)'), (2, app.Rate.HOURS, 'rate(2 hours)'), (1, app.Rate.DAYS, 'rate(1 day)'), (2, app.Rate.DAYS, 'rate(2 days)'), ]) def test_rule_object_converts_to_str(value, unit, expected): assert app.Rate(value=value, unit=unit).to_string() == expected @pytest.mark.parametrize(('minutes,hours,day_of_month,month,' 'day_of_week,year,expected'), [ # These are taken from the scheduled events docs page. # Invoke a Lambda function at 10:00am (UTC) everyday (0, 10, '*', '*', '?', '*', 'cron(0 10 * * ? *)'), # Invoke a Lambda function 12:15pm (UTC) everyday (15, 12, '*', '*', '?', '*', 'cron(15 12 * * ? *)'), # Invoke a Lambda function at 06:00pm (UTC) every Mon-Fri (0, 18, '?', '*', 'MON-FRI', '*', 'cron(0 18 ? * MON-FRI *)'), # Invoke a Lambda function at 8:00am (UTC) every first day of the month (0, 8, 1, '*', '?', '*', 'cron(0 8 1 * ? *)'), # Invoke a Lambda function every 10 min Mon-Fri ('0/10', '*', '?', '*', 'MON-FRI', '*', 'cron(0/10 * ? * MON-FRI *)'), # Invoke a Lambda function every 5 minutes Mon-Fri between 8:00am and # 5:55pm (UTC) ('0/5', '8-17', '?', '*', 'MON-FRI', '*', 'cron(0/5 8-17 ? * MON-FRI *)'), # Invoke a Lambda function at 9 a.m. (UTC) the first Monday of each month (0, 9, '?', '*', '2#1', '*', 'cron(0 9 ? * 2#1 *)'), ]) def test_cron_expression_converts_to_str(minutes, hours, day_of_month, month, day_of_week, year, expected): assert app.Cron( minutes=minutes, hours=hours, day_of_month=day_of_month, month=month, day_of_week=day_of_week, year=year, ).to_string() == expected def test_can_map_schedule_event_dict_to_object(sample_app): @sample_app.schedule('rate(1 hour)') def handler(event): return event # This is the event dict that lambda provides # to the lambda handler lambda_event = { "version": "0", "account": "123456789012", "region": "us-west-2", "detail": {}, "detail-type": "Scheduled Event", "source": "aws.events", "time": "1970-01-01T00:00:00Z", "id": "event-id", "resources": [ "arn:aws:events:us-west-2:123456789012:rule/my-schedule" ] } event_object = handler(lambda_event, context=None) assert event_object.version == '0' assert event_object.event_id == 'event-id' assert event_object.source == 'aws.events' assert event_object.account == '123456789012' assert event_object.time == '1970-01-01T00:00:00Z' assert event_object.region == 'us-west-2' assert event_object.resources == [ "arn:aws:events:us-west-2:123456789012:rule/my-schedule" ] assert event_object.detail == {} assert event_object.detail_type == "Scheduled Event" # This is meant as a fall back in case you need access to # the raw lambda event dict. assert event_object.to_dict() == lambda_event def test_can_create_cwe_event_handler(sample_app): @sample_app.on_cw_event({'source': ['aws.ec2']}) def handler(event): pass assert len(sample_app.event_sources) == 1 event = sample_app.event_sources[0] assert event.name == 'handler' assert event.event_pattern == {'source': ['aws.ec2']} assert event.handler_string == 'app.handler' def test_can_map_cwe_event_dict_to_object(sample_app): @sample_app.on_cw_event({'source': ['aws.ec2']}) def handler(event): return event lambda_event = { "version": 0, "id": "7bf73129-1428-4cd3-a780-95db273d1602", "detail-type": "EC2 Instance State-change Notification", "source": "aws.ec2", "account": "123456789012", "time": "2015-11-11T21:29:54Z", "region": "us-east-1", "resources": [ "arn:aws:ec2:us-east-1:123456789012:instance/i-abcd1111" ], "detail": { "instance-id": "i-abcd1111", "state": "pending" } } event_object = handler(lambda_event, context=None) assert event_object.detail_type == "EC2 Instance State-change Notification" assert event_object.account == '123456789012' assert event_object.region == 'us-east-1' assert event_object.detail == { 'instance-id': 'i-abcd1111', 'state': 'pending' } def test_pure_lambda_function_direct_mapping(sample_app): @sample_app.lambda_function() def handler(event, context): return event, context return_value = handler({'fake': 'event'}, {'fake': 'context'}) assert return_value[0] == {'fake': 'event'} assert return_value[1] == {'fake': 'context'} def test_pure_lambda_functions_are_registered_in_app(sample_app): @sample_app.lambda_function() def handler(event, context): pass assert len(sample_app.pure_lambda_functions) == 1 lambda_function = sample_app.pure_lambda_functions[0] assert lambda_function.name == 'handler' assert lambda_function.handler_string == 'app.handler' def test_aws_execution_env_set(): env = {'AWS_EXECUTION_ENV': 'AWS_Lambda_python2.7'} app.Chalice('app-name', env=env) assert env['AWS_EXECUTION_ENV'] == ( 'AWS_Lambda_python2.7 aws-chalice/%s' % chalice_version ) def test_can_use_out_of_order_args(create_event): demo = app.Chalice('demo-app') # Note how the url params and function args are out of order. @demo.route('/{a}/{b}', methods=['GET']) def index(b, a): return {'a': a, 'b': b} event = create_event('/{a}/{b}', 'GET', {'a': 'first', 'b': 'second'}) response = demo(event, context=None) response = json_response_body(response) assert response == {'a': 'first', 'b': 'second'} def test_ensure_debug_mode_is_false_by_default(): # These logger tests need to each have a unique name because the Chalice # app creates a logger with it's name. If these tests are run in a batch # the logger names will overlap in the logging module and cause test # failures. test_app = app.Chalice('logger-test-1') assert test_app.debug is False assert test_app.log.getEffectiveLevel() == logging.ERROR def test_can_explicitly_set_debug_false_in_initializer(): test_app = app.Chalice('logger-test-2', debug=False) assert test_app.debug is False assert test_app.log.getEffectiveLevel() == logging.ERROR def test_can_set_debug_mode_in_initialzier(): test_app = app.Chalice('logger-test-3', debug=True) assert test_app.debug is True assert test_app.log.getEffectiveLevel() == logging.DEBUG def test_debug_mode_changes_log_level(): test_app = app.Chalice('logger-test-4', debug=False) test_app.debug = True assert test_app.debug is True assert test_app.log.getEffectiveLevel() == logging.DEBUG def test_internal_exception_debug_false(capsys, create_event): test_app = app.Chalice('logger-test-5', debug=False) @test_app.route('/error') def error(): raise Exception('Something bad happened') event = create_event('/error', 'GET', {}) test_app(event, context=None) out, err = capsys.readouterr() assert 'logger-test-5' in out assert 'Caught exception' in out assert 'Something bad happened' in out def test_raw_body_is_none_if_body_is_none(): event = { 'body': None, 'multiValueQueryStringParameters': '', 'headers': {}, 'pathParameters': {}, 'requestContext': { 'httpMethod': 'GET', 'resourcePath': '/', }, 'stageVariables': {}, 'isBase64Encoded': False, } request = app.Request(event, FakeLambdaContext()) assert request.raw_body == b'' @given(http_request_event=HTTP_REQUEST) def test_http_request_to_dict_is_json_serializable(http_request_event): # We have to do some slight pre-preprocessing here # to maintain preconditions. If the # is_base64_encoded arg is True, we'll # base64 encode the body. We assume API Gateway # upholds this precondition. is_base64_encoded = http_request_event['isBase64Encoded'] if is_base64_encoded: # Confirmed that if you send an empty body, # API Gateway will always say the body is *not* # base64 encoded. assume(http_request_event['body'] is not None) body = base64.b64encode( http_request_event['body'].encode('utf-8')) http_request_event['body'] = body.decode('ascii') request = Request(http_request_event, FakeLambdaContext()) assert isinstance(request.raw_body, bytes) request_dict = request.to_dict() # We should always be able to dump the request dict # to JSON. assert json.dumps(request_dict, default=handle_extra_types) @given(body=st.text(), headers=STR_MAP, status_code=st.integers(min_value=200, max_value=599)) def test_http_response_to_dict(body, headers, status_code): r = Response(body=body, headers=headers, status_code=status_code) serialized = r.to_dict() assert 'headers' in serialized assert 'statusCode' in serialized assert 'body' in serialized assert isinstance(serialized['body'], six.string_types) @given(body=st.binary(), content_type=st.sampled_from(BINARY_TYPES)) def test_handles_binary_responses(body, content_type): r = Response(body=body, headers={'Content-Type': content_type}) serialized = r.to_dict(BINARY_TYPES) # A binary response should always result in the # response being base64 encoded. assert serialized['isBase64Encoded'] assert isinstance(serialized['body'], six.string_types) assert isinstance(base64.b64decode(serialized['body']), bytes) def test_can_create_s3_event_handler(sample_app): @sample_app.on_s3_event(bucket='mybucket') def handler(event): pass assert len(sample_app.event_sources) == 1 event = sample_app.event_sources[0] assert event.name == 'handler' assert event.bucket == 'mybucket' assert event.events == ['s3:ObjectCreated:*'] assert event.handler_string == 'app.handler' def test_can_map_to_s3_event_object(sample_app): @sample_app.on_s3_event(bucket='mybucket') def handler(event): return event s3_event = { 'Records': [ {'awsRegion': 'us-west-2', 'eventName': 'ObjectCreated:Put', 'eventSource': 'aws:s3', 'eventTime': '2018-05-22T04:41:23.823Z', 'eventVersion': '2.0', 'requestParameters': {'sourceIPAddress': '174.127.235.55'}, 'responseElements': { 'x-amz-id-2': 'request-id-2', 'x-amz-request-id': 'request-id-1'}, 's3': { 'bucket': { 'arn': 'arn:aws:s3:::mybucket', 'name': 'mybucket', 'ownerIdentity': { 'principalId': 'ABCD' } }, 'configurationId': 'config-id', 'object': { 'eTag': 'd41d8cd98f00b204e9800998ecf8427e', 'key': 'hello-world.txt', 'sequencer': '005B039F73C627CE8B', 'size': 0 }, 's3SchemaVersion': '1.0' }, 'userIdentity': {'principalId': 'AWS:XYZ'} } ] } actual_event = handler(s3_event, context=None) assert actual_event.bucket == 'mybucket' assert actual_event.key == 'hello-world.txt' assert actual_event.to_dict() == s3_event def test_s3_event_urldecodes_keys(): s3_event = { 'Records': [ {'s3': { 'bucket': { 'arn': 'arn:aws:s3:::mybucket', 'name': 'mybucket', }, 'object': { 'key': 'file+with+spaces', 'sequencer': '005B039F73C627CE8B', 'size': 0 }, }}, ] } event = app.S3Event(s3_event, FakeLambdaContext()) # We should urldecode the key name. assert event.key == 'file with spaces' # But the key should remain unchanged in to_dict(). assert event.to_dict() == s3_event def test_s3_event_urldecodes_unicode_keys(): s3_event = { 'Records': [ {'s3': { 'bucket': { 'arn': 'arn:aws:s3:::mybucket', 'name': 'mybucket', }, 'object': { # This is u'\u2713' 'key': '%E2%9C%93', 'sequencer': '005B039F73C627CE8B', 'size': 0 }, }}, ] } event = app.S3Event(s3_event, FakeLambdaContext()) # We should urldecode the key name. assert event.key == u'\u2713' assert event.bucket == u'mybucket' # But the key should remain unchanged in to_dict(). assert event.to_dict() == s3_event def test_can_create_sns_handler(sample_app): @sample_app.on_sns_message(topic='MyTopic') def handler(event): pass assert len(sample_app.event_sources) == 1 event = sample_app.event_sources[0] assert event.name == 'handler' assert event.topic == 'MyTopic' assert event.handler_string == 'app.handler' def test_can_map_sns_event(sample_app): @sample_app.on_sns_message(topic='MyTopic') def handler(event): return event sns_event = {'Records': [{ 'EventSource': 'aws:sns', 'EventSubscriptionArn': 'arn:subscription-arn', 'EventVersion': '1.0', 'Sns': { 'Message': 'This is a raw message', 'MessageAttributes': { 'AttributeKey': { 'Type': 'String', 'Value': 'AttributeValue' } }, 'MessageId': 'abcdefgh-51e4-5ae2-9964-b296c8d65d1a', 'Signature': 'signature', 'SignatureVersion': '1', 'SigningCertUrl': 'https://sns.us-west-2.amazonaws.com/cert.pen', 'Subject': 'ThisIsTheSubject', 'Timestamp': '2018-06-26T19:41:38.695Z', 'TopicArn': 'arn:aws:sns:us-west-2:12345:ConsoleTestTopic', 'Type': 'Notification', 'UnsubscribeUrl': 'https://unsubscribe-url/'}}]} lambda_context = FakeLambdaContext() actual_event = handler(sns_event, context=lambda_context) assert actual_event.message == 'This is a raw message' assert actual_event.subject == 'ThisIsTheSubject' assert actual_event.to_dict() == sns_event assert actual_event.context == lambda_context def test_can_create_sqs_handler(sample_app): @sample_app.on_sqs_message(queue='MyQueue', batch_size=200) def handler(event): pass assert len(sample_app.event_sources) == 1 event = sample_app.event_sources[0] assert event.queue == 'MyQueue' assert event.batch_size == 200 assert event.maximum_batching_window_in_seconds == 0 assert event.handler_string == 'app.handler' def test_can_set_sqs_handler_name(sample_app): @sample_app.on_sqs_message(queue='MyQueue', name='sqs_handler') def handler(event): pass assert len(sample_app.event_sources) == 1 event = sample_app.event_sources[0] assert event.name == 'sqs_handler' def test_can_set_sqs_handler_maximum_batching_window_in_seconds(sample_app): @sample_app.on_sqs_message( queue='MyQueue', maximum_batching_window_in_seconds=60) def handler(event): pass assert len(sample_app.event_sources) == 1 event = sample_app.event_sources[0] assert event.maximum_batching_window_in_seconds == 60 def test_can_map_sqs_event(sample_app): @sample_app.on_sqs_message(queue='queue-name') def handler(event): return event sqs_event = {'Records': [{ 'attributes': { 'ApproximateFirstReceiveTimestamp': '1530576251596', 'ApproximateReceiveCount': '1', 'SenderId': 'sender-id', 'SentTimestamp': '1530576251595' }, 'awsRegion': 'us-west-2', 'body': 'queue message body', 'eventSource': 'aws:sqs', 'eventSourceARN': 'arn:aws:sqs:us-west-2:12345:queue-name', 'md5OfBody': '754ac2f7a12df38320e0c5eafd060145', 'messageAttributes': {}, 'messageId': 'message-id', 'receiptHandle': 'receipt-handle' }]} lambda_context = FakeLambdaContext() actual_event = handler(sqs_event, context=lambda_context) records = list(actual_event) assert len(records) == 1 first_record = records[0] assert first_record.body == 'queue message body' assert first_record.receipt_handle == 'receipt-handle' assert first_record.to_dict() == sqs_event['Records'][0] assert actual_event.to_dict() == sqs_event assert actual_event.context == lambda_context def test_can_create_kinesis_handler(sample_app): @sample_app.on_kinesis_record(stream='MyStream', batch_size=1, starting_position='TRIM_HORIZON') def handler(event): pass assert len(sample_app.event_sources) == 1 config = sample_app.event_sources[0] assert config.stream == 'MyStream' assert config.batch_size == 1 assert config.starting_position == 'TRIM_HORIZON' assert config.maximum_batching_window_in_seconds == 0 def test_can_set_kinesis_handler_maximum_batching_window(sample_app): @sample_app.on_kinesis_record(stream='MyStream', batch_size=1, starting_position='TRIM_HORIZON', maximum_batching_window_in_seconds=60) def handler(event): pass assert len(sample_app.event_sources) == 1 config = sample_app.event_sources[0] assert config.maximum_batching_window_in_seconds == 60 def test_can_map_kinesis_event(sample_app): @sample_app.on_kinesis_record(stream='MyStream') def handler(event): return event kinesis_event = { "Records": [ { "kinesis": { "kinesisSchemaVersion": "1.0", "partitionKey": "1", "sequenceNumber": "12345", "data": "SGVsbG8sIHRoaXMgaXMgYSB0ZXN0Lg==", "approximateArrivalTimestamp": 1545084650.987 }, "eventSource": "aws:kinesis", "eventVersion": "1.0", "eventID": "shardId-000000000006:12345", "eventName": "aws:kinesis:record", "invokeIdentityArn": "arn:aws:iam::123:role/lambda-role", "awsRegion": "us-east-2", "eventSourceARN": ( "arn:aws:kinesis:us-east-2:123:stream/lambda-stream" ) }, { "kinesis": { "kinesisSchemaVersion": "1.0", "partitionKey": "1", "sequenceNumber": "12346", "data": "VGhpcyBpcyBvbmx5IGEgdGVzdC4=", "approximateArrivalTimestamp": 1545084711.166 }, "eventSource": "aws:kinesis", "eventVersion": "1.0", "eventID": "shardId-000000000006:12346", "eventName": "aws:kinesis:record", "invokeIdentityArn": "arn:aws:iam::123:role/lambda-role", "awsRegion": "us-east-2", "eventSourceARN": ( "arn:aws:kinesis:us-east-2:123:stream/lambda-stream" ) } ] } lambda_context = FakeLambdaContext() actual_event = handler(kinesis_event, context=lambda_context) records = list(actual_event) assert len(records) == 2 assert records[0].data == b'Hello, this is a test.' assert records[0].sequence_number == "12345" assert records[0].partition_key == "1" assert records[0].schema_version == "1.0" assert records[0].timestamp == datetime(2018, 12, 17, 22, 10, 50, 987000) assert records[1].data == b'This is only a test.' def test_can_create_ddb_handler(sample_app): @sample_app.on_dynamodb_record( stream_arn='arn:aws:dynamodb:...:stream', batch_size=10, starting_position='TRIM_HORIZON') def handler(event): pass assert len(sample_app.event_sources) == 1 config = sample_app.event_sources[0] assert config.stream_arn == 'arn:aws:dynamodb:...:stream' assert config.batch_size == 10 assert config.starting_position == 'TRIM_HORIZON' assert config.maximum_batching_window_in_seconds == 0 def test_can_set_ddb_handler_maximum_batching_window_in_seconds(sample_app): @sample_app.on_dynamodb_record( stream_arn='arn:aws:dynamodb:...:stream', batch_size=10, starting_position='TRIM_HORIZON', maximum_batching_window_in_seconds=60) def handler(event): pass assert len(sample_app.event_sources) == 1 config = sample_app.event_sources[0] assert config.maximum_batching_window_in_seconds == 60 def test_can_map_ddb_event(sample_app): @sample_app.on_dynamodb_record(stream_arn='arn:aws:...:stream') def handler(event): return event ddb_event = { 'Records': [ {'awsRegion': 'us-west-2', 'dynamodb': {'ApproximateCreationDateTime': 1601317140.0, 'Keys': {'PK': {'S': 'foo'}, 'SK': {'S': 'bar'}}, 'NewImage': {'PK': {'S': 'foo'}, 'SK': {'S': 'bar'}}, 'SequenceNumber': '1700000000020701978607', 'SizeBytes': 20, 'StreamViewType': 'NEW_AND_OLD_IMAGES'}, 'eventID': 'da037887f71a88a1f6f4cfd149709d5a', 'eventName': 'INSERT', 'eventSource': 'aws:dynamodb', 'eventSourceARN': ( 'arn:aws:dynamodb:us-west-2:12345:table/MyTable/stream/' '2020-09-28T16:49:14.209' ), 'eventVersion': '1.1'} ] } lambda_context = FakeLambdaContext() actual_event = handler(ddb_event, context=lambda_context) records = list(actual_event) assert len(records) == 1 assert records[0].timestamp == datetime(2020, 9, 28, 18, 19) assert records[0].keys == {'PK': {'S': 'foo'}, 'SK': {'S': 'bar'}} assert records[0].new_image == {'PK': {'S': 'foo'}, 'SK': {'S': 'bar'}} assert records[0].old_image is None assert records[0].sequence_number == '1700000000020701978607' assert records[0].size_bytes == 20 assert records[0].stream_view_type == 'NEW_AND_OLD_IMAGES' # Mapping from top level keys in a record. assert records[0].aws_region == 'us-west-2' assert records[0].event_id == 'da037887f71a88a1f6f4cfd149709d5a' assert records[0].event_name == 'INSERT' assert records[0].event_source_arn == ( 'arn:aws:dynamodb:us-west-2:12345:table/MyTable/stream/' '2020-09-28T16:49:14.209') # Computed value. assert records[0].table_name == 'MyTable' def test_bytes_when_binary_type_is_application_json(): demo = app.Chalice('demo-app') demo.api.binary_types.append('application/json') @demo.route('/compress_response') def index(): blob = json.dumps({'hello': 'world'}).encode('utf-8') payload = gzip.compress(blob) custom_headers = { 'Content-Type': 'application/json', 'Content-Encoding': 'gzip' } return Response(body=payload, status_code=200, headers=custom_headers) return demo def test_can_register_blueprint_on_app(): myapp = app.Chalice('myapp') foo = app.Blueprint('foo') @foo.route('/foo') def first(): pass myapp.register_blueprint(foo) assert sorted(list(myapp.routes.keys())) == ['/foo'] def test_can_combine_multiple_blueprints_in_single_app(): myapp = app.Chalice('myapp') foo = app.Blueprint('foo') bar = app.Blueprint('bar') @foo.route('/foo') def myfoo(): pass @bar.route('/bar') def mybar(): pass myapp.register_blueprint(foo) myapp.register_blueprint(bar) assert sorted(list(myapp.routes)) == ['/bar', '/foo'] def test_can_preserve_signature_on_blueprint(): myapp = app.Chalice('myapp') foo = app.Blueprint('foo') @foo.lambda_function() def first(event, context): return {'foo': 'bar'} myapp.register_blueprint(foo) # The handler string given to a blueprint # is the "module.function_name" so we have # to ensure we can continue to invoke the # function with its expected signature. assert first({}, None) == {'foo': 'bar'} def test_doc_saved_on_route(): myapp = app.Chalice('myapp') @myapp.route('/') def index(): """My index docstring.""" pass assert index.__doc__ == 'My index docstring.' def test_blueprint_docstring_is_preserved(): foo = app.Blueprint('foo') @foo.route('/foo') def first(): """Blueprint docstring.""" assert first.__doc__ == 'Blueprint docstring.' def test_can_mount_apis_at_url_prefix(): myapp = app.Chalice('myapp') foo = app.Blueprint('foo') @foo.route('/foo') def myfoo(): pass @foo.route('/bar') def mybar(): pass myapp.register_blueprint(foo, url_prefix='/myprefix') assert list(sorted(myapp.routes)) == ['/myprefix/bar', '/myprefix/foo'] def test_can_mount_root_url_in_blueprint(): myapp = app.Chalice('myapp') foo = app.Blueprint('foo') root = app.Blueprint('root') @root.route('/') def myroot(): pass @foo.route('/') def myfoo(): pass @foo.route('/bar') def mybar(): pass myapp.register_blueprint(foo, url_prefix='/foo') myapp.register_blueprint(root) assert list(sorted(myapp.routes)) == ['/', '/foo', '/foo/bar'] def test_can_combine_lambda_functions_and_routes_in_blueprints(): myapp = app.Chalice('myapp') foo = app.Blueprint('app.chalicelib.blueprints.foo') @foo.route('/foo') def myfoo(): pass @foo.lambda_function() def myfunction(event, context): pass myapp.register_blueprint(foo) assert len(myapp.pure_lambda_functions) == 1 lambda_function = myapp.pure_lambda_functions[0] assert lambda_function.name == 'myfunction' assert lambda_function.handler_string == ( 'app.chalicelib.blueprints.foo.myfunction') assert list(myapp.routes) == ['/foo'] def test_can_mount_lambda_functions_with_name_prefix(): myapp = app.Chalice('myapp') foo = app.Blueprint('app.chalicelib.blueprints.foo') @foo.lambda_function() def myfunction(event, context): return event myapp.register_blueprint(foo, name_prefix='myprefix_') assert len(myapp.pure_lambda_functions) == 1 lambda_function = myapp.pure_lambda_functions[0] assert lambda_function.name == 'myprefix_myfunction' assert lambda_function.handler_string == ( 'app.chalicelib.blueprints.foo.myfunction') with Client(myapp) as c: response = c.lambda_.invoke( 'myprefix_myfunction', {'foo': 'bar'} ) assert response.payload == {'foo': 'bar'} def test_can_mount_event_sources_with_blueprint(): myapp = app.Chalice('myapp') foo = app.Blueprint('app.chalicelib.blueprints.foo') @foo.schedule('rate(5 minutes)') def myfunction(event): return event myapp.register_blueprint(foo, name_prefix='myprefix_') assert len(myapp.event_sources) == 1 event_source = myapp.event_sources[0] assert event_source.name == 'myprefix_myfunction' assert event_source.schedule_expression == 'rate(5 minutes)' assert event_source.handler_string == ( 'app.chalicelib.blueprints.foo.myfunction') def test_can_mount_all_decorators_in_blueprint(): myapp = app.Chalice('myapp') foo = app.Blueprint('app.chalicelib.blueprints.foo') @foo.route('/foo') def routefoo(): pass @foo.lambda_function(name='mylambdafunction') def mylambda(event, context): pass @foo.schedule('rate(5 minutes)') def bar(event): pass @foo.on_s3_event('MyBucket') def on_s3(event): pass @foo.on_sns_message('MyTopic') def on_sns(event): pass @foo.on_sqs_message('MyQueue') def on_sqs(event): pass myapp.register_blueprint(foo, name_prefix='myprefix_', url_prefix='/bar') event_sources = myapp.event_sources assert len(event_sources) == 4 lambda_functions = myapp.pure_lambda_functions assert len(lambda_functions) == 1 # Handles the name prefix and the name='' override in the decorator. assert lambda_functions[0].name == 'myprefix_mylambdafunction' assert list(myapp.routes) == ['/bar/foo'] def test_can_call_current_request_on_blueprint_when_mounted(create_event): myapp = app.Chalice('myapp') bp = app.Blueprint('app.chalicelib.blueprints.foo') @bp.route('/todict') def todict(): return bp.current_request.to_dict() myapp.register_blueprint(bp) event = create_event('/todict', 'GET', {}) response = json_response_body(myapp(event, context=None)) assert isinstance(response, dict) assert response['method'] == 'GET' def test_can_call_current_app_on_blueprint_when_mounted(create_event): myapp = app.Chalice('myapp') bp = app.Blueprint('app.chalicelib.blueprints.foo') @bp.route('/appname') def appname(): return {'name': bp.current_app.app_name} myapp.register_blueprint(bp) event = create_event('/appname', 'GET', {}) response = json_response_body(myapp(event, context=None)) assert response == {'name': 'myapp'} def test_can_call_lambda_context_on_blueprint_when_mounted(create_event): myapp = app.Chalice('myapp') bp = app.Blueprint('app.chalicelib.blueprints.foo') @bp.route('/context') def context(): return bp.lambda_context myapp.register_blueprint(bp) event = create_event('/context', 'GET', {}) response = json_response_body(myapp(event, context={'context': 'foo'})) assert response == {'context': 'foo'} def test_can_access_log_when_mounted(create_event): myapp = app.Chalice('myapp') bp = app.Blueprint('app.chalicelib.blueprints.foo') @bp.route('/log') def log_message(): # We shouldn't get an error because we've registered it to # an app. bp.log.info("test log message") return {} myapp.register_blueprint(bp) event = create_event('/log', 'GET', {}) response = json_response_body(myapp(event, context={'context': 'foo'})) assert response == {} def test_can_add_authorizer_with_url_prefix_and_routes(): myapp = app.Chalice('myapp') foo = app.Blueprint('app.chalicelib.blueprints.foo') @foo.authorizer() def myauth(event): pass @foo.route('/foo', authorizer=myauth) def routefoo(): pass myapp.register_blueprint(foo, url_prefix='/bar') assert len(myapp.builtin_auth_handlers) == 1 authorizer = myapp.builtin_auth_handlers[0] assert isinstance(authorizer, app.BuiltinAuthConfig) assert authorizer.name == 'myauth' assert authorizer.handler_string == 'app.chalicelib.blueprints.foo.myauth' def test_runtime_error_if_current_request_access_on_non_registered_blueprint(): bp = app.Blueprint('app.chalicelib.blueprints.foo') with pytest.raises(RuntimeError): bp.current_request def test_every_decorator_added_to_blueprint(): def is_public_method(obj): return inspect.isfunction(obj) and not obj.__name__.startswith('_') public_api = inspect.getmembers( app.DecoratorAPI, predicate=is_public_method ) blueprint_api = [ i[0] for i in inspect.getmembers(app.Blueprint, predicate=is_public_method) ] for method_name, _ in public_api: assert method_name in blueprint_api @pytest.mark.parametrize('input_dict', [ {}, {'key': []} ]) def test_multidict_raises_keyerror(input_dict): d = MultiDict(input_dict) with pytest.raises(KeyError): val = d['key'] assert val is val def test_multidict_pop_raises_del_error(): d = MultiDict({}) with pytest.raises(KeyError): del d['key'] def test_multidict_getlist_does_raise_keyerror(): d = MultiDict({}) with pytest.raises(KeyError): d.getlist('key') @pytest.mark.parametrize('input_dict', [ {'key': ['value']}, {'key': ['']}, {'key': ['value1', 'value2', 'value3']}, {'key': ['value1', 'value2', None]} ]) def test_multidict_returns_lastvalue(input_dict): d = MultiDict(input_dict) assert d['key'] == input_dict['key'][-1] @pytest.mark.parametrize('input_dict', [ {'key': ['value']}, {'key': ['']}, {'key': ['value1', 'value2', 'value3']}, {'key': ['value1', 'value2', None]} ]) def test_multidict_returns_all_values(input_dict): d = MultiDict(input_dict) assert d.getlist('key') == input_dict['key'] @pytest.mark.parametrize('input_dict', [ {'key': ['value']}, {'key': ['']}, {'key': ['value1', 'value2', 'value3']}, {'key': ['value1', 'value2', None]} ]) def test_multidict_list_wont_change_source(input_dict): d = MultiDict(input_dict) dict_copy = deepcopy(input_dict) d.getlist('key')[0] = 'othervalue' assert d.getlist('key') == dict_copy['key'] @pytest.mark.parametrize('input_dict,key,popped,leftover', [ ( {'key': ['value'], 'key2': [[]]}, 'key', 'value', {'key2': []}, ), ( {'key': [''], 'key2': [[]]}, 'key', '', {'key2': []}, ), ( {'key': ['value1', 'value2', 'value3'], 'key2': [[]]}, 'key', 'value3', {'key2': []}, ), ]) def test_multidict_list_can_pop_value(input_dict, key, popped, leftover): d = MultiDict(input_dict) pop_result = d.pop(key) assert popped == pop_result assert leftover == {key: d[key] for key in d} def test_multidict_assignment(): d = MultiDict({}) d['key'] = 'value' assert d['key'] == 'value' def test_multidict_get_reassigned_value(): d = MultiDict({}) d['key'] = 'value' assert d['key'] == 'value' assert d.get('key') == 'value' assert d.getlist('key') == ['value'] def test_multidict_get_list_wraps_key(): d = MultiDict({}) d['key'] = ['value'] assert d.getlist('key') == [['value']] def test_multidict_repr(): d = MultiDict({ 'foo': ['bar', 'baz'], 'buz': ['qux'], }) rep = repr(d) assert rep.startswith('MultiDict({') assert "'foo': ['bar', 'baz']" in rep assert "'buz': ['qux']" in rep def test_multidict_str(): d = MultiDict({ 'foo': ['bar', 'baz'], 'buz': ['qux'], }) rep = str(d) assert rep.startswith('MultiDict({') assert "'foo': ['bar', 'baz']" in rep assert "'buz': ['qux']" in rep def test_can_configure_websockets(sample_websocket_app): demo, _ = sample_websocket_app assert len(demo.websocket_handlers) == 3, demo.websocket_handlers assert '$connect' in demo.websocket_handlers, demo.websocket_handlers assert '$disconnect' in demo.websocket_handlers, demo.websocket_handlers assert '$default' in demo.websocket_handlers, demo.websocket_handlers def test_websocket_event_json_body_available(sample_websocket_app, create_websocket_event): demo = app.Chalice('demo-app') called = {'wascalled': False} @demo.on_ws_message() def message(event): called['wascalled'] = True assert event.json_body == {'foo': 'bar'} # Second access hits the cache. Test that that works as well. assert event.json_body == {'foo': 'bar'} event = create_websocket_event('$default', body='{"foo": "bar"}') handler = websocket_handler_for_route('$default', demo) handler(event, context=None) assert called['wascalled'] is True def test_websocket_event_json_body_can_raise_error(sample_websocket_app, create_websocket_event): demo = app.Chalice('demo-app') called = {'wascalled': False} @demo.on_ws_message() def message(event): called['wascalled'] = True with pytest.raises(BadRequestError): event.json_body event = create_websocket_event('$default', body='{"foo": "bar"') handler = websocket_handler_for_route('$default', demo) handler(event, context=None) assert called['wascalled'] is True def test_can_route_websocket_connect_message(sample_websocket_app, create_websocket_event): demo, calls = sample_websocket_app client = FakeClient() demo.websocket_api.session = FakeSession(client) event = create_websocket_event('$connect') handler = websocket_handler_for_route('$connect', demo) response = handler(event, context=None) assert response == {'statusCode': 200} assert len(calls) == 1 assert calls[0][0] == 'connect' event = calls[0][1] assert isinstance(event, WebsocketEvent) assert event.domain_name == 'abcd1234.execute-api.us-west-2.amazonaws.com' assert event.stage == 'api' assert event.connection_id == 'ABCD1234=' def test_can_route_websocket_connect_response_dict(create_websocket_event): demo = app.Chalice('app-name') client = FakeClient() demo.websocket_api.session = FakeSession(client) @demo.on_ws_connect() def connect(event): return dict( headers={"Sec-WebSocket-Protocol": "Test-Protocol"}, statusCode=200, body="Connected.", ) event = create_websocket_event('$connect') handler = websocket_handler_for_route('$connect', demo) response = handler(event, context=None) assert response == { 'headers': {'Sec-WebSocket-Protocol': 'Test-Protocol'}, 'statusCode': 200, 'body': 'Connected.' } def test_can_route_websocket_connect_response_obj(create_websocket_event): demo = app.Chalice('app-name') client = FakeClient() demo.websocket_api.session = FakeSession(client) @demo.on_ws_connect() def connect(event): return Response( "Connected.", status_code=200, headers={ "Sec-WebSocket-Protocol": "Test-Protocol", }, ) event = create_websocket_event('$connect') handler = websocket_handler_for_route('$connect', demo) response = handler(event, context=None) assert response == { 'headers': {'Sec-WebSocket-Protocol': 'Test-Protocol'}, 'multiValueHeaders': {}, 'statusCode': 200, 'body': 'Connected.', } def test_can_route_websocket_disconnect_message(sample_websocket_app, create_websocket_event): demo, calls = sample_websocket_app client = FakeClient() demo.websocket_api.session = FakeSession(client) event = create_websocket_event('$disconnect') handler = websocket_handler_for_route('$disconnect', demo) response = handler(event, context=None) assert response == {'statusCode': 200} assert len(calls) == 1 assert calls[0][0] == 'disconnect' event = calls[0][1] assert isinstance(event, WebsocketEvent) assert event.domain_name == 'abcd1234.execute-api.us-west-2.amazonaws.com' assert event.stage == 'api' assert event.connection_id == 'ABCD1234=' def test_can_route_websocket_default_message(sample_websocket_app, create_websocket_event): demo, calls = sample_websocket_app client = FakeClient() demo.websocket_api.session = FakeSession(client) event = create_websocket_event('$default', body='foo bar') handler = websocket_handler_for_route('$default', demo) response = handler(event, context=None) assert response == {'statusCode': 200} assert len(calls) == 1 assert calls[0][0] == 'default' event = calls[0][1] assert isinstance(event, WebsocketEvent) assert event.domain_name == 'abcd1234.execute-api.us-west-2.amazonaws.com' assert event.stage == 'api' assert event.connection_id == 'ABCD1234=' assert event.body == 'foo bar' def test_can_configure_client_on_connect(sample_websocket_app, create_websocket_event): demo, calls = sample_websocket_app client = FakeClient() demo.websocket_api.session = FakeSession(client) event = create_websocket_event('$connect') handler = websocket_handler_for_route('$connect', demo) handler(event, context=None) assert demo.websocket_api.session.calls == [ ('apigatewaymanagementapi', 'https://abcd1234.execute-api.us-west-2.amazonaws.com/api'), ] def test_can_configure_non_aws_partition_clients(sample_websocket_app, create_websocket_event, monkeypatch): # tests/conftest.py already monkeypatches out environment variables, # so if we want a test with a specific region we have to use the # same approach and monkeypatch the os.environ. monkeypatch.setenv('AWS_REGION', 'cn-north-1') demo, _ = sample_websocket_app client = FakeClient() demo.websocket_api.session = FakeSession(client) event = create_websocket_event( '$connect', endpoint='abcd1234.execute-api.cn-north-1.amazonaws.com.cn') handler = websocket_handler_for_route('$connect', demo) handler(event, context=None) assert demo.websocket_api.session.calls == [ ('apigatewaymanagementapi', 'https://abcd1234.execute-api.cn-north-1.amazonaws.com.cn/api'), ] def test_uses_api_id_not_domain_name(sample_websocket_app, create_websocket_event): demo, calls = sample_websocket_app client = FakeClient() demo.websocket_api.session = FakeSession(client) event = create_websocket_event('$connect') # If you configure a custom domain name, we should still use the # original domainName generated from API gateway when configuring # the apigatewaymanagementapi client. event['requestContext']['domainName'] = 'api.custom-domain-name.com' handler = websocket_handler_for_route('$connect', demo) handler(event, context=None) assert demo.websocket_api.session.calls == [ ('apigatewaymanagementapi', 'https://abcd1234.execute-api.us-west-2.amazonaws.com/api'), ] def test_fallsback_to_session_if_needed(sample_websocket_app, create_websocket_event): demo, calls = sample_websocket_app client = FakeClient() demo.websocket_api = WebsocketAPI(env={}) demo.websocket_api.session = FakeSession(client, region_name='us-east-2') event = create_websocket_event('$connect') # If you configure a custom domain name, we should still use the # original domainName generated from API gateway when configuring # the apigatewaymanagementapi client. event['requestContext']['domainName'] = 'api.custom-domain-name.com' handler = websocket_handler_for_route('$connect', demo) handler(event, context=None) assert demo.websocket_api.session.calls == [ ('apigatewaymanagementapi', 'https://abcd1234.execute-api.us-east-2.amazonaws.com/api'), ] def test_can_configure_client_on_disconnect(sample_websocket_app, create_websocket_event): demo, calls = sample_websocket_app client = FakeClient() demo.websocket_api.session = FakeSession(client) event = create_websocket_event('$disconnect') handler = websocket_handler_for_route('$disconnect', demo) handler(event, context=None) assert demo.websocket_api.session.calls == [ ('apigatewaymanagementapi', 'https://abcd1234.execute-api.us-west-2.amazonaws.com/api'), ] def test_can_configure_client_on_message(sample_websocket_app, create_websocket_event): demo, calls = sample_websocket_app client = FakeClient() demo.websocket_api.session = FakeSession(client) event = create_websocket_event('$default', body='foo bar') handler = websocket_handler_for_route('$default', demo) handler(event, context=None) assert demo.websocket_api.session.calls == [ ('apigatewaymanagementapi', 'https://abcd1234.execute-api.us-west-2.amazonaws.com/api'), ] def test_does_only_configure_client_once(sample_websocket_app, create_websocket_event): demo, calls = sample_websocket_app client = FakeClient() demo.websocket_api.session = FakeSession(client) event = create_websocket_event('$default', body='foo bar') handler = websocket_handler_for_route('$default', demo) handler(event, context=None) handler(event, context=None) assert demo.websocket_api.session.calls == [ ('apigatewaymanagementapi', 'https://abcd1234.execute-api.us-west-2.amazonaws.com/api'), ] def test_cannot_configure_client_without_session(sample_websocket_app, create_websocket_event): demo, calls = sample_websocket_app demo.websocket_api.session = None event = create_websocket_event('$default', body='foo bar') handler = websocket_handler_for_route('$default', demo) with pytest.raises(ValueError) as e: handler(event, context=None) assert str(e.value) == ( 'Assign app.websocket_api.session to a boto3 session before using ' 'the WebsocketAPI' ) def test_cannot_send_websocket_message_without_configure( sample_websocket_app, create_websocket_event): demo = app.Chalice('app-name') client = FakeClient() demo.websocket_api.session = FakeSession(client) @demo.on_ws_message() def message_handler(event): demo.websocket_api.send('connection_id', event.body) event = create_websocket_event('$default', body='foo bar') event_obj = WebsocketEvent(event, None) handler = demo.websocket_handlers['$default'].handler_function with pytest.raises(ValueError) as e: handler(event_obj) assert str(e.value) == ( 'WebsocketAPI.configure must be called before using the WebsocketAPI' ) def test_can_close_websocket_connection(create_websocket_event): demo = app.Chalice('app-name') client = FakeClient() demo.websocket_api.session = FakeSession(client) @demo.on_ws_message() def message_handler(event): demo.websocket_api.close('connection_id') event = create_websocket_event('$default', body='foo bar') handler = websocket_handler_for_route('$default', demo) handler(event, context=None) calls = client.calls['close'] assert len(calls) == 1 call = calls[0] connection_id = call[0] assert connection_id == 'connection_id' def test_close_does_fail_if_already_disconnected(create_websocket_event): demo = app.Chalice('app-name') client = FakeClient(errors=[FakeGoneException]) demo.websocket_api.session = FakeSession(client) @demo.on_ws_message() def message_handler(event): with pytest.raises(WebsocketDisconnectedError) as e: demo.websocket_api.close('connection_id') assert e.value.connection_id == 'connection_id' event = create_websocket_event('$default', body='foo bar') handler = websocket_handler_for_route('$default', demo) handler(event, context=None) calls = client.calls['close'] assert len(calls) == 1 call = calls[0] connection_id = call[0] assert connection_id == 'connection_id' def test_info_does_fail_if_already_disconnected(create_websocket_event): demo = app.Chalice('app-name') client = FakeClient(errors=[FakeGoneException]) demo.websocket_api.session = FakeSession(client) @demo.on_ws_message() def message_handler(event): with pytest.raises(WebsocketDisconnectedError) as e: demo.websocket_api.info('connection_id') assert e.value.connection_id == 'connection_id' event = create_websocket_event('$default', body='foo bar') handler = websocket_handler_for_route('$default', demo) handler(event, context=None) calls = client.calls['info'] assert len(calls) == 1 call = calls[0] connection_id = call[0] assert connection_id == 'connection_id' def test_can__about_websocket_connection(create_websocket_event): demo = app.Chalice('app-name') client = FakeClient(infos=[{'foo': 'bar'}]) demo.websocket_api.session = FakeSession(client) closure = {} @demo.on_ws_message() def message_handler(event): closure['info'] = demo.websocket_api.info('connection_id') event = create_websocket_event('$default', body='foo bar') handler = websocket_handler_for_route('$default', demo) handler(event, context=None) assert closure['info'] == {'foo': 'bar'} calls = client.calls['info'] assert len(calls) == 1 call = calls[0] connection_id = call[0] assert connection_id == 'connection_id' def test_can_send_websocket_message(create_websocket_event): demo = app.Chalice('app-name') client = FakeClient() demo.websocket_api.session = FakeSession(client) @demo.on_ws_message() def message_handler(event): demo.websocket_api.send('connection_id', event.body) event = create_websocket_event('$default', body='foo bar') handler = websocket_handler_for_route('$default', demo) handler(event, context=None) calls = client.calls['post_to_connection'] assert len(calls) == 1 call = calls[0] connection_id, message = call assert connection_id == 'connection_id' assert message == 'foo bar' def test_does_raise_on_send_to_bad_websocket(create_websocket_event): demo = app.Chalice('app-name') client = FakeClient(errors=[FakeGoneException]) demo.websocket_api.session = FakeSession(client) @demo.on_ws_message() def message_handler(event): with pytest.raises(WebsocketDisconnectedError) as e: demo.websocket_api.send('connection_id', event.body) assert e.value.connection_id == 'connection_id' event = create_websocket_event('$default', body='foo bar') handler = websocket_handler_for_route('$default', demo) handler(event, context=None) def test_does_reraise_on_websocket_send_error(create_websocket_event): class SomeOtherError(Exception): pass demo = app.Chalice('app-name') fake_418_error = SomeOtherError() fake_418_error.response = {'ResponseMetadata': {'HTTPStatusCode': 418}} client = FakeClient(errors=[fake_418_error]) demo.websocket_api.session = FakeSession(client) @demo.on_ws_message() def message_handler(event): with pytest.raises(SomeOtherError): demo.websocket_api.send('connection_id', event.body) event = create_websocket_event('$default', body='foo bar') handler = websocket_handler_for_route('$default', demo) handler(event, context=None) def test_does_reraise_on_other_send_exception(create_websocket_event): demo = app.Chalice('app-name') fake_500_error = Exception() fake_500_error.response = {'ResponseMetadata': {'HTTPStatusCode': 500}} fake_500_error.key = 'foo' client = FakeClient(errors=[fake_500_error]) demo.websocket_api.session = FakeSession(client) @demo.on_ws_message() def message_handler(event): with pytest.raises(Exception) as e: demo.websocket_api.send('connection_id', event.body) assert e.value.key == 'foo' event = create_websocket_event('$default', body='foo bar') demo(event, context=None) def test_cannot_send_message_on_unconfigured_app(): demo = app.Chalice('app-name') demo.websocket_api.session = None with pytest.raises(ValueError) as e: demo.websocket_api.send('connection_id', 'body') assert str(e.value) == ( 'Assign app.websocket_api.session to a boto3 session before ' 'using the WebsocketAPI' ) def test_cannot_re_register_websocket_handlers(create_websocket_event): demo = app.Chalice('app-name') @demo.on_ws_message() def message_handler(event): pass with pytest.raises(ValueError) as e: @demo.on_ws_message() def message_handler_2(event): pass assert str(e.value) == ( "Duplicate websocket handler: 'on_ws_message'. There can only be one " "handler for each websocket decorator." ) @demo.on_ws_connect() def connect_handler(event): pass with pytest.raises(ValueError) as e: @demo.on_ws_connect() def conncet_handler_2(event): pass assert str(e.value) == ( "Duplicate websocket handler: 'on_ws_connect'. There can only be one " "handler for each websocket decorator." ) @demo.on_ws_disconnect() def disconnect_handler(event): pass with pytest.raises(ValueError) as e: @demo.on_ws_disconnect() def disconncet_handler_2(event): pass assert str(e.value) == ( "Duplicate websocket handler: 'on_ws_disconnect'. There can only be " "one handler for each websocket decorator." ) def test_can_parse_json_websocket_body(create_websocket_event): demo = app.Chalice('app-name') client = FakeClient() demo.websocket_api.session = FakeSession(client) @demo.on_ws_message() def message(event): assert event.json_body == {'foo': 'bar'} event = create_websocket_event('$default', body='{"foo": "bar"}') demo(event, context=None) def test_can_access_websocket_json_body_twice(create_websocket_event): demo = app.Chalice('app-name') client = FakeClient() demo.websocket_api.session = FakeSession(client) @demo.on_ws_message() def message(event): assert event.json_body == {'foo': 'bar'} assert event.json_body == {'foo': 'bar'} event = create_websocket_event('$default', body='{"foo": "bar"}') demo(event, context=None) def test_does_raise_on_invalid_json_wbsocket_body(create_websocket_event): demo = app.Chalice('app-name') client = FakeClient() demo.websocket_api.session = FakeSession(client) @demo.on_ws_message() def message(event): with pytest.raises(BadRequestError) as e: event.json_body assert 'Error Parsing JSON' in str(e.value) event = create_websocket_event('$default', body='foo bar') demo(event, context=None) class TestMiddleware: def test_middleware_basic_api(self): demo = app.Chalice('app-name') called = [] @demo.middleware('all') def myhandler(event, get_response): called.append({'name': 'myhandler', 'bucket': event.bucket}) return get_response(event) @demo.middleware('all') def myhandler2(event, get_response): called.append({'name': 'myhandler2', 'bucket': event.bucket}) return get_response(event) @demo.on_s3_event('mybucket') def handler(event): called.append({'name': 'main', 'bucket': event.bucket}) return {'bucket': event.bucket} with Client(demo) as c: response = c.lambda_.invoke( 'handler', c.events.generate_s3_event('mybucket', 'key') ) assert response.payload == {'bucket': 'mybucket'} assert called == [ {'name': 'myhandler', 'bucket': 'mybucket'}, {'name': 'myhandler2', 'bucket': 'mybucket'}, {'name': 'main', 'bucket': 'mybucket'}, ] def test_can_access_original_event_and_context_in_http(self): demo = app.Chalice('app-name') called = [] @demo.middleware('http') def myhandler(event, get_response): called.append({'event': event}) return get_response(event) @demo.route('/') def index(): return {'hello': 'world'} with Client(demo) as c: response = c.http.get('/') assert response.json_body == {'hello': 'world'} actual_event = called[0]['event'] assert actual_event.path == '/' assert actual_event.lambda_context.function_name == 'api_handler' assert actual_event.to_original_event()[ 'requestContext']['resourcePath'] == '/' def test_can_short_circuit_response(self): demo = app.Chalice('app-name') called = [] @demo.middleware('all') def myhandler(event, get_response): called.append({'name': 'myhandler', 'bucket': event.bucket}) return {'short-circuit': True} @demo.middleware('all') def myhandler2(event, get_response): called.append({'name': 'myhandler2', 'bucket': event.bucket}) return get_response(event) @demo.on_s3_event('mybucket') def handler(event): called.append({'name': 'main', 'bucket': event.bucket}) return {'bucket': event.bucket} with Client(demo) as c: response = c.lambda_.invoke( 'handler', c.events.generate_s3_event('mybucket', 'key') ) assert response.payload == {'short-circuit': True} assert called == [ {'name': 'myhandler', 'bucket': 'mybucket'}, ] def test_can_alter_response(self): demo = app.Chalice('app-name') called = [] @demo.middleware('all') def myhandler(event, get_response): called.append({'name': 'myhandler', 'bucket': event.bucket}) response = get_response(event) response['myhandler'] = True return response @demo.middleware('all') def myhandler2(event, get_response): called.append({'name': 'myhandler2', 'bucket': event.bucket}) response = get_response(event) response['myhandler2'] = True return response @demo.on_s3_event('mybucket') def handler(event): called.append({'name': 'main', 'bucket': event.bucket}) return {'bucket': event.bucket} with Client(demo) as c: response = c.lambda_.invoke( 'handler', c.events.generate_s3_event('mybucket', 'key') ) assert response.payload == { 'bucket': 'mybucket', 'myhandler': True, 'myhandler2': True, } assert called == [ {'name': 'myhandler', 'bucket': 'mybucket'}, {'name': 'myhandler2', 'bucket': 'mybucket'}, {'name': 'main', 'bucket': 'mybucket'}, ] def test_can_change_order_of_definitions(self): demo = app.Chalice('app-name') called = [] @demo.on_s3_event('mybucket') def handler(event): called.append({'name': 'main', 'bucket': event.bucket}) return {'bucket': event.bucket} @demo.middleware('all') def myhandler(event, get_response): called.append({'name': 'myhandler', 'bucket': event.bucket}) response = get_response(event) response['myhandler'] = True return response @demo.middleware('all') def myhandler2(event, get_response): called.append({'name': 'myhandler2', 'bucket': event.bucket}) response = get_response(event) response['myhandler2'] = True return response with Client(demo) as c: response = c.lambda_.invoke( 'handler', c.events.generate_s3_event('mybucket', 'key') ) assert response.payload == { 'bucket': 'mybucket', 'myhandler': True, 'myhandler2': True, } assert called == [ {'name': 'myhandler', 'bucket': 'mybucket'}, {'name': 'myhandler2', 'bucket': 'mybucket'}, {'name': 'main', 'bucket': 'mybucket'}, ] def test_can_use_middleware_for_pure_lambda(self): demo = app.Chalice('app-name') called = [] @demo.middleware('all') def mymiddleware(event, get_response): called.append({'name': 'mymiddleware', 'event': event.to_dict()}) return get_response(event) @demo.lambda_function() def myfunction(event, context): called.append({'name': 'myfunction', 'event': event}) return {'foo': 'bar'} with Client(demo) as c: response = c.lambda_.invoke( 'myfunction', {'input-event': True} ) assert response.payload == {'foo': 'bar'} assert called == [ {'name': 'mymiddleware', 'event': {'input-event': True}}, {'name': 'myfunction', 'event': {'input-event': True}}, ] def test_can_use_for_websocket_handlers(self): demo = app.Chalice('app-name') called = [] @demo.middleware('all') def mymiddleware(event, get_response): called.append({'name': 'mymiddleware', 'event': event.to_dict()}) return get_response(event) @demo.on_ws_message() def myfunction(event): called.append({'name': 'myfunction', 'event': event.to_dict()}) return {'foo': 'bar'} with Client(demo) as c: event = { 'requestContext': { 'domainName': 'example.com', 'stage': 'dev', 'connectionId': 'abcd', 'apiId': 'abcd1234', }, 'body': "body" } response = c.lambda_.invoke('myfunction', event) assert response.payload == {'foo': 'bar', 'statusCode': 200} assert called == [ {'name': 'mymiddleware', 'event': event}, {'name': 'myfunction', 'event': event}, ] def test_can_use_rest_api_for_middleware(self): demo = app.Chalice('app-name') called = [] @demo.middleware('all') def mymiddleware(event, get_response): called.append({'name': 'mymiddleware', 'method': event.method}) response = get_response(event) response.status_code = 201 return response @demo.route('/') def index(): called.append({'url': '/'}) return {'index': True} @demo.route('/hello') def hello(): called.append({'url': '/hello'}) return {'hello': True} with Client(demo) as c: assert c.http.get('/').json_body == {'index': True} response = c.http.get('/hello') assert response.json_body == {'hello': True} # Verify middleware can alter the response. assert response.status_code == 201 assert called == [ {'name': 'mymiddleware', 'method': 'GET'}, {'url': '/'}, {'name': 'mymiddleware', 'method': 'GET'}, {'url': '/hello'}, ] def test_error_handler_rest_api_untouched(self): demo = app.Chalice('app-name') @demo.middleware('all') def mymiddleware(event, get_response): return get_response(event) @demo.route('/error') def index(): raise NotFoundError("resource not found") with Client(demo) as c: response = c.http.get('/error') assert response.status_code == 404 assert response.json_body == { 'Code': 'NotFoundError', 'Message': 'resource not found' } def test_unhandled_error_not_caught(self): demo = app.Chalice('app-name') @demo.middleware('all') def mymiddleware(event, get_response): try: return get_response(event) except ChaliceUnhandledError: return Response(body={'foo': 'bar'}, status_code=200) @demo.route('/error') def index(): raise ChaliceUnhandledError("unhandled") with Client(demo) as c: response = c.http.get('/error') assert response.status_code == 200 assert response.json_body == {'foo': 'bar'} def test_middleware_errors_return_500_still_caught(self): demo = app.Chalice('app-name') @demo.middleware('all') def mymiddleware(event, get_response): return get_response(event) @demo.route('/error') def index(): raise ChaliceUnhandledError("unhandled") with Client(demo) as c: # An uncaught ChaliceUnhandledError should still result # in the standard error handler processing for REST APIs # if the exception propagates out of the middleware stack. response = c.http.get('/error') assert response.status_code == 500 assert response.json_body == { 'Code': 'InternalServerError', 'Message': 'An internal server error occurred.' } def test_middleware_errors_result_in_500(self): demo = app.Chalice('app-name') @demo.middleware('all') def mymiddleware(event, get_response): raise Exception("Error from middleware.") @demo.route('/') def index(): return {} with Client(demo) as c: response = c.http.get('/') assert response.status_code == 500 assert response.json_body['Code'] == 'InternalServerError' def test_can_filter_middleware_registration(self, sample_middleware_app): with Client(sample_middleware_app) as c: c.http.get('/') assert sample_middleware_app.calls == [ {'type': 'all', 'event': 'Request'}, {'type': 'http', 'event': 'Request'}, ] sample_middleware_app.calls[:] = [] c.lambda_.invoke( 's3_handler', c.events.generate_s3_event('bucket', 'key')) assert sample_middleware_app.calls == [ {'type': 'all', 'event': 'S3Event'}, {'type': 's3', 'event': 'S3Event'}, ] sample_middleware_app.calls[:] = [] c.lambda_.invoke( 'sns_handler', c.events.generate_sns_event('topic', 'message')) assert sample_middleware_app.calls == [ {'type': 'all', 'event': 'SNSEvent'}, {'type': 'sns', 'event': 'SNSEvent'}, ] sample_middleware_app.calls[:] = [] c.lambda_.invoke( 'sqs_handler', c.events.generate_sns_event('queue', 'message')) # There is no sqs specific middleware. assert sample_middleware_app.calls == [ {'type': 'all', 'event': 'SQSEvent'}, ] sample_middleware_app.calls[:] = [] c.lambda_.invoke('lambda_handler', {}) assert sample_middleware_app.calls == [ {'type': 'all', 'event': 'LambdaFunctionEvent'}, {'type': 'pure_lambda', 'event': 'LambdaFunctionEvent'}, ] sample_middleware_app.calls[:] = [] c.lambda_.invoke('ws_handler', { 'requestContext': { 'domainName': 'example.com', 'stage': 'dev', 'connectionId': 'abcd', 'apiId': 'abcd1234', }, 'body': "body" }) assert sample_middleware_app.calls == [ {'type': 'all', 'event': 'WebsocketEvent'}, {'type': 'websocket', 'event': 'WebsocketEvent'}, ] def test_can_register_middleware_on_blueprints(self): demo = app.Chalice('app-name') bp = app.Blueprint('bpmiddleware') called = [] @demo.middleware('all') def mymiddleware(event, get_response): called.append({'name': 'fromapp', 'bucket': event.bucket}) return get_response(event) @bp.middleware('all') def bp_middleware(event, get_response): called.append({'name': 'frombp', 'bucket': event.bucket}) return get_response(event) @bp.on_s3_event('mybucket') def bp_handler(event): called.append({'name': 'bp_handler', 'bucket': event.bucket}) return {'bucket': event.bucket} @bp.route('/') def index(): pass @demo.on_s3_event('mybucket') def handler(event): called.append({'name': 'main', 'bucket': event.bucket}) return {'bucket': event.bucket} demo.register_blueprint(bp) with Client(demo) as c: # The order is particular here. When we're invoking the lambda # function from the "app" (demo) object, we expect # the order to be mymiddleware, bp_middleware because mymiddleware # is registered before the .register_blueprint(). response = c.lambda_.invoke( 'handler', c.events.generate_s3_event('mybucket', 'key') ) assert response.payload == {'bucket': 'mybucket'} assert called == [ {'name': 'fromapp', 'bucket': 'mybucket'}, {'name': 'frombp', 'bucket': 'mybucket'}, {'name': 'main', 'bucket': 'mybucket'}, ] called[:] = [] response = c.lambda_.invoke( 'bp_handler', c.events.generate_s3_event('mybucket', 'key') ) assert response.payload == {'bucket': 'mybucket'} assert called == [ {'name': 'fromapp', 'bucket': 'mybucket'}, {'name': 'frombp', 'bucket': 'mybucket'}, {'name': 'bp_handler', 'bucket': 'mybucket'}, ] def test_blueprint_gets_middlware_added(self): demo = app.Chalice('app-name') bp = app.Blueprint('bpmiddleware') called = [] @bp.middleware('all') def bp_middleware(event, get_response): called.append({'name': 'frombp', 'bucket': 'mybucket'}) return get_response(event) @demo.on_s3_event('mybucket') def handler(event): called.append({'name': 'main', 'bucket': event.bucket}) return {'bucket': event.bucket} demo.register_blueprint(bp) with Client(demo) as c: response = c.lambda_.invoke( 'handler', c.events.generate_s3_event('mybucket', 'key') ) assert response.payload == {'bucket': 'mybucket'} assert called == [ {'name': 'frombp', 'bucket': 'mybucket'}, {'name': 'main', 'bucket': 'mybucket'}, ] def test_can_register_middleware_without_decorator(self): demo = app.Chalice('app-name') called = [] def mymiddleware(event, get_response): called.append({'name': 'mymiddleware', 'event': event.to_dict()}) return get_response(event) @demo.lambda_function() def myfunction(event, context): called.append({'name': 'myfunction', 'event': event}) return {'foo': 'bar'} demo.register_middleware(mymiddleware, 'all') with Client(demo) as c: response = c.lambda_.invoke( 'myfunction', {'input-event': True} ) assert response.payload == {'foo': 'bar'} assert called == [ {'name': 'mymiddleware', 'event': {'input-event': True}}, {'name': 'myfunction', 'event': {'input-event': True}}, ] def test_can_convert_existing_lambda_decorator_to_middleware(self): demo = app.Chalice('app-name') called = [] def mydecorator(func): def _wrapped(event, context): called.append({'name': 'wrapped', 'event': event}) return func(event, context) return _wrapped @demo.middleware('all') def second_middleware(event, get_response): called.append({'name': 'second', 'event': event.to_dict()}) return get_response(event) @demo.lambda_function() def myfunction(event, context): called.append({'name': 'myfunction', 'event': event}) return {'foo': 'bar'} demo.register_middleware(ConvertToMiddleware(mydecorator)) with Client(demo) as c: response = c.lambda_.invoke( 'myfunction', {'input-event': True} ) assert response.payload == {'foo': 'bar'} assert called == [ {'name': 'second', 'event': {'input-event': True}}, {'name': 'wrapped', 'event': {'input-event': True}}, {'name': 'myfunction', 'event': {'input-event': True}}, ] ================================================ FILE: tests/unit/test_awsclient.py ================================================ from collections import OrderedDict import pytest from chalice.awsclient import TypedAWSClient @pytest.mark.parametrize('service,region,endpoint', [ ('sns', 'us-east-1', OrderedDict([('partition', 'aws'), ('endpointName', 'us-east-1'), ('protocols', ['http', 'https']), ('hostname', 'sns.us-east-1.amazonaws.com'), ('signatureVersions', ['v4']), ('dnsSuffix', 'amazonaws.com')])), ('sqs', 'cn-north-1', OrderedDict([('partition', 'aws-cn'), ('endpointName', 'cn-north-1'), ('protocols', ['http', 'https']), ('sslCommonName', 'cn-north-1.queue.amazonaws.com.cn'), ('hostname', 'sqs.cn-north-1.amazonaws.com.cn'), ('signatureVersions', ['v4']), ('dnsSuffix', 'amazonaws.com.cn')])), ('dynamodb', 'mars-west-1', None) ]) def test_resolve_endpoint(stubbed_session, service, region, endpoint): awsclient = TypedAWSClient(stubbed_session) if endpoint is None: assert awsclient.resolve_endpoint(service, region) is None else: assert endpoint.items() <= awsclient.resolve_endpoint( service, region).items() @pytest.mark.parametrize('arn,endpoint', [ ('arn:aws:sns:us-east-1:123456:MyTopic', OrderedDict([('partition', 'aws'), ('endpointName', 'us-east-1'), ('protocols', ['http', 'https']), ('hostname', 'sns.us-east-1.amazonaws.com'), ('signatureVersions', ['v4']), ('dnsSuffix', 'amazonaws.com')])), ('arn:aws-cn:sqs:cn-north-1:444455556666:queue1', OrderedDict([('partition', 'aws-cn'), ('endpointName', 'cn-north-1'), ('protocols', ['http', 'https']), ('sslCommonName', 'cn-north-1.queue.amazonaws.com.cn'), ('hostname', 'sqs.cn-north-1.amazonaws.com.cn'), ('signatureVersions', ['v4']), ('dnsSuffix', 'amazonaws.com.cn')])), ('arn:aws:dynamodb:mars-west-1:123456:table/MyTable', None) ]) def test_endpoint_from_arn(stubbed_session, arn, endpoint): awsclient = TypedAWSClient(stubbed_session) if endpoint is None: assert awsclient.endpoint_from_arn(arn) is None else: assert endpoint.items() <= awsclient.endpoint_from_arn( arn).items() @pytest.mark.parametrize('service,region,dns_suffix', [ ('sns', 'us-east-1', 'amazonaws.com'), ('sns', 'cn-north-1', 'amazonaws.com.cn'), ('dynamodb', 'mars-west-1', 'amazonaws.com') ]) def test_endpoint_dns_suffix(stubbed_session, service, region, dns_suffix): awsclient = TypedAWSClient(stubbed_session) assert dns_suffix == awsclient.endpoint_dns_suffix(service, region) @pytest.mark.parametrize('arn,dns_suffix', [ ('arn:aws:sns:us-east-1:123456:MyTopic', 'amazonaws.com'), ('arn:aws-cn:sqs:cn-north-1:444455556666:queue1', 'amazonaws.com.cn'), ('arn:aws:dynamodb:mars-west-1:123456:table/MyTable', 'amazonaws.com') ]) def test_endpoint_dns_suffix_from_arn(stubbed_session, arn, dns_suffix): awsclient = TypedAWSClient(stubbed_session) assert dns_suffix == awsclient.endpoint_dns_suffix_from_arn(arn) class TestServicePrincipal(object): @pytest.fixture def region(self): return 'bermuda-triangle-42' @pytest.fixture def url_suffix(self): return '.nowhere.null' @pytest.fixture def non_iso_suffixes(self): return ['', '.amazonaws.com', '.amazonaws.com.cn'] @pytest.fixture def awsclient(self, stubbed_session): return TypedAWSClient(stubbed_session) def test_unmatched_service(self, awsclient): assert awsclient.service_principal('taco.magic.food.com', 'us-east-1', 'amazonaws.com') == \ 'taco.magic.food.com' def test_defaults(self, awsclient): assert awsclient.service_principal('lambda') == 'lambda.amazonaws.com' def test_states(self, awsclient, region, url_suffix, non_iso_suffixes): services = ['states'] for suffix in non_iso_suffixes: for service in services: assert awsclient.service_principal('{}{}'.format(service, suffix), region, url_suffix) == \ '{}.{}.amazonaws.com'.format(service, region) def test_codedeploy_and_logs(self, awsclient, region, url_suffix, non_iso_suffixes): services = ['codedeploy', 'logs'] for suffix in non_iso_suffixes: for service in services: assert awsclient.service_principal('{}{}'.format(service, suffix), region, url_suffix) == \ '{}.{}.{}'.format(service, region, url_suffix) def test_ec2(self, awsclient, region, url_suffix, non_iso_suffixes): services = ['ec2'] for suffix in non_iso_suffixes: for service in services: assert awsclient.service_principal('{}{}'.format(service, suffix), region, url_suffix) == \ '{}.{}'.format(service, url_suffix) def test_others(self, awsclient, region, url_suffix, non_iso_suffixes): services = ['autoscaling', 'lambda', 'events', 'sns', 'sqs', 'foo-service'] for suffix in non_iso_suffixes: for service in services: assert awsclient.service_principal('{}{}'.format(service, suffix), region, url_suffix) == \ '{}.amazonaws.com'.format(service) def test_local_suffix(self, awsclient, region, url_suffix): assert awsclient.service_principal('foo-service.local', region, url_suffix) == 'foo-service.local' def test_states_iso(self, awsclient): assert awsclient.service_principal('states.amazonaws.com', 'us-iso-east-1', 'c2s.ic.gov') == \ 'states.amazonaws.com' def test_states_isob(self, awsclient): assert awsclient.service_principal('states.amazonaws.com', 'us-isob-east-1', 'sc2s.sgov.gov') == \ 'states.amazonaws.com' def test_iso_exceptions(self, awsclient): services = ['cloudhsm', 'config', 'workspaces'] for service in services: assert awsclient.service_principal( '{}.amazonaws.com'.format(service), 'us-iso-east-1', 'c2s.ic.gov') == '{}.c2s.ic.gov'.format(service) ================================================ FILE: tests/unit/test_config.py ================================================ import os import sys import pytest from chalice import __version__ as chalice_version from chalice.config import Config from chalice.config import DeployedResources from chalice import Chalice class FixedDataConfig(Config): def __init__(self, files_to_content, app_name='app'): self.files_to_content = files_to_content self._app_name = app_name @property def app_name(self): return self._app_name @property def project_dir(self): return '.' def _load_json_file(self, filename): return self.files_to_content.get(filename) def test_config_create_method(): c = Config.create(app_name='foo') assert c.app_name == 'foo' # Otherwise attributes default to None meaning 'not set'. assert c.profile is None assert c.api_gateway_stage is None def test_default_chalice_stage(): c = Config() assert c.chalice_stage == 'dev' def test_version_defaults_to_1_when_missing(): c = Config() assert c.config_file_version == '1.0' def test_default_value_of_manage_iam_role(): c = Config.create() assert c.manage_iam_role def test_can_lazy_load_chalice_app(): app = Chalice(app_name='foo') calls = [] def call_recorder(*args, **kwargs): calls.append((args, kwargs)) return app c = Config.create(chalice_app=call_recorder) # Accessing the property multiple times will only # invoke the call once. assert isinstance(c.chalice_app, Chalice) assert isinstance(c.chalice_app, Chalice) assert len(calls) == 1 def test_lazy_load_chalice_app_must_be_callable(): c = Config.create(chalice_app='not a callable') with pytest.raises(TypeError): c.chalice_app def test_manage_iam_role_explicitly_set(): c = Config.create(manage_iam_role=False) assert not c.manage_iam_role c = Config.create(manage_iam_role=True) assert c.manage_iam_role def test_can_chain_lookup(): user_provided_params = { 'api_gateway_stage': 'user_provided_params', } config_from_disk = { 'api_gateway_stage': 'config_from_disk', 'app_name': 'config_from_disk', } default_params = { 'api_gateway_stage': 'default_params', 'app_name': 'default_params', 'project_dir': 'default_params', } c = Config(chalice_stage='dev', user_provided_params=user_provided_params, config_from_disk=config_from_disk, default_params=default_params) assert c.api_gateway_stage == 'user_provided_params' assert c.app_name == 'config_from_disk' assert c.project_dir == 'default_params' assert c.config_from_disk == config_from_disk def test_user_params_is_optional(): c = Config(config_from_disk={'api_gateway_stage': 'config_from_disk'}, default_params={'api_gateway_stage': 'default_params'}) assert c.api_gateway_stage == 'config_from_disk' def test_can_chain_chalice_stage_values(): disk_config = { 'api_gateway_stage': 'dev', 'stages': { 'dev': { }, 'prod': { 'api_gateway_stage': 'prod', 'iam_role_arn': 'foobar', 'manage_iam_role': False, } } } c = Config(chalice_stage='dev', config_from_disk=disk_config) assert c.api_gateway_stage == 'dev' assert c.manage_iam_role prod = Config(chalice_stage='prod', config_from_disk=disk_config) assert prod.api_gateway_stage == 'prod' assert prod.iam_role_arn == 'foobar' assert not prod.manage_iam_role def test_can_chain_function_values(): disk_config = { 'lambda_timeout': 10, 'lambda_functions': { 'api_handler': { 'lambda_timeout': 15, } }, 'stages': { 'dev': { 'lambda_timeout': 20, 'lambda_functions': { 'api_handler': { 'lambda_timeout': 30, } } } } } c = Config(chalice_stage='dev', config_from_disk=disk_config) assert c.lambda_timeout == 30 def test_can_set_stage_independent_function_values(): disk_config = { 'lambda_timeout': 10, 'lambda_functions': { 'api_handler': { 'lambda_timeout': 15, } } } c = Config(chalice_stage='dev', config_from_disk=disk_config) assert c.lambda_timeout == 15 def test_stage_overrides_function_values(): disk_config = { 'lambda_timeout': 10, 'lambda_functions': { 'api_handler': { 'lambda_timeout': 15, } }, 'stages': { 'dev': { 'lambda_timeout': 20, } } } c = Config(chalice_stage='dev', config_from_disk=disk_config) assert c.lambda_timeout == 20 def test_can_create_scope_obj_with_new_function(): disk_config = { 'lambda_timeout': 10, 'stages': { 'dev': { 'manage_iam_role': True, 'iam_role_arn': 'role-arn', 'autogen_policy': True, 'iam_policy_file': 'policy.json', 'environment_variables': {'env': 'stage'}, 'lambda_timeout': 1, 'lambda_memory_size': 1, 'tags': {'tag': 'stage'}, 'lambda_functions': { 'api_handler': { 'lambda_timeout': 30, }, 'myauth': { # We're purposefully using different # values for everything in the stage # level config to ensure we can pull # from function scoped config properly. 'manage_iam_role': True, 'iam_role_arn': 'auth-role-arn', 'autogen_policy': True, 'iam_policy_file': 'function.json', 'environment_variables': {'env': 'function'}, 'lambda_timeout': 2, 'lambda_memory_size': 2, 'tags': {'tag': 'function'}, } } } } } c = Config(chalice_stage='dev', config_from_disk=disk_config) new_config = c.scope(chalice_stage='dev', function_name='myauth') assert new_config.manage_iam_role assert new_config.iam_role_arn == 'auth-role-arn' assert new_config.autogen_policy assert new_config.iam_policy_file == 'function.json' assert new_config.environment_variables == {'env': 'function'} assert new_config.lambda_timeout == 2 assert new_config.lambda_memory_size == 2 assert new_config.tags['tag'] == 'function' @pytest.mark.parametrize('stage_name,function_name,expected', [ ('dev', 'api_handler', 'dev-api-handler'), ('dev', 'myauth', 'dev-myauth'), ('beta', 'api_handler', 'beta-api-handler'), ('beta', 'myauth', 'beta-myauth'), ('prod', 'api_handler', 'prod-stage'), ('prod', 'myauth', 'prod-stage'), ('foostage', 'api_handler', 'global'), ('foostage', 'myauth', 'global'), ]) def test_can_create_scope_new_stage_and_function(stage_name, function_name, expected): disk_config = { 'environment_variables': {'from': 'global'}, 'stages': { 'dev': { 'environment_variables': {'from': 'dev-stage'}, 'lambda_functions': { 'api_handler': { 'environment_variables': { 'from': 'dev-api-handler', } }, 'myauth': { 'environment_variables': { 'from': 'dev-myauth', } } } }, 'beta': { 'environment_variables': {'from': 'beta-stage'}, 'lambda_functions': { 'api_handler': { 'environment_variables': { 'from': 'beta-api-handler', } }, 'myauth': { 'environment_variables': { 'from': 'beta-myauth', } } } }, 'prod': { 'environment_variables': {'from': 'prod-stage'}, } } } c = Config(chalice_stage='dev', config_from_disk=disk_config) new_config = c.scope(chalice_stage=stage_name, function_name=function_name) assert new_config.environment_variables == {'from': expected} def test_new_scope_config_is_separate_copy(): original = Config(chalice_stage='dev', function_name='foo') new_config = original.scope(chalice_stage='prod', function_name='bar') # The original should not have been mutated. assert original.chalice_stage == 'dev' assert original.function_name == 'foo' assert new_config.chalice_stage == 'prod' assert new_config.function_name == 'bar' def test_environment_from_top_level(): config_from_disk = {'environment_variables': {"foo": "bar"}} c = Config('dev', config_from_disk=config_from_disk) assert c.environment_variables == config_from_disk['environment_variables'] def test_environment_from_stage_level(): config_from_disk = { 'stages': { 'prod': { 'environment_variables': {"foo": "bar"} } } } c = Config('prod', config_from_disk=config_from_disk) assert c.environment_variables == ( config_from_disk['stages']['prod']['environment_variables']) def test_env_vars_chain_merge(): config_from_disk = { 'environment_variables': { 'top_level': 'foo', 'shared_stage_key': 'from-top', 'shared_stage': 'from-top', }, 'stages': { 'prod': { 'environment_variables': { 'stage_var': 'bar', 'shared_stage_key': 'from-stage', 'shared_stage': 'from-stage', }, 'lambda_functions': { 'api_handler': { 'environment_variables': { 'function_key': 'from-function', 'shared_stage': 'from-function', } } } } } } c = Config('prod', config_from_disk=config_from_disk) resolved = c.environment_variables assert resolved == { 'top_level': 'foo', 'stage_var': 'bar', 'shared_stage': 'from-function', 'function_key': 'from-function', 'shared_stage_key': 'from-stage', } def test_can_load_python_version(): c = Config('dev') major, minor = sys.version_info[0], sys.version_info[1] assert c.lambda_python_version == f'python{major}.{minor}' class TestConfigureMinimumCompressionSize(object): def test_not_set(self): c = Config('dev', config_from_disk={}) assert c.minimum_compression_size is None def test_set_minimum_compression_size_global(self): config_from_disk = { 'minimum_compression_size': 5000 } c = Config('dev', config_from_disk=config_from_disk) assert c.minimum_compression_size == 5000 def test_set_minimum_compression_size_stage(self): config_from_disk = { 'stages': { 'dev': { 'minimum_compression_size': 5000 } } } c = Config('dev', config_from_disk=config_from_disk) assert c.minimum_compression_size == 5000 def test_set_minimum_compression_size_override(self): config_from_disk = { 'minimum_compression_size': 0, 'stages': { 'dev': { 'minimum_compression_size': 5000 } } } c = Config('dev', config_from_disk=config_from_disk) assert c.minimum_compression_size == 5000 class TestConfigureLambdaMemorySize(object): def test_not_set(self): c = Config('dev', config_from_disk={}) assert c.lambda_memory_size is None def test_set_lambda_memory_size_global(self): config_from_disk = { 'lambda_memory_size': 256 } c = Config('dev', config_from_disk=config_from_disk) assert c.lambda_memory_size == 256 def test_set_lambda_memory_size_stage(self): config_from_disk = { 'stages': { 'dev': { 'lambda_memory_size': 256 } } } c = Config('dev', config_from_disk=config_from_disk) assert c.lambda_memory_size == 256 def test_set_lambda_memory_size_override(self): config_from_disk = { 'lambda_memory_size': 128, 'stages': { 'dev': { 'lambda_memory_size': 256 } } } c = Config('dev', config_from_disk=config_from_disk) assert c.lambda_memory_size == 256 class TestConfigureLambdaTimeout(object): def test_not_set(self): c = Config('dev', config_from_disk={}) assert c.lambda_timeout is None def test_set_lambda_timeout_global(self): config_from_disk = { 'lambda_timeout': 120 } c = Config('dev', config_from_disk=config_from_disk) assert c.lambda_timeout == 120 def test_set_lambda_memory_size_stage(self): config_from_disk = { 'stages': { 'dev': { 'lambda_timeout': 120 } } } c = Config('dev', config_from_disk=config_from_disk) assert c.lambda_timeout == 120 def test_set_lambda_memory_size_override(self): config_from_disk = { 'lambda_timeout': 60, 'stages': { 'dev': { 'lambda_timeout': 120 } } } c = Config('dev', config_from_disk=config_from_disk) assert c.lambda_timeout == 120 class TestConfigureTags(object): def test_default_tags(self): c = Config('dev', config_from_disk={'app_name': 'myapp'}) assert c.tags == { 'aws-chalice': 'version=%s:stage=dev:app=myapp' % chalice_version } def test_tags_global(self): config_from_disk = { 'app_name': 'myapp', 'tags': {'mykey': 'myvalue'} } c = Config('dev', config_from_disk=config_from_disk) assert c.tags == { 'mykey': 'myvalue', 'aws-chalice': 'version=%s:stage=dev:app=myapp' % chalice_version } def test_tags_stage(self): config_from_disk = { 'app_name': 'myapp', 'stages': { 'dev': { 'tags': {'mykey': 'myvalue'} } } } c = Config('dev', config_from_disk=config_from_disk) assert c.tags == { 'mykey': 'myvalue', 'aws-chalice': 'version=%s:stage=dev:app=myapp' % chalice_version } def test_tags_merge(self): config_from_disk = { 'app_name': 'myapp', 'tags': { 'onlyglobalkey': 'globalvalue', 'sharedkey': 'globalvalue', 'sharedstage': 'globalvalue', }, 'stages': { 'dev': { 'tags': { 'sharedkey': 'stagevalue', 'sharedstage': 'stagevalue', 'onlystagekey': 'stagevalue', }, 'lambda_functions': { 'api_handler': { 'tags': { 'sharedkey': 'functionvalue', 'onlyfunctionkey': 'functionvalue', } } } } } } c = Config('dev', config_from_disk=config_from_disk) assert c.tags == { 'onlyglobalkey': 'globalvalue', 'onlystagekey': 'stagevalue', 'onlyfunctionkey': 'functionvalue', 'sharedstage': 'stagevalue', 'sharedkey': 'functionvalue', 'aws-chalice': 'version=%s:stage=dev:app=myapp' % chalice_version } def test_tags_specified_does_not_override_chalice_tag(self): c = Config.create( chalice_stage='dev', app_name='myapp', tags={'aws-chalice': 'attempted-override'}) assert c.tags == { 'aws-chalice': 'version=%s:stage=dev:app=myapp' % chalice_version, } def test_deployed_resource_does_not_exist(): deployed = DeployedResources( {'resources': [{'name': 'foo'}]} ) with pytest.raises(ValueError): deployed.resource_values('bar') def test_deployed_api_mapping_resource(): deployed = DeployedResources( {'resources': [ {'name': 'foo'}, { "name": "api_gateway_custom_domain", "resource_type": "domain_name", "api_mapping": [ { "key": "path_key" } ] } ]} ) name = 'api_gateway_custom_domain.api_mapping.path_key' result = deployed.resource_values(name) assert result == { "name": "api_gateway_custom_domain", "resource_type": "domain_name", "api_mapping": [ { "key": "path_key" } ] } def test_deployed_resource_exists(): deployed = DeployedResources( {'resources': [{'name': 'foo'}]} ) assert deployed.resource_values('foo') == {'name': 'foo'} assert deployed.resource_names() == ['foo'] class TestUpgradeNewDeployer(object): def setup_method(self): # This is the "old deployer" format. deployed = { "region": "us-west-2", "api_handler_name": "app-dev", "api_handler_arn": ( "arn:aws:lambda:us-west-2:123:function:app-dev"), "rest_api_id": "my_rest_api_id", "lambda_functions": { "app-dev-foo": { "type": "pure_lambda", "arn": ( "arn:aws:lambda:us-west-2:123:function:app-dev-foo" )}, }, "chalice_version": "1.1.1", "api_gateway_stage": "api", "backend": "api", } self.old_deployed = {"dev": deployed} # This is "new deployer" format. The deployed resources # are just a list of resources. resources = [ {"role_name": "app-dev", "role_arn": "arn:aws:iam::123:role/app-dev", "name": "default-role", "resource_type": "iam_role"}, {"lambda_arn": "arn:aws:lambda:us-west-2:123:function:app-dev-foo", "name": "foo", "resource_type": "lambda_function"}, {"lambda_arn": ( "arn:aws:lambda:us-west-2:123:function:app-dev"), "name": "api_handler", "resource_type": "lambda_function"}, {"rest_api_id": "my_rest_api_id", "name": "rest_api", "resource_type": "rest_api"} ] self.new_deployed = { 'stages': { 'dev': { 'resources': resources } }, 'schema_version': '2.0', } self.deployed_filename = os.path.join('.', '.chalice', 'deployed.json') self.config = FixedDataConfig( {self.deployed_filename: self.old_deployed}, ) def test_can_upgrade_rest_api(self): resources = self.config.deployed_resources('dev') # The 'default-role' isn't in this list because # it's not in the old deployed.json so it's filled # in on the first deploy with the new deployer. assert sorted(resources.resource_names()) == [ 'api_handler', 'foo', 'rest_api', ] assert resources.resource_values('rest_api') == { 'rest_api_id': 'my_rest_api_id', 'name': 'rest_api', 'resource_type': 'rest_api', } def test_upgrade_for_new_stage_gives_empty_values(self): resources = self.config.deployed_resources('prod') assert resources.resource_names() == [] def test_can_upgrade_pre10_lambda_functions(self): deployed = { "region": "us-west-2", "api_handler_name": "app-dev", "api_handler_arn": ( "arn:aws:lambda:us-west-2:123:function:app-dev"), "rest_api_id": "my_rest_api_id", "lambda_functions": { # This is the old < 1.0 style where the # value was just the lambda arn. "app-dev-foo": "my-lambda-arn", }, "chalice_version": "0.10.0", "api_gateway_stage": "api", "backend": "api", } self.old_deployed = {"dev": deployed} self.config = FixedDataConfig( {self.deployed_filename: self.old_deployed}, ) resources = self.config.deployed_resources('dev') assert sorted(resources.resource_names()) == [ 'api_handler', 'foo', 'rest_api', ] assert resources.resource_values('foo') == { 'lambda_arn': 'my-lambda-arn', 'name': 'foo', 'resource_type': 'lambda_function', } ================================================ FILE: tests/unit/test_invoke.py ================================================ import json import pytest from chalice.awsclient import TypedAWSClient from chalice.invoke import LambdaInvoker from chalice.invoke import LambdaInvokeHandler from chalice.invoke import LambdaResponseFormatter from chalice.invoke import UnhandledLambdaError class FakeUI(object): def __init__(self): self.writes = [] self.errors = [] def write(self, value): self.writes.append(value) def error(self, value): self.errors.append(value) class FakeStreamingBody(object): def __init__(self, value): self._value = value def read(self): return self._value class TestLambdaInvokeHandler(object): def test_invoke_can_format_and_write_success_case(self, stubbed_session): arn = 'arn:aws:lambda:region:id:function:name-dev' stubbed_session.stub('lambda').invoke( FunctionName=arn, InvocationType='RequestResponse' ).returns({ 'StatusCode': 200, 'ExecutedVersion': '$LATEST', 'Payload': FakeStreamingBody(b'foobarbaz') }) ui = FakeUI() stubbed_session.activate_stubs() client = TypedAWSClient(stubbed_session) invoker = LambdaInvoker(arn, client) formatter = LambdaResponseFormatter() invoke_handler = LambdaInvokeHandler(invoker, formatter, ui) invoke_handler.invoke() stubbed_session.verify_stubs() assert ['foobarbaz\n'] == ui.writes def test_invoke_can_format_and_write_error_case(self, stubbed_session): arn = 'arn:aws:lambda:region:id:function:name-dev' error = { "errorMessage": "Something bad happened", "errorType": "Error", "stackTrace": [ ["/path/file.py", 123, "main", "foo(bar)"], ["/path/other_file.py", 456, "function", "bar = baz"] ] } serialized_error = json.dumps(error).encode('utf-8') stubbed_session.stub('lambda').invoke( FunctionName=arn, InvocationType='RequestResponse' ).returns({ 'StatusCode': 200, 'FunctionError': 'Unhandled', 'ExecutedVersion': '$LATEST', 'Payload': FakeStreamingBody(serialized_error) }) ui = FakeUI() stubbed_session.activate_stubs() client = TypedAWSClient(stubbed_session) invoker = LambdaInvoker(arn, client) formatter = LambdaResponseFormatter() invoke_handler = LambdaInvokeHandler(invoker, formatter, ui) with pytest.raises(UnhandledLambdaError): invoke_handler.invoke() stubbed_session.verify_stubs() assert [( 'Traceback (most recent call last):\n' ' File "/path/file.py", line 123, in main\n' ' foo(bar)\n' ' File "/path/other_file.py", line 456, in function\n' ' bar = baz\n' 'Error: Something bad happened\n' )] == ui.errors def test_invoke_can_format_and_write_small_error_case(self, stubbed_session): # Some error response payloads do not have the errorType or # stackTrace key. arn = 'arn:aws:lambda:region:id:function:name-dev' error = { "errorMessage": "Something bad happened", } serialized_error = json.dumps(error).encode('utf-8') stubbed_session.stub('lambda').invoke( FunctionName=arn, InvocationType='RequestResponse' ).returns({ 'StatusCode': 200, 'FunctionError': 'Unhandled', 'ExecutedVersion': '$LATEST', 'Payload': FakeStreamingBody(serialized_error) }) ui = FakeUI() stubbed_session.activate_stubs() client = TypedAWSClient(stubbed_session) invoker = LambdaInvoker(arn, client) formatter = LambdaResponseFormatter() invoke_handler = LambdaInvokeHandler(invoker, formatter, ui) with pytest.raises(UnhandledLambdaError): invoke_handler.invoke() stubbed_session.verify_stubs() assert [( 'Something bad happened\n' )] == ui.errors class TestLambdaInvoker(object): def test_invoke_can_call_api_handler(self, stubbed_session): arn = 'arn:aws:lambda:region:id:function:name-dev' stubbed_session.stub('lambda').invoke( FunctionName=arn, InvocationType='RequestResponse' ).returns({}) stubbed_session.activate_stubs() client = TypedAWSClient(stubbed_session) invoker = LambdaInvoker(arn, client) invoker.invoke() stubbed_session.verify_stubs() def test_invoke_does_forward_payload(self, stubbed_session): arn = 'arn:aws:lambda:region:id:function:name-dev' stubbed_session.stub('lambda').invoke( FunctionName=arn, InvocationType='RequestResponse', Payload=b'foobar', ).returns({}) stubbed_session.activate_stubs() client = TypedAWSClient(stubbed_session) invoker = LambdaInvoker(arn, client) invoker.invoke(b'foobar') stubbed_session.verify_stubs() class TestLambdaResponseFormatter(object): def test_formatter_can_format_success(self): formatter = LambdaResponseFormatter() formatted = formatter.format_response({ 'StatusCode': 200, 'ExecutedVersion': '$LATEST', 'Payload': FakeStreamingBody(b'foobarbaz') }) assert 'foobarbaz\n' == formatted def test_formatter_can_format_list_stack_trace(self): error = { "errorMessage": "Something bad happened", "errorType": "Error", "stackTrace": [ ["/path/file.py", 123, "main", "foo(bar)"], ["/path/more.py", 456, "func", "bar = baz"] ] } serialized_error = json.dumps(error).encode('utf-8') formatter = LambdaResponseFormatter() formatted = formatter.format_response({ 'StatusCode': 200, 'FunctionError': 'Unhandled', 'ExecutedVersion': '$LATEST', 'Payload': FakeStreamingBody(serialized_error) }) assert ( 'Traceback (most recent call last):\n' ' File "/path/file.py", line 123, in main\n' ' foo(bar)\n' ' File "/path/more.py", line 456, in func\n' ' bar = baz\n' 'Error: Something bad happened\n' ) == formatted def test_formatter_can_format_string_stack_trace(self): error = { "errorMessage": "Something bad happened", "errorType": "Error", "stackTrace": [ ' File "/path/file.py", line 123, in main\n foo(bar)\n', ' File "/path/more.py", line 456, in func\n bar = baz\n', ] } serialized_error = json.dumps(error).encode('utf-8') formatter = LambdaResponseFormatter() formatted = formatter.format_response({ 'StatusCode': 200, 'FunctionError': 'Unhandled', 'ExecutedVersion': '$LATEST', 'Payload': FakeStreamingBody(serialized_error) }) assert ( 'Traceback (most recent call last):\n' ' File "/path/file.py", line 123, in main\n' ' foo(bar)\n' ' File "/path/more.py", line 456, in func\n' ' bar = baz\n' 'Error: Something bad happened\n' ) == formatted def test_formatter_can_format_simple_error(self): error = { "errorMessage": "Something bad happened", } serialized_error = json.dumps(error).encode('utf-8') formatter = LambdaResponseFormatter() formatted = formatter.format_response({ 'StatusCode': 200, 'FunctionError': 'Unhandled', 'ExecutedVersion': '$LATEST', 'Payload': FakeStreamingBody(serialized_error) }) assert 'Something bad happened\n' == formatted ================================================ FILE: tests/unit/test_local.py ================================================ import re import json import decimal from unittest import mock import pytest from pytest import fixture from six import BytesIO from six.moves.BaseHTTPServer import HTTPServer from chalice import app from chalice import local, BadRequestError, CORSConfig from chalice import Response from chalice import IAMAuthorizer from chalice import CognitoUserPoolAuthorizer from chalice.config import Config from chalice.local import LambdaContext from chalice.local import LocalARNBuilder from chalice.local import LocalGateway from chalice.local import LocalGatewayAuthorizer from chalice.local import NotAuthorizedError from chalice.local import ForbiddenError from chalice.local import InvalidAuthorizerError from chalice.local import LocalDevServer AWS_REQUEST_ID_PATTERN = re.compile( '^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$', re.I) class FakeTimeSource(object): def __init__(self, times): """Create a fake source of second-precision time. :type time: List :param time: List of times that the time source should return in the order it should return them. These should be in seconds. """ self._times = times def time(self): """Get the next time. This is for mimicing the Clock interface used in local. """ time = self._times.pop(0) return time class ChaliceStubbedHandler(local.ChaliceRequestHandler): requestline = '' request_version = 'HTTP/1.1' def setup(self): self.rfile = BytesIO() self.wfile = BytesIO() self.requestline = '' def finish(self): pass class CustomSampleChalice(app.Chalice): def custom_method(self): return "foo" @pytest.fixture def arn_builder(): return LocalARNBuilder() @pytest.fixture def lambda_context_args(): # LambdaContext has several positional args before the ones that we # care about for the timing tests, this gives reasonable defaults for # those arguments. return ['lambda_name', 256] @fixture def custom_sample_app(): demo = CustomSampleChalice(app_name='custom-demo-app') demo.debug = True return demo @fixture def sample_app(): demo = app.Chalice('demo-app') demo.debug = True @demo.route('/index', methods=['GET']) def index(): return {'hello': 'world'} @demo.route('/names/{name}', methods=['GET']) def name(name): return {'provided-name': name} @demo.route('/put', methods=['PUT']) def put(): return {'body': demo.current_request.json_body} @demo.route('/cors', methods=['GET', 'PUT'], cors=True) def cors(): return {'cors': True} @demo.route('/custom_cors', methods=['GET', 'PUT'], cors=CORSConfig( allow_origin='https://foo.bar', allow_headers=['Header-A', 'Header-B'], expose_headers=['Header-A', 'Header-B'], max_age=600, allow_credentials=True )) def custom_cors(): return {'cors': True} @demo.route('/cors-enabled-for-one-method', methods=['GET']) def without_cors(): return {'ok': True} @demo.route('/cors-enabled-for-one-method', methods=['POST'], cors=True) def with_cors(): return {'ok': True} @demo.route('/options', methods=['OPTIONS']) def options(): return {'options': True} @demo.route('/delete', methods=['DELETE']) def delete(): return {'delete': True} @demo.route('/patch', methods=['PATCH']) def patch(): return {'patch': True} @demo.route('/badrequest') def badrequest(): raise BadRequestError('bad-request') @demo.route('/decimals') def decimals(): return decimal.Decimal('100') @demo.route('/query-string') def query_string(): return demo.current_request.query_params @demo.route('/query-string-multi') def query_string_multi(): params = demo.current_request.query_params keys = {k: params.getlist(k) for k in params} return keys @demo.route('/custom-response') def custom_response(): return Response(body='text', status_code=200, headers={'Content-Type': 'text/plain'}) @demo.route('/binary', methods=['POST'], content_types=['application/octet-stream']) def binary_round_trip(): return Response(body=demo.current_request.raw_body, status_code=200, headers={'Content-Type': 'application/octet-stream'}) @demo.route('/multi-value-header') def multi_value_header(): return Response(body={}, status_code=200, headers={ 'Set-Cookie': ['CookieA=ValueA', 'CookieB=ValueB'] }) return demo @fixture def demo_app_auth(): demo = app.Chalice('app-name') def _policy(effect, resource, action='execute-api:Invoke'): return { 'context': {}, 'principalId': 'user', 'policyDocument': { 'Version': '2012-10-17', 'Statement': [ { 'Action': action, 'Effect': effect, 'Resource': resource, } ] } } @demo.authorizer() def auth_with_explicit_policy(auth_request): token = auth_request.token if token == 'allow': return _policy( effect='Allow', resource=[ "arn:aws:execute-api:mars-west-1:123456789012:" "ymy8tbxw7b/api/GET/explicit"]) else: return _policy( effect='Deny', resource=[ "arn:aws:execute-api:mars-west-1:123456789012:" "ymy8tbxw7b/api/GET/explicit"]) @demo.authorizer() def demo_authorizer_returns_none(auth_request): return None @demo.authorizer() def auth_with_multiple_actions(auth_request): return _policy( effect='Allow', resource=[ "arn:aws:execute-api:mars-west-1:123456789012:" "ymy8tbxw7b/api/GET/multi"], action=['execute-api:Invoke', 'execute-api:Other'] ) @demo.authorizer() def demo_auth(auth_request): token = auth_request.token if token == 'allow': return app.AuthResponse(routes=['/index'], principal_id='user') else: return app.AuthResponse(routes=[], principal_id='user') @demo.authorizer() def resource_auth(auth_request): token = auth_request.token if token == 'allow': return app.AuthResponse(routes=['/resource/foobar'], principal_id='user') else: return app.AuthResponse(routes=[], principal_id='user') @demo.authorizer() def all_auth(auth_request): token = auth_request.token if token == 'allow': return app.AuthResponse(routes=['*'], principal_id='user') else: return app.AuthResponse(routes=[], principal_id='user') @demo.authorizer() def landing_page_auth(auth_request): token = auth_request.token if token == 'allow': return app.AuthResponse(routes=['/'], principal_id='user') else: return app.AuthResponse(routes=[], principal_id='user') iam_authorizer = IAMAuthorizer() cognito_authorizer = CognitoUserPoolAuthorizer('app-name', []) @demo.route('/', authorizer=landing_page_auth) def landing_view(): return {} @demo.route('/index', authorizer=demo_auth) def index_view(): return {} @demo.route('/secret', authorizer=demo_auth) def secret_view(): return {} @demo.route('/resource/{name}', authorizer=resource_auth) def single_value(name): return {'resource': name} @demo.route('/secret/{value}', authorizer=all_auth) def secret_view_value(value): return {'secret': value} @demo.route('/explicit', authorizer=auth_with_explicit_policy) def explicit(): return {} @demo.route('/multi', authorizer=auth_with_multiple_actions) def multi(): return {} @demo.route('/iam', authorizer=iam_authorizer) def iam_route(): return {} @demo.route('/cognito', authorizer=cognito_authorizer) def cognito_route(): return {} @demo.route('/none', authorizer=demo_authorizer_returns_none) def none_auth(): return {} return demo @fixture def handler(sample_app): config = Config() chalice_handler = ChaliceStubbedHandler( None, ('127.0.0.1', 2000), None, app_object=sample_app, config=config) chalice_handler.sample_app = sample_app return chalice_handler @fixture def auth_handler(demo_app_auth): config = Config() chalice_handler = ChaliceStubbedHandler( None, ('127.0.0.1', 2000), None, app_object=demo_app_auth, config=config) chalice_handler.sample_app = demo_app_auth return chalice_handler def _get_raw_body_from_response_stream(handler): # This is going to include things like status code and # response headers in the raw stream. We just care about the # body for now so we'll split lines. raw_response = handler.wfile.getvalue() body = raw_response.splitlines()[-1] return body def _get_body_from_response_stream(handler): body = _get_raw_body_from_response_stream(handler) return json.loads(body) def set_current_request(handler, method, path, headers=None): if headers is None: headers = {'content-type': 'application/json'} handler.command = method handler.path = path handler.headers = headers def test_can_convert_request_handler_to_lambda_event(handler): set_current_request(handler, method='GET', path='/index') handler.do_GET() assert _get_body_from_response_stream(handler) == {'hello': 'world'} def test_uses_http_11(handler): set_current_request(handler, method='GET', path='/index') handler.do_GET() response_lines = handler.wfile.getvalue().splitlines() assert b'HTTP/1.1 200 OK' in response_lines def test_can_route_url_params(handler): set_current_request(handler, method='GET', path='/names/james') handler.do_GET() assert _get_body_from_response_stream(handler) == { 'provided-name': 'james'} def test_can_route_put_with_body(handler): body = b'{"foo": "bar"}' headers = {'content-type': 'application/json', 'content-length': len(body)} set_current_request(handler, method='PUT', path='/put', headers=headers) handler.rfile.write(body) handler.rfile.seek(0) handler.do_PUT() assert _get_body_from_response_stream(handler) == { 'body': {'foo': 'bar'}} def test_will_respond_with_cors_enabled(handler): headers = {'content-type': 'application/json', 'origin': 'null'} set_current_request(handler, method='GET', path='/cors', headers=headers) handler.do_GET() response_lines = handler.wfile.getvalue().splitlines() assert b'Access-Control-Allow-Origin: *' in response_lines def test_will_respond_with_custom_cors_enabled(handler): headers = {'content-type': 'application/json', 'origin': 'null'} set_current_request(handler, method='GET', path='/custom_cors', headers=headers) handler.do_GET() response = handler.wfile.getvalue().splitlines() assert b'HTTP/1.1 200 OK' in response assert b'Access-Control-Allow-Origin: https://foo.bar' in response assert (b'Access-Control-Allow-Headers: Authorization,Content-Type,' b'Header-A,Header-B,X-Amz-Date,X-Amz-Security-Token,' b'X-Api-Key') in response assert b'Access-Control-Expose-Headers: Header-A,Header-B' in response assert b'Access-Control-Max-Age: 600' in response assert b'Access-Control-Allow-Credentials: true' in response def test_will_respond_with_custom_cors_enabled_options(handler): headers = {'content-type': 'application/json', 'origin': 'null'} set_current_request(handler, method='OPTIONS', path='/custom_cors', headers=headers) handler.do_OPTIONS() response = handler.wfile.getvalue().decode().splitlines() assert 'HTTP/1.1 200 OK' in response assert 'Access-Control-Allow-Origin: https://foo.bar' in response assert ('Access-Control-Allow-Headers: Authorization,Content-Type,' 'Header-A,Header-B,X-Amz-Date,X-Amz-Security-Token,' 'X-Api-Key') in response assert 'Access-Control-Expose-Headers: Header-A,Header-B' in response assert 'Access-Control-Max-Age: 600' in response assert 'Access-Control-Allow-Credentials: true' in response assert 'Content-Length: 0' in response # Ensure that the Access-Control-Allow-Methods header is sent # and that it sends all the correct methods over. methods_lines = [line for line in response if line.startswith('Access-Control-Allow-Methods')] assert len(methods_lines) == 1 method_line = methods_lines[0] _, methods_header_value = method_line.split(': ') methods = methods_header_value.strip().split(',') assert ['GET', 'OPTIONS', 'PUT'] == sorted(methods) def test_can_preflight_request(handler): headers = {'content-type': 'application/json', 'origin': 'null'} set_current_request(handler, method='OPTIONS', path='/cors', headers=headers) handler.do_OPTIONS() response_lines = handler.wfile.getvalue().splitlines() assert b'Access-Control-Allow-Origin: *' in response_lines def test_non_preflight_options_request(handler): headers = {'content-type': 'application/json', 'origin': 'null'} set_current_request(handler, method='OPTIONS', path='/options', headers=headers) handler.do_OPTIONS() assert _get_body_from_response_stream(handler) == {'options': True} def test_preflight_request_should_succeed_even_if_cors_disabled(handler): headers = {'content-type': 'application/json', 'origin': 'null'} set_current_request(handler, method='OPTIONS', path='/index', headers=headers) handler.do_OPTIONS() response_lines = handler.wfile.getvalue().splitlines() assert b'HTTP/1.1 200 OK' in response_lines def test_preflight_returns_correct_methods_in_access_allow_header(handler): headers = {'content-type': 'application/json', 'origin': 'null'} set_current_request(handler, method='OPTIONS', path='/cors-enabled-for-one-method', headers=headers) handler.do_OPTIONS() response_lines = handler.wfile.getvalue().splitlines() assert b'HTTP/1.1 200 OK' in response_lines assert b'Access-Control-Allow-Methods: POST,OPTIONS' in response_lines def test_errors_converted_to_json_response(handler): set_current_request(handler, method='GET', path='/badrequest') handler.do_GET() assert _get_body_from_response_stream(handler) == { 'Code': 'BadRequestError', 'Message': 'bad-request' } def test_can_support_delete_method(handler): set_current_request(handler, method='DELETE', path='/delete') handler.do_DELETE() assert _get_body_from_response_stream(handler) == {'delete': True} def test_can_support_patch_method(handler): set_current_request(handler, method='PATCH', path='/patch') handler.do_PATCH() assert _get_body_from_response_stream(handler) == {'patch': True} def test_can_support_decimals(handler): set_current_request(handler, method='GET', path='/decimals') handler.do_PATCH() assert _get_body_from_response_stream(handler) == 100 def test_unsupported_methods_raise_error(handler): set_current_request(handler, method='POST', path='/index') handler.do_POST() assert _get_body_from_response_stream(handler) == { 'Code': 'MethodNotAllowedError', 'Message': 'Unsupported method: POST' } def test_can_round_trip_binary(handler): body = b'\xFE\xED' set_current_request( handler, method='POST', path='/binary', headers={ 'content-type': 'application/octet-stream', 'accept': 'application/octet-stream', 'content-length': len(body) } ) handler.rfile.write(body) handler.rfile.seek(0) handler.do_POST() response = _get_raw_body_from_response_stream(handler) assert response == body def test_querystring_is_mapped(handler): set_current_request(handler, method='GET', path='/query-string?a=b&c=d') handler.do_GET() assert _get_body_from_response_stream(handler) == {'a': 'b', 'c': 'd'} def test_empty_querystring_is_none(handler): set_current_request(handler, method='GET', path='/query-string') handler.do_GET() assert _get_body_from_response_stream(handler) is None def test_querystring_list_is_mapped(handler): set_current_request( handler, method='GET', path='/query-string-multi?a=b&c=d&a=c&e=' ) handler.do_GET() expected = {'a': ['b', 'c'], 'c': ['d'], 'e': ['']} assert _get_body_from_response_stream(handler) == expected def test_querystring_undefined_is_mapped_consistent_with_apigateway(handler): # API Gateway picks up the last element of duplicate keys in a # querystring set_current_request(handler, method='GET', path='/query-string?a=b&a=c') handler.do_GET() assert _get_body_from_response_stream(handler) == {'a': 'c'} def test_content_type_included_once(handler): set_current_request(handler, method='GET', path='/custom-response') handler.do_GET() value = handler.wfile.getvalue() response_lines = value.splitlines() content_header_lines = [line for line in response_lines if line.startswith(b'Content-Type')] assert len(content_header_lines) == 1 def test_can_deny_unauthed_request(auth_handler): set_current_request(auth_handler, method='GET', path='/index') auth_handler.do_GET() value = auth_handler.wfile.getvalue() response_lines = value.splitlines() assert b'HTTP/1.1 401 Unauthorized' in response_lines assert b'x-amzn-ErrorType: UnauthorizedException' in response_lines assert b'Content-Type: application/json' in response_lines assert b'{"message":"Unauthorized"}' in response_lines def test_multi_value_header(handler): set_current_request(handler, method='GET', path='/multi-value-header') handler.do_GET() response = handler.wfile.getvalue().decode().splitlines() assert 'Set-Cookie: CookieA=ValueA' in response assert 'Set-Cookie: CookieB=ValueB' in response @pytest.mark.parametrize('actual_url,matched_url', [ ('/foo', '/foo'), ('/foo/', '/foo'), ('/foo/bar', '/foo/bar'), ('/foo/other', '/foo/{capture}'), ('/names/foo', '/names/{capture}'), ('/names/bar', '/names/{capture}'), ('/names/bar/', '/names/{capture}'), ('/names/', None), ('/nomatch', None), ('/names/bar/wrong', None), ('/a/z/c', '/a/{capture}/c'), ('/a/b/c', '/a/b/c'), ]) def test_can_match_exact_route(actual_url, matched_url): matcher = local.RouteMatcher([ '/foo', '/foo/{capture}', '/foo/bar', '/names/{capture}', '/a/{capture}/c', '/a/b/c' ]) if matched_url is not None: assert matcher.match_route(actual_url).route == matched_url else: with pytest.raises(ValueError): matcher.match_route(actual_url) def test_lambda_event_contains_source_ip(): converter = local.LambdaEventConverter( local.RouteMatcher(['/foo/bar'])) event = converter.create_lambda_event( method='GET', path='/foo/bar', headers={'content-type': 'application/json'} ) source_ip = event.get('requestContext').get('identity').get('sourceIp') assert source_ip == local.LambdaEventConverter.LOCAL_SOURCE_IP def test_can_create_lambda_event(): converter = local.LambdaEventConverter( local.RouteMatcher(['/foo/bar', '/foo/{capture}'])) event = converter.create_lambda_event( method='GET', path='/foo/other', headers={'content-type': 'application/json'} ) assert event == { 'requestContext': { 'httpMethod': 'GET', 'resourcePath': '/foo/{capture}', 'path': '/foo/other', 'identity': { 'sourceIp': local.LambdaEventConverter.LOCAL_SOURCE_IP }, }, 'headers': {'content-type': 'application/json'}, 'pathParameters': {'capture': 'other'}, 'multiValueQueryStringParameters': None, 'body': None, 'stageVariables': {}, } def test_parse_query_string(): converter = local.LambdaEventConverter( local.RouteMatcher(['/foo/bar', '/foo/{capture}'])) event = converter.create_lambda_event( method='GET', path='/foo/other?a=1&b=&c=3', headers={'content-type': 'application/json'} ) assert event == { 'requestContext': { 'httpMethod': 'GET', 'resourcePath': '/foo/{capture}', 'path': '/foo/other', 'identity': { 'sourceIp': local.LambdaEventConverter.LOCAL_SOURCE_IP }, }, 'headers': {'content-type': 'application/json'}, 'pathParameters': {'capture': 'other'}, 'multiValueQueryStringParameters': {'a': ['1'], 'b': [''], 'c': ['3']}, 'body': None, 'stageVariables': {}, } def test_can_create_lambda_event_for_put_request(): converter = local.LambdaEventConverter( local.RouteMatcher(['/foo/bar', '/foo/{capture}'])) event = converter.create_lambda_event( method='PUT', path='/foo/other', headers={'content-type': 'application/json'}, body='{"foo": "bar"}', ) assert event == { 'requestContext': { 'httpMethod': 'PUT', 'resourcePath': '/foo/{capture}', 'path': '/foo/other', 'identity': { 'sourceIp': local.LambdaEventConverter.LOCAL_SOURCE_IP }, }, 'headers': {'content-type': 'application/json'}, 'pathParameters': {'capture': 'other'}, 'multiValueQueryStringParameters': None, 'body': '{"foo": "bar"}', 'stageVariables': {}, } def test_can_create_lambda_event_for_post_with_formencoded_body(): converter = local.LambdaEventConverter( local.RouteMatcher(['/foo/bar', '/foo/{capture}'])) form_body = 'foo=bar&baz=qux' event = converter.create_lambda_event( method='POST', path='/foo/other', headers={'content-type': 'application/x-www-form-urlencoded'}, body=form_body, ) assert event == { 'requestContext': { 'httpMethod': 'POST', 'resourcePath': '/foo/{capture}', 'path': '/foo/other', 'identity': { 'sourceIp': local.LambdaEventConverter.LOCAL_SOURCE_IP }, }, 'headers': {'content-type': 'application/x-www-form-urlencoded'}, 'pathParameters': {'capture': 'other'}, 'multiValueQueryStringParameters': None, 'body': form_body, 'stageVariables': {}, } def test_can_provide_port_to_local_server(sample_app): dev_server = local.create_local_server(sample_app, None, '127.0.0.1', port=23456) assert dev_server.server.server_port == 23456 def test_can_provide_host_to_local_server(sample_app): dev_server = local.create_local_server(sample_app, None, host='0.0.0.0', port=23456) assert dev_server.host == '0.0.0.0' def test_wraps_custom_sample_app_with_local_chalice(custom_sample_app): dev_server = local.create_local_server(custom_sample_app, None, host='0.0.0.0', port=23456) assert isinstance(dev_server.app_object, local.LocalChalice) assert isinstance(dev_server.app_object, custom_sample_app.__class__) assert dev_server.app_object.custom_method() == 'foo' class TestLambdaContext(object): def test_can_get_remaining_time_once(self, lambda_context_args): time_source = FakeTimeSource([0, 5]) context = LambdaContext(*lambda_context_args, max_runtime_ms=10000, time_source=time_source) time_remaining = context.get_remaining_time_in_millis() assert time_remaining == 5000 def test_can_get_remaining_time_multiple(self, lambda_context_args): time_source = FakeTimeSource([0, 3, 7, 9]) context = LambdaContext(*lambda_context_args, max_runtime_ms=10000, time_source=time_source) time_remaining = context.get_remaining_time_in_millis() assert time_remaining == 7000 time_remaining = context.get_remaining_time_in_millis() assert time_remaining == 3000 time_remaining = context.get_remaining_time_in_millis() assert time_remaining == 1000 def test_does_populate_aws_request_id_with_valid_uuid(self, lambda_context_args): context = LambdaContext(*lambda_context_args) assert AWS_REQUEST_ID_PATTERN.match(context.aws_request_id) def test_does_set_version_to_latest(self, lambda_context_args): context = LambdaContext(*lambda_context_args) assert context.function_version == '$LATEST' class TestLocalGateway(object): def test_can_invoke_function(self): demo = app.Chalice('app-name') @demo.route('/') def index_view(): return {'foo': 'bar'} gateway = LocalGateway(demo, Config()) response = gateway.handle_request('GET', '/', {}, '') body = json.loads(response['body']) assert body['foo'] == 'bar' def test_does_populate_context(self): demo = app.Chalice('app-name') @demo.route('/context') def context_view(): context = demo.lambda_context return { 'name': context.function_name, 'memory': context.memory_limit_in_mb, 'version': context.function_version, 'timeout': context.get_remaining_time_in_millis(), 'request_id': context.aws_request_id, } disk_config = { 'lambda_timeout': 10, 'lambda_memory_size': 256, } config = Config(chalice_stage='api', config_from_disk=disk_config) gateway = LocalGateway(demo, config) response = gateway.handle_request('GET', '/context', {}, '') body = json.loads(response['body']) assert body['name'] == 'api_handler' assert body['memory'] == 256 assert body['version'] == '$LATEST' assert body['timeout'] > 10 assert body['timeout'] <= 10000 assert AWS_REQUEST_ID_PATTERN.match(body['request_id']) def test_defaults_timeout_if_needed(self): demo = app.Chalice('app-name') @demo.route('/context') def context_view(): context = demo.lambda_context return { 'remaining': context.get_remaining_time_in_millis(), } disk_config = {} config = Config(chalice_stage='api', config_from_disk=disk_config) gateway = LocalGateway(demo, config) response = gateway.handle_request('GET', '/context', {}, '') body = json.loads(response['body']) assert body['remaining'] <= gateway.MAX_LAMBDA_EXECUTION_TIME * 1000 def test_can_validate_route_with_variables(self, demo_app_auth): gateway = LocalGateway(demo_app_auth, Config()) response = gateway.handle_request( 'GET', '/secret/foobar', {'Authorization': 'allow'}, '') json_body = json.loads(response['body']) assert json_body['secret'] == 'foobar' def test_can_allow_route_with_variables(self, demo_app_auth): gateway = LocalGateway(demo_app_auth, Config()) response = gateway.handle_request( 'GET', '/resource/foobar', {'Authorization': 'allow'}, '') json_body = json.loads(response['body']) assert json_body['resource'] == 'foobar' def test_does_send_500_when_authorizer_returns_none(self, demo_app_auth): gateway = LocalGateway(demo_app_auth, Config()) with pytest.raises(InvalidAuthorizerError): gateway.handle_request( 'GET', '/none', {'Authorization': 'foobarbaz'}, '') def test_can_deny_route_with_variables(self, demo_app_auth): gateway = LocalGateway(demo_app_auth, Config()) with pytest.raises(ForbiddenError): gateway.handle_request( 'GET', '/resource/foobarbaz', {'Authorization': 'allow'}, '') def test_does_deny_unauthed_request(self, demo_app_auth): gateway = LocalGateway(demo_app_auth, Config()) with pytest.raises(ForbiddenError) as ei: gateway.handle_request( 'GET', '/index', {'Authorization': 'deny'}, '') exception_body = str(ei.value.body) assert ('{"Message": ' '"User is not authorized to ' 'access this resource"}') in exception_body def test_does_throw_unauthorized_when_no_auth_token_present_on_valid_route( self, demo_app_auth): gateway = LocalGateway(demo_app_auth, Config()) with pytest.raises(NotAuthorizedError) as ei: gateway.handle_request( 'GET', '/index', {}, '') exception_body = str(ei.value.body) assert '{"message":"Unauthorized"}' in exception_body def test_does_deny_with_forbidden_when_route_not_found( self, demo_app_auth): gateway = LocalGateway(demo_app_auth, Config()) with pytest.raises(ForbiddenError) as ei: gateway.handle_request('GET', '/badindex', {}, '') exception_body = str(ei.value.body) assert 'Missing Authentication Token' in exception_body def test_does_deny_with_forbidden_when_auth_token_present( self, demo_app_auth): gateway = LocalGateway(demo_app_auth, Config()) with pytest.raises(ForbiddenError) as ei: gateway.handle_request('GET', '/badindex', {'Authorization': 'foobar'}, '') # The message should be a more complicated error message to do with # signing the request. It always ends with the Authorization token # that we passed up, so we can check for that. exception_body = str(ei.value.body) assert 'Authorization=foobar' in exception_body class TestLocalBuiltinAuthorizers(object): def test_can_authorize_empty_path(self, lambda_context_args, demo_app_auth, create_event): # Ensures that / routes work since that is a special case in the # API Gateway arn generation where an extra / is appended to the end # of the arn. authorizer = LocalGatewayAuthorizer(demo_app_auth) path = '/' event = create_event(path, 'GET', {}) event['headers']['authorization'] = 'allow' context = LambdaContext(*lambda_context_args) event, context = authorizer.authorize(path, event, context) assert event['requestContext']['authorizer']['principalId'] == 'user' def test_can_call_method_without_auth(self, lambda_context_args, create_event): demo = app.Chalice('app-name') @demo.route('/index') def index_view(): return {} path = '/index' authorizer = LocalGatewayAuthorizer(demo) original_event = create_event(path, 'GET', {}) original_context = LambdaContext(*lambda_context_args) event, context = authorizer.authorize( path, original_event, original_context) # Assert that when the authorizer.authorize is called and there is no # authorizer defined for a particular route that it is a noop. assert original_event == event assert original_context == context def test_does_raise_not_authorized_error(self, demo_app_auth, lambda_context_args, create_event): authorizer = LocalGatewayAuthorizer(demo_app_auth) path = '/index' event = create_event(path, 'GET', {}) context = LambdaContext(*lambda_context_args) with pytest.raises(NotAuthorizedError): authorizer.authorize(path, event, context) def test_does_authorize_valid_requests(self, demo_app_auth, lambda_context_args, create_event): authorizer = LocalGatewayAuthorizer(demo_app_auth) path = '/index' event = create_event(path, 'GET', {}) event['headers']['authorization'] = 'allow' context = LambdaContext(*lambda_context_args) event, context = authorizer.authorize(path, event, context) assert event['requestContext']['authorizer']['principalId'] == 'user' def test_does_authorize_unsupported_authorizer(self, demo_app_auth, lambda_context_args, create_event): authorizer = LocalGatewayAuthorizer(demo_app_auth) path = '/iam' event = create_event(path, 'GET', {}) context = LambdaContext(*lambda_context_args) with pytest.warns(Warning) as recorded_warnings: new_event, new_context = authorizer.authorize(path, event, context) assert event == new_event assert context == new_context assert len(recorded_warnings) == 1 warning = recorded_warnings[0] assert issubclass(warning.category, UserWarning) assert ('IAMAuthorizer is not a supported in local ' 'mode. All requests made against a route will be authorized' ' to allow local testing.') in str(warning.message) def test_cannot_access_view_without_permission(self, demo_app_auth, lambda_context_args, create_event): authorizer = LocalGatewayAuthorizer(demo_app_auth) path = '/secret' event = create_event(path, 'GET', {}) event['headers']['authorization'] = 'allow' context = LambdaContext(*lambda_context_args) with pytest.raises(ForbiddenError): authorizer.authorize(path, event, context) def test_can_understand_explicit_auth_policy(self, demo_app_auth, lambda_context_args, create_event): authorizer = LocalGatewayAuthorizer(demo_app_auth) path = '/explicit' event = create_event(path, 'GET', {}) event['headers']['authorization'] = 'allow' context = LambdaContext(*lambda_context_args) event, context = authorizer.authorize(path, event, context) assert event['requestContext']['authorizer']['principalId'] == 'user' def test_can_understand_explicit_deny_policy(self, demo_app_auth, lambda_context_args, create_event): # Our auto-generated policies from the AuthResponse object do not # contain any Deny clauses, however we also allow the user to return # a dictionary that is transated into a policy, so we have to # account for the ability for a user to set an explicit deny policy. # It should behave exactly as not getting permission added with an # allow. authorizer = LocalGatewayAuthorizer(demo_app_auth) path = '/explicit' event = create_event(path, 'GET', {}) context = LambdaContext(*lambda_context_args) with pytest.raises(NotAuthorizedError): authorizer.authorize(path, event, context) def test_can_understand_multi_actions(self, demo_app_auth, lambda_context_args, create_event): authorizer = LocalGatewayAuthorizer(demo_app_auth) path = '/multi' event = create_event(path, 'GET', {}) event['headers']['authorization'] = 'allow' context = LambdaContext(*lambda_context_args) event, context = authorizer.authorize(path, event, context) assert event['requestContext']['authorizer']['principalId'] == 'user' def test_can_understand_cognito_token(self, lambda_context_args, demo_app_auth, create_event): # Ensures that / routes work since that is a special case in the # API Gateway arn generation where an extra / is appended to the end # of the arn. authorizer = LocalGatewayAuthorizer(demo_app_auth) path = '/cognito' event = create_event(path, 'GET', {}) event["headers"]["authorization"] = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhYWFhYWFhYS1iYmJiLWNjY2MtZGRkZC1lZWVlZWVlZWVlZWUiLCJhdWQiOiJ4eHh4eHh4eHh4eHhleGFtcGxlIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInRva2VuX3VzZSI6ImlkIiwiYXV0aF90aW1lIjoxNTAwMDA5NDAwLCJpc3MiOiJodHRwczovL2NvZ25pdG8taWRwLnVzLWVhc3QtMS5hbWF6b25hd3MuY29tL3VzLWVhc3QtMV9leGFtcGxlIiwiY29nbml0bzp1c2VybmFtZSI6ImphbmVkb2UiLCJleHAiOjE1ODQ3MjM2MTYsImdpdmVuX25hbWUiOiJKYW5lIiwiaWF0IjoxNTAwMDA5NDAwLCJlbWFpbCI6ImphbmVkb2VAZXhhbXBsZS5jb20iLCJqdGkiOiJkN2UxMTMzYS0xZTNhLTQyMzEtYWU3Yi0yOGQ4NWVlMGIxNGQifQ.p35Yj9KJD5RbfPWGL08IJHgson8BhdGLPQqUOiF0-KM" # noqa context = LambdaContext(*lambda_context_args) event, context = authorizer.authorize(path, event, context) principal_id = event['requestContext']['authorizer']['principalId'] assert principal_id == 'janedoe' def test_does_authorize_unsupported_cognito_token(self, lambda_context_args, demo_app_auth, create_event): authorizer = LocalGatewayAuthorizer(demo_app_auth) path = '/cognito' event = create_event(path, 'GET', {}) event["headers"]["authorization"] = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhYWFhYWFhYS1iYmJiLWNjY2MtZGRkZC1lZWVlZWVlZWVlZWUiLCJhdWQiOiJ4eHh4eHh4eHh4eHhleGFtcGxlIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInRva2VuX3VzZSI6ImlkIiwiYXV0aF90aW1lIjoxNTAwMDA5NDAwLCJpc3MiOiJodHRwczovL2NvZ25pdG8taWRwLnVzLWVhc3QtMS5hbWF6b25hd3MuY29tL3VzLWVhc3QtMV9leGFtcGxlIiwiZXhwIjoxNTg0NzIzNjE2LCJnaXZlbl9uYW1lIjoiSmFuZSIsImlhdCI6MTUwMDAwOTQwMCwiZW1haWwiOiJqYW5lZG9lQGV4YW1wbGUuY29tIiwianRpIjoiZDdlMTEzM2EtMWUzYS00MjMxLWFlN2ItMjhkODVlZTBiMTRkIn0.SN5n-A3kxboNYg0sGIOipVUksCdn6xRJmAK9kSZof10" # noqa context = LambdaContext(*lambda_context_args) with pytest.warns(Warning) as recorded_warnings: new_event, new_context = authorizer.authorize(path, event, context) assert event == new_event assert context == new_context assert len(recorded_warnings) == 1 warning = recorded_warnings[0] assert issubclass(warning.category, UserWarning) assert ('CognitoUserPoolAuthorizer for machine-to-machine ' 'communicaiton is not supported in local mode. All requests ' 'made against a route will be authorized to allow local ' 'testing.') in str(warning.message) class TestArnBuilder(object): def test_can_create_basic_arn(self, arn_builder): arn = ('arn:aws:execute-api:mars-west-1:123456789012:ymy8tbxw7b' '/api/GET/resource') built_arn = arn_builder.build_arn('GET', '/resource') assert arn == built_arn def test_can_create_root_arn(self, arn_builder): arn = ('arn:aws:execute-api:mars-west-1:123456789012:ymy8tbxw7b' '/api/GET//') built_arn = arn_builder.build_arn('GET', '/') assert arn == built_arn def test_can_create_multi_part_arn(self, arn_builder): arn = ('arn:aws:execute-api:mars-west-1:123456789012:ymy8tbxw7b' '/api/GET/path/to/resource') built_arn = arn_builder.build_arn('GET', '/path/to/resource') assert arn == built_arn def test_can_create_glob_method_arn(self, arn_builder): arn = ('arn:aws:execute-api:mars-west-1:123456789012:ymy8tbxw7b' '/api/*/resource') built_arn = arn_builder.build_arn('*', '/resource') assert arn == built_arn def test_build_arn_with_query_params(self, arn_builder): arn = ('arn:aws:execute-api:mars-west-1:123456789012:ymy8tbxw7b/api/' '*/resource') built_arn = arn_builder.build_arn('*', '/resource?foo=bar') assert arn == built_arn @pytest.mark.parametrize('arn,pattern', [ ('mars-west-2:123456789012:ymy8tbxw7b/api/GET/foo', 'mars-west-2:123456789012:ymy8tbxw7b/api/GET/foo' ), ('mars-west-1:123456789012:ymy8tbxw7b/api/GET/foobar', 'mars-west-1:123456789012:ymy8tbxw7b/api/GET/*' ), ('mars-west-1:123456789012:ymy8tbxw7b/api/PUT/foobar', 'mars-west-1:123456789012:ymy8tbxw7b/api/???/foobar' ), ('mars-west-1:123456789012:ymy8tbxw7b/api/GET/foobar', 'mars-west-1:123456789012:ymy8tbxw7b/api/???/*' ), ('mars-west-1:123456789012:ymy8tbxw7b/api/GET/foobar', 'mars-west-1:123456789012:*/api/GET/*' ), ('mars-west-2:123456789012:ymy8tbxw7b/api/GET/foobar', '*' ), ('mars-west-2:123456789012:ymy8tbxw7b/api/GET/foo.bar', 'mars-west-2:123456789012:ymy8tbxw7b/*/GET/*') ]) def test_can_allow_route_arns(arn, pattern): prefix = 'arn:aws:execute-api:' full_arn = '%s%s' % (prefix, arn) full_pattern = '%s%s' % (prefix, pattern) matcher = local.ARNMatcher(full_arn) does_match = matcher.does_any_resource_match([full_pattern]) assert does_match is True @pytest.mark.parametrize('arn,pattern', [ ('mars-west-1:123456789012:ymy8tbxw7b/api/GET/foobar', 'mars-west-1:123456789012:ymy8tbxw7b/api/PUT/*' ), ('mars-west-1:123456789012:ymy8tbxw7b/api/GET/foobar', 'mars-west-1:123456789012:ymy8tbxw7b/api/??/foobar' ), ('mars-west-1:123456789012:ymy8tbxw7b/api/GET/foobar', 'mars-west-2:123456789012:ymy8tbxw7b/api/???/*' ), ('mars-west-2:123456789012:ymy8tbxw7b/api/GET/foobar', 'mars-west-2:123456789012:ymy8tbxw7b/*/GET/foo...') ]) def test_can_deny_route_arns(arn, pattern): prefix = 'arn:aws:execute-api:' full_arn = '%s%s' % (prefix, arn) full_pattern = '%s%s' % (prefix, pattern) matcher = local.ARNMatcher(full_arn) does_match = matcher.does_any_resource_match([full_pattern]) assert does_match is False @pytest.mark.parametrize('arn,patterns', [ ('mars-west-1:123456789012:ymy8tbxw7b/api/GET/foobar', [ 'mars-west-1:123456789012:ymy8tbxw7b/api/PUT/*', 'mars-west-1:123456789012:ymy8tbxw7b/api/GET/foobar' ]), ('mars-west-1:123456789012:ymy8tbxw7b/api/GET/foobar', [ 'mars-west-1:123456789012:ymy8tbxw7b/api/GET/foobar', 'mars-west-1:123456789012:ymy8tbxw7b/api/PUT/*' ]), ('mars-west-1:123456789012:ymy8tbxw7b/api/GET/foobar', [ 'mars-west-1:123456789012:ymy8tbxw7b/api/PUT/foobar', '*' ]) ]) def test_can_allow_multiple_resource_arns(arn, patterns): prefix = 'arn:aws:execute-api:' full_arn = '%s%s' % (prefix, arn) full_patterns = ['%s%s' % (prefix, pattern) for pattern in patterns] matcher = local.ARNMatcher(full_arn) does_match = matcher.does_any_resource_match(full_patterns) assert does_match is True @pytest.mark.parametrize('arn,patterns', [ ('mars-west-1:123456789012:ymy8tbxw7b/api/GET/foobar', [ 'mars-west-1:123456789012:ymy8tbxw7b/api/POST/*', 'mars-west-1:123456789012:ymy8tbxw7b/api/PUT/foobar' ]), ('mars-west-1:123456789012:ymy8tbxw7b/api/GET/foobar', [ 'mars-west-2:123456789012:ymy8tbxw7b/api/GET/foobar', 'mars-west-2:123456789012:ymy8tbxw7b/api/*/*' ]) ]) def test_can_deny_multiple_resource_arns(arn, patterns): prefix = 'arn:aws:execute-api:' full_arn = '%s%s' % (prefix, arn) full_patterns = ['%s%s' % (prefix, pattern) for pattern in patterns] matcher = local.ARNMatcher(full_arn) does_match = matcher.does_any_resource_match(full_patterns) assert does_match is False class TestLocalDevServer(object): def test_can_delegate_to_server(self, sample_app): http_server = mock.Mock(spec=HTTPServer) dev_server = LocalDevServer( sample_app, Config(), '0.0.0.0', 8000, server_cls=lambda *args: http_server, ) dev_server.handle_single_request() http_server.handle_request.assert_called_with() dev_server.serve_forever() http_server.serve_forever.assert_called_with() def test_host_and_port_forwarded_to_server_creation(self, sample_app): provided_args = [] def args_recorder(*args): provided_args[:] = list(args) LocalDevServer( sample_app, Config(), '0.0.0.0', 8000, server_cls=args_recorder, ) assert provided_args[0] == ('0.0.0.0', 8000) def test_does_use_daemon_threads(self, sample_app): server = LocalDevServer( sample_app, Config(), '0.0.0.0', 8000 ) assert server.server.daemon_threads ================================================ FILE: tests/unit/test_logs.py ================================================ import time from unittest import mock from datetime import datetime, timedelta from chalice import logs from chalice.awsclient import TypedAWSClient from six import StringIO NO_OPTIONS = logs.LogRetrieveOptions() def message(log_message, log_stream_name='logStreamName'): return { 'logStreamName': log_stream_name, 'message': log_message, } def test_can_convert_since_to_start_time(): options = logs.LogRetrieveOptions.create( follow=True, since='2020-01-01T00:00:00', include_lambda_messages=False) assert options.max_entries is None assert options.start_time == datetime(2020, 1, 1, 0, 0, 0) assert not options.include_lambda_messages def test_can_retrieve_all_logs(): client = mock.Mock(spec=TypedAWSClient) log_message = message('first') client.iter_log_events.return_value = [log_message] retriever = logs.LogRetriever(client, 'loggroup') messages = list(retriever.retrieve_logs(NO_OPTIONS)) expected = log_message.copy() # We also inject a logShortId. expected['logShortId'] = 'logStreamName' assert messages == [expected] def test_can_support_max_entries(): client = mock.Mock(spec=TypedAWSClient) client.iter_log_events.return_value = [message('first'), message('second')] retriever = logs.LogRetriever(client, 'loggroup') messages = list( retriever.retrieve_logs(logs.LogRetrieveOptions(max_entries=1))) assert len(messages) == 1 assert messages[0]['message'] == 'first' def test_can_exclude_lambda_messages(): client = mock.Mock(spec=TypedAWSClient) client.iter_log_events.return_value = [ message('START RequestId: id Version: $LATEST'), message('END RequestId: id'), message('REPORT RequestId: id Duration: 0.42 ms ' 'Billed Duration: 100 ms ' 'Memory Size: 128 MB Max Memory Used: 19 MB'), message('Not a lambda message'), ] retriever = logs.LogRetriever(client, 'loggroup') messages = list(retriever.retrieve_logs( logs.LogRetrieveOptions(include_lambda_messages=False))) assert len(messages) == 1 assert messages[0]['message'] == 'Not a lambda message' def test_can_parse_short_id(): log_message = message( 'Log Message', '2017/04/28/[$LATEST]fc219a0d613b40e9b5c58e6b8fd2320c' ) client = mock.Mock(spec=TypedAWSClient) client.iter_log_events.return_value = [log_message] retriever = logs.LogRetriever(client, 'loggroup') messages = list(retriever.retrieve_logs( logs.LogRetrieveOptions(include_lambda_messages=False))) assert len(messages) == 1 assert messages[0]['logShortId'] == 'fc219a' def test_can_create_from_arn(): retriever = logs.LogRetriever.create_from_lambda_arn( mock.sentinel.client, 'arn:aws:lambda:us-east-1:123:function:my-function' ) assert isinstance(retriever, logs.LogRetriever) def test_can_display_logs(): retriever = mock.Mock(spec=logs.LogRetriever) retriever.retrieve_logs.return_value = [ {'timestamp': 'NOW', 'logShortId': 'shortId', 'message': 'One'}, {'timestamp': 'NOW', 'logShortId': 'shortId', 'message': 'Two'}, {'timestamp': 'NOW', 'logShortId': 'shortId', 'message': 'Three'}, ] stream = StringIO() logs.display_logs(retriever, retrieve_options=NO_OPTIONS, stream=stream) assert stream.getvalue().splitlines() == [ 'NOW shortId One', 'NOW shortId Two', 'NOW shortId Three', ] def test_can_iterate_through_all_log_events(): client = mock.Mock(spec=TypedAWSClient) client.iter_log_events.return_value = [ {'timestamp': 'NOW', 'logShortId': 'shortId', 'message': 'One'}, {'timestamp': 'NOW', 'logShortId': 'shortId', 'message': 'Two'}, {'timestamp': 'NOW', 'logShortId': 'shortId', 'message': 'Three'}, ] event_gen = logs.LogEventGenerator(client) assert list(event_gen.iter_log_events( log_group_name='mygroup', options=NO_OPTIONS)) == [ {'timestamp': 'NOW', 'logShortId': 'shortId', 'message': 'One'}, {'timestamp': 'NOW', 'logShortId': 'shortId', 'message': 'Two'}, {'timestamp': 'NOW', 'logShortId': 'shortId', 'message': 'Three'}, ] def test_can_follow_log_events(): sleep = mock.Mock(spec=time.sleep) client = mock.Mock(spec=TypedAWSClient) client.filter_log_events.side_effect = [ # First page of results has nextToken indicating there's # more results. {'events': [{'eventId': '1', 'timestamp': 1}, {'eventId': '2', 'timestamp': 2}, {'eventId': '3', 'timestamp': 3}], 'nextToken': 'nextToken1'}, # Second page with no more results, also note the # timestamps are out of order. {'events': [{'eventId': '4', 'timestamp': 4}, {'eventId': '6', 'timestamp': 6}, {'eventId': '5', 'timestamp': 5}]}, # We then poll again with no new results for timestamp=6. {'events': [{'eventId': '6', 'timestamp': 6}]}, # And now we get new results. {'events': [{'eventId': '6', 'timestamp': 6}, # Same timestamp we're querying (6) but a new event. {'eventId': '6NEW', 'timestamp': 6}, {'eventId': '7', 'timestamp': 7}, {'eventId': '8', 'timestamp': 8}]}, KeyboardInterrupt(), ] event_gen = logs.FollowLogEventGenerator(client, sleep) options = logs.LogRetrieveOptions(start_time=1) assert list(event_gen.iter_log_events( log_group_name='mygroup', options=options)) == [ {'eventId': '1', 'timestamp': 1}, {'eventId': '2', 'timestamp': 2}, {'eventId': '3', 'timestamp': 3}, {'eventId': '4', 'timestamp': 4}, # Note we don't try to sort these entries. {'eventId': '6', 'timestamp': 6}, {'eventId': '5', 'timestamp': 5}, {'eventId': '6NEW', 'timestamp': 6}, {'eventId': '7', 'timestamp': 7}, {'eventId': '8', 'timestamp': 8}, ] assert client.filter_log_events.call_args_list == [ mock.call(log_group_name='mygroup', start_time=1), mock.call(log_group_name='mygroup', start_time=1, next_token='nextToken1'), mock.call(log_group_name='mygroup', start_time=6), mock.call(log_group_name='mygroup', start_time=6), mock.call(log_group_name='mygroup', start_time=8), ] def test_follow_logs_initially_empty(): sleep = mock.Mock(spec=time.sleep) client = mock.Mock(spec=TypedAWSClient) client.filter_log_events.side_effect = [ {'events': []}, {'events': []}, {'events': [{'eventId': '1', 'timestamp': 1}, {'eventId': '2', 'timestamp': 2}, {'eventId': '3', 'timestamp': 3}]}, KeyboardInterrupt(), ] event_gen = logs.FollowLogEventGenerator(client, sleep) assert list(event_gen.iter_log_events( log_group_name='mygroup', options=NO_OPTIONS)) == [ {'eventId': '1', 'timestamp': 1}, {'eventId': '2', 'timestamp': 2}, {'eventId': '3', 'timestamp': 3}, ] def test_follow_logs_single_pages_only(): sleep = mock.Mock(spec=time.sleep) client = mock.Mock(spec=TypedAWSClient) client.filter_log_events.side_effect = [ {'events': [{'eventId': '1', 'timestamp': 1}]}, {'events': [{'eventId': '2', 'timestamp': 2}]}, {'events': [{'eventId': '3', 'timestamp': 3}]}, KeyboardInterrupt(), ] event_gen = logs.FollowLogEventGenerator(client, sleep) assert list(event_gen.iter_log_events( log_group_name='mygroup', options=NO_OPTIONS)) == [ {'eventId': '1', 'timestamp': 1}, {'eventId': '2', 'timestamp': 2}, {'eventId': '3', 'timestamp': 3}, ] def test_follow_logs_last_page_empty(): sleep = mock.Mock(spec=time.sleep) client = mock.Mock(spec=TypedAWSClient) client.filter_log_events.side_effect = [ {'events': [{'eventId': '1', 'timestamp': 1}, {'eventId': '2', 'timestamp': 2}, {'eventId': '3', 'timestamp': 3}], 'nextToken': 'nextToken1'}, {'events': [{'eventId': '4', 'timestamp': 4}, {'eventId': '6', 'timestamp': 6}, {'eventId': '5', 'timestamp': 5}], 'nextToken': 'nextToken2'}, # You can sometimes get a next token but with no events. {'events': [], 'nextToken': 'nextToken3'}, {'events': []}, {'events': [{'eventId': '7', 'timestamp': 7}]}, KeyboardInterrupt(), ] event_gen = logs.FollowLogEventGenerator(client, sleep) options = logs.LogRetrieveOptions(start_time=1) assert list(event_gen.iter_log_events( log_group_name='mygroup', options=options)) == [ {'eventId': '1', 'timestamp': 1}, {'eventId': '2', 'timestamp': 2}, {'eventId': '3', 'timestamp': 3}, {'eventId': '4', 'timestamp': 4}, {'eventId': '6', 'timestamp': 6}, {'eventId': '5', 'timestamp': 5}, {'eventId': '7', 'timestamp': 7}, ] assert client.filter_log_events.call_args_list == [ mock.call(log_group_name='mygroup', start_time=1), mock.call(log_group_name='mygroup', start_time=1, next_token='nextToken1'), mock.call(log_group_name='mygroup', start_time=1, next_token='nextToken2'), mock.call(log_group_name='mygroup', start_time=1, next_token='nextToken3'), mock.call(log_group_name='mygroup', start_time=6), mock.call(log_group_name='mygroup', start_time=7), ] def test_follow_logs_all_pages_empty_with_pagination(): sleep = mock.Mock(spec=time.sleep) client = mock.Mock(spec=TypedAWSClient) client.filter_log_events.side_effect = [ {'events': [], 'nextToken': 'nextToken1'}, {'events': [], 'nextToken': 'nextToken2'}, {'events': [], 'nextToken': 'nextToken3'}, {'events': []}, KeyboardInterrupt(), ] event_gen = logs.FollowLogEventGenerator(client, sleep) options = logs.LogRetrieveOptions(start_time=1) assert list(event_gen.iter_log_events( log_group_name='mygroup', options=options)) == [] assert client.filter_log_events.call_args_list == [ mock.call(log_group_name='mygroup', start_time=1), mock.call(log_group_name='mygroup', start_time=1, next_token='nextToken1'), mock.call(log_group_name='mygroup', start_time=1, next_token='nextToken2'), mock.call(log_group_name='mygroup', start_time=1, next_token='nextToken3'), # The last call should not use a next token. mock.call(log_group_name='mygroup', start_time=1) ] def test_follow_logs_defaults_to_ten_minutes(): # To avoid having to patch out/pass in utcnow(), we'll just make sure # that the start_time used is more recent than 10 minutes from now. # This is a safe assumption because we're saving the current time before # we invoke iter_log_events(). ten_minutes = datetime.utcnow() - timedelta(minutes=10) options = logs.LogRetrieveOptions.create(follow=True) assert options.start_time >= ten_minutes def test_dont_default_if_explicit_since_is_provided(): utcnow = datetime.utcnow() options = logs.LogRetrieveOptions.create(follow=True, since=str(utcnow)) assert options.start_time == utcnow ================================================ FILE: tests/unit/test_package.py ================================================ import os import json from unittest import mock import pytest from chalice.config import Config from chalice import package from chalice.constants import LAMBDA_TRUST_POLICY from chalice.deploy.appgraph import ApplicationGraphBuilder, DependencyBuilder from chalice.awsclient import TypedAWSClient from chalice.deploy.deployer import BuildStage from chalice.deploy import models from chalice.deploy.swagger import SwaggerGenerator from chalice.package import PackageOptions from chalice.utils import OSUtils @pytest.fixture def mock_swagger_generator(): return mock.Mock(spec=SwaggerGenerator) def test_can_create_app_packager(): config = Config() options = PackageOptions(mock.Mock(spec=TypedAWSClient)) packager = package.create_app_packager(config, options) assert isinstance(packager, package.AppPackager) def test_can_create_terraform_app_packager(): config = Config() options = PackageOptions(mock.Mock(spec=TypedAWSClient)) packager = package.create_app_packager(config, options, 'terraform') assert isinstance(packager, package.AppPackager) def test_template_post_processor_moves_files_once(): mock_osutils = mock.Mock(spec=OSUtils) p = package.SAMCodeLocationPostProcessor(mock_osutils) template = { 'Resources': { 'foo': { 'Type': 'AWS::Serverless::Function', 'Properties': { 'CodeUri': 'old-dir.zip', } }, 'bar': { 'Type': 'AWS::Serverless::Function', 'Properties': { 'CodeUri': 'old-dir.zip', } }, } } p.process(template, config=None, outdir='outdir', chalice_stage_name='dev') mock_osutils.copy.assert_called_with( 'old-dir.zip', os.path.join('outdir', 'deployment.zip')) assert mock_osutils.copy.call_count == 1 assert template['Resources']['foo']['Properties']['CodeUri'] == ( './deployment.zip' ) assert template['Resources']['bar']['Properties']['CodeUri'] == ( './deployment.zip' ) def test_terraform_post_processor_moves_files_once(): mock_osutils = mock.Mock(spec=OSUtils) p = package.TerraformCodeLocationPostProcessor(mock_osutils) template = { 'resource': { 'aws_lambda_function': { 'foo': {'filename': 'old-dir.zip'}, 'bar': {'filename': 'old-dir.zip'}, } } } p.process(template, config=None, outdir='outdir', chalice_stage_name='dev') mock_osutils.copy.assert_called_with( 'old-dir.zip', os.path.join('outdir', 'deployment.zip')) assert mock_osutils.copy.call_count == 1 assert template['resource']['aws_lambda_function'][ 'foo']['filename'] == ('${path.module}/deployment.zip') assert template['resource']['aws_lambda_function'][ 'bar']['filename'] == ('${path.module}/deployment.zip') def test_template_generator_default(): tgen = package.TemplateGenerator(Config(), PackageOptions( mock.Mock(spec=TypedAWSClient) )) with pytest.raises(package.UnsupportedFeatureError): tgen.dispatch(models.Model(), {}) class TestTemplateMergePostProcessor(object): def _test_can_call_merge(self, file_template, template_name): mock_osutils = mock.Mock(spec=OSUtils) mock_osutils.get_file_contents.return_value = json.dumps(file_template) mock_merger = mock.Mock(spec=package.TemplateMerger) mock_merger.merge.return_value = {} p = package.TemplateMergePostProcessor( mock_osutils, mock_merger, package.JSONTemplateSerializer(), merge_template=template_name) template = { 'Resources': { 'foo': { 'Type': 'AWS::Serverless::Function', 'Properties': { 'CodeUri': 'old-dir.zip', } }, 'bar': { 'Type': 'AWS::Serverless::Function', 'Properties': { 'CodeUri': 'old-dir.zip', } }, } } config = mock.MagicMock(spec=Config) p.process( template, config=config, outdir='outdir', chalice_stage_name='dev') assert mock_osutils.file_exists.call_count == 1 assert mock_osutils.get_file_contents.call_count == 1 mock_merger.merge.assert_called_once_with(file_template, template) def test_can_call_merge(self): file_template = { "Resources": { "foo": { "Properties": { "Environment": { "Variables": {"Name": "Foo"} } } } } } self._test_can_call_merge(file_template, 'extras.json') def test_can_call_merge_with_yaml(self): file_template = ''' Resources: foo: Properties: Environment: Variables: Name: Foo ''' self._test_can_call_merge(file_template, 'extras.yaml') def test_raise_on_bad_json(self): mock_osutils = mock.Mock(spec=OSUtils) mock_osutils.get_file_contents.return_value = ( '{' ' "Resources": {' ' "foo": {' ' "Properties": {' ' "Environment": {' ' "Variables": {"Name": "Foo"}' '' ) mock_merger = mock.Mock(spec=package.TemplateMerger) p = package.TemplateMergePostProcessor( mock_osutils, mock_merger, package.JSONTemplateSerializer(), merge_template='extras.json') template = {} config = mock.MagicMock(spec=Config) with pytest.raises(RuntimeError) as e: p.process( template, config=config, outdir='outdir', chalice_stage_name='dev', ) assert str(e.value).startswith('Expected') assert 'to be valid JSON template' in str(e.value) assert mock_merger.merge.call_count == 0 def test_raise_on_bad_yaml(self): mock_osutils = mock.Mock(spec=OSUtils) mock_osutils.get_file_contents.return_value = ( '---' 'Resources:' ' foo:' ' Properties:' ' Environment:' ' - 123' '' ) mock_merger = mock.Mock(spec=package.TemplateMerger) p = package.TemplateMergePostProcessor( mock_osutils, mock_merger, package.YAMLTemplateSerializer(), merge_template='extras.yaml') template = {} config = mock.MagicMock(spec=Config) with pytest.raises(RuntimeError) as e: p.process( template, config=config, outdir='outdir', chalice_stage_name='dev', ) assert str(e.value).startswith('Expected') assert 'to be valid YAML template' in str(e.value) assert mock_merger.merge.call_count == 0 def test_raise_if_file_does_not_exist(self): mock_osutils = mock.Mock(spec=OSUtils) mock_osutils.file_exists.return_value = False mock_merger = mock.Mock(spec=package.TemplateMerger) p = package.TemplateMergePostProcessor( mock_osutils, mock_merger, package.JSONTemplateSerializer(), merge_template='extras.json') template = {} config = mock.MagicMock(spec=Config) with pytest.raises(RuntimeError) as e: p.process( template, config=config, outdir='outdir', chalice_stage_name='dev', ) assert str(e.value).startswith('Cannot find template file:') assert mock_merger.merge.call_count == 0 class TestCompositePostProcessor(object): def test_can_call_no_processors(self): processor = package.CompositePostProcessor([]) template = {} config = mock.MagicMock(spec=Config) processor.process(template, config, 'out', 'dev') assert template == {} def test_does_call_processors_once(self): mock_processor_a = mock.Mock(spec=package.TemplatePostProcessor) mock_processor_b = mock.Mock(spec=package.TemplatePostProcessor) processor = package.CompositePostProcessor( [mock_processor_a, mock_processor_b]) template = {} config = mock.MagicMock(spec=Config) processor.process(template, config, 'out', 'dev') mock_processor_a.process.assert_called_once_with( template, config, 'out', 'dev') mock_processor_b.process.assert_called_once_with( template, config, 'out', 'dev') class TemplateTestBase(object): template_gen_factory = None def setup_method(self, stubbed_session): self.resource_builder = package.ResourceBuilder( application_builder=ApplicationGraphBuilder(), deps_builder=DependencyBuilder(), build_stage=mock.Mock(spec=BuildStage) ) client = TypedAWSClient(None) m_client = mock.Mock(wraps=client, spec=TypedAWSClient) type(m_client).region_name = mock.PropertyMock( return_value='us-west-2') self.pkg_options = PackageOptions(m_client) self.template_gen = self.template_gen_factory( Config(), self.pkg_options) def generate_template(self, config, chalice_stage_name='dev', options=None): resources = self.resource_builder.construct_resources( config, chalice_stage_name) if options is None: options = self.pkg_options return self.template_gen_factory(config, options).generate(resources) def lambda_function(self): return models.LambdaFunction( resource_name='foo', function_name='app-dev-foo', environment_variables={}, runtime='python27', handler='app.app', tags={'foo': 'bar'}, timeout=120, xray=None, memory_size=128, deployment_package=models.DeploymentPackage(filename='foo.zip'), role=models.PreCreatedIAMRole(role_arn='role:arn'), security_group_ids=[], subnet_ids=[], layers=[], reserved_concurrency=None, ) def managed_layer(self): return models.LambdaLayer( resource_name='layer', layer_name='bar', runtime='python2.7', deployment_package=models.DeploymentPackage(filename='layer.zip') ) class TestPackageOptions(object): def test_service_principal(self): awsclient = mock.Mock(spec=TypedAWSClient) awsclient.region_name = 'us-east-1' awsclient.endpoint_dns_suffix.return_value = 'amazonaws.com' awsclient.service_principal.return_value = 'lambda.amazonaws.com' options = package.PackageOptions(awsclient) principal = options.service_principal('lambda') assert principal == 'lambda.amazonaws.com' awsclient.endpoint_dns_suffix.assert_called_once_with('lambda', 'us-east-1') awsclient.service_principal.assert_called_once_with('lambda', 'us-east-1', 'amazonaws.com') class TestTerraformTemplate(TemplateTestBase): template_gen_factory = package.TerraformGenerator EmptyPolicy = { 'Version': '2012-10-18', 'Statement': { 'Sid': '', 'Effect': 'Allow', 'Action': 'lambda:*' } } def generate_template(self, config, chalice_stage_name='dev', options=None): resources = self.resource_builder.construct_resources( config, chalice_stage_name) # Patch up resources that have mocks (due to build stage) # that we need to serialize to json. for r in resources: # For terraform rest api construction, we need a swagger # doc on the api resource as we'll be serializing it to # json. if isinstance(r, models.RestAPI): r.swagger_doc = { 'info': {'title': 'some-app'}, 'x-amazon-apigateway-binary-media-types': [] } if (isinstance(r, models.RestAPI) and config.api_gateway_endpoint_type == 'PRIVATE'): r.swagger_doc['x-amazon-apigateway-policy'] = ( r.policy.document) # Same for iam policies on roles elif isinstance(r, models.FileBasedIAMPolicy): r.document = self.EmptyPolicy if options is None: options = self.pkg_options return self.template_gen_factory(config, options).generate(resources) def get_function(self, template): functions = list(template['resource'][ 'aws_lambda_function'].values()) assert len(functions) == 1 return functions[0] def test_supports_precreated_role(self): builder = DependencyBuilder() resources = builder.build_dependencies( models.Application( stage='dev', resources=[self.lambda_function()], ) ) template = self.template_gen.generate(resources) assert template['resource'][ 'aws_lambda_function']['foo']['role'] == 'role:arn' def test_adds_env_vars_when_provided(self, sample_app): function = self.lambda_function() function.environment_variables = {'foo': 'bar'} template = self.template_gen.generate([function]) tf_resource = self.get_function(template) assert tf_resource['environment'] == { 'variables': { 'foo': 'bar' } } def test_adds_vpc_config_when_provided(self): function = self.lambda_function() function.security_group_ids = ['sg1', 'sg2'] function.subnet_ids = ['sn1', 'sn2'] template = self.template_gen.generate([function]) tf_resource = self.get_function(template) assert tf_resource['vpc_config'] == { 'subnet_ids': ['sn1', 'sn2'], 'security_group_ids': ['sg1', 'sg2']} def test_adds_layers_when_provided(self): function = self.lambda_function() function.layers = layers = ['arn://layer1', 'arn://layer2'] template = self.template_gen.generate([function]) tf_resource = self.get_function(template) assert tf_resource['layers'] == layers def test_adds_managed_layer_when_provided(self): function = self.lambda_function() function.layers = ['arn://layer1', 'arn://layer2'] function.managed_layer = self.managed_layer() template = self.template_gen.generate( [function.managed_layer, function]) tf_resource = self.get_function(template) assert tf_resource['layers'] == [ '${aws_lambda_layer_version.layer.arn}', 'arn://layer1', 'arn://layer2', ] assert template['resource']['aws_lambda_layer_version']['layer'] == { 'layer_name': 'bar', 'compatible_runtimes': ['python2.7'], 'filename': 'layer.zip', } def test_adds_reserved_concurrency_when_provided(self, sample_app): function = self.lambda_function() function.reserved_concurrency = 5 template = self.template_gen.generate([function]) tf_resource = self.get_function(template) assert tf_resource['reserved_concurrent_executions'] == 5 def test_adds_log_group_resource_when_configured(self, sample_app): function = self.lambda_function() name = function.resource_name + '-log-group' function.log_group = models.LogGroup( resource_name=name, log_group_name='/aws/lambda/%s' % function.function_name, retention_in_days=7) template = self.template_gen.generate([function]) log_resource = template['resource']['aws_cloudwatch_log_group'][name] assert log_resource == { 'name': name, 'retention_in_days': 7, } def test_can_add_tracing_config(self, sample_app): function = self.lambda_function() function.xray = True template = self.template_gen.generate([function]) tf_resource = self.get_function(template) assert tf_resource['tracing_config']['mode'] == 'Active' def test_can_generate_cloudwatch_event(self): function = self.lambda_function() event = models.CloudWatchEvent( resource_name='foo-event', rule_name='myrule', event_pattern='{"source": ["aws.ec2"]}', lambda_function=function, ) template = self.template_gen.generate( [function, event] ) rule = template['resource'][ 'aws_cloudwatch_event_rule'][event.resource_name] assert rule == { 'name': event.resource_name, 'event_pattern': event.event_pattern} target = template['resource'][ 'aws_cloudwatch_event_target'][event.resource_name] assert target == { 'target_id': 'foo-event', 'rule': '${aws_cloudwatch_event_rule.foo-event.name}', 'arn': '${aws_lambda_function.foo.arn}', } def test_can_generate_scheduled_event(self): function = self.lambda_function() event = models.ScheduledEvent( resource_name='foo-event', rule_name='myrule', schedule_expression='rate(5 minutes)', lambda_function=function, rule_description='description', ) template = self.template_gen.generate( [function, event] ) rule = template['resource'][ 'aws_cloudwatch_event_rule'][event.resource_name] assert rule == { 'name': event.resource_name, 'schedule_expression': 'rate(5 minutes)', 'description': 'description', } def test_can_generate_rest_api(self, sample_app_with_auth): config = Config.create(chalice_app=sample_app_with_auth, project_dir='.', minimum_compression_size=8192, api_gateway_endpoint_type='PRIVATE', api_gateway_endpoint_vpce='vpce-abc123', app_name='sample_app', api_gateway_stage='api') template = self.generate_template(config) resources = template['resource'] # Lambda function should be created. assert resources['aws_lambda_function'] # Along with permission to invoke from API Gateway. assert list(resources['aws_lambda_permission'].values())[0] == { 'function_name': '${aws_lambda_function.api_handler.arn}', 'action': 'lambda:InvokeFunction', 'principal': 'apigateway.amazonaws.com', 'source_arn': ( '${aws_api_gateway_rest_api.rest_api.execution_arn}/*') } assert 'aws_api_gateway_rest_api' in resources assert 'rest_api' in resources['aws_api_gateway_rest_api'] resource_policy = resources[ 'aws_api_gateway_rest_api']['rest_api']['policy'] assert json.loads(resource_policy) == { 'Version': '2012-10-17', 'Statement': [ { 'Action': 'execute-api:Invoke', 'Resource': 'arn:*:execute-api:*:*:*', 'Effect': 'Allow', 'Condition': { 'StringEquals': { 'aws:SourceVpce': 'vpce-abc123' } }, 'Principal': '*' } ] } assert resources['aws_api_gateway_rest_api'][ 'rest_api']['minimum_compression_size'] == 8192 assert resources['aws_api_gateway_rest_api'][ 'rest_api']['endpoint_configuration'] == {'types': ['PRIVATE']} assert 'aws_api_gateway_stage' not in resources assert resources['aws_api_gateway_deployment']['rest_api'] == { 'rest_api_id': '${aws_api_gateway_rest_api.rest_api.id}', 'stage_description': ( '${md5(local.chalice_api_swagger)}'), 'stage_name': 'api', 'lifecycle': {'create_before_destroy': True} } # We should also create the auth lambda function. assert 'myauth' in resources['aws_lambda_function'] # Along with permission to invoke from API Gateway. assert resources['aws_lambda_permission']['myauth_invoke'] == { 'action': 'lambda:InvokeFunction', 'function_name': '${aws_lambda_function.myauth.arn}', 'principal': 'apigateway.amazonaws.com', 'source_arn': ( '${aws_api_gateway_rest_api.rest_api.execution_arn}/*') } # Also verify we add the expected outputs when using # a Rest API. assert template['output'] == { 'EndpointURL': { 'value': '${aws_api_gateway_deployment.rest_api.invoke_url}'}, 'RestAPIId': { 'value': '${aws_api_gateway_rest_api.rest_api.id}'} } def test_can_package_s3_event_handler_with_tf_ref(self, sample_app): @sample_app.on_s3_event( bucket='${aws_s3_bucket.my_data_bucket.id}') def handler(event): pass config = Config.create(chalice_app=sample_app, project_dir='.', api_gateway_stage='api') template = self.generate_template(config) assert template['resource']['aws_s3_bucket_notification'][ 'my_data_bucket_notify'] == { 'bucket': '${aws_s3_bucket.my_data_bucket.id}', 'lambda_function': [{ 'events': ['s3:ObjectCreated:*'], 'lambda_function_arn': ( '${aws_lambda_function.handler.arn}') }] } def test_can_generate_chalice_terraform_static_data(self, sample_app): config = Config.create(chalice_app=sample_app, project_dir='.', app_name='myfoo', api_gateway_stage='dev') template = self.generate_template(config) assert template['data']['null_data_source']['chalice']['inputs'] == { 'app': 'myfoo', 'stage': 'dev' } def test_can_package_s3_event_handler_sans_filters(self, sample_app): @sample_app.on_s3_event(bucket='foo') def handler(event): pass config = Config.create(chalice_app=sample_app, project_dir='.', api_gateway_stage='api') template = self.generate_template(config) assert template['resource']['aws_s3_bucket_notification'][ 'foo_notify'] == { 'bucket': 'foo', 'lambda_function': [{ 'events': ['s3:ObjectCreated:*'], 'lambda_function_arn': ( '${aws_lambda_function.handler.arn}') }] } def test_can_package_s3_event_handler(self, sample_app): @sample_app.on_s3_event( bucket='foo', prefix='incoming', suffix='.csv') def handler(event): pass config = Config.create(chalice_app=sample_app, project_dir='.', app_name='sample_app', api_gateway_stage='api') template = self.generate_template(config) assert template['resource']['aws_lambda_permission'][ 'handler-s3event'] == { 'action': 'lambda:InvokeFunction', 'function_name': '${aws_lambda_function.handler.arn}', 'principal': 's3.amazonaws.com', 'source_account': ( '${data.aws_caller_identity.chalice.account_id}'), 'source_arn': ( 'arn:${data.aws_partition.chalice.partition}:s3:::foo'), 'statement_id': 'handler-s3event' } assert template['resource']['aws_s3_bucket_notification'][ 'foo_notify'] == { 'bucket': 'foo', 'lambda_function': [{ 'events': ['s3:ObjectCreated:*'], 'filter_prefix': 'incoming', 'filter_suffix': '.csv', 'lambda_function_arn': ( '${aws_lambda_function.handler.arn}') }] } def test_can_package_sns_handler(self, sample_app): @sample_app.on_sns_message(topic='foo') def handler(event): pass config = Config.create(chalice_app=sample_app, project_dir='.', api_gateway_stage='api') template = self.generate_template(config) assert template['resource']['aws_sns_topic_subscription'][ 'handler-sns-subscription'] == { 'topic_arn': ( 'arn:${data.aws_partition.chalice.partition}:sns' ':${data.aws_region.chalice.name}:' '${data.aws_caller_identity.chalice.account_id}:foo'), 'protocol': 'lambda', 'endpoint': '${aws_lambda_function.handler.arn}' } def test_can_package_sns_arn_handler(self, sample_app): arn = 'arn:aws:sns:space-leo-1:1234567890:foo' @sample_app.on_sns_message(topic=arn) def handler(event): pass config = Config.create(chalice_app=sample_app, project_dir='.', app_name='sample_app', api_gateway_stage='api') template = self.generate_template(config) assert template['resource']['aws_sns_topic_subscription'][ 'handler-sns-subscription'] == { 'topic_arn': arn, 'protocol': 'lambda', 'endpoint': '${aws_lambda_function.handler.arn}' } assert template['resource']['aws_lambda_permission'][ 'handler-sns-subscription'] == { 'function_name': '${aws_lambda_function.handler.arn}', 'action': 'lambda:InvokeFunction', 'principal': 'sns.amazonaws.com', 'source_arn': 'arn:aws:sns:space-leo-1:1234567890:foo' } def test_can_package_sqs_handler(self, sample_app): @sample_app.on_sqs_message(queue='foo', batch_size=5) def handler(event): pass config = Config.create(chalice_app=sample_app, project_dir='.', app_name='sample_app', api_gateway_stage='api') template = self.generate_template(config) assert template['resource'][ 'aws_lambda_event_source_mapping'][ 'handler-sqs-event-source'] == { 'event_source_arn': ( 'arn:${data.aws_partition.chalice.partition}:sqs' ':${data.aws_region.chalice.name}:' '${data.aws_caller_identity.chalice.account_id}:foo'), 'function_name': '${aws_lambda_function.handler.arn}', 'batch_size': 5, 'maximum_batching_window_in_seconds': 0 } def test_can_package_sqs_handler_with_max_concurrency(self, sample_app): @sample_app.on_sqs_message( queue='foo', batch_size=5, maximum_concurrency=2 ) def handler(event): pass config = Config.create(chalice_app=sample_app, project_dir='.', app_name='sample_app', api_gateway_stage='api') template = self.generate_template(config) assert template['resource'][ 'aws_lambda_event_source_mapping'][ 'handler-sqs-event-source'] == { 'event_source_arn': ( 'arn:${data.aws_partition.chalice.partition}:sqs' ':${data.aws_region.chalice.name}:' '${data.aws_caller_identity.chalice.account_id}:foo'), 'function_name': '${aws_lambda_function.handler.arn}', 'batch_size': 5, 'maximum_batching_window_in_seconds': 0, 'scaling_config': {'maximum_concurrency': 2} } def test_sqs_arn_does_not_use_fn_sub(self, sample_app): @sample_app.on_sqs_message(queue_arn='arn:foo:bar', batch_size=5) def handler(event): pass config = Config.create(chalice_app=sample_app, project_dir='.', app_name='sample_app', api_gateway_stage='api') template = self.generate_template(config) assert template['resource'][ 'aws_lambda_event_source_mapping'][ 'handler-sqs-event-source'] == { 'event_source_arn': 'arn:foo:bar', 'function_name': '${aws_lambda_function.handler.arn}', 'batch_size': 5, 'maximum_batching_window_in_seconds': 0 } def test_can_package_kinesis_handler(self, sample_app): @sample_app.on_kinesis_record(stream='mystream', batch_size=5, starting_position='TRIM_HORIZON') def handler(event): pass config = Config.create(chalice_app=sample_app, project_dir='.', app_name='sample_app', api_gateway_stage='api') template = self.generate_template(config) assert template['resource'][ 'aws_lambda_event_source_mapping'][ 'handler-kinesis-event-source'] == { 'event_source_arn': ( 'arn:${data.aws_partition.chalice.partition}:kinesis' ':${data.aws_region.chalice.name}:' '${data.aws_caller_identity.chalice.account_id}' ':stream/mystream'), 'function_name': '${aws_lambda_function.handler.arn}', 'starting_position': 'TRIM_HORIZON', 'batch_size': 5, 'maximum_batching_window_in_seconds': 0 } def test_can_package_dynamodb_handler(self, sample_app): @sample_app.on_dynamodb_record(stream_arn='arn:aws:...:stream', batch_size=5, starting_position='TRIM_HORIZON') def handler(event): pass config = Config.create(chalice_app=sample_app, project_dir='.', app_name='sample_app', api_gateway_stage='api') template = self.generate_template(config) assert template['resource'][ 'aws_lambda_event_source_mapping'][ 'handler-dynamodb-event-source'] == { 'event_source_arn': 'arn:aws:...:stream', 'function_name': '${aws_lambda_function.handler.arn}', 'starting_position': 'TRIM_HORIZON', 'batch_size': 5, 'maximum_batching_window_in_seconds': 0 } def test_can_generate_websockets_api(self, sample_websocket_app): config = Config.create(chalice_app=sample_websocket_app, project_dir='.', app_name='sample_app', api_gateway_stage='api') template = self.generate_template(config) assert template['output'] == { 'WebsocketAPIId': { 'value': '${aws_apigatewayv2_api.websocket_api.id}' }, 'WebsocketConnectHandlerArn': { 'value': '${aws_lambda_function.websocket_connect.arn}' }, 'WebsocketConnectHandlerName': { 'value': ( '${aws_lambda_function.websocket_connect.function_name}') }, 'WebsocketMessageHandlerArn': { 'value': '${aws_lambda_function.websocket_message.arn}' }, 'WebsocketMessageHandlerName': { 'value': ( '${aws_lambda_function.websocket_message.function_name}') }, 'WebsocketDisconnectHandlerArn': { 'value': '${aws_lambda_function.websocket_disconnect.arn}' }, 'WebsocketDisconnectHandlerName': { 'value': ( '${aws_lambda_function.websocket_disconnect' '.function_name}') }, 'WebsocketConnectEndpointURL': { 'value': 'wss://${aws_apigatewayv2_api.websocket_api.id}' '.execute-api.${data.aws_region.chalice.name}' '.amazonaws.com/api/' } } assert template['resource']['aws_apigatewayv2_api'] == { 'websocket_api': { 'name': 'sample_app-dev-websocket-api', 'route_selection_expression': '$request.body.action', 'protocol_type': 'WEBSOCKET' } } assert template['resource']['aws_apigatewayv2_integration'] == { 'websocket_connect_api_integration': { 'api_id': '${aws_apigatewayv2_api.websocket_api.id}', 'connection_type': 'INTERNET', 'content_handling_strategy': 'CONVERT_TO_TEXT', 'integration_type': 'AWS_PROXY', 'integration_uri': 'arn:${data.aws_partition.chalice' '.partition}:apigateway:' '${data.aws_region.chalice.name}' ':lambda:path/2015-03-31/functions/arn' ':${data.aws_partition.chalice.partition}' ':lambda:${data.aws_region.chalice.name}' ':${data.aws_caller_identity' '.chalice.account_id}:function' ':${aws_lambda_function.websocket_connect' '.function_name}/invocations' }, 'websocket_message_api_integration': { 'api_id': '${aws_apigatewayv2_api.websocket_api.id}', 'connection_type': 'INTERNET', 'content_handling_strategy': 'CONVERT_TO_TEXT', 'integration_type': 'AWS_PROXY', 'integration_uri': 'arn:${data.aws_partition.chalice' '.partition}:apigateway' ':${data.aws_region.chalice.name}' ':lambda:path/2015-03-31/functions/arn' ':${data.aws_partition.chalice.partition}' ':lambda:${data.aws_region.chalice.name}' ':${data.aws_caller_identity.chalice' '.account_id}:function' ':${aws_lambda_function.websocket_message' '.function_name}/invocations' }, 'websocket_disconnect_api_integration': { 'api_id': '${aws_apigatewayv2_api.websocket_api.id}', 'connection_type': 'INTERNET', 'content_handling_strategy': 'CONVERT_TO_TEXT', 'integration_type': 'AWS_PROXY', 'integration_uri': 'arn:${data.aws_partition' '.chalice.partition}:apigateway' ':${data.aws_region.chalice.name}' ':lambda:path/2015-03-31/functions/arn' ':${data.aws_partition.chalice.partition}' ':lambda:${data.aws_region.chalice.name}' ':${data.aws_caller_identity' '.chalice.account_id}:function' ':${aws_lambda_function' '.websocket_disconnect.function_name}' '/invocations' } } assert template['resource']['aws_lambda_permission'] == { 'websocket_connect_invoke_permission': { 'function_name': '${aws_lambda_function.websocket_connect' '.function_name}', 'action': 'lambda:InvokeFunction', 'principal': 'apigateway.amazonaws.com', 'source_arn': 'arn:${data.aws_partition.chalice.partition}' ':execute-api:${data.aws_region.chalice.name}' ':${data.aws_caller_identity.chalice.account_id}' ':${aws_apigatewayv2_api.websocket_api.id}/*' }, 'websocket_message_invoke_permission': { 'function_name': '${aws_lambda_function.websocket_message' '.function_name}', 'action': 'lambda:InvokeFunction', 'principal': 'apigateway.amazonaws.com', 'source_arn': 'arn:${data.aws_partition.chalice.partition}' ':execute-api:${data.aws_region.chalice.name}' ':${data.aws_caller_identity.chalice.account_id}' ':${aws_apigatewayv2_api.websocket_api.id}/*' }, 'websocket_disconnect_invoke_permission': { 'function_name': '${aws_lambda_function.websocket_disconnect' '.function_name}', 'action': 'lambda:InvokeFunction', 'principal': 'apigateway.amazonaws.com', 'source_arn': 'arn:${data.aws_partition.chalice.partition}' ':execute-api:${data.aws_region.chalice.name}' ':${data.aws_caller_identity.chalice.account_id}' ':${aws_apigatewayv2_api.websocket_api.id}/*' } } assert template['resource']['aws_apigatewayv2_route'] == { 'websocket_connect_route': { 'api_id': '${aws_apigatewayv2_api.websocket_api.id}', 'route_key': '$connect', 'target': 'integrations/${aws_apigatewayv2_integration' '.websocket_connect_api_integration.id}' }, 'websocket_message_route': { 'api_id': '${aws_apigatewayv2_api.websocket_api.id}', 'route_key': '$default', 'target': 'integrations/${aws_apigatewayv2_integration' '.websocket_message_api_integration.id}' }, 'websocket_disconnect_route': { 'api_id': '${aws_apigatewayv2_api.websocket_api.id}', 'route_key': '$disconnect', 'target': 'integrations/${aws_apigatewayv2_integration' '.websocket_disconnect_api_integration.id}' } } assert template['resource']['aws_apigatewayv2_deployment'] == { 'websocket_api_deployment': { 'api_id': '${aws_apigatewayv2_api.websocket_api.id}', 'depends_on': [ 'aws_apigatewayv2_route.websocket_connect_route', 'aws_apigatewayv2_route.websocket_message_route', 'aws_apigatewayv2_route.websocket_disconnect_route' ]}} assert template['resource']['aws_apigatewayv2_stage'] == { 'websocket_api_stage': { 'api_id': '${aws_apigatewayv2_api.websocket_api.id}', 'deployment_id': '${aws_apigatewayv2_deployment' '.websocket_api_deployment.id}', 'name': 'api' } } def test_can_generate_custom_domain_name(self, sample_app): config = Config.create( chalice_app=sample_app, project_dir='.', api_gateway_stage='api', api_gateway_endpoint_type='EDGE', api_gateway_custom_domain={ "certificate_arn": "my_cert_arn", "domain_name": "example.com", "tls_version": "TLS_1_2", "tags": {"foo": "bar"}, } ) template = self.generate_template(config) assert template['resource']['aws_api_gateway_domain_name'][ 'api_gateway_custom_domain'] == { 'domain_name': 'example.com', 'certificate_arn': 'my_cert_arn', 'security_policy': 'TLS_1_2', 'endpoint_configuration': {'types': ['EDGE']}, 'tags': {'foo': 'bar'}, } assert template['resource']['aws_api_gateway_base_path_mapping'][ 'api_gateway_custom_domain_mapping'] == { 'api_id': '${aws_api_gateway_rest_api.rest_api.id}', 'stage_name': 'api', 'domain_name': 'example.com', } outputs = template['output'] assert outputs['AliasDomainName']['value'] == ( '${aws_api_gateway_domain_name.api_gateway_custom_domain' '.cloudfront_domain_name}' ) assert outputs['HostedZoneId']['value'] == ( '${aws_api_gateway_domain_name.api_gateway_custom_domain' '.cloudfront_zone_id}' ) def test_can_generate_domain_for_regional_endpoint(self, sample_app): config = Config.create( chalice_app=sample_app, project_dir='.', api_gateway_stage='api', api_gateway_endpoint_type='REGIONAL', api_gateway_custom_domain={ "certificate_arn": "my_cert_arn", "domain_name": "example.com", } ) template = self.generate_template(config) assert template['resource']['aws_api_gateway_domain_name'][ 'api_gateway_custom_domain'] == { 'domain_name': 'example.com', 'regional_certificate_arn': 'my_cert_arn', 'endpoint_configuration': {'types': ['REGIONAL']}, } assert template['resource']['aws_api_gateway_base_path_mapping'][ 'api_gateway_custom_domain_mapping'] == { 'api_id': '${aws_api_gateway_rest_api.rest_api.id}', 'stage_name': 'api', 'domain_name': 'example.com', } outputs = template['output'] assert outputs['AliasDomainName']['value'] == ( '${aws_api_gateway_domain_name.api_gateway_custom_domain' '.regional_domain_name}' ) assert outputs['HostedZoneId']['value'] == ( '${aws_api_gateway_domain_name.api_gateway_custom_domain' '.regional_zone_id}' ) class TestSAMTemplate(TemplateTestBase): template_gen_factory = package.SAMTemplateGenerator def test_sam_generates_sam_template_basic(self, sample_app): config = Config.create(chalice_app=sample_app, project_dir='.', automatic_layer=True, api_gateway_stage='api') template = self.generate_template(config) # Verify the basic structure is in place. The specific parts # are validated in other tests. assert template['AWSTemplateFormatVersion'] == '2010-09-09' assert template['Transform'] == 'AWS::Serverless-2016-10-31' assert 'Outputs' in template assert 'Resources' in template assert list(sorted(template['Resources'])) == [ 'APIHandler', 'APIHandlerInvokePermission', # This casing on the ApiHandlerRole name is unfortunate, but the 3 # other resources in this list are hardcoded from the old deployer. 'ApiHandlerRole', 'ManagedLayer', 'RestAPI' ] def test_can_generate_lambda_layer_if_configured(self, sample_app): config = Config.create(chalice_app=sample_app, app_name='testapp', project_dir='.', automatic_layer=True, api_gateway_stage='api') template = self.generate_template(config) managed_layer = template['Resources']['ManagedLayer'] assert managed_layer == { 'Type': 'AWS::Serverless::LayerVersion', 'Properties': { 'CompatibleRuntimes': [config.lambda_python_version], 'LayerName': 'testapp-dev-managed-layer', 'ContentUri': models.Placeholder.BUILD_STAGE, } } assert template['Resources']['APIHandler']['Properties']['Layers'] == [ {'Ref': 'ManagedLayer'} ] def test_adds_single_layer_for_multiple_lambdas(self, sample_app): config = Config.create(chalice_app=sample_app, app_name='testapp', project_dir='.', automatic_layer=True, layers=['arn:aws:mylayer'], api_gateway_stage='api') @sample_app.lambda_function() def first(event, context): pass @sample_app.lambda_function() def second(event, context): pass template = self.generate_template(config) managed_layer = template['Resources']['ManagedLayer'] assert managed_layer == { 'Type': 'AWS::Serverless::LayerVersion', 'Properties': { 'CompatibleRuntimes': [config.lambda_python_version], 'LayerName': 'testapp-dev-managed-layer', 'ContentUri': models.Placeholder.BUILD_STAGE, } } assert template['Resources']['First']['Properties']['Layers'] == [ {'Ref': 'ManagedLayer'}, 'arn:aws:mylayer', ] def test_supports_precreated_role(self): builder = DependencyBuilder() resources = builder.build_dependencies( models.Application( stage='dev', resources=[self.lambda_function()], ) ) template = self.template_gen.generate(resources) assert template['Resources']['Foo']['Properties']['Role'] == 'role:arn' def test_sam_injects_policy(self, sample_app): function = models.LambdaFunction( resource_name='foo', function_name='app-dev-foo', environment_variables={}, runtime='python27', handler='app.app', tags={'foo': 'bar'}, timeout=120, memory_size=128, xray=None, deployment_package=models.DeploymentPackage(filename='foo.zip'), role=models.ManagedIAMRole( resource_name='role', role_name='app-role', trust_policy={}, policy=models.AutoGenIAMPolicy(document={'iam': 'policy'}), ), security_group_ids=[], subnet_ids=[], layers=[], reserved_concurrency=None, ) template = self.template_gen.generate([function]) cfn_resource = list(template['Resources'].values())[0] assert cfn_resource == { 'Type': 'AWS::Serverless::Function', 'Properties': { 'CodeUri': 'foo.zip', 'Handler': 'app.app', 'MemorySize': 128, 'Tracing': 'PassThrough', 'Role': {'Fn::GetAtt': ['Role', 'Arn']}, 'Runtime': 'python27', 'Tags': {'foo': 'bar'}, 'Timeout': 120 }, } def test_adds_env_vars_when_provided(self, sample_app): function = self.lambda_function() function.environment_variables = {'foo': 'bar'} template = self.template_gen.generate([function]) cfn_resource = list(template['Resources'].values())[0] assert cfn_resource['Properties']['Environment'] == { 'Variables': { 'foo': 'bar' } } def test_adds_vpc_config_when_provided(self): function = self.lambda_function() function.security_group_ids = ['sg1', 'sg2'] function.subnet_ids = ['sn1', 'sn2'] template = self.template_gen.generate([function]) cfn_resource = list(template['Resources'].values())[0] assert cfn_resource['Properties']['VpcConfig'] == { 'SecurityGroupIds': ['sg1', 'sg2'], 'SubnetIds': ['sn1', 'sn2'], } def test_adds_reserved_concurrency_when_provided(self, sample_app): function = self.lambda_function() function.reserved_concurrency = 5 template = self.template_gen.generate([function]) cfn_resource = list(template['Resources'].values())[0] assert cfn_resource['Properties']['ReservedConcurrentExecutions'] == 5 def test_adds_log_group_resource_when_configured(self, sample_app): function = self.lambda_function() function.log_group = models.LogGroup( resource_name=function.resource_name + '-log-group', log_group_name='/aws/lambda/%s' % function.function_name, retention_in_days=7) template = self.template_gen.generate([function]) log_resource = template['Resources']['FooLogGroup'] assert log_resource == { 'Type': 'AWS::Logs::LogGroup', 'Properties': { 'LogGroupName': {'Fn::Sub': '/aws/lambda/${Foo}'}, 'RetentionInDays': 7 } } def test_adds_layers_when_provided(self, sample_app): function = self.lambda_function() function.layers = ['arn:aws:layer1', 'arn:aws:layer2'] template = self.template_gen.generate([function]) cfn_resource = list(template['Resources'].values())[0] assert cfn_resource['Properties']['Layers'] == [ 'arn:aws:layer1', 'arn:aws:layer2' ] def test_duplicate_resource_name_raises_error(self): one = self.lambda_function() two = self.lambda_function() one.resource_name = 'foo_bar' two.resource_name = 'foo__bar' with pytest.raises(package.DuplicateResourceNameError): self.template_gen.generate([one, two]) def test_role_arn_inserted_when_necessary(self): function = models.LambdaFunction( resource_name='foo', function_name='app-dev-foo', environment_variables={}, runtime='python27', handler='app.app', tags={'foo': 'bar'}, timeout=120, memory_size=128, xray=None, deployment_package=models.DeploymentPackage(filename='foo.zip'), role=models.PreCreatedIAMRole(role_arn='role:arn'), security_group_ids=[], subnet_ids=[], layers=[], reserved_concurrency=None, ) template = self.template_gen.generate([function]) cfn_resource = list(template['Resources'].values())[0] assert cfn_resource == { 'Type': 'AWS::Serverless::Function', 'Properties': { 'CodeUri': 'foo.zip', 'Handler': 'app.app', 'MemorySize': 128, 'Role': 'role:arn', 'Tracing': 'PassThrough', 'Runtime': 'python27', 'Tags': {'foo': 'bar'}, 'Timeout': 120 }, } def test_can_generate_cloudwatch_event(self): function = self.lambda_function() event = models.CloudWatchEvent( resource_name='foo-event', rule_name='myrule', event_pattern='{"source": ["aws.ec2"]}', lambda_function=function, ) template = self.template_gen.generate( [function, event] ) resources = template['Resources'] assert len(resources) == 1 cfn_resource = list(resources.values())[0] assert cfn_resource['Properties']['Events'] == { 'FooEvent': { 'Type': 'CloudWatchEvent', 'Properties': { 'Pattern': { 'source': [ 'aws.ec2' ] } }, }, } def test_can_generate_scheduled_event(self): function = self.lambda_function() event = models.ScheduledEvent( resource_name='foo-event', rule_name='myrule', rule_description="my rule description", schedule_expression='rate(5 minutes)', lambda_function=function, ) template = self.template_gen.generate( [function, event] ) resources = template['Resources'] assert len(resources) == 1 cfn_resource = list(resources.values())[0] assert cfn_resource['Properties']['Events'] == { 'FooEvent': { 'Type': 'Schedule', 'Properties': { 'Schedule': 'rate(5 minutes)' }, }, } def test_can_generate_rest_api_without_compression( self, sample_app_with_auth): config = Config.create(chalice_app=sample_app_with_auth, project_dir='.', api_gateway_stage='api', ) template = self.generate_template(config) resources = template['Resources'] assert 'MinimumCompressionSize' not in \ resources['RestAPI']['Properties'] def test_can_generate_rest_api(self, sample_app_with_auth): config = Config.create(chalice_app=sample_app_with_auth, project_dir='.', api_gateway_stage='api', minimum_compression_size=100, ) template = self.generate_template(config) resources = template['Resources'] # Lambda function should be created. assert resources['APIHandler']['Type'] == 'AWS::Serverless::Function' # Along with permission to invoke from API Gateway. assert resources['APIHandlerInvokePermission'] == { 'Type': 'AWS::Lambda::Permission', 'Properties': { 'Action': 'lambda:InvokeFunction', 'FunctionName': {'Ref': 'APIHandler'}, 'Principal': 'apigateway.amazonaws.com', 'SourceArn': { 'Fn::Sub': [ ('arn:${AWS::Partition}:execute-api:${AWS::Region}' ':${AWS::AccountId}:${RestAPIId}/*'), {'RestAPIId': {'Ref': 'RestAPI'}}]}}, } assert resources['RestAPI']['Type'] == 'AWS::Serverless::Api' assert resources['RestAPI']['Properties']['MinimumCompressionSize'] \ == 100 # We should also create the auth lambda function. assert resources['Myauth']['Type'] == 'AWS::Serverless::Function' # Along with permission to invoke from API Gateway. assert resources['MyauthInvokePermission'] == { 'Type': 'AWS::Lambda::Permission', 'Properties': { 'Action': 'lambda:InvokeFunction', 'FunctionName': {'Fn::GetAtt': ['Myauth', 'Arn']}, 'Principal': 'apigateway.amazonaws.com', 'SourceArn': { 'Fn::Sub': [ ('arn:${AWS::Partition}:execute-api:${AWS::Region}' ':${AWS::AccountId}:${RestAPIId}/*'), {'RestAPIId': {'Ref': 'RestAPI'}}]}}, } # Also verify we add the expected outputs when using # a Rest API. assert template['Outputs'] == { 'APIHandlerArn': { 'Value': { 'Fn::GetAtt': ['APIHandler', 'Arn'] } }, 'APIHandlerName': {'Value': {'Ref': 'APIHandler'}}, 'EndpointURL': { 'Value': { 'Fn::Sub': ( 'https://${RestAPI}.execute-api.' '${AWS::Region}.${AWS::URLSuffix}/api/' ) } }, 'RestAPIId': {'Value': {'Ref': 'RestAPI'}} } @pytest.mark.parametrize('route_key,route', [ ('$default', 'WebsocketMessageRoute'), ('$connect', 'WebsocketConnectRoute'), ('$disconnect', 'WebsocketDisconnectRoute')] ) def test_generate_partial_websocket_api( self, route_key, route, sample_websocket_app): # Remove all but one websocket route. sample_websocket_app.websocket_handlers = { name: handler for name, handler in sample_websocket_app.websocket_handlers.items() if name == route_key } config = Config.create(chalice_app=sample_websocket_app, project_dir='.', api_gateway_stage='api') template = self.generate_template(config) resources = template['Resources'] # Check that the template's deployment only depends on the one route. depends_on = resources['WebsocketAPIDeployment'].pop('DependsOn') assert [route] == depends_on def test_generate_websocket_api(self, sample_websocket_app): config = Config.create(chalice_app=sample_websocket_app, project_dir='.', api_gateway_stage='api') template = self.generate_template(config) resources = template['Resources'] assert resources['WebsocketAPI']['Type'] == 'AWS::ApiGatewayV2::Api' for handler, route in (('WebsocketConnect', '$connect'), ('WebsocketMessage', '$default'), ('WebsocketDisconnect', '$disconnect'),): # Lambda function should be created. assert resources[handler]['Type'] == 'AWS::Serverless::Function' # Along with permission to invoke from API Gateway. assert resources['%sInvokePermission' % handler] == { 'Type': 'AWS::Lambda::Permission', 'Properties': { 'Action': 'lambda:InvokeFunction', 'FunctionName': {'Ref': handler}, 'Principal': 'apigateway.amazonaws.com', 'SourceArn': { 'Fn::Sub': [ ( 'arn:${AWS::Partition}:execute-api' ':${AWS::Region}:${AWS::' 'AccountId}:${WebsocketAPIId}/*' ), {'WebsocketAPIId': {'Ref': 'WebsocketAPI'}}]}}, } # Ensure Integration is created. assert resources['%sAPIIntegration' % handler] == { 'Type': 'AWS::ApiGatewayV2::Integration', 'Properties': { 'ApiId': { 'Ref': 'WebsocketAPI' }, 'ConnectionType': 'INTERNET', 'ContentHandlingStrategy': 'CONVERT_TO_TEXT', 'IntegrationType': 'AWS_PROXY', 'IntegrationUri': { 'Fn::Sub': [ ( 'arn:${AWS::Partition}:apigateway' ':${AWS::Region}:lambda:path' '/2015-03-31/functions/arn:${AWS::Partition}' ':lambda:${AWS::Region}:${AWS::AccountId}' ':function:${WebsocketHandler}/invocations' ), {'WebsocketHandler': {'Ref': handler}} ], } } } # Route for the handler. assert resources['%sRoute' % handler] == { 'Type': 'AWS::ApiGatewayV2::Route', 'Properties': { 'ApiId': { 'Ref': 'WebsocketAPI' }, 'RouteKey': route, 'Target': { 'Fn::Join': [ '/', [ 'integrations', {'Ref': '%sAPIIntegration' % handler}, ] ] } } } # Ensure the deployment is created. It must manually depend on # the routes since it cannot be created for WebsocketAPI that has no # routes. The API has no such implicit contract so CloudFormation can # deploy things out of order without the explicit DependsOn. depends_on = set(resources['WebsocketAPIDeployment'].pop('DependsOn')) assert set(['WebsocketConnectRoute', 'WebsocketMessageRoute', 'WebsocketDisconnectRoute']) == depends_on assert resources['WebsocketAPIDeployment'] == { 'Type': 'AWS::ApiGatewayV2::Deployment', 'Properties': { 'ApiId': { 'Ref': 'WebsocketAPI' } } } # Ensure the stage is created. resources['WebsocketAPIStage'] = { 'Type': 'AWS::ApiGatewayV2::Stage', 'Properties': { 'ApiId': { 'Ref': 'WebsocketAPI' }, 'DeploymentId': {'Ref': 'WebsocketAPIDeployment'}, 'StageName': 'api', } } # Ensure the outputs are created assert template['Outputs'] == { 'WebsocketConnectHandlerArn': { 'Value': { 'Fn::GetAtt': ['WebsocketConnect', 'Arn'] } }, 'WebsocketConnectHandlerName': { 'Value': {'Ref': 'WebsocketConnect'}}, 'WebsocketMessageHandlerArn': { 'Value': { 'Fn::GetAtt': ['WebsocketMessage', 'Arn'] } }, 'WebsocketMessageHandlerName': { 'Value': {'Ref': 'WebsocketMessage'}}, 'WebsocketDisconnectHandlerArn': { 'Value': { 'Fn::GetAtt': ['WebsocketDisconnect', 'Arn'] } }, 'WebsocketDisconnectHandlerName': {'Value': { 'Ref': 'WebsocketDisconnect'}}, 'WebsocketConnectEndpointURL': { 'Value': { 'Fn::Sub': ( 'wss://${WebsocketAPI}.execute-api.' '${AWS::Region}.${AWS::URLSuffix}/api/' ) } }, 'WebsocketAPIId': {'Value': {'Ref': 'WebsocketAPI'}} } def test_managed_iam_role(self): role = models.ManagedIAMRole( resource_name='default_role', role_name='app-dev', trust_policy=LAMBDA_TRUST_POLICY, policy=models.AutoGenIAMPolicy(document={'iam': 'policy'}), ) template = self.template_gen.generate([role]) resources = template['Resources'] assert len(resources) == 1 cfn_role = resources['DefaultRole'] assert cfn_role['Type'] == 'AWS::IAM::Role' assert cfn_role['Properties']['Policies'] == [ {'PolicyName': 'DefaultRolePolicy', 'PolicyDocument': {'iam': 'policy'}} ] # Verify the trust policy is specific to the region assert cfn_role['Properties']['AssumeRolePolicyDocument'] == { 'Statement': [{'Action': 'sts:AssumeRole', 'Effect': 'Allow', 'Principal': {'Service': 'lambda.amazonaws.com'}, 'Sid': ''}], 'Version': '2012-10-17'} # Ensure the RoleName is not in the resource properties # so we don't require CAPABILITY_NAMED_IAM. assert 'RoleName' not in cfn_role['Properties'] def test_single_role_generated_for_default_config(self, sample_app_lambda_only): # The sample_app has one lambda function. # We'll add a few more and verify they all share the same role. @sample_app_lambda_only.lambda_function() def second(event, context): pass @sample_app_lambda_only.lambda_function() def third(event, context): pass config = Config.create(chalice_app=sample_app_lambda_only, project_dir='.', autogen_policy=True, api_gateway_stage='api') template = self.generate_template(config) roles = [resource for resource in template['Resources'].values() if resource['Type'] == 'AWS::IAM::Role'] assert len(roles) == 1 # The lambda functions should all reference this role. functions = [ resource for resource in template['Resources'].values() if resource['Type'] == 'AWS::Serverless::Function' ] role_names = [ function['Properties']['Role'] for function in functions ] assert role_names == [ {'Fn::GetAtt': ['DefaultRole', 'Arn']}, {'Fn::GetAtt': ['DefaultRole', 'Arn']}, {'Fn::GetAtt': ['DefaultRole', 'Arn']}, ] def test_vpc_config_added_to_function(self, sample_app_lambda_only): config = Config.create(chalice_app=sample_app_lambda_only, project_dir='.', autogen_policy=True, api_gateway_stage='api', security_group_ids=['sg1', 'sg2'], subnet_ids=['sn1', 'sn2']) template = self.generate_template(config) resources = template['Resources'].values() lambda_fns = [resource for resource in resources if resource['Type'] == 'AWS::Serverless::Function'] assert len(lambda_fns) == 1 vpc_config = lambda_fns[0]['Properties']['VpcConfig'] assert vpc_config['SubnetIds'] == ['sn1', 'sn2'] assert vpc_config['SecurityGroupIds'] == ['sg1', 'sg2'] def test_helpful_error_message_on_s3_event(self, sample_app): @sample_app.on_s3_event(bucket='foo') def handler(event): pass config = Config.create(chalice_app=sample_app, project_dir='.', api_gateway_stage='api') options = PackageOptions(mock.Mock(spec=TypedAWSClient)) with pytest.raises(NotImplementedError) as excinfo: self.generate_template(config, 'dev', options) # Should mention the decorator name. assert '@app.on_s3_event' in str(excinfo.value) # Should mention you can use `chalice deploy`. assert 'chalice deploy' in str(excinfo.value) def test_can_package_sns_handler(self, sample_app): @sample_app.on_sns_message(topic='foo') def handler(event): pass config = Config.create(chalice_app=sample_app, project_dir='.', api_gateway_stage='api') template = self.generate_template(config) sns_handler = template['Resources']['Handler'] assert sns_handler['Properties']['Events'] == { 'HandlerSnsSubscription': { 'Type': 'SNS', 'Properties': { 'Topic': { 'Fn::Sub': ( 'arn:${AWS::Partition}:sns:${AWS::Region}' ':${AWS::AccountId}:foo' ) } }, } } def test_can_package_sns_arn_handler(self, sample_app): arn = 'arn:aws:sns:space-leo-1:1234567890:foo' @sample_app.on_sns_message(topic=arn) def handler(event): pass config = Config.create(chalice_app=sample_app, project_dir='.', api_gateway_stage='api') template = self.generate_template(config) sns_handler = template['Resources']['Handler'] assert sns_handler['Properties']['Events'] == { 'HandlerSnsSubscription': { 'Type': 'SNS', 'Properties': { 'Topic': arn, } } } def test_can_package_sqs_handler(self, sample_app): @sample_app.on_sqs_message(queue='foo', batch_size=5) def handler(event): pass config = Config.create(chalice_app=sample_app, project_dir='.', api_gateway_stage='api') template = self.generate_template(config) sns_handler = template['Resources']['Handler'] assert sns_handler['Properties']['Events'] == { 'HandlerSqsEventSource': { 'Type': 'SQS', 'Properties': { 'Queue': { 'Fn::Sub': ( 'arn:${AWS::Partition}:sqs:${AWS::Region}' ':${AWS::AccountId}:foo' ) }, 'BatchSize': 5, 'MaximumBatchingWindowInSeconds': 0, }, } } def test_can_package_sqs_handler_with_max_concurrency(self, sample_app): @sample_app.on_sqs_message( queue='foo', batch_size=5, maximum_concurrency=2 ) def handler(event): pass config = Config.create(chalice_app=sample_app, project_dir='.', api_gateway_stage='api') template = self.generate_template(config) sns_handler = template['Resources']['Handler'] assert sns_handler['Properties']['Events'] == { 'HandlerSqsEventSource': { 'Type': 'SQS', 'Properties': { 'Queue': { 'Fn::Sub': ( 'arn:${AWS::Partition}:sqs:${AWS::Region}' ':${AWS::AccountId}:foo' ) }, 'BatchSize': 5, 'MaximumBatchingWindowInSeconds': 0, 'ScalingConfig': {'MaximumConcurrency': 2} }, } } def test_sqs_arn_does_not_use_fn_sub(self, sample_app): @sample_app.on_sqs_message(queue_arn='arn:foo:bar', batch_size=5) def handler(event): pass config = Config.create(chalice_app=sample_app, project_dir='.', api_gateway_stage='api') template = self.generate_template(config) sqs_handler = template['Resources']['Handler'] assert sqs_handler['Properties']['Events'] == { 'HandlerSqsEventSource': { 'Type': 'SQS', 'Properties': { 'Queue': 'arn:foo:bar', 'BatchSize': 5, 'MaximumBatchingWindowInSeconds': 0, }, } } def test_can_package_kinesis_handler(self, sample_app): @sample_app.on_kinesis_record(stream='mystream', batch_size=5) def handler(event): pass config = Config.create(chalice_app=sample_app, project_dir='.', api_gateway_stage='api') template = self.generate_template(config) sns_handler = template['Resources']['Handler'] assert sns_handler['Properties']['Events'] == { 'HandlerKinesisEventSource': { 'Type': 'Kinesis', 'Properties': { 'Stream': { 'Fn::Sub': ( 'arn:${AWS::Partition}:kinesis:${AWS::Region}' ':${AWS::AccountId}:stream/mystream' ) }, 'BatchSize': 5, 'StartingPosition': 'LATEST', 'MaximumBatchingWindowInSeconds': 0, }, } } def test_can_package_dynamodb_handler(self, sample_app): @sample_app.on_dynamodb_record(stream_arn='arn:aws:...:stream', batch_size=5) def handler(event): pass config = Config.create(chalice_app=sample_app, project_dir='.', api_gateway_stage='api') template = self.generate_template(config) ddb_handler = template['Resources']['Handler'] assert ddb_handler['Properties']['Events'] == { 'HandlerDynamodbEventSource': { 'Type': 'DynamoDB', 'Properties': { 'Stream': 'arn:aws:...:stream', 'BatchSize': 5, 'StartingPosition': 'LATEST', 'MaximumBatchingWindowInSeconds': 0, }, } } def test_can_generate_custom_domain_name(self, sample_app): config = Config.create( chalice_app=sample_app, project_dir='.', api_gateway_stage='api', api_gateway_endpoint_type='EDGE', api_gateway_custom_domain={ "certificate_arn": "my_cert_arn", "domain_name": "example.com", "tls_version": "TLS_1_2", "tags": {"foo": "bar", "bar": "baz"}, } ) template = self.generate_template(config) domain = template['Resources']['ApiGatewayCustomDomain'] mapping = template['Resources']['ApiGatewayCustomDomainMapping'] assert domain == { 'Type': 'AWS::ApiGateway::DomainName', 'Properties': { 'CertificateArn': 'my_cert_arn', 'DomainName': 'example.com', 'SecurityPolicy': 'TLS_1_2', 'EndpointConfiguration': { 'Types': ['EDGE'] }, 'Tags': [ {'Key': 'bar', 'Value': 'baz'}, {'Key': 'foo', 'Value': 'bar'} ] } } assert mapping == { 'Type': 'AWS::ApiGateway::BasePathMapping', 'Properties': { 'DomainName': {'Ref': 'ApiGatewayCustomDomain'}, 'RestApiId': {'Ref': 'RestAPI'}, 'Stage': config.api_gateway_stage, 'BasePath': '(none)', } } def test_can_generate_domain_for_regional_endpoint(self, sample_app): config = Config.create( chalice_app=sample_app, project_dir='.', api_gateway_stage='api', api_gateway_endpoint_type='REGIONAL', api_gateway_custom_domain={ "certificate_arn": "my_cert_arn", "domain_name": "example.com", } ) template = self.generate_template(config) domain = template['Resources']['ApiGatewayCustomDomain'] mapping = template['Resources']['ApiGatewayCustomDomainMapping'] assert domain == { 'Type': 'AWS::ApiGateway::DomainName', 'Properties': { 'RegionalCertificateArn': 'my_cert_arn', 'DomainName': 'example.com', 'EndpointConfiguration': { 'Types': ['REGIONAL'] } } } assert mapping == { 'Type': 'AWS::ApiGateway::BasePathMapping', 'Properties': { 'DomainName': {'Ref': 'ApiGatewayCustomDomain'}, 'RestApiId': {'Ref': 'RestAPI'}, 'Stage': config.api_gateway_stage, 'BasePath': '(none)', } } def test_can_generate_domain_for_ws_endpoint(self, sample_websocket_app): config = Config.create( chalice_app=sample_websocket_app, project_dir='.', api_gateway_stage='api', websocket_api_custom_domain={ "certificate_arn": "my_cert_arn", "domain_name": "example.com", 'tags': {'foo': 'bar', 'bar': 'baz'}, } ) template = self.generate_template(config) domain = template['Resources']['WebsocketApiCustomDomain'] mapping = template['Resources']['WebsocketApiCustomDomainMapping'] assert domain == { 'Type': 'AWS::ApiGatewayV2::DomainName', 'Properties': { 'DomainName': 'example.com', 'DomainNameConfigurations': [ {'CertificateArn': 'my_cert_arn', 'EndpointType': 'REGIONAL'}, ], 'Tags': { 'foo': 'bar', 'bar': 'baz' }, } } assert mapping == { 'Type': 'AWS::ApiGatewayV2::ApiMapping', 'Properties': { 'DomainName': {'Ref': 'WebsocketApiCustomDomain'}, 'ApiId': {'Ref': 'WebsocketAPI'}, 'Stage': {'Ref': 'WebsocketAPIStage'}, 'ApiMappingKey': '(none)', } } class TestTemplateDeepMerger(object): def test_can_merge_without_changing_identity(self): merger = package.TemplateDeepMerger() src = {} dst = {} result = merger.merge(src, dst) assert result is not src assert result is not dst assert src is not dst def test_does_not_mutate(self): merger = package.TemplateDeepMerger() src = {'foo': 'bar'} dst = {'baz': 'buz'} merger.merge(src, dst) assert src == {'foo': 'bar'} assert dst == {'baz': 'buz'} def test_can_add_element(self): merger = package.TemplateDeepMerger() src = {'foo': 'bar'} dst = {'baz': 'buz'} result = merger.merge(src, dst) assert result == { 'foo': 'bar', 'baz': 'buz', } def test_can_replace_element(self): merger = package.TemplateDeepMerger() src = {'foo': 'bar'} dst = {'foo': 'buz'} result = merger.merge(src, dst) assert result == { 'foo': 'bar', } def test_can_merge_list(self): merger = package.TemplateDeepMerger() src = {'foo': [1, 2, 3]} dst = {} result = merger.merge(src, dst) assert result == { 'foo': [1, 2, 3], } def test_can_merge_nested_elements(self): merger = package.TemplateDeepMerger() src = { 'foo': { 'bar': 'baz', }, } dst = { 'foo': { 'qux': 'quack', }, } result = merger.merge(src, dst) assert result == { 'foo': { 'bar': 'baz', 'qux': 'quack', } } def test_can_merge_nested_list(self): merger = package.TemplateDeepMerger() src = { 'foo': { 'bar': 'baz', }, } dst = { 'foo': { 'qux': [1, 2, 3, 4], }, } result = merger.merge(src, dst) assert result == { 'foo': { 'bar': 'baz', 'qux': [1, 2, 3, 4], } } def test_list_elements_are_replaced(self): merger = package.TemplateDeepMerger() src = { 'list': [{'foo': 'bar'}], } dst = { 'list': [{'foo': 'buz'}], } result = merger.merge(src, dst) assert result == { 'list': [{'foo': 'bar'}], } def test_merge_can_change_type(self): merger = package.TemplateDeepMerger() src = { 'key': 'foo', } dst = { 'key': 1, } result = merger.merge(src, dst) assert result == { 'key': 'foo' } @pytest.mark.parametrize('filename,is_yaml', [ ('extras.yaml', True), ('extras.YAML', True), ('extras.yml', True), ('extras.YML', True), ('extras.foo.yml', True), ('extras', False), ('extras.json', False), ('extras.yaml.json', False), ('foo/bar/extras.yaml', True), ('foo/bar/extras.YAML', True), ]) def test_to_cfn_resource_name(filename, is_yaml): assert package.YAMLTemplateSerializer.is_yaml_template(filename) == is_yaml @pytest.mark.parametrize('yaml_contents,expected', [ ('foo: bar', {'foo': 'bar'}), ('foo: !Ref bar', {'foo': {'Ref': 'bar'}}), ('foo: !GetAtt Bar.Baz', {'foo': {'Fn::GetAtt': ['Bar', 'Baz']}}), ('foo: !FooBar [!Baz YetAnother, "hello"]', {'foo': {'Fn::FooBar': [{'Fn::Baz': 'YetAnother'}, 'hello']}}), ('foo: !SomeTag {"a": "1"}', {'foo': {'Fn::SomeTag': {'a': '1'}}}), ('foo: !GetAtt Foo.Bar.Baz', {'foo': {'Fn::GetAtt': ['Foo', 'Bar.Baz']}}), ('foo: !Condition Other', {'foo': {'Condition': 'Other'}}), ('foo: !GetAtt ["a", "b"]', {'foo': {'Fn::GetAtt': ['a', 'b']}}), ]) def test_supports_custom_tags(yaml_contents, expected): serialize = package.YAMLTemplateSerializer() actual = serialize.load_template(yaml_contents) assert actual == expected # Also verify we can serialize then parse them back to what we originally # loaded. Note that this is not the same thing as round tripping as # we convert things like '!Ref foo' over to {'Ref': 'foo'}. yaml_str = serialize.serialize_template(actual).strip() reparsed = serialize.load_template(yaml_str) assert actual == reparsed ================================================ FILE: tests/unit/test_pipeline.py ================================================ import pytest from chalice import pipeline from chalice import __version__ as chalice_version from chalice.pipeline import InvalidCodeBuildPythonVersion, PipelineParameters @pytest.fixture def pipeline_gen(): return pipeline.CreatePipelineTemplateLegacy() @pytest.fixture def pipeline_params(): return pipeline.PipelineParameters('appname', 'python2.7') class TestPipelineGenLegacy(object): def setup_method(self): self.pipeline_gen = pipeline.CreatePipelineTemplateLegacy() def generate_template(self, app_name='appname', lambda_python_version='python2.7', codebuild_image=None, code_source='codecommit'): params = PipelineParameters( app_name=app_name, lambda_python_version=lambda_python_version, codebuild_image=codebuild_image, code_source=code_source, ) template = self.pipeline_gen.create_template(params) return template def test_app_name_in_param_default(self): template = self.generate_template(app_name='app') assert template['Parameters']['ApplicationName']['Default'] == 'app' def test_python_version_in_param_default(self): template = self.generate_template(lambda_python_version='python2.7') assert template['Parameters']['CodeBuildImage']['Default'] == \ 'aws/codebuild/python:2.7.12' def test_python_36_in_param_default(self): template = self.generate_template(lambda_python_version='python3.6') assert template['Parameters']['CodeBuildImage']['Default'] == \ 'aws/codebuild/python:3.6.5' def test_invalid_python_throws_error(self): with pytest.raises(InvalidCodeBuildPythonVersion): self.generate_template('app', 'python2.6') def test_nonsense_py_version_throws_error(self): with pytest.raises(InvalidCodeBuildPythonVersion): self.generate_template('app', 'foobar') def test_can_provide_codebuild_image(self): template = self.generate_template('appname', 'python2.7', codebuild_image='python:3.6.1') default_image = template['Parameters']['CodeBuildImage']['Default'] assert default_image == 'python:3.6.1' def test_no_source_resource_when_using_github(self): template = self.generate_template(code_source='github') resources = template['Resources'] assert 'SourceRepository' not in set(resources) def test_can_add_github_as_source_stage(self): template = self.generate_template(code_source='github') resources = template['Resources'] source_stage = resources['AppPipeline']['Properties']['Stages'][0] assert source_stage['Name'] == 'Source' actions = source_stage['Actions'] assert len(actions) == 1 action = actions[0] assert action['ActionTypeId'] == { 'Category': 'Source', 'Provider': 'GitHub', 'Owner': 'ThirdParty', 'Version': '1', } assert action['RunOrder'] == 1 assert action['OutputArtifacts'] == [{'Name': 'SourceRepo'}] assert action['Configuration'] == { 'Owner': {'Ref': 'GithubOwner'}, 'Repo': {'Ref': 'GithubRepoName'}, 'OAuthToken': {'Ref': 'GithubPersonalToken'}, 'Branch': 'master', 'PollForSourceChanges': True, } class TestPipelineGenV2(object): def setup_method(self): self.pipeline_gen = pipeline.CreatePipelineTemplateV2() def generate_template(self, app_name='appname', lambda_python_version='python3.9', codebuild_image=None, code_source='github', pipeline_version='v2'): params = PipelineParameters( app_name=app_name, lambda_python_version=lambda_python_version, codebuild_image=codebuild_image, code_source=code_source, pipeline_version=pipeline_version, ) template = self.pipeline_gen.create_template(params) return template def test_new_default_codebuild_image(self): template = self.generate_template(app_name='app') assert template['Parameters']['CodeBuildImage']['Default'] == ( "aws/codebuild/amazonlinux2-x86_64-standard:3.0" ) def test_validate_python_versions(self): with pytest.raises(InvalidCodeBuildPythonVersion): self.generate_template(lambda_python_version='python2.7') def test_uses_v2_codebuild_spec(self): # The codebuild v2 spec is tested separately, we just need a # sanity check to ensure we're using the v0.2 buildspec version. template = self.generate_template(app_name='app') codebuild_job = template['Resources']['AppPackageBuild'] assert "version: '0.2'" in codebuild_job[ 'Properties']['Source']['BuildSpec'] def test_github_source_uses_secretsmanager_in_v2(self): template = self.generate_template(code_source='github') source_stage = template['Resources'][ 'AppPipeline']['Properties']['Stages'][0] assert source_stage['Name'] == 'Source' oauth_token = source_stage['Actions'][0]['Configuration']['OAuthToken'] assert oauth_token == { 'Fn::Join': [ '', ['{{resolve:secretsmanager:', {'Ref': 'GithubRepoSecretId'}, ':SecretString:', {'Ref': 'GithubRepoSecretJSONKey'}, '}}'] ] } # We should also add these Refs to our Parameters. params = template['Parameters'] assert 'GithubRepoSecretId' in params assert 'GithubRepoSecretJSONKey' in params def test_source_repo_resource(pipeline_params): template = {} pipeline.CodeCommitSourceRepository().add_to_template( template, pipeline_params) assert template == { "Resources": { "SourceRepository": { "Type": "AWS::CodeCommit::Repository", "Properties": { "RepositoryName": { "Ref": "ApplicationName" }, "RepositoryDescription": { "Fn::Sub": "Source code for ${ApplicationName}" } } } }, "Outputs": { "SourceRepoURL": { "Value": { "Fn::GetAtt": "SourceRepository.CloneUrlHttp" } } } } def test_codebuild_resource(pipeline_params): template = {} pipeline.CodeBuild().add_to_template(template, pipeline_params) resources = template['Resources'] assert 'ApplicationBucket' in resources assert 'CodeBuildRole' in resources assert 'CodeBuildPolicy' in resources assert 'AppPackageBuild' in resources assert resources['ApplicationBucket'] == {'Type': 'AWS::S3::Bucket'} assert template['Outputs']['CodeBuildRoleArn'] == { 'Value': {'Fn::GetAtt': 'CodeBuildRole.Arn'} } def test_codepipeline_resource(pipeline_params): template = {} pipeline.CodePipeline().add_to_template(template, pipeline_params) resources = template['Resources'] assert 'AppPipeline' in resources assert 'ArtifactBucketStore' in resources assert 'CodePipelineRole' in resources assert 'CFNDeployRole' in resources # Some basic sanity checks assert resources['AppPipeline']['Type'] == 'AWS::CodePipeline::Pipeline' assert resources['ArtifactBucketStore']['Type'] == 'AWS::S3::Bucket' assert resources['CodePipelineRole']['Type'] == 'AWS::IAM::Role' assert resources['CFNDeployRole']['Type'] == 'AWS::IAM::Role' properties = resources['AppPipeline']['Properties'] stages = properties['Stages'] beta_stage = stages[2] beta_config = beta_stage['Actions'][0]['Configuration'] assert beta_config == { 'ActionMode': 'CHANGE_SET_REPLACE', 'Capabilities': 'CAPABILITY_IAM', 'ChangeSetName': {'Fn::Sub': '${ApplicationName}ChangeSet'}, 'RoleArn': {'Fn::GetAtt': 'CFNDeployRole.Arn'}, 'StackName': {'Fn::Sub': '${ApplicationName}BetaStack'}, 'TemplatePath': 'CompiledCFNTemplate::transformed.yaml' } def test_install_requirements_in_buildspec(pipeline_params): template = {} pipeline_params.chalice_version_range = '>=1.0.0,<2.0.0' pipeline.CodeBuild().add_to_template(template, pipeline_params) build = template['Resources']['AppPackageBuild'] build_spec = build['Properties']['Source']['BuildSpec'] assert 'pip install -r requirements.txt' in build_spec assert "pip install 'chalice>=1.0.0,<2.0.0'" in build_spec def test_default_version_range_locks_minor_version(): parts = [int(p) for p in chalice_version.split('.')] min_version = '%s.%s.%s' % (parts[0], parts[1], 0) max_version = '%s.%s.%s' % (parts[0], parts[1] + 1, 0) params = pipeline.PipelineParameters('appname', 'python2.7') assert params.chalice_version_range == '>=%s,<%s' % ( min_version, max_version ) def test_can_validate_python_version(): with pytest.raises(InvalidCodeBuildPythonVersion): pipeline.PipelineParameters( 'myapp', lambda_python_version='bad-python-value' ) def test_can_extract_python_version(): assert pipeline.PipelineParameters('app', 'python3.9').py_major_minor == ( '3.9') def test_can_generate_github_source(pipeline_params): template = {} pipeline_params.code_source = 'github' pipeline.GithubSource().add_to_template(template, pipeline_params) cfn_params = template['Parameters'] assert set(cfn_params) == set(['GithubOwner', 'GithubRepoName', 'GithubPersonalToken']) def test_can_create_buildspec_v2(): params = pipeline.PipelineParameters('myapp', 'python3.9') buildspec = pipeline.create_buildspec_v2(params) assert buildspec['phases']['install']['runtime-versions'] == { 'python': '3.9', } def test_build_extractor(): template = { 'Resources': { 'AppPackageBuild': { 'Properties': { 'Source': { 'BuildSpec': 'foobar' } } } } } extract = pipeline.BuildSpecExtractor() extracted = extract.extract_buildspec(template) assert extracted == 'foobar' assert 'BuildSpec' not in template[ 'Resources']['AppPackageBuild']['Properties']['Source'] ================================================ FILE: tests/unit/test_policy.py ================================================ from chalice.config import Config from chalice.policy import PolicyBuilder, AppPolicyGenerator from chalice.policy import diff_policies from chalice.utils import OSUtils # noqa class OsUtilsMock(OSUtils): def file_exists(self, *args, **kwargs): return True def get_file_contents(selfs, *args, **kwargs): return '' def iam_policy(client_calls): builder = PolicyBuilder() policy = builder.build_policy_from_api_calls(client_calls) return policy def test_app_policy_generator_vpc_policy(): config = Config.create( subnet_ids=['sn1', 'sn2'], security_group_ids=['sg1', 'sg2'], project_dir='.' ) generator = AppPolicyGenerator(OsUtilsMock()) policy = generator.generate_policy(config) assert policy == {'Statement': [ {'Action': ['logs:CreateLogGroup', 'logs:CreateLogStream', 'logs:PutLogEvents'], 'Effect': 'Allow', 'Resource': 'arn:*:logs:*:*:*'}, {'Action': ['ec2:CreateNetworkInterface', 'ec2:DescribeNetworkInterfaces', 'ec2:DetachNetworkInterface', 'ec2:DeleteNetworkInterface'], 'Effect': 'Allow', 'Resource': '*'}, ], 'Version': '2012-10-17'} def assert_policy_is(actual, expected): # Prune out the autogen's stuff we don't # care about. statements = actual['Statement'] for s in statements: del s['Sid'] assert expected == statements def test_single_call(): assert_policy_is(iam_policy({'dynamodb': set(['list_tables'])}), [{ 'Effect': 'Allow', 'Action': [ 'dynamodb:ListTables' ], 'Resource': [ '*', ] }]) def test_multiple_calls_in_same_service(): expected_policy = [{ 'Effect': 'Allow', 'Action': [ 'dynamodb:DescribeTable', 'dynamodb:ListTables', ], 'Resource': [ '*', ] }] assert_policy_is( iam_policy({'dynamodb': set(['list_tables', 'describe_table'])}), expected_policy ) def test_multiple_services_used(): client_calls = { 'dynamodb': set(['list_tables']), 'cloudformation': set(['create_stack']), } assert_policy_is(iam_policy(client_calls), [ { 'Effect': 'Allow', 'Action': [ 'cloudformation:CreateStack', ], 'Resource': [ '*', ] }, { 'Effect': 'Allow', 'Action': [ 'dynamodb:ListTables', ], 'Resource': [ '*', ] }, ]) def test_not_one_to_one_mapping(): client_calls = { 's3': set(['list_buckets', 'list_objects', 'create_multipart_upload']), } assert_policy_is(iam_policy(client_calls), [ { 'Effect': 'Allow', 'Action': [ 's3:ListAllMyBuckets', 's3:ListBucket', 's3:PutObject', ], 'Resource': [ '*', ] }, ]) def test_can_diff_policy_removed(): first = iam_policy({'s3': {'list_buckets', 'list_objects'}}) second = iam_policy({'s3': {'list_buckets'}}) assert diff_policies(first, second) == {'removed': {'s3:ListBucket'}} def test_can_diff_policy_added(): first = iam_policy({'s3': {'list_buckets'}}) second = iam_policy({'s3': {'list_buckets', 'list_objects'}}) assert diff_policies(first, second) == {'added': {'s3:ListBucket'}} def test_can_diff_multiple_services(): first = iam_policy({ 's3': {'list_buckets'}, 'dynamodb': {'create_table'}, 'cloudformation': {'create_stack', 'delete_stack'}, }) second = iam_policy({ 's3': {'list_buckets', 'list_objects'}, 'cloudformation': {'create_stack', 'update_stack'}, }) assert diff_policies(first, second) == { 'added': {'s3:ListBucket', 'cloudformation:UpdateStack'}, 'removed': {'cloudformation:DeleteStack', 'dynamodb:CreateTable'}, } def test_no_changes(): first = iam_policy({'s3': {'list_buckets', 'list_objects'}}) second = iam_policy({'s3': {'list_buckets', 'list_objects'}}) assert diff_policies(first, second) == {} def test_can_handle_high_level_abstractions(): policy = iam_policy({ 's3': set(['download_file', 'upload_file', 'copy']) }) assert_policy_is(policy, [{ 'Effect': 'Allow', 'Action': [ 's3:AbortMultipartUpload', 's3:GetObject', 's3:PutObject', ], 'Resource': [ '*', ] }]) def test_noop_for_unknown_methods(): assert_policy_is(iam_policy({'s3': set(['unknown_method'])}), []) ================================================ FILE: tests/unit/test_test.py ================================================ import os import json import pytest from chalice.test import Client, FunctionNotFoundError from chalice import Response, BadRequestError, Chalice, Blueprint, AuthResponse def test_can_make_http_request(sample_app): with Client(sample_app) as client: response = client.http.get('/') assert response.status_code == 200 assert response.json_body == {} assert response.body == b'{}' def test_can_pass_http_url(sample_app): @sample_app.route('/{name}') def hello(name): return {'hello': name} with Client(sample_app) as client: response = client.http.get('/james') assert response.json_body == {'hello': 'james'} def test_make_other_http_methods_request(sample_app): @sample_app.route('/methods', methods=['POST', 'PUT', 'PATCH', 'OPTIONS', 'DELETE', 'HEAD']) def method(): return {'method': sample_app.current_request.method} with Client(sample_app) as client: assert client.http.post('/methods').json_body == {'method': 'POST'} assert client.http.put('/methods').json_body == {'method': 'PUT'} assert client.http.patch('/methods').json_body == {'method': 'PATCH'} assert client.http.delete('/methods').json_body == {'method': 'DELETE'} assert client.http.head('/methods').json_body == {'method': 'HEAD'} assert client.http.options('/methods').json_body == { 'method': 'OPTIONS'} def test_can_provide_http_headers(sample_app): @sample_app.route('/header') def headers(): return {'value': sample_app.current_request.headers['x-my-header']} with Client(sample_app) as client: response = client.http.get('/header', headers={'x-my-header': 'foo'}) assert response.json_body == {'value': 'foo'} def test_can_return_error_message(sample_app): @sample_app.route('/error') def error(): raise BadRequestError("bad request") with Client(sample_app) as client: response = client.http.get('/error') assert response.status_code == 400 assert response.json_body['Code'] == 'BadRequestError' assert 'bad request' in response.json_body['Message'] def test_can_return_binary_data(sample_app): @sample_app.route('/bin-echo') def bin_echo(): raw_request_body = sample_app.current_request.raw_body return Response(body=raw_request_body, status_code=200, headers={'Content-Type': 'application/octet-stream'}) with Client(sample_app) as client: random_bytes = os.urandom(16) response = client.http.get( '/bin-echo', body=random_bytes, headers={'Accept': 'application/octet-stream'}) assert response.body == random_bytes assert response.json_body is None def test_can_access_env_vars_in_rest_api(sample_app, tmpdir): fake_config = { "version": "2.0", "app_name": "testenv", "stages": { "prod": { "api_gateway_stage": "api", "environment_variables": { "MY_ENV_VAR": "TOP LEVEL" }, } } } tmpdir.mkdir('.chalice').join('config.json').write( json.dumps(fake_config).encode('utf-8')) project_dir = str(tmpdir) os.environ.pop('MY_ENV_VAR', None) @sample_app.route('/env') def env_vars(): return {'value': os.environ.get('MY_ENV_VAR')} with Client(sample_app, project_dir=project_dir, stage_name='prod') as client: response = client.http.get('/env') assert response.json_body == {'value': 'TOP LEVEL'} def test_authorizers_return_http_response_on_error(sample_app): @sample_app.authorizer() def myauth(event): if event.token == 'allow': return AuthResponse(['*'], principal_id='id') return AuthResponse([], principal_id='noone') @sample_app.route('/needs-auth', authorizer=myauth) def needs_auth(): return {'success': True} with Client(sample_app) as client: response = client.http.get('/needs-auth', headers={'Authorization': 'deny'}) assert response.status_code == 403 assert client.http.get('/needs-auth').status_code == 401 def test_can_test_authorizers(sample_app): @sample_app.authorizer() def myauth(event): if event.token == 'allow': return AuthResponse(['*'], principal_id='id') @sample_app.route('/needs-auth', authorizer=myauth) def needs_auth(): return {'success': True} with Client(sample_app) as client: response = client.http.get('/needs-auth', headers={'Authorization': 'allow'}) assert response.json_body == {'success': True} # Tests for pure lambda and event handlers. def test_can_invoke_pure_lambda_function(): app = Chalice('lambda-only') @app.lambda_function() def foo(event, context): return {'event': event} with Client(app) as client: response = client.lambda_.invoke('foo', {'hello': 'world'}) assert response.payload == {'event': {'hello': 'world'}} def test_error_if_function_does_not_exist(): app = Chalice('lambda-only') with Client(app) as client: with pytest.raises(FunctionNotFoundError): client.lambda_.invoke('unknown-function', {}) def test_payload_not_required_for_invoke(): app = Chalice('lambda-only') @app.lambda_function() def foo(event, context): return {'event': event} with Client(app) as client: response = client.lambda_.invoke('foo') assert response.payload == {'event': {}} def test_can_access_environment_variables_in_function(tmpdir): app = Chalice('lambda-only') fake_config = { "version": "2.0", "app_name": "testenv", "stages": { "prod": { "api_gateway_stage": "api", "environment_variables": { "MY_ENV_VAR": "TOP LEVEL" }, "lambda_functions": { "bar": { "environment_variables": { "MY_ENV_VAR": "OVERRIDE" } } } } } } tmpdir.mkdir('.chalice').join('config.json').write( json.dumps(fake_config).encode('utf-8')) project_dir = str(tmpdir) os.environ.pop('MY_ENV_VAR', None) @app.lambda_function() def foo(event, context): return {'myvalue': os.environ.get('MY_ENV_VAR')} @app.lambda_function() def bar(event, context): return {'myvalue': os.environ.get('MY_ENV_VAR')} with Client(app, project_dir=project_dir, stage_name='prod') as client: assert client.lambda_.invoke('foo', {}).payload == { 'myvalue': 'TOP LEVEL' } assert client.lambda_.invoke('bar', {}).payload == { 'myvalue': 'OVERRIDE' } assert 'MY_ENV_VAR' not in os.environ def test_can_invoke_event_handler(): app = Chalice('lambda-only') @app.on_sns_message(topic='mytopic') def foo(event): return {'message': event.message, 'subject': event.subject, 'message_attributes': event.message_attributes} with Client(app) as client: event = client.events.generate_sns_event(message='my message', subject='hello', message_attributes={ "my_attr": { 'Type': 'String', 'Value': 'some_attr' } }) response = client.lambda_.invoke('foo', event) assert response.payload == {'message': 'my message', 'subject': 'hello', 'message_attributes': { "my_attr": { 'Type': 'String', 'Value': 'some_attr' } }} def test_can_generate_s3_event(): app = Chalice('lambda-only') @app.on_s3_event(bucket='mybucket') def foo(event): return {'bucket': event.bucket, 'key': event.key} with Client(app) as client: event = client.events.generate_s3_event( bucket='mybucket', key='mykey') response = client.lambda_.invoke('foo', event) assert response.payload == {'bucket': 'mybucket', 'key': 'mykey'} def test_can_generate_sqs_event(): app = Chalice('lambda-only') @app.on_sqs_message(queue='myqueue') def foo(event): return [record.body for record in event] with Client(app) as client: event = client.events.generate_sqs_event( message_bodies=['foo', 'bar', 'baz']) response = client.lambda_.invoke('foo', event) assert response.payload == ['foo', 'bar', 'baz'] def test_can_generate_cloudwatch_event(): app = Chalice('lambda-only') @app.on_cw_event({'source': ['aws.ec2']}) def foo(event): return {'detail': event.detail} with Client(app) as client: event = client.events.generate_cw_event( source='aws.ec2', detail_type='EC2 State-change', resources=['arn:aws:ec2:...:instance/i-abc'], detail={'instance-id': 'i-1234', 'state': 'pending'} ) response = client.lambda_.invoke('foo', event) assert response.payload == {'detail': {'instance-id': 'i-1234', 'state': 'pending'}} def test_can_generate_kinesis_event(): app = Chalice('kinesis') @app.on_kinesis_record(stream='mystream') def foo(event): return [record.data for record in event] with Client(app) as client: event = client.events.generate_kinesis_event( message_bodies=[b'foo', b'bar', b'baz']) response = client.lambda_.invoke('foo', event) assert response.payload == [b'foo', b'bar', b'baz'] def test_can_mix_pure_lambda_and_event_handlers(): app = Chalice('lambda-only') @app.on_sns_message(topic='mytopic') def foo(event): return {'message': event.message, 'subject': event.subject} @app.lambda_function() def bar(event, context): return {'event': event} @app.route('/') def index(): return {'hello': 'restapi'} with Client(app) as client: assert client.lambda_.invoke( 'foo', client.events.generate_sns_event( message='my message', subject='hello') ).payload == {'message': 'my message', 'subject': 'hello'} assert client.lambda_.invoke( 'bar', {'hello': 'world'} ).payload == {'event': {'hello': 'world'}} assert client.http.get('/').json_body == {'hello': 'restapi'} def test_can_invoke_handler_from_blueprint(): bp = Blueprint('testblueprint') @bp.lambda_function() def my_foo(event, context): return {'event': event} app = Chalice('myapp') app.register_blueprint(bp) with Client(app) as client: response = client.lambda_.invoke('my_foo', {'hello': 'world'}) assert response.payload == {'event': {'hello': 'world'}} def test_can_invoke_handler_with_blueprint_prefix(): bp = Blueprint('testblueprint') @bp.lambda_function() def my_foo(event, context): return {'event': event} app = Chalice('myapp') app.register_blueprint(bp, name_prefix='bp_prefix_') with Client(app) as client: response = client.lambda_.invoke('bp_prefix_my_foo', {'hello': 'world'}) assert response.payload == {'event': {'hello': 'world'}} def test_lambda_function_with_custom_name(): app = Chalice('lambda-only') @app.lambda_function(name='my-custom-name') def foo(event, context): return {'event': event} with Client(app) as client: response = client.lambda_.invoke('my-custom-name', {'hello': 'world'}) assert response.payload == {'event': {'hello': 'world'}} ================================================ FILE: tests/unit/test_utils.py ================================================ import os import re from unittest import mock import sys import click import pytest from six import StringIO from hypothesis.strategies import text from hypothesis import given import string from dateutil import tz from datetime import datetime from chalice import utils class TestUI: def setup_method(self): self.out = StringIO() self.err = StringIO() self.ui = utils.UI(self.out, self.err) def test_write_goes_to_out_obj(self): self.ui.write("Foo") assert self.out.getvalue() == 'Foo' assert self.err.getvalue() == '' def test_error_goes_to_err_obj(self): self.ui.error("Foo") assert self.err.getvalue() == 'Foo' assert self.out.getvalue() == '' def test_confirm_raises_own_exception(self): confirm = mock.Mock(spec=click.confirm) confirm.side_effect = click.Abort() ui = utils.UI(self.out, self.err, confirm) with pytest.raises(utils.AbortedError): ui.confirm("Confirm?") def test_confirm_returns_value(self): confirm = mock.Mock(spec=click.confirm) confirm.return_value = 'foo' ui = utils.UI(self.out, self.err, confirm) return_value = ui.confirm("Confirm?") assert return_value == 'foo' class TestChaliceZip(object): def test_chalice_zip_file(self, tmpdir): tmpdir.mkdir('foo').join('app.py').write('# Test app') zip_path = tmpdir.join('app.zip') app_filename = str(tmpdir.join('foo', 'app.py')) # Add an executable file to test preserving permissions. script_obj = tmpdir.join('foo', 'myscript.sh') script_obj.write('echo foo') script_file = str(script_obj) os.chmod(script_file, 0o755) with utils.ChaliceZipFile(str(zip_path), 'w') as z: z.write(app_filename) z.write(script_file) with utils.ChaliceZipFile(str(zip_path)) as z: assert len(z.infolist()) == 2 # Remove the leading '/'. app = z.getinfo(app_filename[1:]) assert app.date_time == (1980, 1, 1, 0, 0, 0) assert app.external_attr >> 16 == os.stat(app_filename).st_mode # Verify executable permission is preserved. script = z.getinfo(script_file[1:]) assert script.date_time == (1980, 1, 1, 0, 0, 0) assert script.external_attr >> 16 == os.stat(script_file).st_mode class TestPipeReader(object): def test_pipe_reader_does_read_pipe(self): mock_stream = mock.Mock(spec=sys.stdin) mock_stream.isatty.return_value = False mock_stream.read.return_value = 'foobar' reader = utils.PipeReader(mock_stream) value = reader.read() assert value == 'foobar' def test_pipe_reader_does_not_read_tty(self): mock_stream = mock.Mock(spec=sys.stdin) mock_stream.isatty.return_value = True mock_stream.read.return_value = 'foobar' reader = utils.PipeReader(mock_stream) value = reader.read() assert value is None def test_serialize_json(): assert utils.serialize_to_json({'foo': 'bar'}) == ( '{\n' ' "foo": "bar"\n' '}\n' ) @pytest.mark.parametrize('name,cfn_name', [ ('f', 'F'), ('foo', 'Foo'), ('foo_bar', 'FooBar'), ('foo_bar_baz', 'FooBarBaz'), ('F', 'F'), ('FooBar', 'FooBar'), ('S3Bucket', 'S3Bucket'), ('s3Bucket', 'S3Bucket'), ('123', '123'), ('foo-bar-baz', 'FooBarBaz'), ('foo_bar-baz', 'FooBarBaz'), ('foo-bar_baz', 'FooBarBaz'), # Not actually possible, but we should # ensure we only have alphanumeric chars. ('foo_bar!?', 'FooBar'), ('_foo_bar', 'FooBar'), ]) def test_to_cfn_resource_name(name, cfn_name): assert utils.to_cfn_resource_name(name) == cfn_name @given(name=text(alphabet=string.ascii_letters + string.digits + '-_')) def test_to_cfn_resource_name_properties(name): try: result = utils.to_cfn_resource_name(name) except ValueError: # This is acceptable, the function raises ValueError # on bad input. pass else: assert re.search('[^A-Za-z0-9]', result) is None class TestTimestampUtils: def setup_method(self): self.mock_now = mock.Mock(spec=datetime.utcnow) self.set_now() self.timestamp_convert = utils.TimestampConverter(self.mock_now) def set_now(self, year=2020, month=1, day=1, hour=0, minute=0, sec=0): self.now = datetime( year, month, day, hour, minute, sec, tzinfo=tz.tzutc()) self.mock_now.return_value = self.now def test_iso_no_timezone(self): assert self.timestamp_convert.timestamp_to_datetime( '2020-01-01T00:00:01.000000') == datetime(2020, 1, 1, 0, 0, 1) def test_iso_with_timezone(self): assert ( self.timestamp_convert.timestamp_to_datetime( '2020-01-01T00:00:01.000000-01:00' ) == datetime(2020, 1, 1, 0, 0, 1, tzinfo=tz.tzoffset(None, -3600)) ) def test_to_datetime_relative_second(self): self.set_now(sec=2) assert ( self.timestamp_convert.timestamp_to_datetime('1s') == datetime(2020, 1, 1, 0, 0, 1, tzinfo=tz.tzutc()) ) def test_to_datetime_relative_multiple_seconds(self): self.set_now(sec=5) assert ( self.timestamp_convert.timestamp_to_datetime('2s') == datetime(2020, 1, 1, 0, 0, 3, tzinfo=tz.tzutc()) ) def test_to_datetime_relative_minute(self): self.set_now(minute=2) assert ( self.timestamp_convert.timestamp_to_datetime('1m') == datetime(2020, 1, 1, 0, 1, 0, tzinfo=tz.tzutc()) ) def test_to_datetime_relative_hour(self): self.set_now(hour=2) assert ( self.timestamp_convert.timestamp_to_datetime('1h') == datetime(2020, 1, 1, 1, 0, 0, tzinfo=tz.tzutc()) ) def test_to_datetime_relative_day(self): self.set_now(day=3) # 1970-01-03 assert ( self.timestamp_convert.timestamp_to_datetime('1d') == datetime(2020, 1, 2, 0, 0, 0, tzinfo=tz.tzutc()) ) def test_to_datetime_relative_week(self): self.set_now(day=14) assert ( self.timestamp_convert.timestamp_to_datetime('1w') == datetime(2020, 1, 7, 0, 0, 0, tzinfo=tz.tzutc()) ) @pytest.mark.parametrize('timestamp,expected', [ ('2020-01-01', datetime(2020, 1, 1)), ('2020-01-01T00:00:01', datetime(2020, 1, 1, 0, 0, 1)), ('2020-02-02T01:02:03', datetime(2020, 2, 2, 1, 2, 3)), ('2020-01-01T00:00:00Z', datetime(2020, 1, 1, 0, 0, tzinfo=tz.tzutc())), ('2020-01-01T00:00:00-04:00', datetime(2020, 1, 1, 0, 0, 0, tzinfo=tz.tzoffset('EDT', -14400))), ]) def test_parse_iso8601_timestamp(timestamp, expected): timestamp_convert = utils.TimestampConverter() assert timestamp_convert.parse_iso8601_timestamp(timestamp) == expected ================================================ FILE: tests/unit/vendored/__init__.py ================================================ ================================================ FILE: tests/unit/vendored/botocore/__init__.py ================================================ ================================================ FILE: tests/unit/vendored/botocore/test_regions.py ================================================ # Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"). You # may not use this file except in compliance with the License. A copy of # the License is located at # # http://aws.amazon.com/apache2.0/ # # or in the "license" file accompanying this file. This file is # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. """ This file was 'vendored' from botocore core botocore/tests/unit/test_regions.py from commit 0c55d6c3f900fc856e818f06b31c22c6dbc56788. The vendoring/duplication was due to the concern of utilizing a unexposed class internal to the botocore library for functionality necessary to implicitly support partitions within the chalice microframework. More specifically the determination of the dns suffix for service endpoints based on service and region. """ import pytest from botocore.exceptions import NoRegionError from chalice.vendored.botocore import regions @pytest.fixture def endpoints_template(): return { 'partitions': [ { 'partition': 'aws', 'dnsSuffix': 'amazonaws.com', 'regionRegex': r'^(us|eu)\-\w+$', 'defaults': { 'hostname': '{service}.{region}.{dnsSuffix}' }, 'regions': { 'us-foo': {'regionName': 'a'}, 'us-bar': {'regionName': 'b'}, 'eu-baz': {'regionName': 'd'} }, 'services': { 'ec2': { 'endpoints': { 'us-foo': {}, 'us-bar': {}, 'eu-baz': {}, 'd': {} } }, 's3': { 'defaults': { 'sslCommonName': '{service}.{region}.{dnsSuffix}' }, 'endpoints': { 'us-foo': { 'sslCommonName': '{region}.{service}.{dnsSuffix}' }, 'us-bar': {}, 'eu-baz': {'hostname': 'foo'} } }, 'not-regionalized': { 'isRegionalized': False, 'partitionEndpoint': 'aws', 'endpoints': { 'aws': {'hostname': 'not-regionalized'}, 'us-foo': {}, 'eu-baz': {} } }, 'non-partition': { 'partitionEndpoint': 'aws', 'endpoints': { 'aws': {'hostname': 'host'}, 'us-foo': {} } }, 'merge': { 'defaults': { 'signatureVersions': ['v2'], 'protocols': ['http'] }, 'endpoints': { 'us-foo': {'signatureVersions': ['v4']}, 'us-bar': {'protocols': ['https']} } } } }, { 'partition': 'foo', 'dnsSuffix': 'foo.com', 'regionRegex': r'^(foo)\-\w+$', 'defaults': { 'hostname': '{service}.{region}.{dnsSuffix}', 'protocols': ['http'], 'foo': 'bar' }, 'regions': { 'foo-1': {'regionName': '1'}, 'foo-2': {'regionName': '2'}, 'foo-3': {'regionName': '3'} }, 'services': { 'ec2': { 'endpoints': { 'foo-1': { 'foo': 'baz' }, 'foo-2': {}, 'foo-3': {} } } } } ] } def test_ensures_region_is_not_none(endpoints_template): with pytest.raises(NoRegionError): resolver = regions.EndpointResolver(endpoints_template) resolver.construct_endpoint('foo', None) def test_ensures_required_keys_present(endpoints_template): with pytest.raises(ValueError): regions.EndpointResolver({}) def test_returns_empty_list_when_listing_for_different_partition( endpoints_template): resolver = regions.EndpointResolver(endpoints_template) assert resolver.get_available_endpoints('ec2', 'bar') == [] def test_returns_empty_list_when_no_service_found(endpoints_template): resolver = regions.EndpointResolver(endpoints_template) assert resolver.get_available_endpoints('what?') == [] def test_gets_endpoint_names(endpoints_template): resolver = regions.EndpointResolver(endpoints_template) result = resolver.get_available_endpoints('ec2', allow_non_regional=True) assert sorted(result) == ['d', 'eu-baz', 'us-bar', 'us-foo'] def test_gets_endpoint_names_for_partition(endpoints_template): resolver = regions.EndpointResolver(endpoints_template) result = resolver.get_available_endpoints( 'ec2', allow_non_regional=True, partition_name='foo') assert sorted(result) == ['foo-1', 'foo-2', 'foo-3'] def test_list_regional_endpoints_only(endpoints_template): resolver = regions.EndpointResolver(endpoints_template) result = resolver.get_available_endpoints( 'ec2', allow_non_regional=False) assert sorted(result) == ['eu-baz', 'us-bar', 'us-foo'] def test_returns_none_when_no_match(endpoints_template): resolver = regions.EndpointResolver(endpoints_template) assert resolver.construct_endpoint('foo', 'baz') is None def test_constructs_regionalized_endpoints_for_exact_matches( endpoints_template): resolver = regions.EndpointResolver(endpoints_template) result = resolver.construct_endpoint('not-regionalized', 'eu-baz') assert result['hostname'] == 'not-regionalized.eu-baz.amazonaws.com' assert result['partition'] == 'aws' assert result['endpointName'] == 'eu-baz' def test_constructs_partition_endpoints_for_real_partition_region( endpoints_template): resolver = regions.EndpointResolver(endpoints_template) result = resolver.construct_endpoint('not-regionalized', 'us-bar') assert result['hostname'] == 'not-regionalized' assert result['partition'] == 'aws' assert result['endpointName'] == 'aws' def test_constructs_partition_endpoints_for_regex_match(endpoints_template): resolver = regions.EndpointResolver(endpoints_template) result = resolver.construct_endpoint('not-regionalized', 'us-abc') assert result['hostname'] == 'not-regionalized' def test_constructs_endpoints_for_regionalized_regex_match(endpoints_template): resolver = regions.EndpointResolver(endpoints_template) result = resolver.construct_endpoint('s3', 'us-abc') assert result['hostname'] == 's3.us-abc.amazonaws.com' def test_constructs_endpoints_for_unknown_service_but_known_region( endpoints_template): resolver = regions.EndpointResolver(endpoints_template) result = resolver.construct_endpoint('unknown', 'us-foo') assert result['hostname'] == 'unknown.us-foo.amazonaws.com' def test_merges_service_keys(endpoints_template): resolver = regions.EndpointResolver(endpoints_template) us_foo = resolver.construct_endpoint('merge', 'us-foo') us_bar = resolver.construct_endpoint('merge', 'us-bar') assert us_foo['protocols'] == ['http'] assert us_foo['signatureVersions'] == ['v4'] assert us_bar['protocols'] == ['https'] assert us_bar['signatureVersions'] == ['v2'] def test_merges_partition_default_keys_with_no_overwrite(endpoints_template): resolver = regions.EndpointResolver(endpoints_template) resolved = resolver.construct_endpoint('ec2', 'foo-1') assert resolved['foo'] == 'baz' assert resolved['protocols'] == ['http'] def test_merges_partition_default_keys_with_overwrite(endpoints_template): resolver = regions.EndpointResolver(endpoints_template) resolved = resolver.construct_endpoint('ec2', 'foo-2') assert resolved['foo'] == 'bar' assert resolved['protocols'] == ['http'] def test_gives_hostname_and_common_name_unaltered(endpoints_template): resolver = regions.EndpointResolver(endpoints_template) result = resolver.construct_endpoint('s3', 'eu-baz') assert result['sslCommonName'] == 's3.eu-baz.amazonaws.com' assert result['hostname'] == 'foo' def tests_uses_partition_endpoint_when_no_region_provided(endpoints_template): resolver = regions.EndpointResolver(endpoints_template) result = resolver.construct_endpoint('not-regionalized') assert result['hostname'] == 'not-regionalized' assert result['endpointName'] == 'aws' def test_returns_dns_suffix_if_available(endpoints_template): resolver = regions.EndpointResolver(endpoints_template) result = resolver.construct_endpoint('not-regionalized') assert result['dnsSuffix'] == 'amazonaws.com'