From 7d8e3ae5683f17ad49fe20de6555749907bf244b Mon Sep 17 00:00:00 2001 From: Nikhil Yogendra Murali Date: Wed, 27 Jun 2018 16:40:02 -0700 Subject: [PATCH] Initial commit of Alexa Skills Kit SDK for Python This commit adds the Alexa Skills Kit SDK for Python on the SDK Repo on Github. --- .github/ISSUE_TEMPLATE.md | 53 ++ .github/PULL_REQUEST_TEMPLATE.md | 41 +- .gitignore | 67 ++ CHANGELOG.rst | 8 + README.rst | 116 ++++ ask-sdk-core/.coveragerc | 9 + ask-sdk-core/.gitignore | 67 ++ ask-sdk-core/CHANGELOG.rst | 8 + ask-sdk-core/MANIFEST.in | 5 + ask-sdk-core/README.rst | 50 ++ ask-sdk-core/ask_sdk_core/__init__.py | 17 + ask-sdk-core/ask_sdk_core/__version__.py | 28 + ask-sdk-core/ask_sdk_core/api_client.py | 142 ++++ .../ask_sdk_core/attributes_manager.py | 208 ++++++ ask-sdk-core/ask_sdk_core/dispatch.py | 215 ++++++ .../dispatch_components/__init__.py | 27 + .../exception_components.py | 180 +++++ .../dispatch_components/request_components.py | 454 +++++++++++++ ask-sdk-core/ask_sdk_core/exceptions.py | 54 ++ ask-sdk-core/ask_sdk_core/handler_input.py | 81 +++ ask-sdk-core/ask_sdk_core/response_helper.py | 278 ++++++++ ask-sdk-core/ask_sdk_core/serialize.py | 317 +++++++++ ask-sdk-core/ask_sdk_core/skill.py | 178 +++++ ask-sdk-core/ask_sdk_core/skill_builder.py | 413 ++++++++++++ ask-sdk-core/ask_sdk_core/utils.py | 93 +++ ask-sdk-core/docs/requirements-docs.txt | 0 ask-sdk-core/requirements.txt | 5 + ask-sdk-core/setup.cfg | 2 + ask-sdk-core/setup.py | 68 ++ ask-sdk-core/tests/__init__.py | 22 + ask-sdk-core/tests/unit/__init__.py | 17 + ask-sdk-core/tests/unit/data/__init__.py | 24 + .../tests/unit/data/invalid_model_object.py | 25 + .../unit/data/mock_persistence_adapter.py | 34 + .../tests/unit/data/mock_response_object.py | 26 + .../unit/data/model_abstract_parent_object.py | 53 ++ .../tests/unit/data/model_child_objects.py | 66 ++ .../tests/unit/data/model_enum_object.py | 26 + .../tests/unit/data/model_test_object_1.py | 45 ++ .../tests/unit/data/model_test_object_2.py | 33 + ask-sdk-core/tests/unit/test_api_client.py | 185 ++++++ .../tests/unit/test_attributes_manager.py | 236 +++++++ ask-sdk-core/tests/unit/test_dispatch.py | 448 +++++++++++++ .../tests/unit/test_dispatch_components.py | 499 ++++++++++++++ ask-sdk-core/tests/unit/test_handler_input.py | 46 ++ .../tests/unit/test_response_helper.py | 232 +++++++ ask-sdk-core/tests/unit/test_serialize.py | 508 ++++++++++++++ ask-sdk-core/tests/unit/test_skill.py | 179 +++++ ask-sdk-core/tests/unit/test_skill_builder.py | 626 ++++++++++++++++++ ask-sdk-core/tests/unit/test_utils.py | 92 +++ ask-sdk-core/tox.ini | 11 + .../.coveragerc | 9 + .../.gitignore | 67 ++ .../CHANGELOG.rst | 8 + .../MANIFEST.in | 5 + .../README.rst | 50 ++ .../ask_sdk_dynamodb/__init__.py | 17 + .../ask_sdk_dynamodb/__version__.py | 29 + .../ask_sdk_dynamodb/adapter.py | 186 ++++++ .../ask_sdk_dynamodb/partition_keygen.py | 61 ++ .../docs/requirements-docs.txt | 0 .../requirements.txt | 2 + .../setup.cfg | 2 + ask-sdk-dynamodb-persistence-adapter/setup.py | 68 ++ .../tests/__init__.py | 22 + .../tests/unit/__init__.py | 17 + .../tests/unit/test_adapter.py | 318 +++++++++ .../tests/unit/test_partition_keygen.py | 146 ++++ ask-sdk-dynamodb-persistence-adapter/tox.ini | 11 + ask-sdk/.coveragerc | 9 + ask-sdk/.gitignore | 67 ++ ask-sdk/CHANGELOG.rst | 8 + ask-sdk/MANIFEST.in | 5 + ask-sdk/README.rst | 50 ++ ask-sdk/ask_sdk/__init__.py | 17 + ask-sdk/ask_sdk/__version__.py | 29 + ask-sdk/ask_sdk/standard.py | 83 +++ ask-sdk/docs/requirements-docs.txt | 0 ask-sdk/requirements.txt | 2 + ask-sdk/setup.cfg | 2 + ask-sdk/setup.py | 68 ++ ask-sdk/tests/unit/__init__.py | 17 + ask-sdk/tests/unit/test_standard.py | 81 +++ ask-sdk/tox.ini | 11 + docs/ATTRIBUTES.rst | 168 +++++ docs/DEVELOPING_YOUR_FIRST_SKILL.rst | 624 +++++++++++++++++ docs/GETTING_STARTED.rst | 151 +++++ docs/REQUEST_PROCESSING.rst | 605 +++++++++++++++++ docs/RESPONSE_BUILDING.rst | 88 +++ docs/SERVICE_CLIENTS.rst | 28 + docs/SKILL_BUILDERS.rst | 113 ++++ docs/requirements-docs.txt | 0 requirements.txt | 0 samples/ColorPicker/README.rst | 231 +++++++ samples/ColorPicker/color_picker.py | 192 ++++++ .../speech_assets/interactionSchema.json | 53 ++ samples/GetDeviceAddress/README.rst | 192 ++++++ .../GetDeviceAddress/device_address_api.py | 183 +++++ .../speech_assets/interactionSchema.json | 37 ++ samples/HelloWorld/README.rst | 55 ++ .../skill_using_classes/hello_world.py | 134 ++++ .../skill_using_decorators/hello_world.py | 99 +++ .../speech_assets/interactionSchema.json | 39 ++ samples/HighLowGame/README.rst | 188 ++++++ samples/HighLowGame/high_low_game.py | 225 +++++++ .../speech_assets/interactionSchema.json | 49 ++ setup.cfg | 2 + setup.py | 49 ++ 108 files changed, 11586 insertions(+), 3 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE.md create mode 100644 .gitignore create mode 100644 CHANGELOG.rst create mode 100644 README.rst create mode 100644 ask-sdk-core/.coveragerc create mode 100644 ask-sdk-core/.gitignore create mode 100644 ask-sdk-core/CHANGELOG.rst create mode 100644 ask-sdk-core/MANIFEST.in create mode 100644 ask-sdk-core/README.rst create mode 100644 ask-sdk-core/ask_sdk_core/__init__.py create mode 100644 ask-sdk-core/ask_sdk_core/__version__.py create mode 100644 ask-sdk-core/ask_sdk_core/api_client.py create mode 100644 ask-sdk-core/ask_sdk_core/attributes_manager.py create mode 100644 ask-sdk-core/ask_sdk_core/dispatch.py create mode 100644 ask-sdk-core/ask_sdk_core/dispatch_components/__init__.py create mode 100644 ask-sdk-core/ask_sdk_core/dispatch_components/exception_components.py create mode 100644 ask-sdk-core/ask_sdk_core/dispatch_components/request_components.py create mode 100644 ask-sdk-core/ask_sdk_core/exceptions.py create mode 100644 ask-sdk-core/ask_sdk_core/handler_input.py create mode 100644 ask-sdk-core/ask_sdk_core/response_helper.py create mode 100644 ask-sdk-core/ask_sdk_core/serialize.py create mode 100644 ask-sdk-core/ask_sdk_core/skill.py create mode 100644 ask-sdk-core/ask_sdk_core/skill_builder.py create mode 100644 ask-sdk-core/ask_sdk_core/utils.py create mode 100644 ask-sdk-core/docs/requirements-docs.txt create mode 100644 ask-sdk-core/requirements.txt create mode 100644 ask-sdk-core/setup.cfg create mode 100644 ask-sdk-core/setup.py create mode 100644 ask-sdk-core/tests/__init__.py create mode 100644 ask-sdk-core/tests/unit/__init__.py create mode 100644 ask-sdk-core/tests/unit/data/__init__.py create mode 100644 ask-sdk-core/tests/unit/data/invalid_model_object.py create mode 100644 ask-sdk-core/tests/unit/data/mock_persistence_adapter.py create mode 100644 ask-sdk-core/tests/unit/data/mock_response_object.py create mode 100644 ask-sdk-core/tests/unit/data/model_abstract_parent_object.py create mode 100644 ask-sdk-core/tests/unit/data/model_child_objects.py create mode 100644 ask-sdk-core/tests/unit/data/model_enum_object.py create mode 100644 ask-sdk-core/tests/unit/data/model_test_object_1.py create mode 100644 ask-sdk-core/tests/unit/data/model_test_object_2.py create mode 100644 ask-sdk-core/tests/unit/test_api_client.py create mode 100644 ask-sdk-core/tests/unit/test_attributes_manager.py create mode 100644 ask-sdk-core/tests/unit/test_dispatch.py create mode 100644 ask-sdk-core/tests/unit/test_dispatch_components.py create mode 100644 ask-sdk-core/tests/unit/test_handler_input.py create mode 100644 ask-sdk-core/tests/unit/test_response_helper.py create mode 100644 ask-sdk-core/tests/unit/test_serialize.py create mode 100644 ask-sdk-core/tests/unit/test_skill.py create mode 100644 ask-sdk-core/tests/unit/test_skill_builder.py create mode 100644 ask-sdk-core/tests/unit/test_utils.py create mode 100644 ask-sdk-core/tox.ini create mode 100644 ask-sdk-dynamodb-persistence-adapter/.coveragerc create mode 100644 ask-sdk-dynamodb-persistence-adapter/.gitignore create mode 100644 ask-sdk-dynamodb-persistence-adapter/CHANGELOG.rst create mode 100644 ask-sdk-dynamodb-persistence-adapter/MANIFEST.in create mode 100644 ask-sdk-dynamodb-persistence-adapter/README.rst create mode 100644 ask-sdk-dynamodb-persistence-adapter/ask_sdk_dynamodb/__init__.py create mode 100644 ask-sdk-dynamodb-persistence-adapter/ask_sdk_dynamodb/__version__.py create mode 100644 ask-sdk-dynamodb-persistence-adapter/ask_sdk_dynamodb/adapter.py create mode 100644 ask-sdk-dynamodb-persistence-adapter/ask_sdk_dynamodb/partition_keygen.py create mode 100644 ask-sdk-dynamodb-persistence-adapter/docs/requirements-docs.txt create mode 100644 ask-sdk-dynamodb-persistence-adapter/requirements.txt create mode 100644 ask-sdk-dynamodb-persistence-adapter/setup.cfg create mode 100644 ask-sdk-dynamodb-persistence-adapter/setup.py create mode 100644 ask-sdk-dynamodb-persistence-adapter/tests/__init__.py create mode 100644 ask-sdk-dynamodb-persistence-adapter/tests/unit/__init__.py create mode 100644 ask-sdk-dynamodb-persistence-adapter/tests/unit/test_adapter.py create mode 100644 ask-sdk-dynamodb-persistence-adapter/tests/unit/test_partition_keygen.py create mode 100644 ask-sdk-dynamodb-persistence-adapter/tox.ini create mode 100644 ask-sdk/.coveragerc create mode 100644 ask-sdk/.gitignore create mode 100644 ask-sdk/CHANGELOG.rst create mode 100644 ask-sdk/MANIFEST.in create mode 100644 ask-sdk/README.rst create mode 100644 ask-sdk/ask_sdk/__init__.py create mode 100644 ask-sdk/ask_sdk/__version__.py create mode 100644 ask-sdk/ask_sdk/standard.py create mode 100644 ask-sdk/docs/requirements-docs.txt create mode 100644 ask-sdk/requirements.txt create mode 100644 ask-sdk/setup.cfg create mode 100644 ask-sdk/setup.py create mode 100644 ask-sdk/tests/unit/__init__.py create mode 100644 ask-sdk/tests/unit/test_standard.py create mode 100644 ask-sdk/tox.ini create mode 100644 docs/ATTRIBUTES.rst create mode 100644 docs/DEVELOPING_YOUR_FIRST_SKILL.rst create mode 100644 docs/GETTING_STARTED.rst create mode 100644 docs/REQUEST_PROCESSING.rst create mode 100644 docs/RESPONSE_BUILDING.rst create mode 100644 docs/SERVICE_CLIENTS.rst create mode 100644 docs/SKILL_BUILDERS.rst create mode 100644 docs/requirements-docs.txt create mode 100644 requirements.txt create mode 100644 samples/ColorPicker/README.rst create mode 100644 samples/ColorPicker/color_picker.py create mode 100644 samples/ColorPicker/speech_assets/interactionSchema.json create mode 100644 samples/GetDeviceAddress/README.rst create mode 100644 samples/GetDeviceAddress/device_address_api.py create mode 100644 samples/GetDeviceAddress/speech_assets/interactionSchema.json create mode 100644 samples/HelloWorld/README.rst create mode 100644 samples/HelloWorld/skill_using_classes/hello_world.py create mode 100644 samples/HelloWorld/skill_using_decorators/hello_world.py create mode 100644 samples/HelloWorld/speech_assets/interactionSchema.json create mode 100644 samples/HighLowGame/README.rst create mode 100644 samples/HighLowGame/high_low_game.py create mode 100644 samples/HighLowGame/speech_assets/interactionSchema.json create mode 100644 setup.cfg create mode 100644 setup.py diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..a48ebdc --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,53 @@ + + +## I'm submitting a... + +

+[ ] Regression (a behavior that used to work and stopped working in a new release)
+[ ] Bug report  
+[ ] Performance issue
+[ ] Feature request
+[ ] Documentation issue or request
+[ ] Other... Please describe:
+
+ + + +## Expected Behavior + + + +## Current Behavior + + + + + +## Possible Solution +``` +// Not required, but suggest a fix/reason for the bug, +// or ideas how to implement the addition or change +``` + +## Steps to Reproduce (for bugs) +``` +// Provide a self-contained, concise snippet of code +// For more complex issues provide a repo with the smallest sample that reproduces the bug +// Including business logic or unrelated code makes diagnosis more difficult +``` + +## Context + + + +## Your Environment + +* ASK SDK for Python used: x.x.x +* Operating System and version: + +## Python version info +* Python version used for development: diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index ab40d21..fbba527 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,6 +1,41 @@ -*Issue #, if available:* + -*Description of changes:* +## Description + +## Motivation and Context + + -By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. +## Testing + + + + +## Screenshots (if appropriate) + +## Types of changes + +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to change) + +## Checklist + + +- [ ] My code follows the code style of this project +- [ ] My change requires a change to the documentation +- [ ] I have updated the documentation accordingly +- [ ] I have read the **README** document +- [ ] I have added tests to cover my changes +- [ ] All new and existing tests passed + +## License + + + +- [ ] By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. + +[issues]: https://github.com/alexa-labs/alexa-skills-kit-sdk-for-python/issues +[license]: http://aws.amazon.com/apache2.0/ +[cla]: http://en.wikipedia.org/wiki/Contributor_License_Agreement diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e057098 --- /dev/null +++ b/.gitignore @@ -0,0 +1,67 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ +venv/ +.python-version + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +#Ipython Notebook +.ipynb_checkpoints + +# IntelliJ configs +*.iml diff --git a/CHANGELOG.rst b/CHANGELOG.rst new file mode 100644 index 0000000..d4eee4e --- /dev/null +++ b/CHANGELOG.rst @@ -0,0 +1,8 @@ +========= +CHANGELOG +========= + +0.1 +------- + +* Initial release of alexa skills kit core sdk. diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..d43893a --- /dev/null +++ b/README.rst @@ -0,0 +1,116 @@ +=================== +ASK SDK for Python +=================== + +The ASK SDK for Python makes it easier for you to build highly engaging skills, +by allowing you to spend more time on implementing features and less on writing +boiler-plate code. + +To help you get started with the SDK we have included the following guides. +In the future, we plan to include more documentation and samples too. + +Guides +------ + +`Setting Up The ASK SDK `_ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This guide will show you how to include the SDK as a dependency in your +Python project. + + +`Developing Your First Skill `_ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Walks you through step-by-step instructions for building the Hello World +sample. + + +SDK Features +------------ + +`Request Processing `_ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Covers how to build request handlers, exception handlers, and request and +response interceptors. + +`Skill Attributes `_ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Covers how to use skill attributes to store and retrieve skill data. + +`Response Building `_ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Covers how to use the ResponseBuilder to compose multiple elements like +text, cards, and audio into a single response. + +`Alexa Service Clients `_ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Covers how to use service clients in your skill to access Alexa APIs. + +`Skill Builders `_ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Covers how to configure and construct a skill instance. + +Samples +------- + +`Hello World Skill Sample `_ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This code sample will allow you to hear a response from Alexa when you +trigger it. It is a minimal sample to get you familiarized with the +Alexa Skills Kit and AWS Lambda. + +`Color Picker Skill Sample `_ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This is a step-up in functionality from Hello World. It allows you to +capture input from your user and demonstrates the use of Slots. It also +demonstrates use of session attributes and request, response interceptors. + +`High Low Game Skill Sample `_ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Template for a basic high-low game skill. When the user guesses a number, +Alexa tells the user whether the number she has in mind is higher or lower. + +`Device Address API Skill Sample `_ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Sample skill that shows how to request and access the configured address in +the user’s device settings. + + +Got Feedback? +------------- + +- We would like to hear about your bugs, feature requests, questions or quick feedback. + Please search for + `existing issues `_ + before opening a new one. It would also be helpful if you follow the + templates for issue and pull request creation. + Please follow the `contributing guidelines `_ for + pull requests!! +- Request and vote for + `Alexa features `_! + + +Additional Resources +-------------------- + +Community +~~~~~~~~~ + +- `Amazon Developer Forums `_ : Join the conversation! +- `Hackster.io `_ - See what others are building with Alexa. + +Tutorials & Guides +~~~~~~~~~~~~~~~~~~ + +- `Voice Design Guide `_ - + A great resource for learning conversational and voice user interface design. \ No newline at end of file diff --git a/ask-sdk-core/.coveragerc b/ask-sdk-core/.coveragerc new file mode 100644 index 0000000..ab4e4db --- /dev/null +++ b/ask-sdk-core/.coveragerc @@ -0,0 +1,9 @@ +[run] +branch = True + +[report] +include = + ask_sdk_core/* +exclude_lines = + if typing.TYPE_CHECKING: + pass \ No newline at end of file diff --git a/ask-sdk-core/.gitignore b/ask-sdk-core/.gitignore new file mode 100644 index 0000000..e057098 --- /dev/null +++ b/ask-sdk-core/.gitignore @@ -0,0 +1,67 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ +venv/ +.python-version + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +#Ipython Notebook +.ipynb_checkpoints + +# IntelliJ configs +*.iml diff --git a/ask-sdk-core/CHANGELOG.rst b/ask-sdk-core/CHANGELOG.rst new file mode 100644 index 0000000..d4eee4e --- /dev/null +++ b/ask-sdk-core/CHANGELOG.rst @@ -0,0 +1,8 @@ +========= +CHANGELOG +========= + +0.1 +------- + +* Initial release of alexa skills kit core sdk. diff --git a/ask-sdk-core/MANIFEST.in b/ask-sdk-core/MANIFEST.in new file mode 100644 index 0000000..314a533 --- /dev/null +++ b/ask-sdk-core/MANIFEST.in @@ -0,0 +1,5 @@ +include README.rst +include CHANGELOG.rst +include LICENSE +include requirements.txt +recursive-exclude tests * \ No newline at end of file diff --git a/ask-sdk-core/README.rst b/ask-sdk-core/README.rst new file mode 100644 index 0000000..e35bb43 --- /dev/null +++ b/ask-sdk-core/README.rst @@ -0,0 +1,50 @@ +==================================================== +ASK SDK Core - Base components of Python ASK SDK +==================================================== + +ask-sdk-core is the core SDK package for Alexa Skills Kit (ASK) by +the Software Development Kit (SDK) team for Python. It provides the +base components and default implementations which work as the boiler +plate code for developing Alexa Skills. + + +Quick Start +----------- + +Installation +~~~~~~~~~~~~~~~ +Assuming that you have Python and ``virtualenv`` installed, you can +install the package and it's dependencies (``ask-sdk-model``) from PyPi +as follows: + +.. code-block:: sh + + >>> virtualenv venv + >>> . venv/bin/activate + >>> pip install ask-sdk-core + + +You can also install the whole standard package locally by following these steps: + +.. code-block:: sh + + >>> git clone https://github.com/alexalabs/alexa-skills-kit-for-python-sdk.git + >>> cd alexa-skills-kit-for-python-sdk/ask-sdk-core + >>> virtualenv venv + ... + >>> . venv/bin/activate + >>> python setup.py install + + +Usage and Getting Started +------------------------- +A Getting Started guide can be found `here <../docs/GETTING_STARTED.rst>`_ + + +Got Feedback? +------------- + +- We would like to hear about your bugs, feature requests, questions or quick feedback. + Please search for the `existing issues `_ before opening a new one. It would also be helpful + if you follow the templates for issue and pull request creation. Please follow the `contributing guidelines <../CONTRIBUTING.rst>`_!! +- Request and vote for `Alexa features `_! diff --git a/ask-sdk-core/ask_sdk_core/__init__.py b/ask-sdk-core/ask_sdk_core/__init__.py new file mode 100644 index 0000000..2b850df --- /dev/null +++ b/ask-sdk-core/ask_sdk_core/__init__.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 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. +# diff --git a/ask-sdk-core/ask_sdk_core/__version__.py b/ask-sdk-core/ask_sdk_core/__version__.py new file mode 100644 index 0000000..76393a4 --- /dev/null +++ b/ask-sdk-core/ask_sdk_core/__version__.py @@ -0,0 +1,28 @@ +# +# Copyright 2018 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. +# + +__pip_package_name__ = 'ask-sdk-core' +__description__ = ('The ASK SDK Core package provides core Alexa Skills Kit ' + 'functionality, for building Alexa Skills.') +__url__ = 'http://developer.amazon.com/ask' +__version__ = '0.1' +__author__ = 'Alexa Skills Kit' +__author_email__ = 'ask-sdk-dynamic@amazon.com' +__license__ = 'Apache 2.0' +__keywords__ = ['ASK SDK', 'Alexa Skills Kit', 'Alexa', 'Core'] +__install_requires__ = ["six", "requests", "python_dateutil", "ask-sdk-model"] + diff --git a/ask-sdk-core/ask_sdk_core/api_client.py b/ask-sdk-core/ask_sdk_core/api_client.py new file mode 100644 index 0000000..e2f5081 --- /dev/null +++ b/ask-sdk-core/ask_sdk_core/api_client.py @@ -0,0 +1,142 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 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. +# +import typing +import requests +import six + +from urllib3.util import parse_url + +from ask_sdk_model.services import ApiClient, ApiClientResponse + +from .exceptions import ApiClientException + +if typing.TYPE_CHECKING: + from typing import Callable, Dict, List, Tuple + from ask_sdk_model.services import ApiClientRequest + + +class DefaultApiClient(ApiClient): + """Default ApiClient implementation of :py:class:`ApiClient` using + the `requests` library. + """ + + def invoke(self, request): + # type: (ApiClientRequest) -> ApiClientResponse + """Dispatches a request to an API endpoint described in the + request. + + Resolves the method from input request object, converts the + list of header tuples to the required format (dict) for the + `requests` lib call and invokes the method with corresponding + parameters on `requests` library. The response from the call is + wrapped under the `ApiClientResponse` object and the + responsibility of translating a response code and response/ + error lies with the caller. + + :param request: Request to dispatch to the ApiClient + :type request: :py:class: + `ask_sdk_model.services.api_client_request.ApiClientRequest` + :return: Response from the client call + :rtype: :py:class: + `ask_sdk_model.services.api_client_response.ApiClientResponse` + :raises ApiClientException + """ + try: + http_method = self._resolve_method(request) + http_headers = self._convert_list_tuples_to_dict( + headers_list=request.headers) + + parsed_url = parse_url(request.url) + if parsed_url.scheme is None or parsed_url.scheme != "https": + raise ApiClientException( + "Requests against non-HTTPS endpoints are not allowed.") + + http_response = http_method( + url=request.url, headers=http_headers, data=request.body) + + return ApiClientResponse( + headers=self._convert_dict_to_list_tuples( + http_response.headers), + status_code=http_response.status_code, + body=http_response.text) + except Exception as e: + raise ApiClientException( + "Error executing the request: {}".format(str(e))) + + def _resolve_method(self, request): + # type: (ApiClientRequest) -> Callable + """Resolve the method from request object to `requests` http + call. + + :param request: Request to dispatch to the ApiClient + :type request: :py:class: + `ask_sdk_model.services.api_client_request.ApiClientRequest` + :return: The HTTP method that maps to the request call. + :rtype: Callable + :raises ApiClientException if invalid http request method is + being called + """ + try: + return getattr(requests, request.method.lower()) + except AttributeError: + raise ApiClientException( + "Invalid request method: {}".format(request.method)) + + def _convert_list_tuples_to_dict(self, headers_list): + # type: (List[Tuple[str, str]]) -> Dict[str, str] + """Convert list of tuples from headers of request object to + dictionary format. + + :param headers_list: List of tuples made up of two element + strings from `ApiClientRequest` headers variable + :type headers_list: List[Tuple[str, str]] + :return: Dictionary of headers in keys as strings and values + as comma separated strings + :rtype: Dict[str, str] + """ + headers_dict = {} + if headers_list is not None: + for header_tuple in headers_list: + key, value = header_tuple[0], header_tuple[1] + if key in headers_dict: + headers_dict[key] = "{}, {}".format( + headers_dict[key], value) + else: + headers_dict[header_tuple[0]] = value + return headers_dict + + def _convert_dict_to_list_tuples(self, headers_dict): + # type: (Dict[str, str]) -> List[Tuple[str, str]] + """Convert headers dict to list of string tuples format for + `ApiClientResponse` headers variable. + + :param headers_dict: Dictionary of headers in keys as strings + and values as comma separated strings + :type headers_dict: Dict[str, str] + :return: List of tuples made up of two element strings from + headers of client response + :rtype: List[Tuple[str, str]] + """ + headers_list = [] + if headers_dict is not None: + for key, values in six.iteritems(headers_dict): + for value in values.split(","): + value = value.strip() + if value is not None and value is not '': + headers_list.append((key, value.strip())) + return headers_list diff --git a/ask-sdk-core/ask_sdk_core/attributes_manager.py b/ask-sdk-core/ask_sdk_core/attributes_manager.py new file mode 100644 index 0000000..7d80da4 --- /dev/null +++ b/ask-sdk-core/ask_sdk_core/attributes_manager.py @@ -0,0 +1,208 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 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. +# +import typing +from abc import ABCMeta, abstractmethod +from copy import deepcopy + +from .exceptions import AttributesManagerException + +if typing.TYPE_CHECKING: + from typing import Dict + from ask_sdk_model import RequestEnvelope + + +class AbstractPersistenceAdapter(object): + """Abstract class for storing and retrieving persistent attributes + from persistence tier given request envelope. + + User needs to implement 'get_attributes' method to get attributes + from persistent tier and 'save_attributes' method to save + attributes to persistent tier. + """ + __metaclass__ = ABCMeta + + @abstractmethod + def get_attributes(self, request_envelope): + # type: (RequestEnvelope) -> Dict[str, object] + """Get attributes from persistent tier. + + :param request_envelope: request envelope. + :type request_envelope : :py:class: + `ask_sdk_model.RequestEnvelope` + :return A dictionary of attributes retrieved from persistent + tier + :rtype: Dict[str, object] + """ + pass + + @abstractmethod + def save_attributes(self, request_envelope, attributes): + # type: (RequestEnvelope, Dict[str, object]) -> None + """Save attributes to persistent tier. + + :param request_envelope: request envelope. + :type request_envelope: :py:class: + `ask_sdk_model.RequestEnvelope` + :param attributes: attributes to be saved to persistent tier + :type attributes: Dict[str, object] + :rtype: None + """ + pass + + +class AttributesManager(object): + """AttributesManager is a class that handles three level + attributes: request, session and persistence. + """ + + def __init__(self, request_envelope, persistence_adapter=None): + # type: (RequestEnvelope, AbstractPersistenceAdapter) -> None + """AttributesManager handling three level of + attributes: request, session and persistence. + + :param request_envelope: request envelope. + :type request_envelope: :py:class: + `ask_sdk_model.RequestEnvelope` + :param persistence_adapter: class used for storing and + retrieving persistent attributes from persistence tier + :type persistence_adapter: AbstractPersistenceAdapter + """ + if request_envelope is None: + raise AttributesManagerException("RequestEnvelope cannot be none!") + self._request_envelope = request_envelope + self._persistence_adapter = persistence_adapter + self._persistence_attributes = None + self._request_attributes = {} + if not self._request_envelope.session: + self._session_attributes = None + else: + if not self._request_envelope.session.attributes: + self._session_attributes = {} + else: + self._session_attributes = deepcopy( + request_envelope.session.attributes) + self._persistent_attributes_set = False + + @property + def request_attributes(self): + # type: () -> Dict[str, object] + """ + + :return request_attributes: attributes for the + request life cycle + :rtype Dict[str, object] + """ + return self._request_attributes + + @request_attributes.setter + def request_attributes(self, request_attributes): + # type: (Dict[str, object]) -> None + """ + + :param request_attributes: attributes for the request life cycle + :type request_attributes: Dict[str, object] + """ + self._request_attributes = request_attributes + + @property + def session_attributes(self): + # type: () -> Dict[str, object] + """ + + :return session_attributes: attributes extracted from + request envelope + :rtype: Dict[str, object] + """ + if not self._request_envelope.session: + raise AttributesManagerException( + "Cannot get SessionAttributes from out of session request!") + return self._session_attributes + + @session_attributes.setter + def session_attributes(self, session_attributes): + # type: (Dict[str, object]) -> None + """ + + :param session_attributes: attributes during the session + :type session_attributes: Dict[str, object] + :raises AttributesManagerException if trying to set session + attributes to out of session request + """ + if not self._request_envelope.session: + raise AttributesManagerException( + "Cannot set SessionAttributes to out of session request!") + self._session_attributes = session_attributes + + @property + def persistent_attributes(self): + # type: () -> Dict[str, object] + """ + + :return persistent_attributes: attributes retrieved from + persistence adapter + :rtype: Dict[str, object] + :raises AttributesManagerException if trying to get + persistent attributes without persistence adapter + """ + if not self._persistence_adapter: + raise AttributesManagerException( + "Cannot get PersistentAttributes without Persistence adapter") + if not self._persistent_attributes_set: + self._persistence_attributes = ( + self._persistence_adapter.get_attributes( + request_envelope=self._request_envelope)) + self._persistent_attributes_set = True + return self._persistence_attributes + + @persistent_attributes.setter + def persistent_attributes(self, persistent_attributes): + # type: (Dict[str, object]) -> None + """Overwrites and caches the persistent attributes value. + + Note that the persistent attributes will not be saved to + persistence layer until the save_persistent_attributes method + is called. + + :param persistent_attributes: attributes in persistence layer + :type persistent_attributes: Dict[str, object] + :raises AttributesManagerException if trying to set + persistent attributes without persistence adapter + """ + if not self._persistence_adapter: + raise AttributesManagerException( + "Cannot set PersistentAttributes without persistence adapter!") + self._persistence_attributes = persistent_attributes + self._persistent_attributes_set = True + + def save_persistent_attributes(self): + # type: () -> None + """Save persistent attributes to the persistence layer if a + persistence adapter is provided. + + :rtype: None + :raises AttributesManagerException if trying to save + persistence attributes without persistence adapter + """ + if not self._persistence_adapter: + raise AttributesManagerException( + "Cannot save PersistentAttributes without " + "persistence adapter!") + if self._persistent_attributes_set: + self._persistence_adapter.save_attributes( + request_envelope=self._request_envelope, + attributes=self._persistence_attributes) diff --git a/ask-sdk-core/ask_sdk_core/dispatch.py b/ask-sdk-core/ask_sdk_core/dispatch.py new file mode 100644 index 0000000..e83762e --- /dev/null +++ b/ask-sdk-core/ask_sdk_core/dispatch.py @@ -0,0 +1,215 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 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. +# +import typing +from abc import ABCMeta, abstractmethod + +from .exceptions import DispatchException + +if typing.TYPE_CHECKING: + from typing import Union, List + from ask_sdk_model import Response + from .handler_input import HandlerInput + from .dispatch_components import ( + HandlerAdapter, RequestMapper, ExceptionMapper, + AbstractRequestInterceptor, AbstractResponseInterceptor) + + +class AbstractRequestDispatcher(object): + """Dispatcher which handles dispatching input request to the + corresponding handler. + + User needs to implement the dispatch method, to handle the + processing of the incoming request in the handler input. A response + may be expected out of the dispatch method. User also has the + flexibility of processing invalid requests by raising custom + exceptions wrapped under :py:class:`DispatchException`. + """ + __metaclass__ = ABCMeta + + @abstractmethod + def dispatch(self, handler_input): + # type: (HandlerInput) -> Union[Response, None] + """Dispatches an incoming request to the appropriate request + handler and returns the output. + + :param handler_input: input to the dispatcher containing + incoming request and other context + :type handler_input: :py:class: + `ask_sdk_core.handler_input.HandlerInput` + :return: output optionally containing a response + :rtype: Union[None, :py:class:`ask_sdk_model.response.Response`] + :raises :py:class:`ask_sdk_core.exceptions.DispatchException` + """ + pass + + +class RequestDispatcher(AbstractRequestDispatcher): + """Default implementation of :py:class:`RequestDispatcher`. + + When the dispatch method is invoked, using a list of + :py:class:`RequestMapper`, the Dispatcher finds a handler for the + request and delegates the invocation to the supported + :py:class:`HandlerAdapter`. If the handler raises any exception, + it is delegated to :py:class:`ExceptionMapper` to handle or raise + it to the upper stack. + """ + + def __init__( + self, handler_adapters=None, request_mappers=None, + exception_mapper=None, request_interceptors=None, + response_interceptors=None): + # type: (List[HandlerAdapter], List[RequestMapper], ExceptionMapper, List[AbstractRequestInterceptor], List[AbstractResponseInterceptor]) -> None + """Default implementation of :py:class:`RequestDispatcher`. + + :param handler_adapters: List of handler adapters that are + supported by the dispatcher. + :type handler_adapters: List[:py:class: + `ask_sdk_core.dispatch_components.HandlerAdapter`] + :param request_mappers: List of Request Mappers containing + user defined handlers. + :type request_mappers: List[:py:class: + `ask_sdk_core.dispatch_components.RequestMapper`] + :param exception_mapper: Exception mapper containing custom + exception handlers. + :type exception_mapper: :py:class: + `ask_sdk_core.dispatch_components.ExceptionMapper` + :param request_interceptors: List of Request Interceptors + :type request_interceptors: List[:py:class: + `ask_sdk_core.dispatch_components. + AbstractRequestInterceptors`] + :param response_interceptors: List of Response Interceptors + :type response_interceptors: List[:py:class: + `ask_sdk_core.dispatch_components. + AbstractResponseInterceptors`] + """ + if handler_adapters is None: + handler_adapters = [] + + if request_mappers is None: + request_mappers = [] + + if request_interceptors is None: + request_interceptors = [] + + if response_interceptors is None: + response_interceptors = [] + + self.handler_adapters = handler_adapters + self.request_mappers = request_mappers + self.exception_mapper = exception_mapper + self.request_interceptors = request_interceptors + self.response_interceptors = response_interceptors + + def dispatch(self, handler_input): + # type: (HandlerInput) -> Union[Response, None] + """Dispatches an incoming request to the appropriate + request handler and returns the output. + + Before running the request on the appropriate request handler, + dispatcher runs any predefined global request interceptors. + On successful response returned from request handler, dispatcher + runs predefined global response interceptors, before returning + the response. + + :param handler_input: input to the dispatcher containing + incoming request and other context + :type handler_input: ask_sdk_core.handler_input.HandlerInput + :return: output optionally containing a response + :rtype: Union[None, ask_sdk_model.response.Response] + :raises ask_sdk_core.exceptions.DispatchException + """ + try: + for request_interceptor in self.request_interceptors: + request_interceptor.process(handler_input=handler_input) + + response = self.__dispatch_request(handler_input) + + for response_interceptor in self.response_interceptors: + response_interceptor.process( + handler_input=handler_input, response=response) + + return response + except Exception as e: + if self.exception_mapper is not None: + exception_handler = self.exception_mapper.get_handler( + handler_input, e) + if exception_handler is None: + raise e + return exception_handler.handle(handler_input, e) + else: + raise e + + def __dispatch_request(self, handler_input): + # type: (HandlerInput) -> Union[Response, None] + """Process the request in handler input and return + handler output. + + When the method is invoked, using the registered list of + :py:class:`RequestMapper`, a Handler Chain is found that can + handle the request. The handler invocation is delegated to the + supported :py:class:`HandlerAdapter`. The registered + request interceptors in the handler chain are processed before + executing the handler. The registered response interceptors in + the handler chain are processed after executing the handler. + + :param handler_input: input to the dispatcher containing + incoming request and other context. + :type handler_input: ask_sdk_core.handler_input.HandlerInput + :return: Output from the 'handle' method execution of the + supporting handler. + :rtype: Union[None, Response] + :raises DispatchException if there is no supporting + handler chain or adapter + """ + request_handler_chain = None + for mapper in self.request_mappers: + request_handler_chain = mapper.get_request_handler_chain( + handler_input) + if request_handler_chain is not None: + break + + if request_handler_chain is None: + raise DispatchException( + "Couldn't find handler that can handle the " + "request: {}".format(handler_input.request_envelope.request)) + + request_handler = request_handler_chain.request_handler + supported_handler_adapter = None + for adapter in self.handler_adapters: + if adapter.supports(request_handler): + supported_handler_adapter = adapter + break + + if supported_handler_adapter is None: + raise DispatchException( + "Couldn't find adapter that can handle the " + "request: {}".format(handler_input.request_envelope.request)) + + local_request_interceptors = request_handler_chain.request_interceptors + for interceptor in local_request_interceptors: + interceptor.process(handler_input=handler_input) + + response = supported_handler_adapter.execute( + handler_input=handler_input, handler=request_handler) + + local_response_interceptors = ( + request_handler_chain.response_interceptors) + for interceptor in local_response_interceptors: + interceptor.process(handler_input=handler_input, response=response) + + return response diff --git a/ask-sdk-core/ask_sdk_core/dispatch_components/__init__.py b/ask-sdk-core/ask_sdk_core/dispatch_components/__init__.py new file mode 100644 index 0000000..acfff68 --- /dev/null +++ b/ask-sdk-core/ask_sdk_core/dispatch_components/__init__.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 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. +# + +# Importing the most commonly used component classes, for +# short-circuiting purposes. + +from .request_components import ( + AbstractRequestHandler, AbstractRequestInterceptor, + AbstractResponseInterceptor, HandlerAdapter, RequestMapper, + RequestHandlerChain) +from .exception_components import ( + AbstractExceptionHandler, ExceptionMapper) diff --git a/ask-sdk-core/ask_sdk_core/dispatch_components/exception_components.py b/ask-sdk-core/ask_sdk_core/dispatch_components/exception_components.py new file mode 100644 index 0000000..ac76846 --- /dev/null +++ b/ask-sdk-core/ask_sdk_core/dispatch_components/exception_components.py @@ -0,0 +1,180 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 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. +# +import typing +from abc import ABCMeta, abstractmethod + +from ..exceptions import DispatchException + +if typing.TYPE_CHECKING: + from typing import Union, List + from ask_sdk_model import Response + from ..handler_input import HandlerInput + + +class AbstractExceptionHandler(object): + """"Handles exception types and optionally produce a response. + + The abstract class is similar to Request Handler, with methods + can_handle and handle. The can_handle method checks if the handler + can support the input and the exception. The handle method + processes the input and exception, to optionally produce a response. + """ + __metaclass__ = ABCMeta + + @abstractmethod + def can_handle(self, handler_input, exception): + # type: (HandlerInput, Exception) -> bool + """Checks if the handler can support the exception raised + during dispatch. + + :param handler_input: Handler Input instance. + :type handler_input: :py:class:`HandlerInput` + :param exception: Exception raised during dispatch. + :type exception: Exception + :return: Boolean whether handler can handle exception or not. + :rtype: bool + """ + pass + + @abstractmethod + def handle(self, handler_input, exception): + # type: (HandlerInput, Exception) -> Union[Response, None] + """Process the handler input and exception. + + :param handler_input: Handler Input instance. + :type handler_input: :py:class:`HandlerInput` + :param exception: Exception raised during dispatch. + :type exception: Exception + :return: Optional response object to serve as dispatch return. + :rtype: Union[None, :py:class:`Response`] + """ + pass + + +class AbstractExceptionMapper(object): + """Mapper to register custom Exception Handler instances. + + The exception mapper is used by :py:class:`RequestDispatcher` + dispatch method, to handle exceptions. The mapper can contain one + or more exception handlers. Handlers are accessed through the + mapper to attempt to find a handler that is compatible with the + current exception. + """ + __metaclass__ = ABCMeta + + @abstractmethod + def get_handler(self, handler_input, exception): + # type: (HandlerInput, Exception) -> Union[AbstractExceptionHandler, None] + """Returns a suitable exception handler to dispatch the + specified exception, if one exists. + + :param handler_input: Handler Input instance. + :type handler_input: :py:class:`HandlerInput` + :param exception: Exception thrown by + :py:class:`RequestDispatcher` dispatch method. + :type exception: Exception + :return: Exception Handler that can handle the input or None. + :rtype: Union[None, :py:class:`ExceptionHandler`] + """ + pass + + +class ExceptionMapper(AbstractExceptionMapper): + """Implementation of exception mapper, to register + :py:class:`AbstractExceptionHandler` instances. + + The class accepts exception handlers of type + :py:class:`ExceptionHandler` only. The 'get_handler' method returns + the :py:class:`ExceptionHandler` instance that can handle the + handler input and the exception raised from the dispatch method. + """ + + def __init__(self, exception_handlers): + # type: (List[AbstractExceptionHandler]) -> None + """Implementation of :py:class:`AbstractExceptionMapper` that + registers :py:class:`AbstractExceptionHandler`. + + The class accepts exception handlers of type + :py:class:`AbstractExceptionHandler` only. + + :param exception_handlers: List of + :py:class:`AbstractExceptionHandler` instances. + :type exception_handlers: list(AbstractExceptionHandler) + """ + self.exception_handlers = exception_handlers + + @property + def exception_handlers(self): + # type: () -> List[AbstractExceptionHandler] + """ + :return: List of :py:class:`AbstractExceptionHandler` instances. + :rtype: list(AbstractExceptionHandler) + """ + return self._exception_handlers + + @exception_handlers.setter + def exception_handlers(self, exception_handlers): + # type: (List[AbstractExceptionHandler]) -> None + """ + + :param exception_handlers: List of + :py:class:`AbstractExceptionHandler` instances. + :type exception_handlers: list( + :py:class:`AbstractExceptionHandler`) + :raises DispatchException when any object inside the input list + is of invalid type + """ + self._exception_handlers = [] + if exception_handlers is not None: + for handler in exception_handlers: + self.add_exception_handler(exception_handler=handler) + + def add_exception_handler(self, exception_handler): + # type: (AbstractExceptionHandler) -> None + """Checks the type before adding it to the exception_handlers + instance variable. + + :param exception_handler: Exception Handler instance. + :type exception_handler: AbstractExceptionHandler + :raises DispatchException if a null input is provided or if + the input is of invalid type + """ + if exception_handler is None or not isinstance( + exception_handler, AbstractExceptionHandler): + raise DispatchException( + "Input is not an AbstractExceptionHandler instance") + self._exception_handlers.append(exception_handler) + + def get_handler(self, handler_input, exception): + # type: (HandlerInput, Exception) -> Union[AbstractExceptionHandler, None] + """Get the exception handler that can handle the input and + exception. + + :param handler_input: Handler Input instance. + :type handler_input: HandlerInput + :param exception: Exception thrown by + :py:class:`RequestDispatcher` dispatch method. + :type exception: Exception + :return: Exception Handler that can handle the input or None. + :rtype: Union[None, :py:class:`AbstractExceptionHandler`] + """ + for handler in self.exception_handlers: + if handler.can_handle( + handler_input=handler_input, exception=exception): + return handler + return None diff --git a/ask-sdk-core/ask_sdk_core/dispatch_components/request_components.py b/ask-sdk-core/ask_sdk_core/dispatch_components/request_components.py new file mode 100644 index 0000000..66ecdb0 --- /dev/null +++ b/ask-sdk-core/ask_sdk_core/dispatch_components/request_components.py @@ -0,0 +1,454 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 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. +# +import typing +from abc import ABCMeta, abstractmethod + +from ..exceptions import DispatchException + +if typing.TYPE_CHECKING: + from typing import Union, List + from ask_sdk_model import Response + from ..handler_input import HandlerInput + + +class AbstractRequestHandler(object): + """Request Handlers are responsible for processing Request inside + the Handler Input and generating Response. + + Custom request handlers needs to implement 'can_handle' and + 'handle' methods. 'can_handle' returns True if the handler can + handle the current request. 'handle' processes the Request and + may return a Response. + """ + __metaclass__ = ABCMeta + + @abstractmethod + def can_handle(self, handler_input): + # type: (HandlerInput) -> bool + """Returns true if Request Handler can handle the Request + inside Handler Input. + + :param handler_input: Handler Input instance with + Request Envelope containing Request. + :type handler_input: :py:class:'HandlerInput' + :return: Boolean value that tells the dispatcher if the + current request can be handled by this handler. + :rtype: bool + """ + pass + + @abstractmethod + def handle(self, handler_input): + # type: (HandlerInput) -> Union[None, Response] + """Handles the Request inside handler input and provides a + Response for dispatcher to return. + + :param handler_input: Handler Input instance with + Request Envelope containing Request. + :type handler_input: :py:class:'HandlerInput' + :return: Response for the dispatcher to return or None + :rtype: Union[:py:class:'ask_sdk_model.Response', None] + """ + pass + + +class AbstractRequestInterceptor(object): + """Interceptor that runs before the handler is called. + + The process method has to be implemented, to run custom logic on + the input, before it is handled by the Handler. + """ + __metaclass__ = ABCMeta + + @abstractmethod + def process(self, handler_input): + # type: (HandlerInput) -> None + """Process the input before the Handler is run. + + :param handler_input: Handler Input instance. + :type handler_input: HandlerInput + :rtype: None + """ + pass + + +class AbstractResponseInterceptor(object): + """Interceptor that runs after the handler is called. + + The process method has to be implemented, to run custom logic on + the input and the response generated after the handler is executed + on the input. + """ + __metaclass__ = ABCMeta + + @abstractmethod + def process(self, handler_input, response): + # type: (HandlerInput, Response) -> None + """Process the input and the response after the Handler is run. + + :param handler_input: Handler Input instance. + :type handler_input: HandlerInput + :param response: Execution result of the Handler on + handler input. + :type response: Union[None, Response] + :rtype: None + """ + pass + + +class AbstractRequestHandlerChain(object): + """Abstract class containing Request Handler and corresponding + Interceptors. + """ + __metaclass__ = ABCMeta + + @abstractmethod + def request_handler(self): + # type: () -> object + """ + + :return: Registered Request Handler instance. + :rtype: object + """ + pass + + @abstractmethod + def request_interceptors(self): + # type: () -> List[AbstractRequestInterceptor] + """ + :return: List of registered Request Interceptors. + :rtype: list(RequestInterceptor) + """ + pass + + @abstractmethod + def response_interceptors(self): + # type: () -> List[AbstractResponseInterceptor] + """ + + :return: List of registered Response Interceptors. + :rtype: list(ResponseInterceptor) + """ + pass + + +class GenericRequestHandlerChain(AbstractRequestHandlerChain): + """Generic implementation of + :py:class:`AbstractRequestHandlerChain`. + + Generic Request Handler Chain accepts request handler of any type. + This class can be used to register request handler of type other + than :py:class:`RequestHandler`. + """ + def __init__( + self, request_handler, request_interceptors=None, + response_interceptors=None): + # type: (AbstractRequestHandler, List[AbstractRequestInterceptor], List[AbstractResponseInterceptor]) -> None + """Generic implementation of + :py:class:`AbstractRequestHandlerChain`. + + :param request_handler: Registered Request Handler instance of + generic type. + :type request_handler: AbstractRequestHandler + :param request_interceptors: List of registered Request + Interceptors. + :type request_interceptors: list(RequestInterceptor) + :param response_interceptors: List of registered Response + Interceptors. + :type response_interceptors: list(ResponseInterceptor) + """ + self.request_handler = request_handler + self.request_interceptors = request_interceptors + self.response_interceptors = response_interceptors + + @property + def request_handler(self): + # type: () -> object + return self._request_handler + + @request_handler.setter + def request_handler(self, request_handler): + # type: (AbstractRequestHandler) -> None + if request_handler is None: + raise DispatchException("No Request Handler provided") + self._request_handler = request_handler + + @property + def request_interceptors(self): + # type: () -> List[AbstractRequestInterceptor] + return self._request_interceptors + + @request_interceptors.setter + def request_interceptors(self, request_interceptors): + # type: (List[AbstractRequestInterceptor]) -> None + if request_interceptors is None: + request_interceptors = [] + self._request_interceptors = request_interceptors + + @property + def response_interceptors(self): + # type: () -> List[AbstractResponseInterceptor] + return self._response_interceptors + + @response_interceptors.setter + def response_interceptors(self, response_interceptors): + # type: (List[AbstractResponseInterceptor]) -> None + if response_interceptors is None: + response_interceptors = [] + self._response_interceptors = response_interceptors + + def add_request_interceptor(self, interceptor): + # type: (AbstractRequestInterceptor) -> None + """Add interceptor to Request Interceptors list. + + :param interceptor: Request Interceptor instance. + :type interceptor: :py:class:`RequestInterceptor` + """ + self.request_interceptors.append(interceptor) + + def add_response_interceptor(self, interceptor): + # type: (AbstractResponseInterceptor) -> None + """Add interceptor to Response Interceptors list. + + :param interceptor: Response Interceptor instance. + :type interceptor: :py:class:`ResponseInterceptor` + """ + self.response_interceptors.append(interceptor) + + +class RequestHandlerChain(GenericRequestHandlerChain): + """Implementation of :py:class:`AbstractRequestHandlerChain` which + handles :py:class:`RequestHandler`. + """ + + def __init__( + self, request_handler, request_interceptors=None, + response_interceptors=None): + # type: (AbstractRequestHandler, List[AbstractRequestInterceptor], List[AbstractResponseInterceptor]) -> None + """Implementation of :py:class:`AbstractRequestHandlerChain` + which handles :py:class:`RequestHandler`. + + :param request_handler: Registered Request Handler instance. + :type request_handler: RequestHandler + :param request_interceptors: List of registered Request + Interceptors. + :type request_interceptors: list(RequestInterceptor) + :param response_interceptors: List of registered Response + Interceptors. + :type response_interceptors: list(ResponseInterceptor) + :raises DispatchException when invalid request handler is + provided. + """ + if request_handler is None or not isinstance( + request_handler, AbstractRequestHandler): + raise DispatchException( + "Invalid Request Handler provided. Expected " + "Request Handler instance") + + super(RequestHandlerChain, self).__init__( + request_handler=request_handler, + request_interceptors=request_interceptors, + response_interceptors=response_interceptors) + + @GenericRequestHandlerChain.request_handler.setter + def request_handler(self, request_handler): + # type: (AbstractRequestHandler) -> None + if request_handler is None or not isinstance( + request_handler, AbstractRequestHandler): + raise DispatchException( + "Invalid Request Handler provided. Expected " + "Request Handler instance") + + GenericRequestHandlerChain.request_handler.fset( + self, request_handler) + + +class AbstractRequestMapper(object): + """Class for request routing to the appropriate handler chain. + + User needs to implement 'get_request_handler_chain' method, to + provide a routing mechanism of the input to the appropriate request + handler chain containing the handler and the interceptors. + """ + __metaclass__ = ABCMeta + + @abstractmethod + def get_request_handler_chain(self, handler_input): + # type: (HandlerInput) -> AbstractRequestHandlerChain + """Get the handler chain that can process the handler input. + + :param handler_input: Handler Input instance. + :type handler_input: HandlerInput + :return: Handler Chain that can handle the request under + handler input. + :rtype: RequestHandlerChain + """ + pass + + +class RequestMapper(AbstractRequestMapper): + """Implementation of :py:class:`AbstractRequestMapper` that + registers :py:class:`RequestHandlerChain`. + + The class accepts request handler chains of type + :py:class:`RequestHandlerChain` only. The + 'get_request_handler_chain' method returns the + :py:class:`RequestHandlerChain` instance that can + handle the request in the handler input. + """ + + def __init__(self, request_handler_chains): + # type: (List[RequestHandlerChain]) -> None + """Implementation of :py:class:`AbstractRequestMapper` that + registers :py:class:`RequestHandlerChain`. + + The class accepts request handler chains of type + :py:class:`RequestHandlerChain` only. + + :param request_handler_chains: List of + :py:class:`RequestHandlerChain` instances. + :type request_handler_chains: list(RequestHandlerChain) + """ + self.request_handler_chains = request_handler_chains + + @property + def request_handler_chains(self): + # type: () -> List[RequestHandlerChain] + """ + + :return: List of :py:class:`RequestHandlerChain` instances. + :rtype: list(RequestHandlerChain) + """ + return self._request_handler_chains + + @request_handler_chains.setter + def request_handler_chains(self, request_handler_chains): + # type: (List[RequestHandlerChain]) -> None + """ + + :param request_handler_chains: List of + :py:class:`RequestHandlerChain` instances. + :type request_handler_chains: list( + :py:class:`RequestHandlerChain`) + :raises DispatchException when any object inside the input + list is of invalid type + """ + self._request_handler_chains = [] + if request_handler_chains is not None: + for chain in request_handler_chains: + self.add_request_handler_chain(request_handler_chain=chain) + + def add_request_handler_chain(self, request_handler_chain): + # type: (RequestHandlerChain) -> None + """Checks the type before adding it to the + request_handler_chains instance variable. + + :param request_handler_chain: Request Handler Chain instance. + :type request_handler_chain: RequestHandlerChain + :raises DispatchException if a null input is provided or if + the input is of invalid type + """ + if request_handler_chain is None or not isinstance( + request_handler_chain, RequestHandlerChain): + raise DispatchException( + "Request Handler Chain is not a RequestHandlerChain instance") + self._request_handler_chains.append(request_handler_chain) + + def get_request_handler_chain(self, handler_input): + # type: (HandlerInput) -> Union[RequestHandlerChain, None] + """Get the request handler chain that can handle the input. + + :param handler_input: Handler Input instance. + :type handler_input: HandlerInput + :return: Handler Chain that can handle the input. + :rtype: Union[None, RequestHandlerChain] + """ + for chain in self.request_handler_chains: + handler = chain.request_handler + if handler.can_handle(handler_input=handler_input): + return chain + return None + + +class AbstractHandlerAdapter(object): + """Abstracts handling of a request for specific handler types.""" + __metaclass__ = ABCMeta + + @abstractmethod + def supports(self, handler): + # type: (AbstractRequestHandler) -> bool + """Returns true if adapter supports the handler. + + This method checks if the adapter supports the handler + execution. This is usually checked by the type of the handler. + + :param handler: Request Handler instance. + :type handler: object + :return: Boolean denoting whether the adapter supports the + handler. + :rtype: bool + """ + pass + + @abstractmethod + def execute(self, handler_input, handler): + # type: (HandlerInput, AbstractRequestHandler) -> Union[None, Response] + """Executes the handler with the provided handler input. + + :param handler_input: Input containing request envelope, + context and other fields for request handling. + :type handler_input: HandlerInput + :param handler: Request Handler instance. + :type handler: object + :return: Result executed by passing handler_input to handler. + :rtype: Union[None, ask_sdk_model.response.Response] + """ + pass + + +class HandlerAdapter(AbstractHandlerAdapter): + """Handler Adapter for handlers of type + :py:class:`AbstractRequestHandler`. + """ + + def supports(self, handler): + # type: (AbstractRequestHandler) -> bool + """Returns true if handler is + :py:class:`AbstractRequestHandler` instance. + + :param handler: Request Handler instance + :type handler: AbstractRequestHandler + :return: Boolean denoting whether the adapter supports the + handler. + :rtype: bool + """ + return isinstance(handler, AbstractRequestHandler) + + def execute(self, handler_input, handler): + # type: (HandlerInput, AbstractRequestHandler) -> Union[None, Response] + """Executes the handler with the provided handler input. + + :param handler_input: Input containing request envelope, + context and other fields for request handling. + :type handler_input: HandlerInput + :param handler: Request Handler instance. + :type handler: object + :return: Result executed by passing handler_input to handler. + :rtype: Union[None, ask_sdk_model.response.Response] + """ + return handler.handle(handler_input) diff --git a/ask-sdk-core/ask_sdk_core/exceptions.py b/ask-sdk-core/ask_sdk_core/exceptions.py new file mode 100644 index 0000000..0677de5 --- /dev/null +++ b/ask-sdk-core/ask_sdk_core/exceptions.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 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. +# + + +class AskSdkException(Exception): + """Base class for exceptions raised by the SDK.""" + pass + + +class DispatchException(AskSdkException): + """Class for exceptions raised during dispatch logic.""" + pass + + +class AttributesManagerException(AskSdkException): + """Class for exceptions raised during handling attributes logic""" + pass + + +class SerializationException(AskSdkException): + """Class for exceptions raised during + serialization/deserialization. + """ + pass + + +class SkillBuilderException(AskSdkException): + """Base exception class for Skill Builder exceptions.""" + pass + + +class PersistenceException(AskSdkException): + """Exception class for Persistence Adapter processing.""" + pass + + +class ApiClientException(AskSdkException): + """Exception class for ApiClient Adapter processing.""" + pass diff --git a/ask-sdk-core/ask_sdk_core/handler_input.py b/ask-sdk-core/ask_sdk_core/handler_input.py new file mode 100644 index 0000000..61c6d75 --- /dev/null +++ b/ask-sdk-core/ask_sdk_core/handler_input.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 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. +# +import typing +from .response_helper import ResponseFactory + +if typing.TYPE_CHECKING: + from ask_sdk_model import Context, RequestEnvelope + from ask_sdk_model.services import ServiceClientFactory + from .attributes_manager import AttributesManager + + +class HandlerInput(object): + """Input to Request Handler and Exception Handler. + + Handler Input instantiations are passed to + :py:class:`RequestHandler` and :py:class:`ExceptionHandler`, during + skill invocation. The class provides a + :py:class:`AttributesManager` and a :py:class:`ResponseBuilder` + instance, apart from :py:class:`RequestEnvelope`, Context and + :py:class:`ServiceClientFactory` instances, to utilize during the + lifecycle of skill. + + :type request_envelope: ask_sdk_model.RequestEnvelope + :type attributes_manager: ask_sdk_core.attributes_manager. + AttributesManager + :type context: object + :type service_client_factory: ask_sdk_model.services. + ServiceClientFactory + """ + def __init__( + self, request_envelope, attributes_manager=None, + context=None, service_client_factory=None): + # type: (RequestEnvelope, AttributesManager, Context, ServiceClientFactory) -> None + """Input to Request Handler and Exception Handler. + + :type request_envelope: ask_sdk_model.RequestEnvelope + :type attributes_manager: ask_sdk_core.attributes_manager. + AttributesManager + :type context: object + :type service_client_factory: ask_sdk_model.services. + ServiceClientFactory + """ + self.request_envelope = request_envelope + self.context = context + self.service_client_factory = service_client_factory + self.attributes_manager = attributes_manager + self.response_builder = ResponseFactory() + + @property + def service_client_factory(self): + # type: () -> ServiceClientFactory + if self._service_client_factory is None: + raise ValueError( + "Attempting to use service client factory with no " + "configured API client") + + return self._service_client_factory + + @service_client_factory.setter + def service_client_factory(self, service_client_factory): + # type: (ServiceClientFactory) -> None + """ + :type service_client_factory: ask_sdk_model.services. + ServiceClientFactory + """ + self._service_client_factory = service_client_factory diff --git a/ask-sdk-core/ask_sdk_core/response_helper.py b/ask-sdk-core/ask_sdk_core/response_helper.py new file mode 100644 index 0000000..a16675c --- /dev/null +++ b/ask-sdk-core/ask_sdk_core/response_helper.py @@ -0,0 +1,278 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 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. +# + +import typing +from ask_sdk_model import Response +from ask_sdk_model.ui import SsmlOutputSpeech, Reprompt +from ask_sdk_model.interfaces.display import ( + TextContent, PlainText, RichText) + +if typing.TYPE_CHECKING: + from typing import Union + from ask_sdk_model import Directive + from ask_sdk_model.ui import Card + + +PLAIN_TEXT_TYPE = "PlainText" +"""str: Helper variable for plain text type.""" + +RICH_TEXT_TYPE = "RichText" +"""str: Helper variable for rich text type.""" + + +class ResponseFactory(object): + """ResponseFactory is class which provides helper functions to help + building a response. + """ + + def __init__(self): + # type: () -> None + """The ResponseFactory has property Response with all + parameters initialized to None. + """ + self.response = Response( + output_speech=None, card=None, reprompt=None, + directives=None, should_end_session=None) + + def speak(self, speech): + # type: (str) -> 'ResponseFactory' + """Say the provided speech to the user. + + :param speech: the output speech sent back to the user. + :type speech: str + :return response factory with partial response being built and + access from self.response. + :rtype: ResponseFactory + """ + ssml = "{}".format(self.__trim_outputspeech( + speech_output=speech)) + self.response.output_speech = SsmlOutputSpeech(ssml=ssml) + return self + + def ask(self, reprompt): + # type: (str) -> 'ResponseFactory' + """Provide reprompt speech to the user, if no response for + 8 seconds. + + The should_end_session value will be set to false except when + the video app launch directive is present in directives. + + :param reprompt: the output speech to reprompt. + :type reprompt: str + :return response factory with partial response being built and + access from self.response. + :rtype: ResponseFactory + """ + ssml = "{}".format(self.__trim_outputspeech( + speech_output=reprompt)) + output_speech = SsmlOutputSpeech(ssml=ssml) + self.response.reprompt = Reprompt(output_speech=output_speech) + if not self.__is_video_app_launch_directive_present(): + self.response.should_end_session = False + return self + + def set_card(self, card): + # type: (Card) -> 'ResponseFactory' + """Renders a card within the response. + + For more information about card object in response, click here: + https://developer.amazon.com/docs/custom-skills/request-and-response-json-reference.html#card-object. + + :param card: card object in response sent back to user. + :type card: :py:class:`ask_sdk_model.ui.card.Card` + :return response factory with partial response being built + and access from self.response. + :rtype: ResponseFactory + """ + self.response.card = card + return self + + def add_directive(self, directive): + # type: (Directive) -> 'ResponseFactory' + """Adds directive to response. + + :param directive: the directive sent back to Alexa device. + :type directive: :py:class:`ask_sdk_model.Directive` + :return response factory with partial response being built and + access from self.response. + :rtype: ResponseFactory + """ + if self.response.directives is None: + self.response.directives = [] + + if (directive is not None and + directive.object_type == "VideoApp.Launch"): + self.response.should_end_session = None + self.response.directives.append(directive) + return self + + def set_should_end_session(self, should_end_session): + # type: (bool) -> 'ResponseFactory' + """Sets shouldEndSession value to null/false/true. + + :param should_end_session: value to show if the session should + be ended or not. + :type should_end_session: bool + :return response factory with partial response being built and + access from self.response. + :rtype: ResponseFactory + """ + if not self.__is_video_app_launch_directive_present(): + self.response.should_end_session = should_end_session + return self + + def __trim_outputspeech(self, speech_output=None): + # type: (Union[str, None]) -> str + """Trims the output speech if it already has the + tag. + + :param speech_output: the output speech sent back to user. + :type speech_output: str + :return the trimmed output speech. + :rtype: Union[bool, None] + """ + if speech_output is None: + return "" + speech = speech_output.strip() + if speech.startswith("") and speech.endswith(""): + return speech[7:-8].strip() + return speech + + def __is_video_app_launch_directive_present(self): + # type: () -> bool + """Checks if the video app launch directive is present or not. + + :return boolean to show if video app launch directive is + present or not. + :rtype: bool + """ + if self.response.directives is None: + return False + + for directive in self.response.directives: + if (directive is not None and + directive.object_type == "VideoApp.Launch"): + return True + return False + + +def get_plain_text_content(primary_text=None, secondary_text=None, tertiary_text=None): + # type: (str, str, str) -> TextContent + """Responsible for building plain text content object using + ask-sdk-model in Alexa skills kit display interface. + https://developer.amazon.com/docs/custom-skills/display-interface-reference.html#textcontent-object-specifications. + + :type primary_text: (optional) str + :type secondary_text: (optional) str + :type tertiary_text: (optional) str + :return Text Content instance with primary, secondary and tertiary + text set as Plain Text objects. + :rtype :py:class:`ask_sdk_model.interfaces.display.TextContent` + :raises ValueError + """ + return get_text_content( + primary_text=primary_text, primary_text_type=PLAIN_TEXT_TYPE, + secondary_text=secondary_text, secondary_text_type=PLAIN_TEXT_TYPE, + tertiary_text=tertiary_text, tertiary_text_type=PLAIN_TEXT_TYPE) + + +def get_rich_text_content(primary_text=None, secondary_text=None, tertiary_text=None): + # type: (str, str, str) -> TextContent + """Responsible for building plain text content object using + ask-sdk-model in Alexa skills kit display interface. + https://developer.amazon.com/docs/custom-skills/display-interface-reference.html#textcontent-object-specifications. + + :type primary_text: (optional) str + :type secondary_text: (optional) str + :type tertiary_text: (optional) str + :return Text Content instance with primary, secondary and tertiary + text set as Plain Text objects. + :rtype :py:class:`ask_sdk_model.interfaces.display.TextContent` + :raises ValueError + """ + return get_text_content( + primary_text=primary_text, primary_text_type=RICH_TEXT_TYPE, + secondary_text=secondary_text, secondary_text_type=RICH_TEXT_TYPE, + tertiary_text=tertiary_text, tertiary_text_type=RICH_TEXT_TYPE) + + +def get_text_content( + primary_text=None, primary_text_type=PLAIN_TEXT_TYPE, + secondary_text=None, secondary_text_type=PLAIN_TEXT_TYPE, + tertiary_text=None, tertiary_text_type=PLAIN_TEXT_TYPE): + # type: (str, str, str, str, str, str) -> TextContent + """Responsible for building text content object using ask-sdk-model + in Alexa skills kit display interface. + https://developer.amazon.com/docs/custom-skills/display-interface-reference.html#textcontent-object-specifications. + + :type primary_text: (optional) str + :param primary_text_type: Type of the primary text field. Allowed + values are `PlainText` and `RichText`. + Defaulted to `PlainText`. + :type primary_text_type: (optional) str + :type secondary_text: (optional) str + :param secondary_text_type: Type of the secondary text field. + Allowed values are `PlainText` and `RichText`. + Defaulted to `PlainText`. + :type tertiary_text: (optional) str + :param tertiary_text_type: Type of the tertiary text field. + Allowed values are `PlainText` and `RichText`. + Defaulted to `PlainText`. + :return Text Content instance with primary, secondary and tertiary + text set. + :rtype :py:class:`ask_sdk_model.interfaces.display.TextContent` + :raises ValueError + """ + text_content = TextContent() + if primary_text: + text_content.primary_text = __set_text_field( + primary_text, primary_text_type) + if secondary_text: + text_content.secondary_text = __set_text_field( + secondary_text, secondary_text_type) + if tertiary_text: + text_content.tertiary_text = __set_text_field( + tertiary_text, tertiary_text_type) + return text_content + + +def __set_text_field(text, text_type): + # type: (str, str) -> Union[None, PlainText, RichText] + """Helper method to create text field according to text type. + + :type text: str + :param text_type: Type of the primary text field. Allowed values + are `PlainText` and `RichText`. + :type text_type: str + :return Object of type :py:class: + `ask_sdk_model.interfaces.display.PlainText` or + :py:class:`ask_sdk_model.interfaces.display.RichText` depending + on text_type + :rtype object + :raises ValueError + """ + if text: + if text_type not in [PLAIN_TEXT_TYPE, RICH_TEXT_TYPE]: + raise ValueError("Invalid type provided: {}".format(text_type)) + + if text_type == PLAIN_TEXT_TYPE: + return PlainText(text=text) + else: + return RichText(text=text) + else: + return None diff --git a/ask-sdk-core/ask_sdk_core/serialize.py b/ask-sdk-core/ask_sdk_core/serialize.py new file mode 100644 index 0000000..e22303a --- /dev/null +++ b/ask-sdk-core/ask_sdk_core/serialize.py @@ -0,0 +1,317 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 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. +# +import sys +import re +import json +import typing +import decimal +from datetime import date, datetime + +from six import iteritems +from six import PY3 +from six import text_type +from six import integer_types +from enum import Enum + +from ask_sdk_model.services import Serializer + +from .exceptions import SerializationException + +if PY3: + unicode_type = str +else: + unicode_type = unicode + +if typing.TYPE_CHECKING: + from typing import TypeVar, Dict, List, Tuple, Union, Any + T = TypeVar('T') + + +class DefaultSerializer(Serializer): + PRIMITIVE_TYPES = (float, bool, bytes, text_type) + integer_types + NATIVE_TYPES_MAPPING = { + 'int': int, + 'long': int if PY3 else long, + 'float': float, + 'str': str, + 'bool': bool, + 'date': date, + 'datetime': datetime, + 'object': object, + } + + def serialize(self, obj): + # type: (Any) -> Union[Dict[str, Any], List, Tuple, str, None] + """Builds a serialized object. + + If obj is None, return None. + If obj is str, int, long, float, bool, return directly. + If obj is datetime.datetime, datetime.date + convert to string in iso8601 format. + If obj is list, serialize each element in the list. + If obj is dict, return the dict with serialized values. + If obj is ask sdk model, return the dict with keys resolved + from model's 'attribute_map' and values serialized + based on 'deserialized_types'. + + :param obj: The data to serialize. + :type obj: object + :return: The serialized form of data. + :rtype: Union[Dict[str, Any], List[Any], Tuple[Any], str, None] + """ + if obj is None: + return None + elif isinstance(obj, self.PRIMITIVE_TYPES): + return obj + elif isinstance(obj, list): + return [self.serialize(sub_obj) for sub_obj in obj] + elif isinstance(obj, tuple): + return tuple(self.serialize(sub_obj) for sub_obj in obj) + elif isinstance(obj, (datetime, date)): + return obj.isoformat() + elif isinstance(obj, Enum): + return obj.value + elif isinstance(obj, decimal.Decimal): + if obj % 1 == 0: + return int(obj) + else: + return float(obj) + + if isinstance(obj, dict): + obj_dict = obj + else: + # Convert model obj to dict except + # attributes `deserialized_types`, `attribute_map` + # and attributes which value is not None. + # Convert attribute name to json key in + # model definition for request. + obj_dict = { + obj.attribute_map[attr]: getattr(obj, attr) + for attr, _ in iteritems(obj.deserialized_types) + if getattr(obj, attr) is not None + } + + return {key: self.serialize(val) for key, val in iteritems(obj_dict)} + + def deserialize(self, payload, obj_type): + # type: (str, Union[T, str]) -> Any + """Deserializes payload into ask sdk model object. + + :param payload: data to be deserialized. + :type payload: str + :param obj_type: 'resolved class name for deserialized object' + :type obj_type: Union[str, object] + :return: deserialized object + :rtype: object + """ + if payload is None: + return None + + try: + payload = json.loads(payload) + except Exception: + raise SerializationException( + "Couldn't parse response body: {}".format(payload)) + + return self.__deserialize(payload, obj_type) + + def __deserialize(self, payload, obj_type): + # type: (str, Union[T, str]) -> Any + """Deserializes payload into ask sdk model object. + + :param payload: data to be deserialized. + :type payload: str + :param obj_type: resolved class name for deserialized object + :type obj_type: Union[str, object] + :return: deserialized object + :rtype: T + """ + if type(obj_type) == str: + if obj_type.startswith('list['): + # Get object type for each item in the list + # Deserialize each item using the object type. + sub_obj_types = re.match( + 'list\[(.*)\]', obj_type).group(1) + deserialized_list = [] + if "," in sub_obj_types: + # list contains objects of different types + for sub_payload, sub_obj_type in zip( + payload, sub_obj_types.split(",")): + deserialized_list.append(self.__deserialize( + sub_payload, sub_obj_type.strip())) + else: + for sub_payload in payload: + deserialized_list.append(self.__deserialize( + sub_payload, sub_obj_types.strip())) + return deserialized_list + + if obj_type.startswith('dict('): + # Get object type for each k,v pair in the dict + # Deserialize each value using the object type of v. + sub_obj_type = re.match( + 'dict\(([^,]*), (.*)\)', obj_type).group(2) + return { + k: self.__deserialize(v, sub_obj_type) + for k, v in iteritems(payload) + } + + # convert str to class + if obj_type in self.NATIVE_TYPES_MAPPING: + obj_type = self.NATIVE_TYPES_MAPPING[obj_type] + else: + # deserialize ask sdk models + obj_type = self.__load_class_from_name(obj_type) + + if obj_type in self.PRIMITIVE_TYPES: + return self.__deserialize_primitive(payload, obj_type) + elif obj_type == object: + return payload + elif obj_type == date: + return self.__deserialize_datetime(payload, obj_type) + elif obj_type == datetime: + return self.__deserialize_datetime(payload, obj_type) + else: + return self.__deserialize_model(payload, obj_type) + + def __load_class_from_name(self, class_name): + # type: (str) -> str + try: + module_class_list = class_name.rsplit(".", 1) + if len(module_class_list) > 1: + module_name = module_class_list[0] + resolved_class_name = module_class_list[1] + module = __import__( + module_name, fromlist=[resolved_class_name]) + resolved_class = getattr(module, resolved_class_name) + else: + resolved_class_name = module_class_list[0] + resolved_class = getattr( + sys.modules[__name__], resolved_class_name) + return resolved_class + except Exception as e: + raise SerializationException( + "Unable to resolve class {} from installed " + "modules: {}".format(class_name, str(e))) + + def __deserialize_primitive(self, payload, obj_type): + # type: (str, Union[T, str]) -> Any + """Deserialize primitive datatypes. + + :param payload: data to be deserialized + :type payload: str + :param obj_type: primitive datatype str + :type obj_type: object + :return: deserialized primitive datatype object + :rtype: object + :raises SerializationException + """ + try: + return obj_type(payload) + except UnicodeEncodeError: + return unicode_type(payload) + except TypeError: + return payload + except ValueError: + raise SerializationException( + "Failed to parse {} into '{}' object".format( + payload, obj_type.__name__)) + + def __deserialize_datetime(self, payload, obj_type): + # type: (str, Union[T, str]) -> Any + """Deserialize datetime instance in ISO8601 format to + date/datetime object. + + :param payload: data to be deserialized in ISO8601 format + :type payload: str + :param obj_type: primitive datatype str + :type obj_type: object + :return: deserialized primitive datatype object + :rtype: object + :raises SerializationException + """ + try: + from dateutil.parser import parse + parsed_datetime = parse(payload) + if obj_type is date: + return parsed_datetime.date() + else: + return parsed_datetime + except ImportError: + return payload + except ValueError: + raise SerializationException( + "Failed to parse {} into '{}' object".format( + payload, obj_type.__name__)) + + def __deserialize_model(self, payload, obj_type): + # type: (str, Union[T, str]) -> Any + """Deserialize instance to model object. + + :param payload: data to be deserialized + :type payload: str + :param obj_type: sdk model class + :type obj_type: object + :return: deserialized sdk model object + :rtype: object + :raises SerializationException + """ + try: + if issubclass(obj_type, Enum): + return obj_type(payload) + + if hasattr(obj_type, 'deserialized_types') and hasattr( + obj_type, 'attribute_map'): + if hasattr(obj_type, 'get_real_child_model'): + obj_type = self.__get_obj_by_discriminator( + payload, obj_type) + + class_deserialized_types = obj_type.deserialized_types + class_attribute_map = obj_type.attribute_map + + deserialized_model = obj_type() + for class_param_name, payload_param_name in iteritems( + class_attribute_map): + if payload_param_name in payload: + setattr( + deserialized_model, + class_param_name, + self.__deserialize( + payload[payload_param_name], + class_deserialized_types[class_param_name])) + + additional_params = [ + param for param in payload + if param not in class_attribute_map.values()] + + for add_param in additional_params: + setattr(deserialized_model, add_param, payload[add_param]) + return deserialized_model + else: + return payload + except Exception as e: + raise SerializationException(str(e)) + + def __get_obj_by_discriminator(self, payload, obj_type): + # type: (str, Union[T, str]) -> str + namespaced_class_name = obj_type.get_real_child_model(payload) + if not namespaced_class_name: + raise SerializationException( + "Couldn't resolve object by discriminator type " + "for {} class".format(obj_type)) + + return self.__load_class_from_name(namespaced_class_name) diff --git a/ask-sdk-core/ask_sdk_core/skill.py b/ask-sdk-core/ask_sdk_core/skill.py new file mode 100644 index 0000000..4da334b --- /dev/null +++ b/ask-sdk-core/ask_sdk_core/skill.py @@ -0,0 +1,178 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 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. +# +import typing + +from ask_sdk_model.services import ServiceClientFactory, ApiConfiguration +from ask_sdk_model import ResponseEnvelope + +from .dispatch import RequestDispatcher +from .serialize import DefaultSerializer +from .handler_input import HandlerInput +from .exceptions import AskSdkException +from .attributes_manager import AttributesManager +from .utils import user_agent_info, RESPONSE_FORMAT_VERSION + +if typing.TYPE_CHECKING: + from typing import List, TypeVar, Any + from ask_sdk_model.services import ApiClient + from ask_sdk_model import RequestEnvelope + from .dispatch_components import ( + RequestMapper, HandlerAdapter, ExceptionMapper, + AbstractRequestInterceptor, AbstractResponseInterceptor) + T = TypeVar['T'] + + +class SkillConfiguration(object): + """Configuration Object that represents standard components + needed to build :py:class:`Skill`. + """ + + def __init__( + self, request_mappers, handler_adapters, + request_interceptors=None, response_interceptors=None, + exception_mapper=None, persistence_adapter=None, + api_client=None, custom_user_agent=None, skill_id=None): + # type: (List[RequestMapper], List[HandlerAdapter], List[AbstractRequestInterceptor], List[AbstractResponseInterceptor], ExceptionMapper, PersistenceAdapter, ApiClient, str, str) -> None + """Configuration object that represents standard components + needed for building :py:class:`Skill`. + + :param request_mappers: List of request mapper instances. + :type request_mappers: list(:py:class:`RequestMapper`) + :param handler_adapters: List of handler adapter instances. + :type handler_adapters: list(:py:class:`HandlerAdapter`) + :param request_interceptors: List of + request interceptor instances. + :type request_interceptors: list( + :py:class:`AbstractRequestInterceptor`) + :param response_interceptors: List of + response interceptor instances. + :type response_interceptors: list( + :py:class:`AbstractResponseInterceptor`) + :param exception_mapper: Exception mapper instance. + :type exception_mapper: :py:class:`ExceptionMapper` + :param persistence_adapter: Persistence adapter instance. + :type persistence_adapter: :py:class:`PersistenceAdapter` + :param api_client: Api Client instance. + :type api_client: :py:class: + `ask_sdk_model.services.api_client.ApiClient` + :param custom_user_agent: Custom User Agent string + :type custom_user_agent: str + :param skill_id: ID of the skill. + :type skill_id: str + """ + if request_mappers is None: + request_mappers = [] + self.request_mappers = request_mappers + + if handler_adapters is None: + handler_adapters = [] + self.handler_adapters = handler_adapters + + if request_interceptors is None: + request_interceptors = [] + self.request_interceptors = request_interceptors + + if response_interceptors is None: + response_interceptors = [] + self.response_interceptors = response_interceptors + + self.exception_mapper = exception_mapper + self.persistence_adapter = persistence_adapter + self.api_client = api_client + self.custom_user_agent = custom_user_agent + self.skill_id = skill_id + + +class Skill(object): + """Top level container for Request Dispatcher, + Persistence Adapter and Api Client. + """ + + def __init__(self, skill_configuration): + # type: (SkillConfiguration) -> None + """Top level container for Request Dispatcher, + Persistence Adapter and Api Client. + + :param skill_configuration: Configuration object that holds + information about different components needed to build the + skill object. + :type skill_configuration: SkillConfiguration + """ + self.persistence_adapter = skill_configuration.persistence_adapter + self.api_client = skill_configuration.api_client + self.serializer = DefaultSerializer() + self.skill_id = skill_configuration.skill_id + self.custom_user_agent = skill_configuration.custom_user_agent + + self.request_dispatcher = RequestDispatcher( + request_mappers=skill_configuration.request_mappers, + handler_adapters=skill_configuration.handler_adapters, + exception_mapper=skill_configuration.exception_mapper, + request_interceptors=skill_configuration.request_interceptors, + response_interceptors=skill_configuration.response_interceptors) + + def invoke(self, request_envelope, context): + # type: (RequestEnvelope, T) -> ResponseEnvelope + """Invokes the dispatcher, to handle the request envelope and + return a response envelope. + + :param request_envelope: Request Envelope instance containing + request information + :type request_envelope: RequestEnvelope + :param context: Context passed during invocation + :type context: Any + :return: Response Envelope generated by handling the request + :rtype ResponseEnvelope + """ + if (self.skill_id is not None and + request_envelope.context.system.application.application_id != + self.skill_id): + raise AskSdkException("Skill ID Verification failed!!") + + if self.api_client is not None: + api_token = request_envelope.context.system.api_access_token + api_endpoint = request_envelope.context.system.api_endpoint + api_configuration = ApiConfiguration( + serializer=self.serializer, api_client=self.api_client, + authorization_value=api_token, + api_endpoint=api_endpoint) + factory = ServiceClientFactory(api_configuration=api_configuration) + else: + factory = None + + attributes_manager = AttributesManager( + request_envelope=request_envelope, + persistence_adapter=self.persistence_adapter) + + handler_input = HandlerInput( + request_envelope=request_envelope, + attributes_manager=attributes_manager, + context=context, + service_client_factory=factory) + + response = self.request_dispatcher.dispatch(handler_input) + session_attributes = None + + if request_envelope.session is not None: + session_attributes = ( + handler_input.attributes_manager.session_attributes) + + return ResponseEnvelope( + response=response, version=RESPONSE_FORMAT_VERSION, + session_attributes=session_attributes, + user_agent=user_agent_info(self.custom_user_agent)) diff --git a/ask-sdk-core/ask_sdk_core/skill_builder.py b/ask-sdk-core/ask_sdk_core/skill_builder.py new file mode 100644 index 0000000..7b22af1 --- /dev/null +++ b/ask-sdk-core/ask_sdk_core/skill_builder.py @@ -0,0 +1,413 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 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. +# +import json +import inspect +import typing + +from ask_sdk_model import RequestEnvelope + +from .dispatch_components import ( + AbstractRequestHandler, RequestHandlerChain, RequestMapper, + HandlerAdapter, AbstractRequestInterceptor, AbstractResponseInterceptor, + ExceptionMapper, AbstractExceptionHandler) +from .skill import Skill, SkillConfiguration +from .exceptions import SkillBuilderException + + +if typing.TYPE_CHECKING: + from typing import Callable, TypeVar, Dict + from ask_sdk_model.services import ApiClient + from .handler_input import HandlerInput + from .attributes_manager import AbstractPersistenceAdapter + T = TypeVar('T') + + +class SkillBuilder(object): + """Skill Builder with helper functions for building + :py:class:`Skill` object. + """ + + def __init__(self): + # type: () -> None + self.request_handlers = [] + self.exception_handlers = [] + self.global_request_interceptors = [] + self.global_response_interceptors = [] + self.custom_user_agent = None + self.skill_id = None + + @property + def skill_configuration(self): + # type: () -> SkillConfiguration + """Create the skill configuration object using the + registered components. + """ + request_handler_chains = [] + + for handler in self.request_handlers: + request_handler_chains.append( + RequestHandlerChain(request_handler=handler)) + + request_mapper = RequestMapper( + request_handler_chains=request_handler_chains) + + if self.exception_handlers: + exception_mapper = ExceptionMapper( + exception_handlers=self.exception_handlers) + else: + exception_mapper = None + + return SkillConfiguration( + request_mappers=[request_mapper], + handler_adapters=[HandlerAdapter()], + exception_mapper=exception_mapper, + request_interceptors=self.global_request_interceptors, + response_interceptors=self.global_response_interceptors, + custom_user_agent=self.custom_user_agent, + skill_id=self.skill_id + ) + + def add_request_handler(self, request_handler): + # type: (AbstractRequestHandler) -> None + """Register input to the request handlers list. + + :param request_handler: Request Handler instance to be + registered. + :type request_handler: AbstractRequestHandler + :return: None + """ + if request_handler is None: + raise SkillBuilderException( + "Valid Request Handler instance to be provided") + + if not isinstance(request_handler, AbstractRequestHandler): + raise SkillBuilderException( + "Input should be a RequestHandler instance") + + self.request_handlers.append(request_handler) + + def add_exception_handler(self, exception_handler): + # type: (AbstractExceptionHandler) -> None + """Register input to the exception handlers list. + + :param exception_handler: Exception Handler instance to be + registered. + :type exception_handler: AbstractExceptionHandler + :return: None + """ + if exception_handler is None: + raise SkillBuilderException( + "Valid Exception Handler instance to be provided") + + if not isinstance(exception_handler, AbstractExceptionHandler): + raise SkillBuilderException( + "Input should be an ExceptionHandler instance") + + self.exception_handlers.append(exception_handler) + + def add_global_request_interceptor(self, request_interceptor): + # type: (AbstractRequestInterceptor) -> None + """Register input to the global request interceptors list. + + :param request_interceptor: Request Interceptor instance to be + registered. + :type request_interceptor: AbstractRequestInterceptor + :return: None + """ + if request_interceptor is None: + raise SkillBuilderException( + "Valid Request Interceptor instance to be provided") + + if not isinstance(request_interceptor, AbstractRequestInterceptor): + raise SkillBuilderException( + "Input should be a RequestInterceptor instance") + + self.global_request_interceptors.append(request_interceptor) + + def add_global_response_interceptor(self, response_interceptor): + # type: (AbstractResponseInterceptor) -> None + """Register input to the global response interceptors list. + + :param response_interceptor: Response Interceptor instance to + be registered. + :type response_interceptor: AbstractResponseInterceptor + :return: None + """ + if response_interceptor is None: + raise SkillBuilderException( + "Valid Response Interceptor instance to be provided") + + if not isinstance(response_interceptor, AbstractResponseInterceptor): + raise SkillBuilderException( + "Input should be a ResponseInterceptor instance") + + self.global_response_interceptors.append(response_interceptor) + + def create(self): + # type: () -> Skill + """Create a skill object using the registered components. + + :return: a skill object that can be used for invocation. + :rtype: Skill + """ + return Skill(skill_configuration=self.skill_configuration) + + def lambda_handler(self): + # type: () -> Callable[[RequestEnvelope, T], Dict[str, T]] + """Create a handler function that can be used as handler in + AWS Lambda console. + + The lambda handler provides a handler function, that acts as + an entry point to the AWS Lambda console. Users can set the + lambda_handler output to a variable and set the variable as + AWS Lambda Handler on the console. + + :return: Handler function to tag on AWS Lambda console. + """ + def wrapper(event, context): + # type: (RequestEnvelope, T) -> Dict[str, T] + skill = Skill(skill_configuration=self.skill_configuration) + request_envelope = skill.serializer.deserialize( + payload=json.dumps(event), obj_type=RequestEnvelope) + response_envelope = skill.invoke( + request_envelope=request_envelope, context=context) + return skill.serializer.serialize(response_envelope) + return wrapper + + def request_handler(self, can_handle_func): + # type: (Callable[[HandlerInput], bool]) -> Callable + """Decorator that can be used to add request handlers easily to + the builder. + + The can_handle_func has to be a Callable instance, which takes + a single parameter and no varargs or kwargs. This is because + of the RequestHandler class signature restrictions. The + returned wrapper function can be applied as a decorator on any + function that returns a response object by the skill. The + function should follow the signature of the handle function in + :py:class:`AbstractRequestHandler` class. + + :param can_handle_func: The function that validates if the + request can be handled. + :return: Wrapper function that can be decorated on a handle + function. + """ + def wrapper(handle_func): + if not callable(can_handle_func) or not callable(handle_func): + raise SkillBuilderException( + "Request Handler can_handle_func and handle_func " + "input parameters should be callable") + + can_handle_arg_spec = inspect.getargspec(can_handle_func) + if (len(can_handle_arg_spec.args) != 1 or + can_handle_arg_spec.varargs is not None or + can_handle_arg_spec.keywords is not None): + raise SkillBuilderException( + "Request Handler can_handle_func should only accept a " + "single input arg, handler input") + + handle_arg_spec = inspect.getargspec(handle_func) + if (len(handle_arg_spec.args) != 1 or + handle_arg_spec.varargs is not None or + handle_arg_spec.keywords is not None): + raise SkillBuilderException( + "Request Handler handle_func should only accept a single " + "input arg, handler input") + + class_attributes = { + "can_handle": lambda self, handler_input: can_handle_func( + handler_input), + "handle": lambda self, handler_input: handle_func( + handler_input) + } + + request_handler_class = type( + "RequestHandler{}".format( + handle_func.__name__.title().replace("_", "")), + (AbstractRequestHandler,), class_attributes) + + self.add_request_handler(request_handler=request_handler_class()) + return wrapper + + def exception_handler(self, can_handle_func): + # type: (Callable[[HandlerInput, Exception], bool]) -> Callable + """Decorator that can be used to add exception handlers easily + to the builder. + + The can_handle_func has to be a Callable instance, which takes + two parameters and no varargs or kwargs. This is because of the + ExceptionHandler class signature restrictions. The returned + wrapper function can be applied as a decorator on any function + that processes the exception raised during dispatcher and + returns a response object by the skill. The function should + follow the signature of the handle function in + :py:class:`AbstractExceptionHandler` class. + + :param can_handle_func: The function that validates if the + exception can be handled. + :return: Wrapper function that can be decorated on a handle + function. + """ + def wrapper(handle_func): + if not callable(can_handle_func) or not callable(handle_func): + raise SkillBuilderException( + "Exception Handler can_handle_func and handle_func input " + "parameters should be callable") + + can_handle_arg_spec = inspect.getargspec(can_handle_func) + if (len(can_handle_arg_spec.args) != 2 or + can_handle_arg_spec.varargs is not None or + can_handle_arg_spec.keywords is not None): + raise SkillBuilderException( + "Exception Handler can_handle_func should only accept " + "two input args, handler input and exception") + + handle_arg_spec = inspect.getargspec(handle_func) + if (len(handle_arg_spec.args) != 2 or + handle_arg_spec.varargs is not None or + handle_arg_spec.keywords is not None): + raise SkillBuilderException( + "Exception Handler handle_func should only accept two " + "input args, handler input and exception") + + class_attributes = { + "can_handle": ( + lambda self, handler_input, exception: can_handle_func( + handler_input, exception)), + "handle": lambda self, handler_input, exception: handle_func( + handler_input, exception) + } + + exception_handler_class = type( + "ExceptionHandler{}".format( + handle_func.__name__.title().replace("_", "")), + (AbstractExceptionHandler,), class_attributes) + + self.add_exception_handler( + exception_handler=exception_handler_class()) + return wrapper + + def global_request_interceptor(self): + # type: () -> Callable + """Decorator that can be used to add global request + interceptors easily to the builder. + + The returned wrapper function can be applied as a decorator on + any function that processes the input. The function should + follow the signature of the process function in + :py:class:`AbstractRequestInterceptor` class. + + :return: Wrapper function that can be decorated on a + interceptor process function. + """ + def wrapper(process_func): + if not callable(process_func): + raise SkillBuilderException( + "Global Request Interceptor process_func input parameter " + "should be callable") + + process_arg_spec = inspect.getargspec(process_func) + if (len(process_arg_spec.args) != 1 or + process_arg_spec.varargs is not None or + process_arg_spec.keywords is not None): + raise SkillBuilderException( + "Global Request Interceptor process_func should only " + "accept a single input arg, handler input") + + class_attributes = { + "process": lambda self, handler_input: process_func( + handler_input) + } + + request_interceptor = type( + "RequestInterceptor{}".format( + process_func.__name__.title().replace("_", "")), + (AbstractRequestInterceptor,), class_attributes) + + self.add_global_request_interceptor( + request_interceptor=request_interceptor()) + return wrapper + + def global_response_interceptor(self): + # type: () -> Callable + """Decorator that can be used to add global + response interceptors easily to the builder. + + The returned wrapper function can be applied as a decorator + on any function that processes the input and the response + generated by the request handler. The function should follow + the signature of the process function in + :py:class:`AbstractResponseInterceptor` class. + + :return: Wrapper function that can be decorated on a + interceptor process function. + """ + def wrapper(process_func): + if not callable(process_func): + raise SkillBuilderException( + "Global Response Interceptor process_func input " + "parameter should be callable") + + process_arg_spec = inspect.getargspec(process_func) + if (len(process_arg_spec.args) != 2 or + process_arg_spec.varargs is not None or + process_arg_spec.keywords is not None): + raise SkillBuilderException( + "Global Response Interceptor process_func should only " + "accept two input args, handler input and response") + + class_attributes = { + "process": ( + lambda self, handler_input, response: process_func( + handler_input, response)) + } + + response_interceptor = type( + "ResponseInterceptor{}".format( + process_func.__name__.title().replace("_", "")), + (AbstractResponseInterceptor,), class_attributes) + + self.add_global_response_interceptor( + response_interceptor=response_interceptor()) + return wrapper + + +class CustomSkillBuilder(SkillBuilder): + """Skill Builder with api client and persistence adapter setter + functions. + """ + + def __init__(self, persistence_adapter=None, api_client=None): + # type: (AbstractPersistenceAdapter, ApiClient) -> None + """Skill Builder with api client and persistence adapter + setter functions. + """ + super(CustomSkillBuilder, self).__init__() + self.persistence_adapter = persistence_adapter + self.api_client = api_client + + @property + def skill_configuration(self): + # type: () -> SkillConfiguration + """Create the skill configuration object using the + registered components. + """ + skill_config = super(CustomSkillBuilder, self).skill_configuration + skill_config.persistence_adapter = self.persistence_adapter + skill_config.api_client = self.api_client + return skill_config diff --git a/ask-sdk-core/ask_sdk_core/utils.py b/ask-sdk-core/ask_sdk_core/utils.py new file mode 100644 index 0000000..af4fa29 --- /dev/null +++ b/ask-sdk-core/ask_sdk_core/utils.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 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. +# +import sys +import typing + +from ask_sdk_model import IntentRequest + +from .__version__ import __version__ + +if typing.TYPE_CHECKING: + from typing import Callable + from .handler_input import HandlerInput + + +SDK_VERSION = __version__ +RESPONSE_FORMAT_VERSION = "1.0" + + +def user_agent_info(custom_user_agent): + # type: (str) -> str + """Return the user agent info along with the SDK and Python + Version information. + + :param custom_user_agent: Custom User Agent string provided by + the developer. + :type custom_user_agent: str + :return: User Agent Info string + :rtype str + """ + python_version = ".".join(str(x) for x in sys.version_info[0:3]) + user_agent = "ask-python/{} Python/{}".format( + SDK_VERSION, python_version) + if custom_user_agent is None: + return user_agent + else: + return user_agent + " {}".format(custom_user_agent) + + +def is_intent_name(name): + # type: (str) -> Callable[[HandlerInput], bool] + """A predicate function returning a boolean, when name matches the + name in Intent Request. + + The function can be applied on a :py:class:`HandlerInput`, to + check if the input is of :py:class:`IntentRequest` type and if the + name of the request matches with the passed name. + + :param name: Name to be matched with the Intent Request Name + :return: Predicate function that can be used to check name of the + request + :rtype: Callable[[HandlerInput], bool] + """ + def can_handle_wrapper(handler_input): + # type: (HandlerInput) -> bool + return (isinstance( + handler_input.request_envelope.request, IntentRequest) and + handler_input.request_envelope.request.intent.name == name) + return can_handle_wrapper + + +def is_request_type(request_type): + # type: (str) -> Callable[[HandlerInput], bool] + """A predicate function returning a boolean, when request type is + the passed-in type. + + The function can be applied on a :py:class:`HandlerInput`, to check + if the input request type is the passed in request type. + + :param request_type: Class to be matched with the input's request + :return: Predicate function that can be used to check the type of + the request + :rtype: Callable[[HandlerInput], bool] + """ + def can_handle_wrapper(handler_input): + # type: (HandlerInput) -> bool + return (handler_input.request_envelope.request.object_type == + request_type) + return can_handle_wrapper diff --git a/ask-sdk-core/docs/requirements-docs.txt b/ask-sdk-core/docs/requirements-docs.txt new file mode 100644 index 0000000..e69de29 diff --git a/ask-sdk-core/requirements.txt b/ask-sdk-core/requirements.txt new file mode 100644 index 0000000..3d555ae --- /dev/null +++ b/ask-sdk-core/requirements.txt @@ -0,0 +1,5 @@ +six +typing +requests +python_dateutil +ask-sdk-model diff --git a/ask-sdk-core/setup.cfg b/ask-sdk-core/setup.cfg new file mode 100644 index 0000000..7c2b287 --- /dev/null +++ b/ask-sdk-core/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal = 1 \ No newline at end of file diff --git a/ask-sdk-core/setup.py b/ask-sdk-core/setup.py new file mode 100644 index 0000000..70a9282 --- /dev/null +++ b/ask-sdk-core/setup.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 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. +# +import os +from setuptools import setup, find_packages +from codecs import open + +here = os.path.abspath(os.path.dirname(__file__)) + +about = {} +with open(os.path.join( + here, 'ask_sdk_core', '__version__.py'), 'r', 'utf-8') as f: + exec(f.read(), about) + +with open('README.rst', 'r', 'utf-8') as f: + readme = f.read() +with open('CHANGELOG.rst', 'r', 'utf-8') as f: + history = f.read() + +setup( + name=about['__pip_package_name__'], + version=about['__version__'], + description=about['__description__'], + long_description=readme + '\n\n' + history, + author=about['__author__'], + author_email=about['__author_email__'], + url=about['__url__'], + keywords=about['__keywords__'], + license=about['__license__'], + include_package_data=True, + install_requires=about['__install_requires__'], + extras_require={ + ':python_version == "2.7"': [ + 'enum34', + 'typing', + ], + }, + packages=find_packages( + exclude=["*.tests", "*.tests.*", "tests.*", "tests"]), + zip_safe=False, + classifiers=( + 'Development Status :: 3 - Alpha', + 'Intended Audience :: Developers', + 'Natural Language :: English', + 'License :: OSI Approved :: Apache Software License', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.6' + ), + python_requires=(">2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, " + "!=3.5.*"), +) diff --git a/ask-sdk-core/tests/__init__.py b/ask-sdk-core/tests/__init__.py new file mode 100644 index 0000000..5bfcf97 --- /dev/null +++ b/ask-sdk-core/tests/__init__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 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. +# +import os +import sys +sys.path.insert( + 0, os.path.abspath( + os.path.join(os.path.dirname(__file__), '..', 'ask_sdk_core'))) diff --git a/ask-sdk-core/tests/unit/__init__.py b/ask-sdk-core/tests/unit/__init__.py new file mode 100644 index 0000000..2b850df --- /dev/null +++ b/ask-sdk-core/tests/unit/__init__.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 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. +# diff --git a/ask-sdk-core/tests/unit/data/__init__.py b/ask-sdk-core/tests/unit/data/__init__.py new file mode 100644 index 0000000..99271b2 --- /dev/null +++ b/ask-sdk-core/tests/unit/data/__init__.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 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. +# +from .model_enum_object import ModelEnumObject +from .model_test_object_1 import ModelTestObject1 +from .model_test_object_2 import ModelTestObject2 +from .invalid_model_object import InvalidModelObject +from .model_abstract_parent_object import ModelAbstractParentObject +from .model_child_objects import ModelChildObject1 +from .model_child_objects import ModelChildObject2 diff --git a/ask-sdk-core/tests/unit/data/invalid_model_object.py b/ask-sdk-core/tests/unit/data/invalid_model_object.py new file mode 100644 index 0000000..1f2aadc --- /dev/null +++ b/ask-sdk-core/tests/unit/data/invalid_model_object.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 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. +# + + +class InvalidModelObject(object): + def __init__(self, some_var=None): + self.some_var = some_var + + def __eq__(self, other): + return self.__dict__ == other.__dict__ diff --git a/ask-sdk-core/tests/unit/data/mock_persistence_adapter.py b/ask-sdk-core/tests/unit/data/mock_persistence_adapter.py new file mode 100644 index 0000000..8e9d89c --- /dev/null +++ b/ask-sdk-core/tests/unit/data/mock_persistence_adapter.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 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. +# +from ask_sdk_core.attributes_manager import AbstractPersistenceAdapter + + +class MockPersistenceAdapter(AbstractPersistenceAdapter): + def __init__(self): + self.attributes = {"key_1": "v1", "key_2": "v2"} + self.get_count = 0 + self.save_count = 0 + + def get_attributes(self, request_envelope): + self.get_count += 1 + return self.attributes + + def save_attributes(self, request_envelope, attributes): + self.save_count += 1 + self.attributes = attributes + diff --git a/ask-sdk-core/tests/unit/data/mock_response_object.py b/ask-sdk-core/tests/unit/data/mock_response_object.py new file mode 100644 index 0000000..1ab0878 --- /dev/null +++ b/ask-sdk-core/tests/unit/data/mock_response_object.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 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. +# + + +class MockResponse(object): + def __init__(self, json_data, status_code, headers=None): + self.json_data = json_data + self.status_code = status_code + self.headers = headers + + self.text = self.json_data diff --git a/ask-sdk-core/tests/unit/data/model_abstract_parent_object.py b/ask-sdk-core/tests/unit/data/model_abstract_parent_object.py new file mode 100644 index 0000000..6e15bff --- /dev/null +++ b/ask-sdk-core/tests/unit/data/model_abstract_parent_object.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 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. +# +from abc import ABCMeta, abstractmethod + + +class ModelAbstractParentObject(object): + __metaclass__ = ABCMeta + + deserialized_types = { + 'child_type': 'str', + 'str_var': 'str', + 'obj_var': 'tests.unit.data.ModelTestObject2', + } + + attribute_map = { + 'child_type': 'ChildType', + 'str_var': 'var1', + 'obj_var': 'var3Object', + } + + discriminator_value_class_map = { + 'ChildType1': 'tests.unit.data.ModelChildObject1', + 'ChildType2': 'tests.unit.data.ModelChildObject2' + } + + json_discriminator_key = "ChildType" + + @abstractmethod + def __init__(self, child_type=None, str_var=None, obj_var=None): + self.child_type = child_type + self.str_var = str_var + self.obj_var = obj_var + + @classmethod + def get_real_child_model(cls, data): + """Returns the real base class specified by the discriminator""" + discriminator_value = data[cls.json_discriminator_key] + return cls.discriminator_value_class_map.get(discriminator_value) \ No newline at end of file diff --git a/ask-sdk-core/tests/unit/data/model_child_objects.py b/ask-sdk-core/tests/unit/data/model_child_objects.py new file mode 100644 index 0000000..16fb12b --- /dev/null +++ b/ask-sdk-core/tests/unit/data/model_child_objects.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 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. +# +import six + +from .model_abstract_parent_object import ModelAbstractParentObject + + +class ModelChildObject1(ModelAbstractParentObject): + deserialized_types = { + 'child_type': 'str', + 'str_var': 'str', + 'obj_var': 'tests.unit.data.ModelTestObject2', + 'test_var': 'str' + } + + attribute_map = { + 'child_type': 'ChildType', + 'str_var': 'var1', + 'obj_var': 'var3Object', + 'test_var': 'testVar' + } + + def __init__(self, str_var=None, obj_var=None, test_var=None): + super(ModelChildObject1, self).__init__(child_type="ChildType1", str_var=str_var, obj_var=obj_var) + self.test_var = test_var + + def __eq__(self, other): + return self.__dict__ == other.__dict__ + + +class ModelChildObject2(ModelAbstractParentObject): + deserialized_types = { + 'child_type': 'str', + 'str_var': 'str', + 'obj_var': 'tests.unit.data.ModelTestObject2', + 'test_int_var': 'int' + } + + attribute_map = { + 'child_type': 'ChildType', + 'str_var': 'var1', + 'obj_var': 'var3Object', + 'test_int_var': 'testIntVar' + } + + def __init__(self, str_var=None, obj_var=None, test_int_var=None): + super(ModelChildObject2, self).__init__(child_type="ChildType2", str_var=str_var, obj_var=obj_var) + self.test_int_var = test_int_var + + def __eq__(self, other): + return self.__dict__ == other.__dict__ diff --git a/ask-sdk-core/tests/unit/data/model_enum_object.py b/ask-sdk-core/tests/unit/data/model_enum_object.py new file mode 100644 index 0000000..1565c04 --- /dev/null +++ b/ask-sdk-core/tests/unit/data/model_enum_object.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 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. +# +from enum import Enum + + +class ModelEnumObject(Enum): + ENUM_VAL_1 = "ENUM_VAL_1" + ENUM_VAL_2 = "ENUM_VAL_2" + + def __eq__(self, other): + return self.value == other.value diff --git a/ask-sdk-core/tests/unit/data/model_test_object_1.py b/ask-sdk-core/tests/unit/data/model_test_object_1.py new file mode 100644 index 0000000..f1f1d10 --- /dev/null +++ b/ask-sdk-core/tests/unit/data/model_test_object_1.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 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. +# + + +class ModelTestObject1(object): + deserialized_types = { + 'str_var': 'str', + 'datetime_var': 'datetime', + 'obj_var': 'tests.unit.data.ModelTestObject2', + 'none_var': 'None', + 'enum_var': 'tests.unit.data.ModelEnumObject' + } + + attribute_map = { + 'str_var': 'var1', + 'datetime_var': 'var2Time', + 'obj_var': 'var3Object', + 'none_var': 'var5None', + 'enum_var': 'var6Enum' + } + + def __init__(self, str_var=None, datetime_var=None, obj_var=None, none_var=None, enum_var=None): + self.str_var = str_var + self.datetime_var = datetime_var + self.obj_var = obj_var + self.none_var = none_var + self.enum_var = enum_var + + def __eq__(self, other): + return self.__dict__ == other.__dict__ diff --git a/ask-sdk-core/tests/unit/data/model_test_object_2.py b/ask-sdk-core/tests/unit/data/model_test_object_2.py new file mode 100644 index 0000000..ddf7d88 --- /dev/null +++ b/ask-sdk-core/tests/unit/data/model_test_object_2.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 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. +# + + +class ModelTestObject2(object): + deserialized_types = { + 'int_var': 'int' + } + + attribute_map = { + 'int_var': 'var4Int' + } + + def __init__(self, int_var=None): + self.int_var = int_var + + def __eq__(self, other): + return self.__dict__ == other.__dict__ diff --git a/ask-sdk-core/tests/unit/test_api_client.py b/ask-sdk-core/tests/unit/test_api_client.py new file mode 100644 index 0000000..6a94a40 --- /dev/null +++ b/ask-sdk-core/tests/unit/test_api_client.py @@ -0,0 +1,185 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 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. +# +import unittest +from six import PY3 + +from ask_sdk_model.services import ApiClientRequest +from ask_sdk_core.api_client import DefaultApiClient +from ask_sdk_core.exceptions import ApiClientException + +from .data.mock_response_object import MockResponse + +if PY3: + from unittest import mock +else: + import mock + + +class TestDefaultApiClient(unittest.TestCase): + def setUp(self): + self.valid_request = ApiClientRequest( + method="GET", url="https://test.com", body=None, headers=None) + self.valid_mock_response = MockResponse( + json_data="some test data", status_code=200) + self.test_api_client = DefaultApiClient() + + def test_convert_null_header_tuples_to_dict(self): + test_headers_list = None + expected_headers_dict = {} + + assert self.test_api_client._convert_list_tuples_to_dict( + test_headers_list) == expected_headers_dict, ( + "DefaultApiClient failed to convert null headers list to empty " + "dict object") + + def test_convert_header_tuples_to_dict(self): + test_headers_list = [ + ("header_1", "test_1"), ("header_2", "test_2"), + ("header_1", "test_3")] + expected_headers_dict = { + "header_1": "test_1, test_3", "header_2": "test_2"} + + assert self.test_api_client._convert_list_tuples_to_dict( + test_headers_list) == expected_headers_dict, ( + "DefaultApiClient failed to convert header list of tuples to " + "dictionary format needed for http " + "request call") + + def test_convert_null_header_dict_to_tuples(self): + test_headers_dict = None + expected_headers_list = [] + + assert self.test_api_client._convert_dict_to_list_tuples( + test_headers_dict) == expected_headers_list, ( + "DefaultApiClient failed to convert null headers dict to empty " + "list object") + + def test_convert_header_dict_to_tuples(self): + test_headers_dict = { + "header_1": "test_1, test_3", "header_2": "test_2", + "header_3": "test_4,"} + expected_headers_list = [ + ("header_1", "test_1"), ("header_1", "test_3"), + ("header_2", "test_2"), ("header_3", "test_4")] + + assert set(self.test_api_client._convert_dict_to_list_tuples( + test_headers_dict)) == set( + expected_headers_list), ( + "DefaultApiClient failed to convert headers dict to list of " + "tuples format for ApiClientResponse") + + def test_resolve_valid_http_method(self): + with mock.patch("requests.get", + side_effect=lambda *args, **kwargs: + self.valid_mock_response): + try: + actual_response = self.test_api_client.invoke( + self.valid_request) + except: + # Should never reach here + raise Exception("DefaultApiClient couldn't resolve valid " + "HTTP Method for calling") + + def test_resolve_invalid_http_method_throw_exception(self): + test_invalid_method_request = ApiClientRequest( + method="GET_TEST", url="http://test.com", body=None, headers=None) + + with mock.patch("requests.get", + side_effect=lambda *args, **kwargs: + self.valid_mock_response): + with self.assertRaises(ApiClientException) as exc: + self.test_api_client.invoke(test_invalid_method_request) + + assert "Invalid request method: GET_TEST" in str(exc.exception) + + def test_invoke_http_method_throw_exception(self): + with mock.patch("requests.get", + side_effect=Exception("test exception")): + with self.assertRaises(ApiClientException) as exc: + self.test_api_client.invoke(self.valid_request) + + assert "Error executing the request: test exception" in str(exc.exception) + + def test_api_client_invoke_with_method_headers_processed(self): + self.valid_request.headers = [ + ("request_header_1", "test_1"), ("request_header_2", "test_2"), + ("request_header_1", "test_3")] + self.valid_request.method = "PUT" + + test_response = MockResponse( + headers={ + "response_header_1": "test_1, test_3", + "response_header_2": "test_2", "response_header_3": "test_4,"}, + status_code=400, + json_data="test response body") + + with mock.patch("requests.put", + side_effect=lambda *args, **kwargs: test_response): + actual_response = self.test_api_client.invoke(self.valid_request) + + assert set(actual_response.headers) == set([ + ("response_header_1", "test_1"), + ("response_header_1", "test_3"), + ("response_header_2", "test_2"), + ("response_header_3", "test_4")]), ( + "Response headers from client doesn't match with the " + "expected headers") + + assert actual_response.status_code == 400, ( + "Status code from client response doesn't match with the " + "expected response status code") + + assert actual_response.body == "test response body", ( + "Body from client response doesn't match with the expected " + "response body") + + def test_api_client_invoke_with_http_url_throw_error(self): + test_invalid_url_scheme_request = ApiClientRequest( + method="GET", url="http://test.com", body=None, headers=None) + + with mock.patch("requests.get", + side_effect=lambda *args, **kwargs: + self.valid_mock_response): + with self.assertRaises(ApiClientException) as exc: + self.test_api_client.invoke(test_invalid_url_scheme_request) + + assert "Requests against non-HTTPS endpoints are not allowed." in str(exc.exception) + + def test_api_client_invoke_with_http_case_sensitive_url_throw_error(self): + test_invalid_url_scheme_request = ApiClientRequest( + method="GET", url="HTTP://test.com", body=None, headers=None) + + with mock.patch("requests.get", + side_effect=lambda *args, **kwargs: + self.valid_mock_response): + with self.assertRaises(ApiClientException) as exc: + self.test_api_client.invoke(test_invalid_url_scheme_request) + + assert "Requests against non-HTTPS endpoints are not allowed." in str(exc.exception) + + def test_api_client_invoke_with_no_url_schema_throw_error(self): + test_invalid_url_scheme_request = ApiClientRequest( + method="GET", url="test.com", body=None, headers=None) + + with mock.patch("requests.get", + side_effect=lambda *args, **kwargs: + self.valid_mock_response): + with self.assertRaises(ApiClientException) as exc: + self.test_api_client.invoke(test_invalid_url_scheme_request) + + assert "Requests against non-HTTPS endpoints are not allowed." in str(exc.exception) diff --git a/ask-sdk-core/tests/unit/test_attributes_manager.py b/ask-sdk-core/tests/unit/test_attributes_manager.py new file mode 100644 index 0000000..628c499 --- /dev/null +++ b/ask-sdk-core/tests/unit/test_attributes_manager.py @@ -0,0 +1,236 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 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. +# + +import unittest + +from ask_sdk_model.request_envelope import RequestEnvelope +from ask_sdk_model.session import Session +from ask_sdk_core.attributes_manager import ( + AttributesManager, AttributesManagerException) +from .data.mock_persistence_adapter import MockPersistenceAdapter + + +class TestAttributesManager(unittest.TestCase): + def test_attributes_manager_with_no_request_envelope(self): + with self.assertRaises(AttributesManagerException) as exc: + self.attributes_manager = AttributesManager( + request_envelope=None) + + assert "RequestEnvelope cannot be none!" in str(exc.exception), ( + "AttributesManager should raise error when requestEnvelope is " + "none") + + def test_get_initial_request_attributes(self): + request_envelope = RequestEnvelope( + version=None, session=None, context=None, request=None) + attributes_manager = AttributesManager( + request_envelope=request_envelope) + + assert attributes_manager.request_attributes == {}, ( + "AttributesManager fails to set the initial request attributes " + "to be {}") + + def test_get_session_attributes_from_in_session_request_envelope(self): + session = Session( + new=None, session_id=None, user=None, + attributes={"mockKey": "mockValue"}, application=None) + request_envelope = RequestEnvelope( + version=None, session=session, context=None, request=None) + attributes_manager = AttributesManager( + request_envelope=request_envelope) + + assert attributes_manager.session_attributes == { + "mockKey": "mockValue"}, ( + "AttributesManager fails to get session attributes from in " + "session request envelope") + + def test_get_default_session_attributes_from_new_session_request_envelope(self): + session = Session( + new=True, session_id=None, user=None, + attributes=None, application=None) + request_envelope = RequestEnvelope( + version=None, session=session, context=None, request=None) + attributes_manager = AttributesManager( + request_envelope=request_envelope) + + assert attributes_manager.session_attributes == {}, ( + "AttributesManager fails to get default session attributes from " + "new session request envelope") + + def test_get_session_attributes_from_out_of_session_request_envelope(self): + request_envelope = RequestEnvelope( + version=None, session=None, context=None, request=None) + attributes_manager = AttributesManager( + request_envelope=request_envelope) + + with self.assertRaises(AttributesManagerException) as exc: + test_session_attributes = attributes_manager.session_attributes + + assert "Cannot get SessionAttributes from out of session request!" in str(exc.exception), ( + "AttributesManager should raise error when trying to get session " + "attributes from out of session envelope") + + def test_get_persistent_attributes(self): + request_envelope = RequestEnvelope( + version=None, session=None, context=None, request=None) + attributes_manager = AttributesManager( + request_envelope=request_envelope, + persistence_adapter=MockPersistenceAdapter()) + + assert attributes_manager.persistent_attributes == { + "key_1": "v1", "key_2": "v2"}, ( + "AttributesManager fails to get persistent attributes from " + "persistent adapter") + + def test_get_persistent_attributes_without_persistence_adapter(self): + request_envelope = RequestEnvelope( + version=None, session=None, context=None, request=None) + attributes_manager = AttributesManager( + request_envelope=request_envelope) + + with self.assertRaises(AttributesManagerException) as exc: + test_persistent_attributes = attributes_manager.persistent_attributes + + assert "Cannot get PersistentAttributes without Persistence adapter" in str(exc.exception), ( + "AttributesManager should raise error when trying to get " + "persistent attributes without persistence adapter") + + def test_set_request_attributes(self): + request_envelope = RequestEnvelope( + version=None, session=None, context=None, request=None) + attributes_manager = AttributesManager( + request_envelope=request_envelope) + attributes_manager.request_attributes = {"key": "value"} + + assert attributes_manager.request_attributes == {"key": "value"}, ( + "AttributesManager fails to set the request attributes") + + def test_set_session_attributes(self): + session = Session( + new=None, session_id=None, user=None, + attributes={"mockKey": "mockValue"}, application=None) + request_envelope = RequestEnvelope( + version=None, session=session, context=None, request=None) + attributes_manager = AttributesManager( + request_envelope=request_envelope) + + attributes_manager.session_attributes = { + "mockKey": "updatedMockValue"} + + assert attributes_manager.session_attributes == { + "mockKey": "updatedMockValue"}, ( + "AttributesManager fails to set the session attributes") + + def test_set_session_attributes_to_out_of_session_request_envelope(self): + request_envelope = RequestEnvelope( + version=None, session=None, context=None, request=None) + attributes_manager = AttributesManager( + request_envelope=request_envelope) + + with self.assertRaises(AttributesManagerException) as exc: + attributes_manager.session_attributes = {"key": "value"} + + assert "Cannot set SessionAttributes to out of session request!" in str(exc.exception), ( + "AttributesManager should raise error when trying to set session " + "attributes to out of session request") + + def test_set_persistent_attributes(self): + request_envelope = RequestEnvelope( + version=None, session=None, context=None, request=None) + attributes_manager = AttributesManager( + request_envelope=request_envelope, + persistence_adapter=MockPersistenceAdapter()) + + attributes_manager.persistent_attributes = {"key": "value"} + + assert attributes_manager.persistent_attributes == { + "key": "value"}, ( + "AttributesManager fails to set the persistent attributes") + + def test_set_persistent_attributes_without_persistence_adapter(self): + request_envelope = RequestEnvelope( + version=None, session=None, context=None, request=None) + attributes_manager = AttributesManager( + request_envelope=request_envelope) + + with self.assertRaises(AttributesManagerException) as exc: + attributes_manager.persistent_attributes = {"key": "value"} + + assert "Cannot set PersistentAttributes without persistence adapter!" in str(exc.exception), ( + "AttributesManager should raise error when trying to set " + "persistent attributes without persistence adapter") + + def test_save_persistent_attributes(self): + request_envelope = RequestEnvelope( + version=None, session=None, context=None, request=None) + attributes_manager = AttributesManager( + request_envelope=request_envelope, + persistence_adapter=MockPersistenceAdapter()) + + attributes_manager.persistent_attributes = {"key": "value"} + + attributes_manager.save_persistent_attributes() + + assert attributes_manager._persistence_adapter.attributes == { + "key": "value"}, ( + "AttributesManager fails to save persistent attributes via " + "persistence adapter") + + def test_save_persistent_attributes_without_persistence_adapter(self): + request_envelope = RequestEnvelope( + version=None, session=None, context=None, request=None) + attributes_manager = AttributesManager( + request_envelope=request_envelope) + + with self.assertRaises(AttributesManagerException) as exc: + attributes_manager.save_persistent_attributes() + + assert "Cannot save PersistentAttributes without persistence adapter!" in str(exc.exception), ( + "AttributesManager should raise error when trying to save " + "persistent attributes without persistence adapter" + ) + + def test_save_persistent_attributes_without_changing_persistent_attributes(self): + request_envelope = RequestEnvelope( + version=None, session=None, context=None, request=None) + attributes_manager = AttributesManager( + request_envelope=request_envelope, + persistence_adapter=MockPersistenceAdapter()) + + attributes_manager.save_persistent_attributes() + + assert attributes_manager._persistence_adapter.attributes == { + "key_1": "v1", "key_2": "v2"}, ( + "AttributesManager should do nothing if persistent attributes " + "has not been changed") + + def test_get_persistent_attributes_with_calling_get_persistent_attributes_multiple_times(self): + request_envelope = RequestEnvelope( + version=None, session=None, context=None, request=None) + attributes_manager = AttributesManager( + request_envelope=request_envelope, + persistence_adapter=MockPersistenceAdapter()) + + attributes_manager.persistent_attributes + attributes_manager.persistent_attributes + attributes_manager.persistent_attributes + attributes_manager.persistent_attributes + + assert attributes_manager._persistence_adapter.get_count == 1, ( + "AttributesManager should make only 1 get_attributes call " + "during multiple get_persistent_attributes calls") \ No newline at end of file diff --git a/ask-sdk-core/tests/unit/test_dispatch.py b/ask-sdk-core/tests/unit/test_dispatch.py new file mode 100644 index 0000000..487e555 --- /dev/null +++ b/ask-sdk-core/tests/unit/test_dispatch.py @@ -0,0 +1,448 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 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. +# +import unittest +from six import PY3 + +from ask_sdk_model.request_envelope import RequestEnvelope +from ask_sdk_model.intent_request import IntentRequest +from ask_sdk_model.response import Response + +from ask_sdk_core.handler_input import HandlerInput +from ask_sdk_core.dispatch import RequestDispatcher +from ask_sdk_core.dispatch_components import ( + RequestMapper, AbstractRequestHandler, RequestHandlerChain, + HandlerAdapter, AbstractRequestInterceptor, + AbstractResponseInterceptor, AbstractExceptionHandler, ExceptionMapper) +from ask_sdk_core.exceptions import DispatchException + +if PY3: + from unittest import mock +else: + import mock + + +class TestRequestDispatcher(unittest.TestCase): + def setUp(self): + test_request = mock.MagicMock(spec=IntentRequest) + test_request_envelope = mock.MagicMock(RequestEnvelope) + test_request_envelope.request = test_request + self.valid_handler_input = HandlerInput( + request_envelope=test_request_envelope) + self.test_dispatcher = RequestDispatcher() + + def test_dispatch_input_with_no_chains_in_request_mapper(self): + with self.assertRaises(DispatchException) as exc: + self.test_dispatcher.dispatch( + handler_input=self.valid_handler_input) + + assert "Couldn't find handler that can handle the request" in str(exc.exception), ( + "Dispatcher didn't throw Dispatch Exception when no chains are " + "registered in request mappers") + + def test_dispatch_input_with_unsupported_chains_in_request_mapper(self): + test_request_handler = mock.MagicMock(spec=AbstractRequestHandler) + test_request_handler.can_handle.return_value = False + test_request_handler_chain = mock.MagicMock(spec=RequestHandlerChain) + test_request_handler_chain.request_handler = test_request_handler + test_request_mapper = RequestMapper( + request_handler_chains=[test_request_handler_chain]) + + self.test_dispatcher.request_mappers = [test_request_mapper] + with self.assertRaises(DispatchException) as exc: + self.test_dispatcher.dispatch( + handler_input=self.valid_handler_input) + + assert "Couldn't find handler that can handle the request" in str(exc.exception), ( + "Dispatcher didn't throw Dispatch Exception when no suitable " + "chains are registered in " + "request mappers") + + def test_dispatch_input_with_supported_chain_in_mapper_no_adapters(self): + test_request_handler = mock.MagicMock(spec=AbstractRequestHandler) + test_request_handler.can_handle.return_value = True + test_request_handler_chain = mock.MagicMock(spec=RequestHandlerChain) + test_request_handler_chain.request_handler = test_request_handler + test_request_mapper = RequestMapper( + request_handler_chains=[test_request_handler_chain]) + + self.test_dispatcher.request_mappers = [test_request_mapper] + with self.assertRaises(DispatchException) as exc: + self.test_dispatcher.dispatch( + handler_input=self.valid_handler_input) + + assert "Couldn't find adapter that can handle the request" in str(exc.exception), ( + "Dispatcher didn't throw Dispatch Exception when no adapters are " + "registered in " + "dispatcher") + + def test_dispatch_input_with_supported_chain_in_mapper_and_unsupported_adapter(self): + test_request_handler = mock.MagicMock(spec=AbstractRequestHandler) + test_request_handler.can_handle.return_value = True + test_request_handler_chain = mock.MagicMock(spec=RequestHandlerChain) + test_request_handler_chain.request_handler = test_request_handler + test_request_mapper = RequestMapper( + request_handler_chains=[test_request_handler_chain]) + test_adapter = mock.MagicMock(spec=HandlerAdapter) + test_adapter.supports.return_value = False + + self.test_dispatcher.request_mappers = [test_request_mapper] + self.test_dispatcher.handler_adapters = [test_adapter] + with self.assertRaises(DispatchException) as exc: + self.test_dispatcher.dispatch( + handler_input=self.valid_handler_input) + + assert "Couldn't find adapter that can handle the request" in str(exc.exception), ( + "Dispatcher didn't throw Dispatch Exception when no suitable " + "adapters are registered in " + "dispatcher") + + def test_dispatch_input_successful_execution_with_supported_chain_and_supported_adapter(self): + test_request_handler = mock.MagicMock(spec=AbstractRequestHandler) + test_request_handler.can_handle.return_value = True + test_request_handler.handle.return_value = "Test Response" + + test_request_handler_chain = RequestHandlerChain( + request_handler=test_request_handler) + test_request_mapper = RequestMapper( + request_handler_chains=[test_request_handler_chain]) + + test_adapter = HandlerAdapter() + + self.test_dispatcher.request_mappers = [test_request_mapper] + self.test_dispatcher.handler_adapters = [test_adapter] + + assert self.test_dispatcher.dispatch( + handler_input=self.valid_handler_input) == "Test Response", ( + "Dispatcher dispatch method return invalid response when " + "supported handler chain and " + "supported handler adapter are found") + + test_request_handler.handle.assert_called_once_with( + self.valid_handler_input), ( + "Dispatcher dispatch method called handle on Request Handler " + "more than once") + + def test_dispatch_input_successful_local_request_interceptors_execution(self): + test_request_handler = mock.MagicMock(spec=AbstractRequestHandler) + test_request_handler.can_handle.return_value = True + test_request_handler.handle.return_value = "Test Response" + + test_interceptor_1 = mock.MagicMock(spec=AbstractRequestInterceptor) + test_interceptor_2 = mock.MagicMock(spec=AbstractRequestInterceptor) + + test_request_handler_chain = RequestHandlerChain( + request_handler=test_request_handler, + request_interceptors=[test_interceptor_1, test_interceptor_2]) + test_request_mapper = RequestMapper( + request_handler_chains=[test_request_handler_chain]) + + test_adapter = HandlerAdapter() + + self.test_dispatcher.request_mappers = [test_request_mapper] + self.test_dispatcher.handler_adapters = [test_adapter] + + self.test_dispatcher.dispatch( + handler_input=self.valid_handler_input) + + test_interceptor_1.process.assert_called_once_with( + handler_input=self.valid_handler_input), ( + "Dispatcher dispatch method didn't process local request " + "interceptors before calling request handler " + "handle") + test_interceptor_2.process.assert_called_once_with( + handler_input=self.valid_handler_input), ( + "Dispatcher dispatch method didn't process local request " + "interceptors before calling request handler " + "handle") + + def test_dispatch_input_successful_global_request_interceptors_execution(self): + test_request_handler = mock.MagicMock(spec=AbstractRequestHandler) + test_request_handler.can_handle.return_value = True + test_request_handler.handle.return_value = "Test Response" + + test_interceptor_1 = mock.MagicMock(spec=AbstractRequestInterceptor) + test_interceptor_2 = mock.MagicMock(spec=AbstractRequestInterceptor) + + test_request_handler_chain = RequestHandlerChain( + request_handler=test_request_handler) + test_request_mapper = RequestMapper( + request_handler_chains=[test_request_handler_chain]) + + test_adapter = HandlerAdapter() + + self.test_dispatcher.request_mappers = [test_request_mapper] + self.test_dispatcher.handler_adapters = [test_adapter] + self.test_dispatcher.request_interceptors = [ + test_interceptor_1, test_interceptor_2] + + self.test_dispatcher.dispatch(handler_input=self.valid_handler_input) + + test_interceptor_1.process.assert_called_once_with( + handler_input=self.valid_handler_input), ( + "Dispatcher dispatch method didn't process global request " + "interceptors before calling dispatch request") + test_interceptor_2.process.assert_called_once_with( + handler_input=self.valid_handler_input), ( + "Dispatcher dispatch method didn't process global request " + "interceptors before calling dispatch request") + + def test_dispatch_input_unsuccessful_global_request_interceptors_execution(self): + test_request_handler = mock.MagicMock(spec=AbstractRequestHandler) + test_request_handler.can_handle.return_value = True + test_request_handler.handle.return_value = "Test Response" + + test_request_interceptor_1 = mock.MagicMock( + spec=AbstractRequestInterceptor) + test_request_interceptor_1.process.side_effect = ValueError( + "Test exception") + test_request_interceptor_2 = mock.MagicMock( + spec=AbstractRequestInterceptor) + test_response_interceptor_1 = mock.MagicMock( + spec=AbstractResponseInterceptor) + + test_request_handler_chain = RequestHandlerChain( + request_handler=test_request_handler) + test_request_mapper = RequestMapper( + request_handler_chains=[test_request_handler_chain]) + + test_adapter = HandlerAdapter() + + self.test_dispatcher.request_mappers = [test_request_mapper] + self.test_dispatcher.handler_adapters = [test_adapter] + self.test_dispatcher.request_interceptors = [ + test_request_interceptor_1, test_request_interceptor_2] + self.test_dispatcher.response_interceptors = [ + test_response_interceptor_1] + + with self.assertRaises(ValueError) as exc: + self.test_dispatcher.dispatch( + handler_input=self.valid_handler_input) + + assert "Test exception" in str(exc.exception), ( + "Dispatcher didn't throw exception raised by global request " + "interceptor") + + test_request_interceptor_1.process.assert_called_once_with( + handler_input=self.valid_handler_input), ( + "Dispatcher dispatch method didn't process global request " + "interceptors before calling dispatch request") + test_request_interceptor_2.process.assert_not_called(), ( + "Dispatcher dispatch method processed remaining global " + "request interceptors when one of them threw " + "exception") + test_request_handler.assert_not_called(), ( + "Dispatcher dispatch method processed request handler 'handle' " + "method when one of the global request " + "interceptors threw exception") + test_response_interceptor_1.process.assert_not_called(), ( + "Dispatcher dispatch method processed global response " + "interceptors when one of the global request " + "interceptors threw exception") + + + def test_dispatch_input_successful_local_response_interceptors_execution(self): + test_request_handler = mock.MagicMock(spec=AbstractRequestHandler) + test_request_handler.can_handle.return_value = True + test_response = mock.MagicMock(spec=Response) + test_response_before_interceptor = test_response + test_request_handler.handle.return_value = test_response + + test_interceptor_1 = mock.MagicMock(spec=AbstractResponseInterceptor) + test_response.interceptor = "Interceptor 1" + test_response_from_interceptor_1 = test_response + test_interceptor_1.process.return_value = test_response + + test_interceptor_2 = mock.MagicMock(spec=AbstractResponseInterceptor) + test_response.interceptor = "Interceptor 2" + test_response_from_interceptor_2 = test_response + test_interceptor_2.process.return_value = test_response + + test_request_handler_chain = RequestHandlerChain( + request_handler=test_request_handler, + response_interceptors=[test_interceptor_1, test_interceptor_2]) + test_request_mapper = RequestMapper( + request_handler_chains=[test_request_handler_chain]) + + test_adapter = HandlerAdapter() + + self.test_dispatcher.request_mappers = [test_request_mapper] + self.test_dispatcher.handler_adapters = [test_adapter] + + assert self.test_dispatcher.dispatch( + handler_input=self.valid_handler_input) == test_response_from_interceptor_2, ( + "Dispatcher dispatch method returned invalid response after " + "processing response through " + "local response interceptors") + + test_interceptor_1.process.assert_called_once_with( + handler_input=self.valid_handler_input, response=test_response_before_interceptor), ( + "Dispatcher dispatch method didn't process local response " + "interceptors after calling request handler " + "handle") + + test_interceptor_2.process.assert_called_once_with( + handler_input=self.valid_handler_input, response=test_response_from_interceptor_1), ( + "Dispatcher dispatch method didn't process local response " + "interceptors after calling request handler " + "handle") + + def test_dispatch_input_successful_global_response_interceptors_execution(self): + test_request_handler = mock.MagicMock(spec=AbstractRequestHandler) + test_request_handler.can_handle.return_value = True + test_response = mock.MagicMock(spec=Response) + test_response_before_interceptor = test_response + test_request_handler.handle.return_value = test_response + + test_interceptor_1 = mock.MagicMock(spec=AbstractResponseInterceptor) + test_response.interceptor = "Interceptor 1" + test_response_from_interceptor_1 = test_response + test_interceptor_1.process.return_value = test_response + + test_interceptor_2 = mock.MagicMock(spec=AbstractResponseInterceptor) + test_response.interceptor = "Interceptor 2" + test_response_from_interceptor_2 = test_response + test_interceptor_2.process.return_value = test_response + + test_request_handler_chain = RequestHandlerChain( + request_handler=test_request_handler) + test_request_mapper = RequestMapper( + request_handler_chains=[test_request_handler_chain]) + + test_adapter = HandlerAdapter() + + self.test_dispatcher.request_mappers = [test_request_mapper] + self.test_dispatcher.handler_adapters = [test_adapter] + self.test_dispatcher.response_interceptors = [ + test_interceptor_1, test_interceptor_2] + + assert self.test_dispatcher.dispatch( + handler_input=self.valid_handler_input) == test_response_from_interceptor_2, ( + "Dispatcher dispatch method returned invalid response after " + "processing response through " + "global response interceptors") + + test_interceptor_1.process.assert_called_once_with( + handler_input=self.valid_handler_input, response=test_response_before_interceptor), ( + "Dispatcher dispatch method didn't process global request " + "interceptors after calling dispatch request") + + test_interceptor_2.process.assert_called_once_with( + handler_input=self.valid_handler_input, response=test_response_from_interceptor_1), ( + "Dispatcher dispatch method didn't process global request " + "interceptors after calling dispatch request") + + def test_dispatch_raise_low_level_exception_when_exception_handler_not_registered(self): + test_request_handler = mock.MagicMock(spec=AbstractRequestHandler) + test_request_handler.can_handle.return_value = True + test_request_handler.handle.return_value = "Test Response" + + test_request_handler_chain = RequestHandlerChain( + request_handler=test_request_handler) + test_request_mapper = RequestMapper( + request_handler_chains=[test_request_handler_chain]) + + test_adapter = mock.MagicMock(spec=HandlerAdapter) + test_adapter.supports.return_value = True + test_adapter.execute.side_effect = Exception( + "Test low level Exception") + + self.test_dispatcher.request_mappers = [test_request_mapper] + self.test_dispatcher.handler_adapters = [test_adapter] + + with self.assertRaises(Exception) as exc: + self.test_dispatcher.dispatch( + handler_input=self.valid_handler_input) + + assert "Test low level Exception" in str(exc.exception), ( + "Dispatcher didn't throw low level exception when request " + "dispatch throws exception and " + "no exception handler is registered") + + def test_dispatch_raise_low_level_exception_when_no_suitable_exception_handler_registered(self): + test_request_handler = mock.MagicMock(spec=AbstractRequestHandler) + test_request_handler.can_handle.return_value = True + test_request_handler.handle.return_value = "Test Response" + + test_request_handler_chain = RequestHandlerChain( + request_handler=test_request_handler) + test_request_mapper = RequestMapper( + request_handler_chains=[test_request_handler_chain]) + + test_adapter = mock.MagicMock(spec=HandlerAdapter) + test_adapter.supports.return_value = True + test_adapter.execute.side_effect = Exception( + "Test low level Exception") + + test_exception_handler = mock.MagicMock(spec=AbstractExceptionHandler) + test_exception_handler.can_handle.return_value = False + test_exception_mapper = ExceptionMapper( + exception_handlers=[test_exception_handler]) + + self.test_dispatcher.request_mappers = [test_request_mapper] + self.test_dispatcher.handler_adapters = [test_adapter] + self.test_dispatcher.exception_mapper = test_exception_mapper + + with self.assertRaises(Exception) as exc: + self.test_dispatcher.dispatch( + handler_input=self.valid_handler_input) + + assert "Test low level Exception" in str(exc.exception), ( + "Dispatcher didn't throw low level exception when request " + "dispatch throws exception and " + "no suitable exception handler is registered") + + def test_dispatch_process_handled_exception_when_suitable_exception_handler_registered(self): + test_request_handler = mock.MagicMock(spec=AbstractRequestHandler) + test_request_handler.can_handle.return_value = True + test_request_handler.handle.return_value = "Test Response" + + test_request_handler_chain = RequestHandlerChain( + request_handler=test_request_handler) + test_request_mapper = RequestMapper( + request_handler_chains=[test_request_handler_chain]) + + test_adapter = mock.MagicMock(spec=HandlerAdapter) + test_adapter.supports.return_value = True + test_adapter.execute.side_effect = DispatchException( + "Custom dispatch exception") + + test_exception_handler_1 = mock.MagicMock( + spec=AbstractExceptionHandler) + test_exception_handler_1.can_handle.return_value = False + test_exception_handler_2 = mock.MagicMock( + spec=AbstractExceptionHandler) + test_exception_handler_2.can_handle.return_value = True + test_exception_handler_2.handle.return_value = "Custom exception " \ + "handler response" + + self.test_dispatcher = RequestDispatcher( + request_mappers=[test_request_mapper], + handler_adapters=[test_adapter], + exception_mapper=ExceptionMapper( + exception_handlers=[test_exception_handler_1, + test_exception_handler_2])) + + assert self.test_dispatcher.dispatch( + handler_input=self.valid_handler_input) == "Custom exception handler response", ( + "Dispatcher didn't handle exception when a suitable exception handler is registered") + + test_exception_handler_1.handle.assert_not_called(), ( + "Incorrect Exception Handler called when handling custom " + "exception") + test_exception_handler_2.handle.assert_called_once(), ( + "Suitable exception handler didn't handle custom exception") diff --git a/ask-sdk-core/tests/unit/test_dispatch_components.py b/ask-sdk-core/tests/unit/test_dispatch_components.py new file mode 100644 index 0000000..7f44da4 --- /dev/null +++ b/ask-sdk-core/tests/unit/test_dispatch_components.py @@ -0,0 +1,499 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 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. +# +import unittest +from six import PY3 + +from ask_sdk_model.request_envelope import RequestEnvelope +from ask_sdk_model.intent_request import IntentRequest +from ask_sdk_model.events.skillevents.skill_enabled_request import ( + SkillEnabledRequest) + +from ask_sdk_core.handler_input import HandlerInput +from ask_sdk_core.dispatch_components import ( + RequestMapper, RequestHandlerChain, AbstractRequestHandler, + RequestHandlerChain, AbstractRequestInterceptor, + AbstractResponseInterceptor, HandlerAdapter, AbstractExceptionHandler, + ExceptionMapper) +from ask_sdk_core.dispatch_components.request_components import ( + GenericRequestHandlerChain) +from ask_sdk_core.exceptions import DispatchException + +if PY3: + from unittest import mock +else: + import mock + + +class TestDefaultRequestMapper(unittest.TestCase): + def test_default_request_mapper_initialization_with_null_handler_chains(self): + test_request_mapper = RequestMapper(request_handler_chains=None) + + assert test_request_mapper.request_handler_chains == [], ( + "Request Mapper didn't initialize empty request handler chains " + "instance variable on initialization") + + def test_default_request_mapper_initialization_with_chain_containing_null_throw_error(self): + with self.assertRaises(DispatchException) as exc: + test_request_mapper = RequestMapper(request_handler_chains=[None]) + + assert "Request Handler Chain is not a RequestHandlerChain instance" in str(exc.exception), ( + "Request Mapper didn't throw error during initialization when a " + "Null Handler Chain is passed") + + def test_default_request_mapper_initialization_with_chain_containing_invalid_type_throw_error(self): + test_request_handler_chain = mock.Mock() + with self.assertRaises(DispatchException) as exc: + test_request_mapper = RequestMapper( + request_handler_chains=[test_request_handler_chain]) + + assert "Request Handler Chain is not a RequestHandlerChain instance" in str(exc.exception), ( + "Request Mapper didn't throw error during initialization when an " + "invalid Handler Chain is passed") + + def test_default_request_mapper_initialization_with_chain_containing_valid_type(self): + test_request_handler = mock.MagicMock(spec=AbstractRequestHandler) + test_request_handler_chain = RequestHandlerChain( + request_handler=test_request_handler) + test_request_mapper = RequestMapper( + request_handler_chains=[test_request_handler_chain]) + + assert test_request_mapper.request_handler_chains == [test_request_handler_chain], ( + "Request Mapper initialization throws exception when a valid " + "Handler Chain is provided in the " + "handler chains list") + + def test_no_handler_registered_for_intent_request(self): + test_intent_request = mock.MagicMock(spec=IntentRequest) + test_request_envelope = mock.MagicMock(spec=RequestEnvelope) + test_request_envelope.request = test_intent_request + test_handler_input = HandlerInput( + request_envelope=test_request_envelope) + + test_request_handler = mock.MagicMock(spec=AbstractRequestHandler) + test_request_handler.can_handle.return_value = False + test_request_handler_chain = RequestHandlerChain( + request_handler=test_request_handler) + test_request_mapper = RequestMapper( + request_handler_chains=[test_request_handler_chain]) + + assert test_request_mapper.get_request_handler_chain( + test_handler_input) is None, ( + "get_request_handler_chain in Request Mapper found an unsupported " + "request handler chain for " + "intent request") + + def test_no_handler_registered_for_event_request(self): + test_event_request = mock.MagicMock(spec=SkillEnabledRequest) + test_request_envelope = mock.MagicMock(spec=RequestEnvelope) + test_request_envelope.request = test_event_request + test_handler_input = HandlerInput( + request_envelope=test_request_envelope) + + test_request_handler = mock.MagicMock(spec=AbstractRequestHandler) + test_request_handler.can_handle.return_value = False + test_request_handler_chain = RequestHandlerChain( + request_handler=test_request_handler) + test_request_mapper = RequestMapper( + request_handler_chains=[test_request_handler_chain]) + + assert test_request_mapper.get_request_handler_chain(test_handler_input) is None, ( + "get_request_handler_chain in Request Mapper found an unsupported " + "request handler chain for " + "event request") + + def test_get_handler_chain_registered_for_intent_request(self): + test_intent_request = mock.MagicMock(spec=IntentRequest) + test_request_envelope = mock.MagicMock(spec=RequestEnvelope) + test_request_envelope.request = test_intent_request + test_handler_input = HandlerInput( + request_envelope=test_request_envelope) + + test_intent_handler = mock.MagicMock(spec=AbstractRequestHandler) + test_intent_handler.can_handle.return_value = True + test_intent_request_handler_chain = RequestHandlerChain( + request_handler=test_intent_handler) + + test_event_handler = mock.MagicMock(spec=AbstractRequestHandler) + test_event_handler.can_handle.return_value = False + test_event_request_handler_chain = RequestHandlerChain( + request_handler=test_event_handler) + + test_request_mapper = RequestMapper( + request_handler_chains=[test_event_request_handler_chain, + test_intent_request_handler_chain]) + + assert test_request_mapper.get_request_handler_chain( + test_handler_input).request_handler == test_intent_handler, ( + "get_request_handler_chain in Request Mapper found incorrect " + "request handler chain for " + "intent request") + + def test_get_handler_chain_registered_for_event_request(self): + test_intent_request = mock.MagicMock(spec=IntentRequest) + test_request_envelope = mock.MagicMock(spec=RequestEnvelope) + test_request_envelope.request = test_intent_request + test_handler_input = HandlerInput( + request_envelope=test_request_envelope) + + test_intent_handler = mock.MagicMock(spec=AbstractRequestHandler) + test_intent_handler.can_handle.return_value = False + test_intent_request_handler_chain = RequestHandlerChain( + request_handler=test_intent_handler) + + test_event_handler = mock.MagicMock(spec=AbstractRequestHandler) + test_event_handler.can_handle.return_value = True + test_event_request_handler_chain = RequestHandlerChain( + request_handler=test_event_handler) + + test_request_mapper = RequestMapper( + request_handler_chains=[ + test_event_request_handler_chain, + test_intent_request_handler_chain]) + + assert test_request_mapper.get_request_handler_chain( + test_handler_input).request_handler == test_event_handler, ( + "get_request_handler_chain in Request Mapper found incorrect " + "request handler chain for " + "intent request") + + def test_add_request_handler_chain_for_valid_chain_type(self): + test_request_handler = mock.MagicMock(spec=AbstractRequestHandler) + test_request_handler_chain = RequestHandlerChain( + request_handler=test_request_handler) + test_request_mapper = RequestMapper( + request_handler_chains=None) + + test_request_mapper.add_request_handler_chain( + test_request_handler_chain) + + assert test_request_mapper.request_handler_chains == [ + test_request_handler_chain], ( + "Default Request Mapper throws exception when a valid Handler " + "Chain is provided in the " + "add_request_handler_chain method") + + def test_add_request_handler_chain_throw_error_for_invalid_chain_type(self): + test_request_handler_chain = mock.Mock() + test_request_mapper = RequestMapper(request_handler_chains=None) + + with self.assertRaises(DispatchException) as exc: + test_request_mapper.add_request_handler_chain( + test_request_handler_chain) + + assert "Request Handler Chain is not a RequestHandlerChain instance" in str(exc.exception), ( + "Request Mapper didn't throw error during " + "add_request_handler_chain method call when " + "an invalid Handler Chain is passed") + + def test_add_request_handler_chain_throw_error_for_null_chain(self): + test_request_mapper = RequestMapper(request_handler_chains=None) + + with self.assertRaises(DispatchException) as exc: + test_request_mapper.add_request_handler_chain(None) + + assert "Request Handler Chain is not a RequestHandlerChain instance" in str(exc.exception), ( + "Request Mapper didn't throw error during " + "add_request_handler_chain method call when " + "a Null Handler Chain is passed") + + +class TestGenericRequestHandlerChain(unittest.TestCase): + def test_generic_handler_chain_with_null_request_handler_throws_error(self): + with self.assertRaises(DispatchException) as exc: + test_handler_chain = GenericRequestHandlerChain( + request_handler=None) + + assert "No Request Handler provided" in str(exc.exception), ( + "Generic Request Handler Chain didn't raise exception when no " + "request handler is provided during " + "instantiation") + + def test_generic_handler_chain_instantiate_request_interceptors(self): + test_handler = mock.Mock() + test_handler_chain = GenericRequestHandlerChain( + request_handler=test_handler) + + assert test_handler_chain.request_interceptors == [], ( + "Generic Request Handler Chain didn't instantiate empty list of " + "request interceptors when no " + "request interceptors are provided during instantiation") + + def test_generic_handler_chain_instantiate_response_interceptors(self): + test_handler = mock.Mock() + test_handler_chain = GenericRequestHandlerChain( + request_handler=test_handler) + + assert test_handler_chain.response_interceptors == [], ( + "Generic Request Handler Chain didn't instantiate empty list of " + "response interceptors when no " + "response interceptors are provided during instantiation") + + def test_generic_handler_chain_add_request_interceptors_to_empty_list(self): + test_handler = mock.Mock() + test_handler_chain = GenericRequestHandlerChain( + request_handler=test_handler) + test_request_interceptor = mock.Mock() + test_handler_chain.add_request_interceptor( + interceptor=test_request_interceptor) + + assert test_handler_chain.request_interceptors == [test_request_interceptor], ( + "Generic Request Handler Chain didn't add interceptor to list of " + "request interceptors when no " + "request interceptors are provided during instantiation") + + def test_generic_handler_chain_add_request_interceptors_to_non_empty_list(self): + test_handler = mock.Mock() + test_interceptor_1 = mock.MagicMock(spec=AbstractRequestInterceptor) + test_interceptors = [test_interceptor_1] + test_handler_chain = GenericRequestHandlerChain( + request_handler=test_handler, + request_interceptors=test_interceptors) + test_request_interceptor = mock.MagicMock( + spec=AbstractRequestInterceptor) + test_handler_chain.add_request_interceptor( + interceptor=test_request_interceptor) + + assert test_handler_chain.request_interceptors == [ + test_interceptor_1, test_request_interceptor], ( + "Generic Request Handler Chain didn't add interceptor to list of " + "request interceptors when " + "request interceptors are provided during instantiation") + + def test_generic_handler_chain_add_response_interceptors_to_empty_list(self): + test_handler = mock.Mock() + test_handler_chain = GenericRequestHandlerChain( + request_handler=test_handler) + test_response_interceptor = mock.Mock() + test_handler_chain.add_response_interceptor( + interceptor=test_response_interceptor) + + assert test_handler_chain.response_interceptors == [test_response_interceptor], ( + "Generic Request Handler Chain didn't add interceptor to list of " + "response interceptors when no " + "response interceptors are provided during instantiation") + + def test_generic_handler_chain_add_response_interceptors_to_non_empty_list(self): + test_handler = mock.Mock() + test_interceptor_1 = mock.MagicMock(spec=AbstractResponseInterceptor) + test_interceptors = [test_interceptor_1] + test_handler_chain = GenericRequestHandlerChain( + request_handler=test_handler, + response_interceptors=test_interceptors) + test_response_interceptor = mock.MagicMock( + spec=AbstractResponseInterceptor) + test_handler_chain.add_response_interceptor( + interceptor=test_response_interceptor) + + assert test_handler_chain.response_interceptors == [ + test_interceptor_1, test_response_interceptor], ( + "Generic Request Handler Chain didn't add interceptor to list of " + "response interceptors when " + "response interceptors are provided during instantiation") + + +class TestDefaultRequestHandlerChain(unittest.TestCase): + def test_default_handler_chain_with_null_request_handler_throws_error(self): + with self.assertRaises(DispatchException) as exc: + test_handler_chain = RequestHandlerChain(request_handler=None) + + assert "Invalid Request Handler provided. Expected Request Handler instance" in str(exc.exception), ( + "Default Request Handler Chain didn't raise exception when no " + "request handler is provided during " + "instantiation") + + def test_default_handler_chain_with_invalid_request_handler_throws_error(self): + test_handler = mock.Mock() + with self.assertRaises(DispatchException) as exc: + test_handler_chain = RequestHandlerChain( + request_handler=test_handler) + + assert "Invalid Request Handler provided. Expected Request Handler instance" in str(exc.exception), ( + "Default Request Handler Chain didn't raise exception when " + "invalid request handler is provided during " + "instantiation") + + def test_default_handler_chain_with_invalid_request_handler_in_setter_throws_error(self): + test_handler = mock.MagicMock(spec=AbstractRequestHandler) + test_handler_chain = RequestHandlerChain(request_handler=test_handler) + + test_invalid_handler = mock.Mock() + + with self.assertRaises(DispatchException) as exc: + test_handler_chain.request_handler = test_invalid_handler + + assert "Invalid Request Handler provided. Expected Request Handler instance" in str(exc.exception), ( + "Default Request Handler Chain didn't raise exception when " + "invalid request handler is provided during " + "instantiation") + + def test_generic_handler_chain_instantiate_call_parent_instantiation(self): + test_handler = mock.Mock() + test_handler_chain = GenericRequestHandlerChain( + request_handler=test_handler) + + assert test_handler_chain.request_interceptors == [] and test_handler_chain.response_interceptors == [], ( + "Default Request Handler Chain didn't call parent instantiation " + "method during init") + + +class TestDefaultHandlerAdapter(unittest.TestCase): + def setUp(self): + self.test_handler_adapter = HandlerAdapter() + + def test_adapter_supports_valid_handler(self): + test_handler = mock.MagicMock(spec=AbstractRequestHandler) + test_handler.can_handle.return_value = True + + assert self.test_handler_adapter.supports(test_handler), \ + "Handler Adapter supports method returns False for supported " \ + "Request Handler implementation" + + def test_adapter_doesnt_supports_invalid_handler(self): + test_handler = mock.Mock() + + assert not self.test_handler_adapter.supports(test_handler), \ + "Handler Adapter supports method returns True for unsupported " \ + "Request Handler implementation" + + def test_adapter_executes_valid_handler(self): + test_handler = mock.MagicMock(spec=AbstractRequestHandler) + test_handler.handle.return_value = "Test Response" + test_input = mock.MagicMock(spec=HandlerInput) + + assert self.test_handler_adapter.execute( + handler=test_handler, handler_input=test_input) == "Test Response", ( + "Handler Adapter executes method returns unexpected response " + "output for supported Request Handler " + "implementation") + + +class TestDefaultExceptionMapper(unittest.TestCase): + def test_exception_mapper_with_null_input(self): + test_exception_mapper = ExceptionMapper(exception_handlers=None) + + assert test_exception_mapper.exception_handlers == [], ( + "Exception Mapper instantiated empty list of exceptions handlers " + "when no input provided") + + def test_exception_mapper_initialization_with_handler_list_containing_null_throw_error(self): + with self.assertRaises(DispatchException) as exc: + test_exception_mapper = ExceptionMapper(exception_handlers=[None]) + + assert "Input is not an AbstractExceptionHandler instance" in str(exc.exception), ( + "Exception Mapper didn't throw error during initialization when a " + "Null handler is passed") + + def test_exception_mapper_initialization_with_handler_list_containing_invalid_handler_throw_error(self): + test_invalid_handler = mock.Mock() + + with self.assertRaises(DispatchException) as exc: + test_exception_mapper = ExceptionMapper( + exception_handlers=[test_invalid_handler]) + + assert "Input is not an AbstractExceptionHandler instance" in str(exc.exception), ( + "Exception Mapper didn't throw error during initialization when " + "an invalid handler is passed") + + def test_exception_mapper_initialization_with_handler_list_containing_valid_handler(self): + test_exception_handler = mock.MagicMock(spec=AbstractExceptionHandler) + + test_exception_mapper = ExceptionMapper( + exception_handlers=[test_exception_handler]) + + assert test_exception_mapper.exception_handlers == [test_exception_handler], ( + "Exception Mapper initialization throws exception when a valid " + "Handler is provided in the " + "exception handlers list") + + def test_add_exception_handler_for_valid_handler_type(self): + test_exception_handler = mock.MagicMock(spec=AbstractExceptionHandler) + test_exception_mapper = ExceptionMapper(exception_handlers=None) + + test_exception_mapper.add_exception_handler(test_exception_handler) + + assert test_exception_mapper.exception_handlers == [ + test_exception_handler], ( + "Exception Mapper throws exception when a valid Exception Handler " + "is provided in the " + "add_handler method") + + def test_add_request_handler_chain_throw_error_for_invalid_chain_type(self): + test_exception_handler = mock.Mock() + test_exception_mapper = ExceptionMapper(exception_handlers=None) + + with self.assertRaises(DispatchException) as exc: + test_exception_mapper.add_exception_handler(test_exception_handler) + + assert "Input is not an AbstractExceptionHandler instance" in str(exc.exception), ( + "Exception Mapper didn't throw error during add_exception_handler " + "method call when " + "an invalid Exception Handler is passed") + + def test_add_request_handler_chain_throw_error_for_null_chain(self): + test_exception_mapper = ExceptionMapper(exception_handlers=None) + + with self.assertRaises(DispatchException) as exc: + test_exception_mapper.add_exception_handler(None) + + assert "Input is not an AbstractExceptionHandler instance" in str(exc.exception), ( + "Exception Mapper didn't throw error during add_exception_handler " + "method call when " + "an invalid Exception Handler is passed") + + def test_no_handler_registered_for_custom_exception(self): + test_intent_request = mock.MagicMock(spec=IntentRequest) + test_request_envelope = mock.MagicMock(spec=RequestEnvelope) + test_request_envelope.request = test_intent_request + test_handler_input = HandlerInput( + request_envelope=test_request_envelope) + + test_exception_handler = mock.MagicMock(spec=AbstractExceptionHandler) + test_exception_handler.can_handle.return_value = False + test_exception_mapper = ExceptionMapper( + exception_handlers=[test_exception_handler]) + + test_custom_exception = DispatchException("Test Custom Exception") + + assert test_exception_mapper.get_handler( + handler_input=test_handler_input, exception=test_custom_exception) is None, ( + "get_handler in Default Exception Mapper found an unsupported " + "exception handler for " + "handler input and custom exception") + + def test_get_handler_registered_for_custom_exception(self): + test_intent_request = mock.MagicMock(spec=IntentRequest) + test_request_envelope = mock.MagicMock(spec=RequestEnvelope) + test_request_envelope.request = test_intent_request + test_handler_input = HandlerInput( + request_envelope=test_request_envelope) + + test_exception_handler_1 = mock.MagicMock(spec=AbstractExceptionHandler) + test_exception_handler_1.can_handle.return_value = False + test_exception_handler_2 = mock.MagicMock(spec=AbstractExceptionHandler) + test_exception_handler_2.can_handle.return_value = True + test_exception_mapper = ExceptionMapper( + exception_handlers=[ + test_exception_handler_1, test_exception_handler_2]) + + test_custom_exception = DispatchException("Test Custom Exception") + + assert test_exception_mapper.get_handler( + handler_input=test_handler_input, exception=test_custom_exception) == test_exception_handler_2, ( + "get_handler in Default Exception Mapper found incorrect request " + "exception handler for " + "input and custom exception") \ No newline at end of file diff --git a/ask-sdk-core/tests/unit/test_handler_input.py b/ask-sdk-core/tests/unit/test_handler_input.py new file mode 100644 index 0000000..7c398fc --- /dev/null +++ b/ask-sdk-core/tests/unit/test_handler_input.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 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. +# +import unittest +from six import PY3 + +from ask_sdk_core.handler_input import HandlerInput + +if PY3: + from unittest import mock +else: + import mock + + +class TestHandlerInput(unittest.TestCase): + def test_error_thrown_when_service_client_factory_getter_called_without_setting(self): + test_input = HandlerInput(request_envelope=None) + with self.assertRaises(ValueError) as exc: + test_client_factory = test_input.service_client_factory + + assert "Attempting to use service client factory with no configured API client" in str(exc.exception), ( + "Handler Input didn't raise Value Error when service client " + "factory is not set and a get is called") + + def test_no_error_thrown_when_service_client_factory_getter_called_after_setting(self): + test_input = HandlerInput( + request_envelope=None, service_client_factory=mock.Mock()) + test_client_factory = test_input.service_client_factory + + assert isinstance(test_client_factory, mock.Mock), ( + "Handler Input service client factory getter returned incorrect " + "value for client factory after setter") diff --git a/ask-sdk-core/tests/unit/test_response_helper.py b/ask-sdk-core/tests/unit/test_response_helper.py new file mode 100644 index 0000000..6e16869 --- /dev/null +++ b/ask-sdk-core/tests/unit/test_response_helper.py @@ -0,0 +1,232 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 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. +# + +import unittest + +from ask_sdk_model.interfaces.videoapp import LaunchDirective, VideoItem, Metadata +from ask_sdk_model.ui import SsmlOutputSpeech, Reprompt +from ask_sdk_model.interfaces.display import TextContent +from ask_sdk_model.interfaces.display import PlainText +from ask_sdk_model.interfaces.display import RichText + +from ask_sdk_core.response_helper import ( + ResponseFactory, get_text_content, get_plain_text_content, + get_rich_text_content, PLAIN_TEXT_TYPE, RICH_TEXT_TYPE) + + +class TestResponseFactory(unittest.TestCase): + def setUp(self): + self.response_factory = ResponseFactory() + + def test_speak(self): + response_factory = self.response_factory.speak(speech=None) + + assert response_factory.response.output_speech == SsmlOutputSpeech( + ssml=""), ( + "The speak method of ResponseFactory fails to set output speech") + + def test_ask(self): + response_factory = self.response_factory.ask(reprompt=None) + + assert response_factory.response.reprompt == Reprompt( + output_speech=SsmlOutputSpeech(ssml="")), ( + "The ask method of ResponseFactory fails to set reprompt") + assert response_factory.response.should_end_session is False, ( + "The ask method of ResponseFactory fails to set the " + "should_end_session to False") + + def test_ask_with_video_app_launch_directive(self): + directive = LaunchDirective(video_item=VideoItem( + source=None, metadata=Metadata(title=None, subtitle=None))) + response_factory = self.response_factory.add_directive( + directive).ask(reprompt=None) + + assert response_factory.response.should_end_session is None, ( + "The ask method of ResponseFactory fails to set the should_end " + "session to None when video app directive" + " presents") + + def test_ask_with_other_directive(self): + response_factory = self.response_factory.add_directive( + directive=None).ask(reprompt=None) + + assert response_factory.response.should_end_session is False, ( + "The ask method of ResponseFactory fails to set the " + "should_end_session to False when other directives " + "except video app directive presents") + + def test_set_card(self): + response_factory = self.response_factory.set_card(card=None) + + assert response_factory.response.card is None, ( + "The set_card method of ResponseFactory fails to set card in " + "response") + + def test_add_directives(self): + response_factory = self.response_factory.add_directive(directive=None) + + assert len(response_factory.response.directives) == 1, ( + "The add_directive method of ResponseFactory fails to add " + "directive") + + def test_add_two_directives(self): + response_factory = self.response_factory.add_directive( + directive=None).add_directive(directive=None) + + assert len(response_factory.response.directives) == 2, ( + "The add_directive method of ResponseFactory fails to add " + "multiple directives") + + def test_add_video_app_launch_directive(self): + directive = LaunchDirective(video_item=VideoItem( + source=None, metadata=Metadata(title=None, subtitle=None))) + response_factory = self.response_factory.add_directive( + directive).set_should_end_session(False) + + assert response_factory.response.directives[0] == directive, ( + "The add_directive method of ResponseFactory fails to add " + "LaunchDirective") + assert response_factory.response.should_end_session is None, ( + "The add_directive() method of ResponseFactory fails to " + "remove should_end_session value") + + def test_set_should_end_session(self): + response_factory = self.response_factory.set_should_end_session(False) + + assert response_factory.response.should_end_session is False, ( + "The set_should_end_session method of ResponseFactory fails to " + "set should_end_session value") + + def test_trim_outputspeech(self): + speech_output1 = "Hello World" + speech_output2 = " Hello World " + speech_output3 = "Hello World" + speech_output4 = " Hello World " + + assert self.response_factory.speak( + speech=speech_output1).response.output_speech.ssml == "Hello World", ( + "The trim_outputspeech method fails to trim the outputspeech") + assert self.response_factory.speak( + speech=speech_output2).response.output_speech.ssml == "Hello World", ( + "The trim_outputspeech method fails to trim the outputspeech") + assert self.response_factory.speak( + speech=speech_output3).response.output_speech.ssml == "Hello World", ( + "The trim_outputspeech method fails to trim the outputspeech") + assert self.response_factory.speak( + speech=speech_output4).response.output_speech.ssml == "Hello World", ( + "The trim_outputspeech method fails to trim the outputspeech") + + +class TestTextHelper(unittest.TestCase): + def test_build_primary_text_default(self): + text_val = "test" + + plain_text = PlainText(text=text_val) + text_content = TextContent(primary_text=plain_text) + + assert get_text_content(primary_text=text_val) == text_content, \ + "get_text_content helper returned wrong text content with " \ + "primary text and default type" + + def test_build_primary_text_rich(self): + text_val = "test" + + rich_text = RichText(text=text_val) + text_content = TextContent(primary_text=rich_text) + + assert get_text_content( + primary_text=text_val, primary_text_type=RICH_TEXT_TYPE) == text_content, \ + "get_text_content helper returned wrong text content with " \ + "primary text and rich type" + + def test_build_secondary_text_default(self): + text_val = "test" + + plain_text = PlainText(text=text_val) + text_content = TextContent(secondary_text=plain_text) + + assert get_text_content(secondary_text=text_val) == text_content, \ + "get_text_content helper returned wrong text content with " \ + "secondary text and default type" + + def test_build_secondary_text_rich(self): + text_val = "test" + + rich_text = RichText(text=text_val) + text_content = TextContent(secondary_text=rich_text) + + assert get_text_content( + secondary_text=text_val, secondary_text_type=RICH_TEXT_TYPE) == text_content, \ + "get_text_content helper returned wrong text content with " \ + "secondary text and rich type" + + def test_build_tertiary_text_default(self): + text_val = "test" + + plain_text = PlainText(text=text_val) + text_content = TextContent(tertiary_text=plain_text) + + assert get_text_content(tertiary_text=text_val) == text_content, \ + "get_text_content helper returned wrong text content with " \ + "tertiary text and default type" + + def test_build_tertiary_text_rich(self): + text_val = "test" + + plain_text = RichText(text=text_val) + text_content = TextContent(tertiary_text=plain_text) + + assert get_text_content( + tertiary_text=text_val, tertiary_text_type=RICH_TEXT_TYPE) == text_content, \ + "get_text_content helper returned wrong text content with " \ + "tertiary text and rich type" + + def test_raise_value_error_with_invalid_text_type(self): + text_val = "test" + text_type = "InvalidType" + + with self.assertRaises(ValueError) as exc: + get_text_content(primary_text=text_val, primary_text_type=text_type) + + assert "Invalid type provided" in str(exc.exception), \ + "get_text_content helper didn't raise ValueError when an " \ + "invalid type has been passed" + + +class TestPlainTextHelper(unittest.TestCase): + def test_build_primary_text(self): + text_val = "test" + + plain_text = PlainText(text=text_val) + text_content = TextContent(primary_text=plain_text) + + assert get_plain_text_content(primary_text=text_val) == text_content, \ + "get_plain_text_content helper returned wrong text content " \ + "with primary text" + + +class TestRichTextHelper(unittest.TestCase): + def test_build_primary_text(self): + text_val = "test" + + rich_text = RichText(text=text_val) + text_content = TextContent(primary_text=rich_text) + + assert get_rich_text_content(primary_text=text_val) == text_content, \ + "get_rich_text_content helper returned wrong text content " \ + "with primary text" diff --git a/ask-sdk-core/tests/unit/test_serialize.py b/ask-sdk-core/tests/unit/test_serialize.py new file mode 100644 index 0000000..aa5f7ae --- /dev/null +++ b/ask-sdk-core/tests/unit/test_serialize.py @@ -0,0 +1,508 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 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. +# +import unittest +import datetime +import decimal + +from six import PY3 +from mock import patch + +from ask_sdk_core.serialize import DefaultSerializer +from ask_sdk_core.exceptions import SerializationException + +from . import data + +if PY3: + from unittest import mock + unicode_type = str + long_type = int +else: + import mock + unicode_type = unicode + long_type = long + + +class TestSerialization(unittest.TestCase): + def setUp(self): + self.test_serializer = DefaultSerializer() + + def test_none_obj_serialization(self): + test_obj = None + assert self.test_serializer.serialize(test_obj) is None, \ + "Default Serializer serialized None object incorrectly" + + def test_primitive_obj_serialization(self): + test_obj = "test" + assert self.test_serializer.serialize(test_obj) == test_obj, \ + "Default Serializer serialized str object incorrectly" + + test_obj = 123 + assert self.test_serializer.serialize(test_obj) == test_obj, \ + "Default Serializer serialized int object incorrectly" + + test_obj = u"test" + assert self.test_serializer.serialize(test_obj) == test_obj, \ + "Default Serializer serialized unicode object incorrectly" + + test_obj = b"test" + assert self.test_serializer.serialize(test_obj) == test_obj, \ + "Default Serializer serialized bytes object incorrectly" + + test_obj = False + assert self.test_serializer.serialize(test_obj) == test_obj, \ + "Default Serializer serialized bool object incorrectly" + + def test_list_obj_serialization(self): + test_obj_inst = data.ModelTestObject2(int_var=123) + test_list_obj = ["test", 123, test_obj_inst] + + expected_list = ["test", 123, {"var4Int": 123}] + assert self.test_serializer.serialize(test_list_obj) == expected_list, \ + "Default Serializer serialized list object incorrectly" + + def test_tuple_obj_serialization(self): + test_obj_inst = data.ModelTestObject2(int_var=123) + test_tuple_obj = ("test", 123, test_obj_inst) + + expected_tuple = ("test", 123, {"var4Int": 123}) + assert self.test_serializer.serialize(test_tuple_obj) == expected_tuple, \ + "Default Serializer serialized tuple object incorrectly" + + def test_datetime_obj_serialization(self): + test_obj = datetime.datetime(2018, 1, 1, 10, 20, 30) + expected_datetime = "2018-01-01T10:20:30" + assert self.test_serializer.serialize(test_obj) == expected_datetime, \ + "Default Serializer serialized datetime object incorrectly" + + def test_date_obj_serialization(self): + test_obj = datetime.date(2018, 1, 1) + expected_date = "2018-01-01" + assert self.test_serializer.serialize(test_obj) == expected_date, \ + "Default Serializer serialized datetime object incorrectly" + + def test_dict_obj_serialization(self): + test_obj_inst = data.ModelTestObject2(int_var=123) + test_dict_obj = { + "test_str": "test", + "test_int": 123, + "test_obj": test_obj_inst + } + + expected_dict = { + "test_str": "test", + "test_obj": { + "var4Int": 123 + }, + "test_int": 123, + } + assert self.test_serializer.serialize(test_dict_obj) == expected_dict, \ + "Default Serializer serialized dict object incorrectly" + + def test_model_obj_serialization(self): + test_model_obj_2 = data.ModelTestObject2(int_var=123) + test_model_obj_1 = data.ModelTestObject1( + str_var="test", datetime_var=datetime.datetime( + 2018, 1, 1, 10, 20, 30), obj_var=test_model_obj_2) + + expected_serialized_obj = { + "var1": "test", + "var2Time": "2018-01-01T10:20:30", + "var3Object": { + "var4Int": 123 + } + } + assert self.test_serializer.serialize(test_model_obj_1) == expected_serialized_obj, \ + "Default Serializer serialized model object incorrectly" + + def test_enum_obj_serialization(self): + test_model_obj_2 = data.ModelTestObject2(int_var=123) + test_enum_obj = data.ModelEnumObject("ENUM_VAL_1") + test_model_obj_1 = data.ModelTestObject1( + str_var="test", datetime_var=datetime.datetime( + 2018, 1, 1, 10, 20, 30), obj_var=test_model_obj_2, + enum_var=test_enum_obj) + + expected_serialized_obj = { + "var1": "test", + "var2Time": "2018-01-01T10:20:30", + "var6Enum": "ENUM_VAL_1", + "var3Object": { + "var4Int": 123 + } + } + assert self.test_serializer.serialize(test_model_obj_1) == expected_serialized_obj, \ + "Default Serializer serialized enum object incorrectly" + + def test_decimal_obj_without_decimals_serialization(self): + test_decimal_obj = decimal.Decimal(10) + expected_obj = 10 + actual_obj = self.test_serializer.serialize(test_decimal_obj) + + assert actual_obj == expected_obj, ( + "Default Serializer serialized decimal object containing no " + "decimals incorrectly") + assert type(actual_obj) == int, ( + "Default Serializer serialized decimal object containing no " + "decimals to incorrect type") + + def test_decimal_obj_with_decimals_serialization(self): + test_decimal_obj = decimal.Decimal(10.5) + expected_obj = 10.5 + actual_obj = self.test_serializer.serialize(test_decimal_obj) + + assert actual_obj == expected_obj, ( + "Default Serializer serialized decimal object containing " + "decimals incorrectly") + assert type(actual_obj) == float, ( + "Default Serializer serialized decimal object containing " + "decimals to incorrect type") + + +class TestDeserialization(unittest.TestCase): + def setUp(self): + self.test_serializer = DefaultSerializer() + + def test_none_obj_deserialization(self): + test_payload = None + test_obj_type = str + assert self.test_serializer.deserialize( + test_payload, test_obj_type) is None, \ + "Default Serializer deserialized None object incorrectly" + + def test_str_obj_deserialization(self): + test_payload = "test" + test_obj_type = str + with patch("json.loads") as mock_json_loader: + mock_json_loader.return_value = test_payload + assert self.test_serializer.deserialize( + test_payload, test_obj_type) == test_payload, \ + "Default Serializer deserialized string object incorrectly" + + def test_unicode_obj_deserialization(self): + test_payload = u"√" + test_obj_type = unicode_type + with patch("json.loads") as mock_json_loader: + mock_json_loader.return_value = test_payload + assert self.test_serializer.deserialize( + test_payload, test_obj_type) == u"\u221a", \ + "Default Serializer deserialized unicode string object incorrectly" + + def test_int_obj_deserialization(self): + test_payload = 123 + test_obj_type = int + with patch("json.loads") as mock_json_loader: + mock_json_loader.return_value = test_payload + assert self.test_serializer.deserialize( + test_payload, test_obj_type) == test_payload, \ + "Default Serializer deserialized int object incorrectly" + + def test_long_obj_deserialization(self): + test_payload = 123 + test_obj_type = long_type + with patch("json.loads") as mock_json_loader: + mock_json_loader.return_value = test_payload + assert self.test_serializer.deserialize( + test_payload, test_obj_type) == long_type(test_payload), \ + "Default Serializer deserialized long object incorrectly" + + def test_primitive_obj_deserialization_raising_unicode_exception(self): + test_serializer = DefaultSerializer() + mocked_primitive_type = mock.Mock( + side_effect=UnicodeEncodeError('hitchhiker', u"", 42, 43, 'the universe and everything else')) + + test_serializer.PRIMITIVE_TYPES = [mocked_primitive_type] + + test_payload = u"√" + test_obj_type = mocked_primitive_type + with patch("json.loads") as mock_json_loader: + mock_json_loader.return_value = test_payload + assert test_serializer.deserialize( + test_payload, test_obj_type) == u"\u221a", \ + "Default Serializer deserialized primitive type which raises UnicodeEncodeError incorrectly" + + def test_primitive_obj_deserialization_raising_type_error(self): + test_serializer = DefaultSerializer() + mocked_primitive_type = mock.Mock(side_effect=TypeError()) + + test_serializer.PRIMITIVE_TYPES = [mocked_primitive_type] + + test_payload = "test" + test_obj_type = mocked_primitive_type + with patch("json.loads") as mock_json_loader: + mock_json_loader.return_value = test_payload + assert test_serializer.deserialize( + test_payload, test_obj_type) == test_payload, \ + "Default Serializer deserialized primitive type which raises TypeError incorrectly" + + def test_primitive_obj_deserialization_raising_value_error(self): + test_payload = "test" + test_obj_type = int + + with self.assertRaises(SerializationException) as exc: + with patch("json.loads") as mock_json_loader: + mock_json_loader.return_value = test_payload + self.test_serializer.deserialize(test_payload, test_obj_type) + + assert "Failed to parse test into 'int' object" in str(exc.exception), \ + "Default Serializer didn't throw SerializationException when invalid primitive type is deserialized" + + def test_datetime_obj_serialization(self): + # payload in iso8601 format + test_payload = "2018-01-01T10:20:30" + test_obj_type = datetime.datetime + + expected_obj = datetime.datetime(2018, 1, 1, 10, 20, 30) + with patch("json.loads") as mock_json_loader: + mock_json_loader.return_value = test_payload + assert self.test_serializer.deserialize(test_payload, test_obj_type) == expected_obj, \ + "Default Serializer deserialized datetime object incorrectly" + + def test_date_obj_serialization(self): + # payload in iso8601 format + test_payload = "2018-01-01" + test_obj_type = datetime.date + + expected_obj = datetime.date(2018, 1, 1) + with patch("json.loads") as mock_json_loader: + mock_json_loader.return_value = test_payload + assert self.test_serializer.deserialize(test_payload, test_obj_type) == expected_obj, \ + "Default Serializer deserialized date object incorrectly" + + def test_datetime_obj_deserialization_raising_value_error(self): + test_payload = "abc-wx-yzT25:80:90" + test_obj_type = datetime.datetime + + with self.assertRaises(SerializationException) as exc: + with patch("json.loads") as mock_json_loader: + mock_json_loader.return_value = test_payload + self.test_serializer.deserialize(test_payload, test_obj_type) + + assert "Failed to parse abc-wx-yzT25:80:90 into 'datetime' object" in str(exc.exception), \ + "Default Serializer didn't throw SerializationException when invalid datetime type is deserialized" + + def test_datetime_obj_deserialization_raising_import_error(self): + test_payload = "abc-wx-yzT25:80:90" + test_obj_type = datetime.datetime + + with patch("json.loads") as mock_json_loader: + mock_json_loader.return_value = test_payload + with mock.patch('dateutil.parser.parse') as parse_class: + parse_class.side_effect = ImportError + assert self.test_serializer.deserialize( + test_payload, test_obj_type) == test_payload, \ + "Default Serializer didn't return datetime correctly for import errors" + parse_class.assert_called_once_with(test_payload) + + def test_obj_type_deserialization(self): + test_payload = "test" + test_obj_type = object + + with patch("json.loads") as mock_json_loader: + mock_json_loader.return_value = test_payload + assert self.test_serializer.deserialize( + test_payload, test_obj_type) == test_payload, \ + "Default Serializer deserialization of object returned other than the object itself" + + def test_native_type_mapping_deserialization(self): + test_payload = "test" + test_obj_type = "str" + + with patch("json.loads") as mock_json_loader: + mock_json_loader.return_value = test_payload + assert self.test_serializer.deserialize( + test_payload, test_obj_type) == test_payload, ( + "Default Serializer deserialization of object with object_type of string class under native mapping " + "not deserialized correctly") + + def test_polymorphic_list_obj_deserialization(self): + test_payload = ["test", 123, "2018-01-01T10:20:30"] + test_obj_type = "list[str, long, datetime]" + + deserialized_datetime_obj = datetime.datetime(2018, 1, 1, 10, 20, 30) + expected_obj = ["test", 123, deserialized_datetime_obj] + + with patch("json.loads") as mock_json_loader: + mock_json_loader.return_value = test_payload + assert self.test_serializer.deserialize( + test_payload, test_obj_type) == expected_obj, ( + "Default Serializer deserialized list containing poly type object incorrectly") + + def test_similar_list_obj_deserialization(self): + test_payload = ["test", "test1", "2018-01-01T10:20:30"] + test_obj_type = "list[str]" + expected_obj = ["test", "test1", "2018-01-01T10:20:30"] + + with patch("json.loads") as mock_json_loader: + mock_json_loader.return_value = test_payload + assert self.test_serializer.deserialize( + test_payload, test_obj_type) == expected_obj, ( + "Default Serializer deserialized list object containing similar type objects incorrectly") + + def test_dict_obj_deserialization(self): + test_payload = { + "test_key": ["test_val_1", "test_val_2"], + "test_date_str": ["2018-01-01T10:20:30"] + } + test_obj_type = "dict(str, list[str])" + + with patch("json.loads") as mock_json_loader: + mock_json_loader.return_value = test_payload + assert self.test_serializer.deserialize( + test_payload, test_obj_type) == test_payload, ( + "Default Serializer deserialized dict object incorrectly") + + def test_model_obj_deserialization(self): + test_payload = { + "var1": "test", + "var2Time": "2018-01-01T10:20:30", + "var3Object": { + "var4Int": 123 + }, + "var6Enum": "ENUM_VAL_1" + } + test_obj_type = data.ModelTestObject1 + expected_datetime_obj = datetime.datetime(2018, 1, 1, 10, 20, 30) + expected_sub_obj = data.ModelTestObject2(int_var=123) + expected_enum_obj = data.ModelEnumObject("ENUM_VAL_1") + expected_obj = data.ModelTestObject1( + str_var="test", datetime_var=expected_datetime_obj, obj_var=expected_sub_obj, enum_var=expected_enum_obj) + + with patch("json.loads") as mock_json_loader: + mock_json_loader.return_value = test_payload + assert self.test_serializer.deserialize( + test_payload, test_obj_type) == expected_obj, ( + "Default Serializer deserialized model object incorrectly") + + def test_model_obj_with_additional_params_in_payload_deserialization(self): + test_payload = { + "var4Int": 123, + "add_param_1": "Test" + } + test_obj_type = data.ModelTestObject2 + expected_obj = data.ModelTestObject2(int_var=123) + expected_obj.add_param_1 = "Test" + + with patch("json.loads") as mock_json_loader: + mock_json_loader.return_value = test_payload + assert self.test_serializer.deserialize( + test_payload, test_obj_type) == expected_obj, ( + "Default Serializer deserialized model object incorrectly when payload has additional parameters") + + def test_invalid_model_obj_deserialization(self): + test_payload = { + "var_1": "some value" + } + test_obj_type = data.InvalidModelObject + + with patch("json.loads") as mock_json_loader: + mock_json_loader.return_value = test_payload + assert self.test_serializer.deserialize( + test_payload, test_obj_type) == test_payload, ( + "Default Serializer didn't provide payload back when an invalid model object type " + "(without attribute map and swagger type dict) is passed") + + def test_invalid_model_obj_type_deserialization(self): + test_payload = { + "var_1": "some value" + } + test_obj_type = "InvalidModelObject" + + with patch("json.loads") as mock_json_loader: + mock_json_loader.return_value = test_payload + with self.assertRaises(SerializationException) as exc: + self.test_serializer.deserialize(test_payload, test_obj_type) + + assert "Unable to resolve class {} from installed modules".format(test_obj_type) in str(exc.exception), ( + "Default Serializer didn't throw SerializationException when deserialization is called with invalid " + "object type") + + def test_invalid_json_deserialization(self): + test_payload = { + "var_1": "some value" + } + test_obj_type = "str" + + with patch("json.loads") as mock_json_loader: + mock_json_loader.side_effect = Exception + with self.assertRaises(SerializationException) as exc: + self.test_serializer.deserialize(test_payload, test_obj_type) + + assert "Couldn't parse response body" in str(exc.exception), \ + "Default Serializer didn't throw SerializationException when invalid json is deserialized" + + def test_parent_model_obj_with_discriminator_deserialization(self): + test_payload = { + "ChildType": 'ChildType1', + "var1": "Some string", + "var3Object": { + "var4Int": 123 + }, + "testVar": "test string" + } + test_obj_type = data.ModelAbstractParentObject + expected_sub_obj = data.ModelTestObject2(int_var=123) + expected_obj = data.ModelChildObject1( + str_var="Some string", obj_var=expected_sub_obj, test_var="test string") + + with patch("json.loads") as mock_json_loader: + mock_json_loader.return_value = test_payload + assert self.test_serializer.deserialize( + test_payload, test_obj_type) == expected_obj, ( + "Default Serializer deserialized model object incorrectly when object type is parent class " + "with discriminator") + + def test_child_discriminator_model_obj_deserialization(self): + test_payload = { + "ChildType": 'ChildType2', + "var1": "Some string", + "var3Object": { + "var4Int": 123 + }, + "testIntVar": 456 + } + test_obj_type = data.ModelChildObject2 + expected_sub_obj = data.ModelTestObject2(int_var=123) + expected_obj = data.ModelChildObject2( + str_var="Some string", obj_var=expected_sub_obj, test_int_var=456) + + with patch("json.loads") as mock_json_loader: + mock_json_loader.return_value = test_payload + assert self.test_serializer.deserialize( + test_payload, test_obj_type) == expected_obj, ( + "Default Serializer deserialized model object incorrectly when object type is parent class " + "with discriminator") + + def test_parent_model_obj_with_invalid_discriminator_deserialization(self): + test_payload = { + "ChildType": 'InvalidType', + "var1": "Some string", + "var3Object": { + "var4Int": 123 + }, + "testVar": "test string" + } + test_obj_type = data.ModelAbstractParentObject + + with patch("json.loads") as mock_json_loader: + mock_json_loader.return_value = test_payload + with self.assertRaises(SerializationException) as exc: + self.test_serializer.deserialize(test_payload, test_obj_type) + + assert "Couldn't resolve object by discriminator type" in str(exc.exception), ( + "Default Serializer didn't throw SerializationException when deserialization is called with invalid " + "discriminator type in payload and parent model") diff --git a/ask-sdk-core/tests/unit/test_skill.py b/ask-sdk-core/tests/unit/test_skill.py new file mode 100644 index 0000000..4061abc --- /dev/null +++ b/ask-sdk-core/tests/unit/test_skill.py @@ -0,0 +1,179 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 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. +# +import unittest +from six import PY3 + +from ask_sdk_model import ( + RequestEnvelope, Context, Application, Response, Session) +from ask_sdk_model.interfaces.system import SystemState + +from ask_sdk_core.skill import SkillConfiguration, Skill +from ask_sdk_core.dispatch_components import ( + HandlerAdapter, RequestMapper, RequestHandlerChain) +from ask_sdk_core.exceptions import AskSdkException + +if PY3: + from unittest import mock +else: + import mock + + +class TestSkillConfiguration(unittest.TestCase): + def test_no_mappers_adapters_init(self): + test_skill_config = SkillConfiguration( + request_mappers=None, handler_adapters=None) + assert test_skill_config.request_mappers == [], ( + "Empty request mappers list not set during skill configuration " + "initialization, when nothing is provided") + assert test_skill_config.handler_adapters == [], ( + "Empty handler adapters list not set during skill configuration " + "initialization, when nothing is provided") + assert test_skill_config.request_interceptors == [], ( + "Empty request interceptors list not set during skill " + "configuration initialization, when nothing is " + "provided") + assert test_skill_config.response_interceptors == [], ( + "Empty response interceptors list not set during skill " + "configuration initialization, when nothing is " + "provided") + + +class TestSkill(unittest.TestCase): + def setUp(self): + self.mock_request_mapper = mock.MagicMock(spec=RequestMapper) + self.mock_handler_adapter = mock.MagicMock(spec=HandlerAdapter) + self.mock_request_handler_chain = mock.MagicMock( + spec=RequestHandlerChain) + self.mock_request_mapper.get_request_handler_chain.return_value = \ + self.mock_request_handler_chain + + def create_skill_config(self): + return SkillConfiguration( + request_mappers=[self.mock_request_mapper], + handler_adapters=[self.mock_handler_adapter]) + + def test_skill_invoke_throw_exception_when_skill_id_doesnt_match(self): + skill_config = self.create_skill_config() + skill_config.skill_id = "123" + mock_request_envelope = RequestEnvelope( + context=Context(system=SystemState( + application=Application(application_id="test")))) + skill = Skill(skill_configuration=skill_config) + + with self.assertRaises(AskSdkException) as exc: + skill.invoke(request_envelope=mock_request_envelope, context=None) + + assert "Skill ID Verification failed" in str(exc.exception), ( + "Skill invocation didn't throw verification error when Skill ID " + "doesn't match Application ID") + + def test_skill_invoke_non_empty_response_in_response_envelope(self): + mock_request_envelope = RequestEnvelope() + mock_response = Response() + + self.mock_handler_adapter.supports.return_value = True + self.mock_handler_adapter.execute.return_value = mock_response + + skill_config = self.create_skill_config() + skill = Skill(skill_configuration=skill_config) + + response_envelope = skill.invoke( + request_envelope=mock_request_envelope, context=None) + + assert response_envelope.response == mock_response, ( + "Skill invocation returned incorrect response from " + "request dispatch") + + def test_skill_invoke_null_response_in_response_envelope(self): + mock_request_envelope = RequestEnvelope() + + self.mock_handler_adapter.supports.return_value = True + self.mock_handler_adapter.execute.return_value = None + + skill_config = self.create_skill_config() + skill = Skill(skill_configuration=skill_config) + + response_envelope = skill.invoke( + request_envelope=mock_request_envelope, context=None) + + assert response_envelope.response is None, ( + "Skill invocation returned incorrect response from " + "request dispatch") + + def test_skill_invoke_set_service_client_factory_if_api_client_provided(self): + mock_request_envelope = RequestEnvelope( + context=Context( + system=SystemState( + application=Application(application_id="test"), + api_access_token="test_api_access_token", + api_endpoint="test_api_endpoint"))) + + self.mock_handler_adapter.supports.return_value = True + self.mock_handler_adapter.execute.return_value = None + + skill_config = self.create_skill_config() + skill_config.skill_id = "test" + skill_config.api_client = "test_api_client" + skill = Skill(skill_configuration=skill_config) + + skill.invoke(request_envelope=mock_request_envelope, context=None) + + called_args, called_kwargs = self.mock_request_mapper.get_request_handler_chain.call_args + test_handler_input = called_args[0] + + assert test_handler_input.service_client_factory is not None, ( + "Service Client Factory not initialized when api client is " + "provided in skill configuration, " + "during skill invocation") + assert test_handler_input.service_client_factory.api_configuration.api_client == "test_api_client", ( + "Api Client value in Service Client Factory different than the " + "one provided in skill configuration") + assert test_handler_input.service_client_factory.api_configuration.authorization_value == \ + "test_api_access_token", ("Api Access Token value in Service " + "Client Factory different than the one " + "present " + "in request envelope") + assert test_handler_input.service_client_factory.api_configuration.api_endpoint == \ + "test_api_endpoint", ("Api Endpoint value in Service Client " + "Factory different than the one present " + "in request envelope") + + def test_skill_invoke_pass_session_attributes_to_response_envelope(self): + mock_request_envelope = RequestEnvelope( + context=Context(system=SystemState( + application=Application(application_id="test"))), + session=Session(attributes={"foo":"bar"})) + + self.mock_handler_adapter.supports.return_value = True + self.mock_handler_adapter.execute.return_value = None + + skill_config = self.create_skill_config() + skill_config.skill_id = "test" + skill = Skill(skill_configuration=skill_config) + + response_envelope = skill.invoke( + request_envelope=mock_request_envelope, context=None) + + assert response_envelope.session_attributes is not None, ( + "Session Attributes are not propagated from Request Envelope " + "session to Response Envelope, " + "during skill invocation") + assert response_envelope.session_attributes["foo"] == "bar", ( + "Invalid Session Attributes propagated from Request Envelope " + "session to Response Envelope, " + "during skill invocation") diff --git a/ask-sdk-core/tests/unit/test_skill_builder.py b/ask-sdk-core/tests/unit/test_skill_builder.py new file mode 100644 index 0000000..d698351 --- /dev/null +++ b/ask-sdk-core/tests/unit/test_skill_builder.py @@ -0,0 +1,626 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 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. +# +import unittest +import inspect +from six import PY3 + +from ask_sdk_model import Response + +from ask_sdk_core.skill import SkillConfiguration, Skill +from ask_sdk_core.skill_builder import SkillBuilder, CustomSkillBuilder +from ask_sdk_core.dispatch_components import ( + HandlerAdapter, RequestMapper, RequestHandlerChain, + AbstractRequestHandler, AbstractExceptionHandler, + AbstractRequestInterceptor, AbstractResponseInterceptor, ExceptionMapper) +from ask_sdk_core.exceptions import SkillBuilderException +from ask_sdk_core.utils import RESPONSE_FORMAT_VERSION, user_agent_info + +if PY3: + from unittest import mock +else: + import mock + + +class TestSkillBuilder(unittest.TestCase): + def setUp(self): + self.sb = SkillBuilder() + + def test_add_null_request_handler_throw_error(self): + with self.assertRaises(SkillBuilderException) as exc: + self.sb.add_request_handler(request_handler=None) + + assert "Valid Request Handler instance to be provided" in str( + exc.exception), ( + "Add Request Handler method didn't throw exception when a null " + "request handler is added") + + def test_add_invalid_request_handler_throw_error(self): + invalid_request_handler = mock.Mock() + + with self.assertRaises(SkillBuilderException) as exc: + self.sb.add_request_handler( + request_handler=invalid_request_handler) + + assert "Input should be a RequestHandler instance" in str( + exc.exception), ( + "Add Request Handler method didn't throw exception when an " + "invalid request handler is added") + + def test_add_valid_request_handler(self): + mock_request_handler = mock.MagicMock(spec=AbstractRequestHandler) + + self.sb.add_request_handler(request_handler=mock_request_handler) + + assert self.sb.request_handlers[0] == mock_request_handler, ( + "Add Request Handler method didn't add valid request handler to " + "Skill Builder Request Handlers list") + + def test_add_null_exception_handler_throw_error(self): + with self.assertRaises(SkillBuilderException) as exc: + self.sb.add_exception_handler(exception_handler=None) + + assert "Valid Exception Handler instance to be provided" in str( + exc.exception), ( + "Add Exception Handler method didn't throw exception when a null " + "exception handler is added") + + def test_add_invalid_exception_handler_throw_error(self): + invalid_exception_handler = mock.Mock() + + with self.assertRaises(SkillBuilderException) as exc: + self.sb.add_exception_handler( + exception_handler=invalid_exception_handler) + + assert "Input should be an ExceptionHandler instance" in str( + exc.exception), ( + "Add Exception Handler method didn't throw exception when an " + "invalid exception handler is added") + + def test_add_valid_exception_handler(self): + mock_exception_handler = mock.MagicMock(spec=AbstractExceptionHandler) + + self.sb.add_exception_handler(exception_handler=mock_exception_handler) + + assert self.sb.exception_handlers[0] == mock_exception_handler, ( + "Add Exception Handler method didn't add valid exception handler " + "to Skill Builder Exception Handlers list") + + def test_add_null_global_request_interceptor_throw_error(self): + with self.assertRaises(SkillBuilderException) as exc: + self.sb.add_global_request_interceptor(request_interceptor=None) + + assert "Valid Request Interceptor instance to be provided" in str( + exc.exception), ( + "Add Global Request Interceptor method didn't throw exception " + "when a null request interceptor is added") + + def test_add_invalid_global_request_interceptor_throw_error(self): + invalid_request_interceptor = mock.Mock() + + with self.assertRaises(SkillBuilderException) as exc: + self.sb.add_global_request_interceptor( + request_interceptor=invalid_request_interceptor) + + assert "Input should be a RequestInterceptor instance" in str( + exc.exception), ( + "Add Global Request Interceptor method didn't throw exception " + "when an invalid request interceptor is added") + + def test_add_valid_global_request_interceptor(self): + mock_request_interceptor = mock.MagicMock( + spec=AbstractRequestInterceptor) + + self.sb.add_global_request_interceptor( + request_interceptor=mock_request_interceptor) + + assert self.sb.global_request_interceptors[0] == \ + mock_request_interceptor, ( + "Add Global Request Interceptor method didn't add valid request " + "interceptor to Skill Builder " + "Request Interceptors list") + + def test_add_null_global_response_interceptor_throw_error(self): + with self.assertRaises(SkillBuilderException) as exc: + self.sb.add_global_response_interceptor(response_interceptor=None) + + assert "Valid Response Interceptor instance to be provided" in str( + exc.exception), ( + "Add Global Response Interceptor method didn't throw exception " + "when a null response interceptor is added") + + def test_add_invalid_global_response_interceptor_throw_error(self): + invalid_response_interceptor = mock.Mock() + + with self.assertRaises(SkillBuilderException) as exc: + self.sb.add_global_response_interceptor( + response_interceptor=invalid_response_interceptor) + + assert "Input should be a ResponseInterceptor instance" in str( + exc.exception), ( + "Add Global Response Interceptor method didn't throw exception " + "when an invalid response interceptor " + "is added") + + def test_add_valid_global_response_interceptor(self): + mock_response_interceptor = mock.MagicMock( + spec=AbstractResponseInterceptor) + + self.sb.add_global_response_interceptor( + response_interceptor=mock_response_interceptor) + + assert self.sb.global_response_interceptors[0] == \ + mock_response_interceptor, ( + "Add Global Response Interceptor method didn't add valid response " + "interceptor to Skill Builder " + "Response Interceptors list") + + def test_skill_configuration_getter_no_registered_components(self): + actual_config = self.sb.skill_configuration + + assert actual_config.request_mappers is not None, ( + "Skill Configuration getter in Skill Builder didn't set request " + "mappers correctly") + assert actual_config.request_mappers[0].request_handler_chains is not None, ( + "Skill Configuration getter in Skill Builder didn't set handler " + "chains in request mappers correctly") + assert len(actual_config.request_mappers[0].request_handler_chains) == 0, ( + "Skill Configuration getter in Skill Builder added invalid " + "handler in handler chain, " + "when no request handlers are registered") + assert actual_config.handler_adapters is not None, ( + "Skill Configuration getter in Skill Builder didn't set handler " + "adapters correctly") + assert isinstance(actual_config.handler_adapters[0], HandlerAdapter), ( + "Skill Configuration getter in Skill Builder didn't set default " + "handler adapter") + assert actual_config.exception_mapper is None, ( + "Skill Configuration getter in Skill Builder created invalid " + "exception mapper, " + "when no exception handlers are registered") + assert actual_config.request_interceptors == [], ( + "Skill Configuration getter in Skill Builder created invalid " + "request interceptors, " + "when no global request interceptors are registered") + assert actual_config.response_interceptors == [], ( + "Skill Configuration getter in Skill Builder created invalid " + "response interceptors, " + "when no global response interceptors are registered") + assert actual_config.custom_user_agent is None, ( + "Skill Configuration getter in Skill Builder set invalid custom " + "user agent") + assert actual_config.skill_id is None, ( + "Skill Configuration getter in Skill Builder set invalid skill id") + + def test_skill_configuration_getter_handlers_registered(self): + mock_request_handler = mock.MagicMock(spec=AbstractRequestHandler) + self.sb.add_request_handler(request_handler=mock_request_handler) + + mock_exception_handler = mock.MagicMock(spec=AbstractExceptionHandler) + self.sb.add_exception_handler(exception_handler=mock_exception_handler) + + actual_config = self.sb.skill_configuration + + assert actual_config.request_mappers is not None, ( + "Skill Configuration getter in Skill Builder didn't set request " + "mappers correctly") + assert actual_config.request_mappers[0].request_handler_chains is not None, ( + "Skill Configuration getter in Skill Builder didn't set handler " + "chains in request mappers correctly") + assert len(actual_config.request_mappers[0].request_handler_chains) == 1, ( + "Skill Configuration getter in Skill Builder didn't add valid " + "handler in handler chain, " + "when request handlers are registered") + assert actual_config.request_mappers[0].request_handler_chains[0].request_handler == mock_request_handler, ( + "Skill Configuration getter in Skill Builder added invalid " + "handler in handler chain, " + "when request handlers are registered") + + assert actual_config.exception_mapper is not None, ( + "Skill Configuration getter in Skill Builder didn't create " + "exception mapper, " + "when exception handlers are registered") + assert len(actual_config.exception_mapper.exception_handlers) == 1, ( + "Skill Configuration getter in Skill Builder added additional " + "exception handlers than the registered ones " + "in exception mapper") + assert actual_config.exception_mapper.exception_handlers[0] == mock_exception_handler, ( + "Skill Configuration getter in Skill Builder added invalid " + "handler in exception mapper, " + "when exception handlers are registered") + + def test_create_skill(self): + mock_request_handler = mock.MagicMock(spec=AbstractRequestHandler) + self.sb.add_request_handler(request_handler=mock_request_handler) + + mock_exception_handler = mock.MagicMock(spec=AbstractExceptionHandler) + self.sb.add_exception_handler(exception_handler=mock_exception_handler) + + actual_skill = self.sb.create() + expected_skill = Skill(self.sb.skill_configuration) + + assert actual_skill.request_dispatcher.request_mappers[0].request_handler_chains[0].request_handler == \ + expected_skill.request_dispatcher.request_mappers[0].request_handler_chains[0].request_handler, ( + "Skill Builder created skill with incorrect request handlers when " + "using create method") + + assert actual_skill.request_dispatcher.exception_mapper.exception_handlers[0] == \ + expected_skill.request_dispatcher.exception_mapper.exception_handlers[0], ( + "Skill Builder created skill with incorrect exception handlers " + "when using create method") + + def test_lambda_handler_creation(self): + handler_func = self.sb.lambda_handler() + assert callable(handler_func), "Skill Builder Lambda Handler " \ + "function returned an invalid object" + + actual_arg_spec = inspect.getargspec(handler_func) + assert len(actual_arg_spec.args) == 2, ( + "Skill Builder Lambda Handler function created a handler of " + "different signature than AWS Lambda") + assert "event" in actual_arg_spec.args, ( + "Skill Builder Lambda Handler function created a handler without " + "named parameter event") + assert "context" in actual_arg_spec.args, ( + "Skill Builder Lambda Handler function created a handler without " + "named parameter context") + + def test_lambda_handler_invocation(self): + mock_request_handler = mock.MagicMock(spec=AbstractRequestHandler) + mock_request_handler.can_handle.return_value = True + mock_response = Response() + mock_response.output_speech = "test output speech" + mock_request_handler.handle.return_value = mock_response + self.sb.add_request_handler(request_handler=mock_request_handler) + + mock_request_envelope_payload = { + "context": { + "System": { + "application": { + "applicationId": "test" + } + } + } + } + + self.sb.skill_id = "test" + lambda_handler = self.sb.lambda_handler() + + response_envelope = lambda_handler( + event=mock_request_envelope_payload, context=None) + + assert response_envelope["version"] == RESPONSE_FORMAT_VERSION, ( + "Response Envelope from lambda handler invocation has version " + "different than expected") + assert response_envelope["userAgent"] == user_agent_info( + custom_user_agent=None), ( + "Response Envelope from lambda handler invocation has user agent " + "info different than expected") + assert response_envelope["response"]["outputSpeech"] == "test output speech", ( + "Response Envelope from lambda handler invocation has incorrect " + "response than built by skill") + + def test_request_handler_decorator_creation(self): + request_handler_wrapper = self.sb.request_handler(can_handle_func=None) + assert callable(request_handler_wrapper), ( + "Skill Builder Request Handler decorator returned an invalid " + "wrapper object") + + actual_arg_spec = inspect.getargspec(request_handler_wrapper) + assert len(actual_arg_spec.args) == 1, ( + "Skill Builder Request Handler decorator created a wrapper of " + "different signature than expected") + assert "handle_func" in actual_arg_spec.args, ( + "Skill Builder Request Handler decorator created a wrapper " + "without named parameter handler_func") + + def test_request_handler_decorator_invalid_can_handle_func(self): + request_handler_wrapper = self.sb.request_handler( + can_handle_func=None) + + with self.assertRaises(SkillBuilderException) as exc: + request_handler_wrapper(handle_func=None) + + assert "can_handle_func and handle_func input parameters should be callable" in str(exc.exception), ( + "Request Handler Decorator accepted invalid can_handle_func " + "parameter") + + def test_request_handler_decorator_invalid_handle_func(self): + request_handler_wrapper = self.sb.request_handler( + can_handle_func=lambda x: True) + + with self.assertRaises(SkillBuilderException) as exc: + request_handler_wrapper(handle_func=None) + + assert "can_handle_func and handle_func input parameters should be callable" in str(exc.exception), ( + "Request Handler Decorator was decorated on an invalid object") + + def test_request_handler_decorator_on_valid_handle_func(self): + def test_can_handle(input): + return True + + def test_handle(input): + return "something" + + self.sb.request_handler(can_handle_func=test_can_handle)( + handle_func=test_handle) + + actual_request_handler = self.sb.request_handlers[0] + + assert actual_request_handler.__class__.__name__ == "RequestHandlerTestHandle", ( + "Request Handler decorator created Request Handler of incorrect " + "name") + assert actual_request_handler.can_handle(None) is True, ( + "Request Handler decorator created Request Handler with incorrect " + "can_handle function") + assert actual_request_handler.handle(None) == "something", ( + "Request Handler decorator created Request Handler with incorrect " + "handle function") + + def test_request_handler_decorator_on_can_handle_func_with_incorrect_params(self): + def test_can_handle(): + return True + + def test_handle(input): + return "something" + + with self.assertRaises(SkillBuilderException) as exc: + self.sb.request_handler(can_handle_func=test_can_handle)( + handle_func=test_handle) + + assert "can_handle_func should only accept a single input arg, handler input" in str(exc.exception), ( + "Request Handler Decorator accepted invalid can_handle_func " + "parameter") + + def test_request_handler_decorator_on_handle_func_with_incorrect_params(self): + def test_can_handle(input): + return True + + def test_handle(*args, **kwargs): + return "something" + + with self.assertRaises(SkillBuilderException) as exc: + self.sb.request_handler(can_handle_func=test_can_handle)( + handle_func=test_handle) + + assert "handle_func should only accept a single input arg, handler input" in str(exc.exception), ( + "Request Handler Decorator was decorated with invalid " + "handle_func which takes more than one input args") + + def test_exception_handler_decorator_creation(self): + exception_handler_wrapper = self.sb.exception_handler( + can_handle_func=None) + assert callable(exception_handler_wrapper), ( + "Skill Builder Exception Handler decorator returned an invalid " + "wrapper object") + + actual_arg_spec = inspect.getargspec(exception_handler_wrapper) + assert len(actual_arg_spec.args) == 1, ( + "Skill Builder Exception Handler decorator created a wrapper of " + "different signature than expected") + assert "handle_func" in actual_arg_spec.args, ( + "Skill Builder Exception Handler decorator created a wrapper " + "without named parameter handler_func") + + def test_exception_handler_decorator_invalid_can_handle_func(self): + exception_handler_wrapper = self.sb.exception_handler( + can_handle_func=None) + + with self.assertRaises(SkillBuilderException) as exc: + exception_handler_wrapper(handle_func=None) + + assert "can_handle_func and handle_func input parameters should be callable" in str(exc.exception), ( + "Exception Handler Decorator accepted invalid can_handle_func " + "parameter") + + def test_exception_handler_decorator_invalid_handle_func(self): + exception_handler_wrapper = self.sb.exception_handler( + can_handle_func=lambda x: True) + + with self.assertRaises(SkillBuilderException) as exc: + exception_handler_wrapper(handle_func=None) + + assert "can_handle_func and handle_func input parameters should be callable" in str(exc.exception), ( + "Exception Handler Decorator was decorated on an invalid object") + + def test_exception_handler_decorator_on_valid_handle_func(self): + def test_can_handle(input, exc): + return True + + def test_handle(input, exc): + return "something" + + self.sb.exception_handler(can_handle_func=test_can_handle)( + handle_func=test_handle) + + actual_exception_handler = self.sb.exception_handlers[0] + + assert actual_exception_handler.__class__.__name__ == "ExceptionHandlerTestHandle", ( + "Exception Handler decorator created Exception Handler of incorrect name") + assert actual_exception_handler.can_handle(None, None) is True, ( + "Exception Handler decorator created Exception Handler with " + "incorrect can_handle function") + assert actual_exception_handler.handle(None, None) == "something", ( + "Exception Handler decorator created Exception Handler with " + "incorrect handle function") + + def test_exception_handler_decorator_on_can_handle_func_with_incorrect_params(self): + def test_can_handle(*args): + return True + + def test_handle(input, exc): + return "something" + + with self.assertRaises(SkillBuilderException) as exc: + self.sb.exception_handler(can_handle_func=test_can_handle)( + handle_func=test_handle) + + assert "can_handle_func should only accept two input args, handler input and exception" in str(exc.exception), ( + "Exception Handler Decorator accepted invalid can_handle_func " + "parameter") + + def test_exception_handler_decorator_on_handle_func_with_incorrect_params(self): + def test_can_handle(input, exc): + return True + + def test_handle(exc, **kwargs): + return "something" + + with self.assertRaises(SkillBuilderException) as exc: + self.sb.exception_handler(can_handle_func=test_can_handle)( + handle_func=test_handle) + + assert "handle_func should only accept two input args, handler input and exception" in str(exc.exception), ( + "Exception Handler Decorator was decorated with invalid " + "handle_func which takes more than two input args") + + def test_global_request_interceptor_decorator_creation(self): + request_interceptor_wrapper = self.sb.global_request_interceptor() + assert callable(request_interceptor_wrapper), ( + "Skill Builder Global Request Interceptor decorator returned an " + "invalid wrapper object") + + actual_arg_spec = inspect.getargspec(request_interceptor_wrapper) + assert len(actual_arg_spec.args) == 1, ( + "Skill Builder Global Request Interceptor decorator created a " + "wrapper of different signature than expected") + assert "process_func" in actual_arg_spec.args, ( + "Skill Builder Global Request Interceptor decorator created a " + "wrapper without named parameter process_func") + + def test_global_request_interceptor_decorator_invalid_process_func(self): + request_interceptor_wrapper = self.sb.global_request_interceptor() + + with self.assertRaises(SkillBuilderException) as exc: + request_interceptor_wrapper(process_func=None) + + assert "process_func input parameter should be callable" in str( + exc.exception), ( + "Global Request Interceptor Decorator accepted invalid process_func parameter") + + def test_global_request_interceptor_decorator_on_valid_process_func(self): + def test_process(input): + return "something" + + self.sb.global_request_interceptor()(process_func=test_process) + + actual_global_request_interceptor = self.sb.global_request_interceptors[0] + + assert actual_global_request_interceptor.__class__.__name__ == "RequestInterceptorTestProcess" + assert actual_global_request_interceptor.process(None) == "something" + + def test_global_request_interceptor_on_process_func_with_incorrect_params(self): + def test_process(**kwargs): + return "something" + + with self.assertRaises(SkillBuilderException) as exc: + self.sb.global_request_interceptor()(process_func=test_process) + + assert "process_func should only accept a single input arg, handler input" in str(exc.exception), ( + "Global Request Interceptor Decorator was decorated with invalid " + "process_func which takes more than " + "one input args") + + def test_global_response_interceptor_decorator_creation(self): + response_interceptor_wrapper = self.sb.global_response_interceptor() + assert callable(response_interceptor_wrapper), ( + "Skill Builder Global Request Interceptor decorator returned an " + "invalid wrapper object") + + actual_arg_spec = inspect.getargspec(response_interceptor_wrapper) + assert len(actual_arg_spec.args) == 1, ( + "Skill Builder Global Response Interceptor decorator created a " + "wrapper of different signature than " + "expected") + assert "process_func" in actual_arg_spec.args, ( + "Skill Builder Global Response Interceptor decorator created a " + "wrapper without named parameter " + "process_func") + + def test_global_response_interceptor_decorator_invalid_process_func(self): + response_interceptor_wrapper = self.sb.global_response_interceptor() + + with self.assertRaises(SkillBuilderException) as exc: + response_interceptor_wrapper(process_func=None) + + assert "process_func input parameter should be callable" in str( + exc.exception), ( + "Global Response Interceptor Decorator accepted invalid " + "process_func parameter") + + def test_global_response_interceptor_decorator_on_valid_process_func(self): + def test_process(input, response): + return "something" + + self.sb.global_response_interceptor()(process_func=test_process) + + actual_global_response_interceptor = self.sb.global_response_interceptors[0] + + assert actual_global_response_interceptor.__class__.__name__ == "ResponseInterceptorTestProcess" + assert actual_global_response_interceptor.process(None, None) == "something" + + def test_global_response_interceptor_on_process_func_with_incorrect_params(self): + def test_process(**kwargs): + return "something" + + with self.assertRaises(SkillBuilderException) as exc: + self.sb.global_response_interceptor()(process_func=test_process) + + assert "process_func should only accept two input args, handler input and response" in str(exc.exception), ( + "Global Response Interceptor Decorator was decorated with invalid " + "process_func which takes more than " + "one input args") + + +class TestCustomSkillBuilder(unittest.TestCase): + def test_custom_persistence_adapter_default_null(self): + test_custom_skill_builder = CustomSkillBuilder() + + actual_skill_config = test_custom_skill_builder.skill_configuration + + assert actual_skill_config.persistence_adapter is None, ( + "Custom Skill Builder didn't set the default persistence adapter " + "as None") + + def test_custom_persistence_adapter_used(self): + mock_adapter = mock.Mock() + test_custom_skill_builder = CustomSkillBuilder( + persistence_adapter=mock_adapter) + + actual_skill_config = test_custom_skill_builder.skill_configuration + + assert actual_skill_config.persistence_adapter == mock_adapter, ( + "Custom Skill Builder didn't set the persistence adapter provided") + + def test_custom_api_client_default_null(self): + test_custom_skill_builder = CustomSkillBuilder() + + actual_skill_config = test_custom_skill_builder.skill_configuration + + assert actual_skill_config.api_client is None, ( + "Custom Skill Builder didn't set the default api client " + "as None") + + def test_custom_api_client_used(self): + mock_api_client = mock.Mock() + test_custom_skill_builder = CustomSkillBuilder( + api_client=mock_api_client) + + actual_skill_config = test_custom_skill_builder.skill_configuration + + assert actual_skill_config.api_client == mock_api_client, ( + "Custom Skill Builder didn't set the api client provided") diff --git a/ask-sdk-core/tests/unit/test_utils.py b/ask-sdk-core/tests/unit/test_utils.py new file mode 100644 index 0000000..68154cd --- /dev/null +++ b/ask-sdk-core/tests/unit/test_utils.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 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. +# +import sys + +from ask_sdk_model import ( + IntentRequest, RequestEnvelope, Intent, SessionEndedRequest) +from ask_sdk_core.utils import ( + user_agent_info, is_intent_name, is_request_type) +from ask_sdk_core.handler_input import HandlerInput +from ask_sdk_core.__version__ import __version__ + + +def test_user_agent_info_with_no_custom_user_agent(): + py_major_version = str(sys.version_info.major) + py_minor_version = str(sys.version_info.minor) + py_micro_version = str(sys.version_info.micro) + + expected_user_agent = "ask-python/{} Python/{}.{}.{}".format( + __version__, py_major_version, py_minor_version, py_micro_version) + assert user_agent_info(custom_user_agent=None) == expected_user_agent, ( + "Incorrect User Agent info for Null custom user agent") + + +def test_user_agent_info_with_custom_user_agent(): + py_major_version = str(sys.version_info.major) + py_minor_version = str(sys.version_info.minor) + py_micro_version = str(sys.version_info.micro) + custom_user_agent = "test" + + expected_user_agent = "ask-python/{} Python/{}.{}.{} {}".format( + __version__, py_major_version, py_minor_version, + py_micro_version, custom_user_agent) + assert user_agent_info(custom_user_agent=custom_user_agent) == expected_user_agent, ( + "Incorrect User Agent info for custom user agent") + + +def test_is_intent_name_match(): + test_intent_name = "TestIntent" + test_handler_input = HandlerInput( + request_envelope=RequestEnvelope(request=IntentRequest( + intent=Intent(name=test_intent_name)))) + + intent_name_wrapper = is_intent_name(test_intent_name) + assert intent_name_wrapper( + test_handler_input), "is_intent_name matcher didn't match with the " \ + "correct intent name" + + +def test_is_intent_name_not_match(): + test_intent_name = "TestIntent" + test_handler_input = HandlerInput( + request_envelope=RequestEnvelope(request=IntentRequest( + intent=Intent(name=test_intent_name)))) + + intent_name_wrapper = is_intent_name("TestIntent1") + assert not intent_name_wrapper( + test_handler_input), "is_intent_name matcher matched with the " \ + "incorrect intent name" + + +def test_is_request_type_match(): + test_handler_input = HandlerInput( + request_envelope=RequestEnvelope(request=IntentRequest())) + + request_type_wrapper = is_request_type("IntentRequest") + assert request_type_wrapper(test_handler_input), ( + "is_request_type matcher didn't match with the correct request type") + + +def test_is_request_type_not_match(): + test_handler_input = HandlerInput( + request_envelope=RequestEnvelope(request=SessionEndedRequest())) + + intent_name_wrapper = is_request_type("IntentRequest") + assert not intent_name_wrapper(test_handler_input), ( + "is_request_type matcher matched with the incorrect request type") + diff --git a/ask-sdk-core/tox.ini b/ask-sdk-core/tox.ini new file mode 100644 index 0000000..d9ce5f0 --- /dev/null +++ b/ask-sdk-core/tox.ini @@ -0,0 +1,11 @@ +[tox] +envlist = py27,py36 + +[testenv] +deps = + -rrequirements.txt + nose + mock + coverage +commands = + nosetests --cover-package=ask_sdk_core --with-coverage --cover-erase \ No newline at end of file diff --git a/ask-sdk-dynamodb-persistence-adapter/.coveragerc b/ask-sdk-dynamodb-persistence-adapter/.coveragerc new file mode 100644 index 0000000..d47a112 --- /dev/null +++ b/ask-sdk-dynamodb-persistence-adapter/.coveragerc @@ -0,0 +1,9 @@ +[run] +branch = True + +[report] +include = + ask_sdk_dynamodb/* +exclude_lines = + if typing.TYPE_CHECKING: + pass \ No newline at end of file diff --git a/ask-sdk-dynamodb-persistence-adapter/.gitignore b/ask-sdk-dynamodb-persistence-adapter/.gitignore new file mode 100644 index 0000000..e057098 --- /dev/null +++ b/ask-sdk-dynamodb-persistence-adapter/.gitignore @@ -0,0 +1,67 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ +venv/ +.python-version + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +#Ipython Notebook +.ipynb_checkpoints + +# IntelliJ configs +*.iml diff --git a/ask-sdk-dynamodb-persistence-adapter/CHANGELOG.rst b/ask-sdk-dynamodb-persistence-adapter/CHANGELOG.rst new file mode 100644 index 0000000..e2ed0d0 --- /dev/null +++ b/ask-sdk-dynamodb-persistence-adapter/CHANGELOG.rst @@ -0,0 +1,8 @@ +========= +CHANGELOG +========= + +0.1 +------- + +* Initial release of ASK SDK DynamoDB Persistence Adapter package. diff --git a/ask-sdk-dynamodb-persistence-adapter/MANIFEST.in b/ask-sdk-dynamodb-persistence-adapter/MANIFEST.in new file mode 100644 index 0000000..314a533 --- /dev/null +++ b/ask-sdk-dynamodb-persistence-adapter/MANIFEST.in @@ -0,0 +1,5 @@ +include README.rst +include CHANGELOG.rst +include LICENSE +include requirements.txt +recursive-exclude tests * \ No newline at end of file diff --git a/ask-sdk-dynamodb-persistence-adapter/README.rst b/ask-sdk-dynamodb-persistence-adapter/README.rst new file mode 100644 index 0000000..b4e1310 --- /dev/null +++ b/ask-sdk-dynamodb-persistence-adapter/README.rst @@ -0,0 +1,50 @@ +======================================================== +ASK SDK DynamoDB Adapter of Python ASK SDK +======================================================== + +ask-sdk-dynamodb-persistence-adapter is the persistence adapter package for Alexa Skills Kit (ASK) by +the Software Development Kit (SDK) team for Python. It has the persistence adapter implementation +for connecting the Skill to the AWS DynamoDB. It also provides partition key generator functions, +to get the user id or device id from skill request, that can be used as partition keys. + + +Quick Start +----------- + +Installation +~~~~~~~~~~~~~~~ +Assuming that you have Python and ``virtualenv`` installed, you can +install the package and it's dependencies (``ask-sdk-model``, ``ask-sdk-core``) from PyPi +as follows: + +.. code-block:: sh + + >>> virtualenv venv + >>> . venv/bin/activate + >>> pip install ask-sdk-dynamodb-persistence-adapter + + +You can also install the whole dynamodb persistence adapter package locally by following these steps: + +.. code-block:: sh + + >>> git clone https://github.com/alexalabs/alexa-skills-kit-for-python-sdk.git + >>> cd alexa-skills-kit-for-python-sdk/ask-sdk-dynamodb-persistence-adapter + >>> virtualenv venv + ... + >>> . venv/bin/activate + >>> python setup.py install + + +Usage and Getting Started +------------------------- +A Getting Started guide can be found `here <../docs/GETTING_STARTED.rst>`_ + + +Got Feedback? +------------- + +- We would like to hear about your bugs, feature requests, questions or quick feedback. + Please search for the `existing issues `_ before opening a new one. It would also be helpful + if you follow the templates for issue and pull request creation. Please follow the `contributing guidelines <../CONTRIBUTING.rst>`_!! +- Request and vote for `Alexa features `_! diff --git a/ask-sdk-dynamodb-persistence-adapter/ask_sdk_dynamodb/__init__.py b/ask-sdk-dynamodb-persistence-adapter/ask_sdk_dynamodb/__init__.py new file mode 100644 index 0000000..5deb7d9 --- /dev/null +++ b/ask-sdk-dynamodb-persistence-adapter/ask_sdk_dynamodb/__init__.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 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. +# \ No newline at end of file diff --git a/ask-sdk-dynamodb-persistence-adapter/ask_sdk_dynamodb/__version__.py b/ask-sdk-dynamodb-persistence-adapter/ask_sdk_dynamodb/__version__.py new file mode 100644 index 0000000..5cfdf12 --- /dev/null +++ b/ask-sdk-dynamodb-persistence-adapter/ask_sdk_dynamodb/__version__.py @@ -0,0 +1,29 @@ +# +# Copyright 2018 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. +# + +__pip_package_name__ = 'ask-sdk-dynamodb-persistence-adapter' +__description__ = ( + 'The ASK SDK DynamoDB Persistence Adapter package provides DynamoDB ' + 'Adapter, that can be used with ASK SDK Core, for persistence management') +__url__ = 'http://developer.amazon.com/ask' +__version__ = '0.1' +__author__ = 'Alexa Skills Kit' +__author_email__ = 'ask-sdk-dynamic@amazon.com' +__license__ = 'Apache 2.0' +__keywords__ = ['ASK SDK', 'Alexa Skills Kit', 'Alexa', 'ASK SDK Core', + 'Persistence', 'DynamoDB'] +__install_requires__ = ["boto3", "ask-sdk-core"] diff --git a/ask-sdk-dynamodb-persistence-adapter/ask_sdk_dynamodb/adapter.py b/ask-sdk-dynamodb-persistence-adapter/ask_sdk_dynamodb/adapter.py new file mode 100644 index 0000000..010f981 --- /dev/null +++ b/ask-sdk-dynamodb-persistence-adapter/ask_sdk_dynamodb/adapter.py @@ -0,0 +1,186 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 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. +# +import boto3 +import typing +from boto3.session import ResourceNotExistsError +from ask_sdk_core.attributes_manager import AbstractPersistenceAdapter +from ask_sdk_core.exceptions import PersistenceException + +from .partition_keygen import user_id_partition_keygen + +if typing.TYPE_CHECKING: + from typing import Callable, Dict + from ask_sdk_model import RequestEnvelope + from boto3.resources.base import ServiceResource + + +class DynamoDbAdapter(AbstractPersistenceAdapter): + """Persistence Adapter implementation using Amazon DynamoDb.""" + + def __init__( + self, table_name, partition_key_name="id", + attribute_name="attributes", create_table=False, + partition_keygen=user_id_partition_keygen, + dynamodb_resource=boto3.resource("dynamodb")): + # type: (str, str, str, bool, Callable[[RequestEnvelope], str], ServiceResource) -> None + """Persistence Adapter implementation using Amazon DynamoDb. + + Amazon DynamoDb based persistence adapter implementation. This + internally uses the AWS Python SDK (`boto3`) to process the + dynamodb operations. The adapter tries to create the table if + `create_table` is set, during initialization. + + :param table_name: Name of the table to be created or used + :type table_name: str + :param partition_key_name: Partition key name to be used. + Defaulted to 'id' + :type partition_key_name: str + :param attribute_name: Attribute name for storing and + retrieving attributes from dynamodb. + Defaulted to 'attributes' + :type attribute_name: str + :param create_table: Should the adapter try to create the table + if it doesn't exist. Defaulted to False + :type create_table: bool + :param partition_keygen: Callable function that takes a + request envelope and provides a unique partition key value. + Defaulted to user id keygen function + :type partition_keygen: Callable[[RequestEnvelope], str] + :param dynamodb_resource: Resource to be used, to perform + dynamo operations. Defaulted to resource generated from + boto3 + :type dynamodb_resource: ServiceResource + """ + self.table_name = table_name + self.partition_key_name = partition_key_name + self.attribute_name = attribute_name + self.create_table = create_table + self.partition_keygen = partition_keygen + self.dynamodb = dynamodb_resource + self.__create_table_if_not_exists() + + def get_attributes(self, request_envelope): + # type: (RequestEnvelope) -> Dict[str, object] + """Get attributes from table in Dynamodb resource. + + Retrieves the attributes from Dynamodb table. If the table + doesn't exist, returns an empty attribute {} if the + create_table variable is set as True, else it raises + PersistenceException. Raises PersistenceException if `get_item` + fails on the table. + + :param request_envelope: Request Envelope passed during skill + invocation + :type request_envelope: RequestEnvelope + :return: Attributes stored under the partition keygen mapping + in the table + :rtype: Dict[str, object] + :raises PersistenceException + """ + try: + table = self.dynamodb.Table(self.table_name) + partition_key_val = self.partition_keygen(request_envelope) + response = table.get_item( + Key={self.partition_key_name: partition_key_val}) + if "Item" in response: + return response["Item"][self.attribute_name] + else: + return {} + except ResourceNotExistsError: + raise PersistenceException( + "DynamoDb table {} doesn't exist or in the process of " + "being created. Failed to get attributes from " + "DynamoDb table.".format(self.table_name)) + except Exception as e: + raise PersistenceException( + "Failed to retrieve attributes from DynamoDb table. " + "Exception of type {} occurred: {}".format( + type(e).__name__, str(e))) + + def save_attributes(self, request_envelope, attributes): + # type: (RequestEnvelope, Dict[str, object]) -> None + """Saves attributes to table in Dynamodb resource. + + Saves the attributes into Dynamodb table. Raises + PersistenceException if table doesn't exist or `put_item` fails + on the table. + + :param request_envelope: Request Envelope passed during skill + invocation + :type request_envelope: RequestEnvelope + :param attributes: Attributes stored under the partition keygen + mapping in the table + :type attributes: Dict[str, object] + :rtype: None + :raises PersistenceException + """ + try: + table = self.dynamodb.Table(self.table_name) + partition_key_val = self.partition_keygen(request_envelope) + table.put_item( + Item={self.partition_key_name: partition_key_val, + self.attribute_name: attributes}) + except ResourceNotExistsError: + raise PersistenceException( + "DynamoDb table {} doesn't exist. Failed to save attributes " + "to DynamoDb table.".format( + self.table_name)) + except Exception as e: + raise PersistenceException( + "Failed to save attributes to DynamoDb table. Exception of " + "type {} occurred: {}".format( + type(e).__name__, str(e))) + + def __create_table_if_not_exists(self): + # type: () -> None + """Creates table in Dynamodb resource if it doesn't exist and + create_table is set as True. + + :rtype: None + :raises PersistenceException: When `create_table` fails on + dynamodb resource. + """ + if self.create_table: + try: + self.dynamodb.create_table( + TableName=self.table_name, + KeySchema=[ + { + 'AttributeName': self.partition_key_name, + 'KeyType': 'HASH' + } + ], + AttributeDefinitions=[ + { + 'AttributeName': self.partition_key_name, + 'AttributeType': 'S' + } + + ], + ProvisionedThroughput={ + 'ReadCapacityUnits': 5, + 'WriteCapacityUnits': 5 + } + ) + except Exception as e: + if e.__class__.__name__ != "ResourceInUseException": + raise PersistenceException( + "Create table if not exists request " + "failed: Exception of type {} " + "occurred: {}".format( + type(e).__name__, str(e))) diff --git a/ask-sdk-dynamodb-persistence-adapter/ask_sdk_dynamodb/partition_keygen.py b/ask-sdk-dynamodb-persistence-adapter/ask_sdk_dynamodb/partition_keygen.py new file mode 100644 index 0000000..1bef576 --- /dev/null +++ b/ask-sdk-dynamodb-persistence-adapter/ask_sdk_dynamodb/partition_keygen.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 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. +# +import typing + +from ask_sdk_core.exceptions import PersistenceException + +if typing.TYPE_CHECKING: + from ask_sdk_model import RequestEnvelope + + +def user_id_partition_keygen(request_envelope): + # type: (RequestEnvelope) -> str + """Retrieve user id from request envelope, to use as partition key. + + :param request_envelope: Request Envelope passed during skill + invocation + :type request_envelope: RequestEnvelope + :return: User Id retrieved from request envelope + :rtype str + :raises PersistenceException + """ + try: + user_id = request_envelope.context.system.user.user_id + return user_id + except AttributeError: + raise PersistenceException("Couldn't retrieve user id from request " + "envelope, for partition key use") + + +def device_id_partition_keygen(request_envelope): + # type: (RequestEnvelope) -> str + """Retrieve device id from request envelope, to use as partition key. + + :param request_envelope: Request Envelope passed during skill + invocation + :type request_envelope: RequestEnvelope + :return: Device Id retrieved from request envelope + :rtype str + :raises PersistenceException + """ + try: + device_id = request_envelope.context.system.device.device_id + return device_id + except AttributeError: + raise PersistenceException("Couldn't retrieve device id from " + "request envelope, for partition key use") diff --git a/ask-sdk-dynamodb-persistence-adapter/docs/requirements-docs.txt b/ask-sdk-dynamodb-persistence-adapter/docs/requirements-docs.txt new file mode 100644 index 0000000..e69de29 diff --git a/ask-sdk-dynamodb-persistence-adapter/requirements.txt b/ask-sdk-dynamodb-persistence-adapter/requirements.txt new file mode 100644 index 0000000..6f99268 --- /dev/null +++ b/ask-sdk-dynamodb-persistence-adapter/requirements.txt @@ -0,0 +1,2 @@ +boto3 +ask-sdk-core diff --git a/ask-sdk-dynamodb-persistence-adapter/setup.cfg b/ask-sdk-dynamodb-persistence-adapter/setup.cfg new file mode 100644 index 0000000..7c2b287 --- /dev/null +++ b/ask-sdk-dynamodb-persistence-adapter/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal = 1 \ No newline at end of file diff --git a/ask-sdk-dynamodb-persistence-adapter/setup.py b/ask-sdk-dynamodb-persistence-adapter/setup.py new file mode 100644 index 0000000..8b847fb --- /dev/null +++ b/ask-sdk-dynamodb-persistence-adapter/setup.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 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. +# +import os +from setuptools import setup, find_packages +from codecs import open + +here = os.path.abspath(os.path.dirname(__file__)) + +about = {} +with open(os.path.join( + here, 'ask_sdk_dynamodb', '__version__.py'), 'r', 'utf-8') as f: + exec(f.read(), about) + +with open('README.rst', 'r', 'utf-8') as f: + readme = f.read() +with open('CHANGELOG.rst', 'r', 'utf-8') as f: + history = f.read() + +setup( + name=about['__pip_package_name__'], + version=about['__version__'], + description=about['__description__'], + long_description=readme + '\n\n' + history, + author=about['__author__'], + author_email=about['__author_email__'], + url=about['__url__'], + keywords=about['__keywords__'], + license=about['__license__'], + include_package_data=True, + install_requires=about['__install_requires__'], + extras_require={ + ':python_version == "2.7"': [ + 'enum34', + 'typing', + ], + }, + packages=find_packages( + exclude=["*.tests", "*.tests.*", "tests.*", "tests"]), + zip_safe=False, + classifiers=( + 'Development Status :: 3 - Alpha', + 'Intended Audience :: Developers', + 'Natural Language :: English', + 'License :: OSI Approved :: Apache Software License', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.6' + ), + python_requires=(">2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, " + "!=3.5.*"), +) diff --git a/ask-sdk-dynamodb-persistence-adapter/tests/__init__.py b/ask-sdk-dynamodb-persistence-adapter/tests/__init__.py new file mode 100644 index 0000000..93bb6a9 --- /dev/null +++ b/ask-sdk-dynamodb-persistence-adapter/tests/__init__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 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. +# +import os +import sys +sys.path.insert( + 0, os.path.abspath( + os.path.join(os.path.dirname(__file__), '..', 'ask_sdk_dynamodb'))) diff --git a/ask-sdk-dynamodb-persistence-adapter/tests/unit/__init__.py b/ask-sdk-dynamodb-persistence-adapter/tests/unit/__init__.py new file mode 100644 index 0000000..2b850df --- /dev/null +++ b/ask-sdk-dynamodb-persistence-adapter/tests/unit/__init__.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 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. +# diff --git a/ask-sdk-dynamodb-persistence-adapter/tests/unit/test_adapter.py b/ask-sdk-dynamodb-persistence-adapter/tests/unit/test_adapter.py new file mode 100644 index 0000000..1915890 --- /dev/null +++ b/ask-sdk-dynamodb-persistence-adapter/tests/unit/test_adapter.py @@ -0,0 +1,318 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 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. +# +import unittest +from six import PY3 + +from boto3.exceptions import ResourceNotExistsError +from ask_sdk_model import RequestEnvelope +from ask_sdk_core.exceptions import PersistenceException +from ask_sdk_dynamodb.adapter import DynamoDbAdapter + +if PY3: + from unittest import mock +else: + import mock + + +class ResourceInUseException(Exception): + pass + + +class TestDynamoDbAdapter(unittest.TestCase): + def setUp(self): + self.dynamodb_resource = mock.Mock() + self.partition_keygen = mock.Mock() + self.request_envelope = RequestEnvelope() + self.expected_key_schema = [{'AttributeName': 'id', 'KeyType': 'HASH'}] + self.expected_attribute_definitions = [ + {'AttributeName': 'id', 'AttributeType': 'S'}] + self.expected_provision_throughput = { + 'ReadCapacityUnits': 5, 'WriteCapacityUnits': 5} + self.attributes = {"test_key": "test_val"} + + def test_get_attributes_from_existing_table(self): + mock_table = mock.Mock() + mock_table.get_item.return_value = { + "Item": {"attributes": self.attributes}} + self.dynamodb_resource.Table.return_value = mock_table + self.partition_keygen.return_value = "test_partition_key" + + test_dynamodb_adapter = DynamoDbAdapter( + table_name="test_table", partition_keygen=self.partition_keygen, + dynamodb_resource=self.dynamodb_resource) + + assert test_dynamodb_adapter.get_attributes( + request_envelope=self.request_envelope) == self.attributes, ( + "Get attributes from dynamodb table retrieves wrong values") + self.dynamodb_resource.Table.assert_called_once_with("test_table"), ( + "Existing table name passed incorrectly to dynamodb get " + "table call") + self.partition_keygen.assert_called_once_with(self.request_envelope), ( + "Partition Keygen provided incorrect input parameters during get " + "attributes call") + mock_table.get_item.assert_called_once_with( + Key={"id": "test_partition_key"}), ( + "Partition keygen provided incorrect key for get attributes call") + + def test_get_attributes_from_existing_table_custom_key_name_attribute_name(self): + mock_table = mock.Mock() + mock_table.get_item.return_value = { + "Item": {"custom_attr": self.attributes}} + self.dynamodb_resource.Table.return_value = mock_table + self.partition_keygen.return_value = "test_partition_key" + + test_dynamodb_adapter = DynamoDbAdapter( + table_name="test_table", partition_keygen=self.partition_keygen, + dynamodb_resource=self.dynamodb_resource, + partition_key_name="custom_key", attribute_name="custom_attr") + + assert test_dynamodb_adapter.get_attributes( + request_envelope=self.request_envelope) == self.attributes, ( + "Get attributes from dynamodb table retrieves wrong values when " + "custom partition key name and " + "custom attribute name passed") + self.dynamodb_resource.Table.assert_called_once_with("test_table"), ( + "Existing table name passed incorrectly to dynamodb get table " + "call") + self.partition_keygen.assert_called_once_with(self.request_envelope), ( + "Partition Keygen provided incorrect input parameters during get " + "item call") + mock_table.get_item.assert_called_once_with( + Key={"custom_key": "test_partition_key"}), ( + "Partition keygen provided incorrect key for get attributes call") + + def test_get_attributes_from_existing_table_get_item_fails(self): + mock_table = mock.Mock() + mock_table.get_item.side_effect = Exception("test exception") + self.dynamodb_resource.Table.return_value = mock_table + self.partition_keygen.return_value = "test_partition_key" + + test_dynamodb_adapter = DynamoDbAdapter( + table_name="test_table", partition_keygen=self.partition_keygen, + dynamodb_resource=self.dynamodb_resource) + + with self.assertRaises(PersistenceException) as exc: + test_dynamodb_adapter.get_attributes( + request_envelope=self.request_envelope) + + assert "Failed to retrieve attributes from DynamoDb table" in str( + exc.exception), ( + "Get attributes didn't raise Persistence Exception when get item " + "failed on dynamodb resource") + mock_table.get_item.assert_called_once_with( + Key={"id": "test_partition_key"}), ( + "Partition keygen provided incorrect key for get attributes call") + + def test_get_attributes_from_existing_table_get_item_returns_no_item(self): + mock_table = mock.Mock() + mock_table.get_item.return_value = {"attributes": self.attributes} + self.dynamodb_resource.Table.return_value = mock_table + self.partition_keygen.return_value = "test_partition_key" + + test_dynamodb_adapter = DynamoDbAdapter( + table_name="test_table", partition_keygen=self.partition_keygen, + dynamodb_resource=self.dynamodb_resource) + + assert test_dynamodb_adapter.get_attributes( + request_envelope=self.request_envelope) == {}, ( + "Get attributes returns incorrect response when no item is " + "present in dynamodb table for provided key") + + def test_get_attributes_fails_with_no_existing_table_create_table_default_false(self): + self.dynamodb_resource.Table.side_effect = ResourceNotExistsError( + "test", "test", "test") + self.dynamodb_resource.create_table.return_value = "test" + test_dynamodb_adapter = DynamoDbAdapter( + table_name="test_table", partition_keygen=self.partition_keygen, + dynamodb_resource=self.dynamodb_resource) + + with self.assertRaises(PersistenceException) as exc: + test_dynamodb_adapter.get_attributes( + request_envelope=self.request_envelope) + + assert "DynamoDb table test_table doesn't exist" in str( + exc.exception), ( + "Get attributes didn't raise Persistence Exception when no " + "existing table and create table set as false") + self.dynamodb_resource.create_table.assert_not_called(), ( + "Create table called on dynamodb resource when create_table " + "flag is set as False") + + def test_save_attributes_to_existing_table(self): + mock_table = mock.Mock() + mock_table.put_item.return_value = True + self.dynamodb_resource.Table.return_value = mock_table + self.partition_keygen.return_value = "test_partition_key" + + test_dynamodb_adapter = DynamoDbAdapter( + table_name="test_table", partition_keygen=self.partition_keygen, + dynamodb_resource=self.dynamodb_resource) + + try: + test_dynamodb_adapter.save_attributes( + request_envelope=self.request_envelope, attributes=self.attributes) + except: + # Should not reach here + raise Exception("Save attributes failed on existing table") + + self.dynamodb_resource.Table.assert_called_once_with("test_table"), ( + "Existing table name passed incorrectly to dynamodb get table " + "call") + self.partition_keygen.assert_called_once_with( + self.request_envelope), ( + "Partition Keygen provided incorrect input parameters during " + "save attributes call") + mock_table.put_item.assert_called_once_with( + Item={"id": "test_partition_key", "attributes": + self.attributes}), ( + "Partition keygen provided incorrect partition key in item for " + "save attributes call") + + def test_save_attributes_to_existing_table_custom_key_name_attribute_name(self): + mock_table = mock.Mock() + mock_table.put_item.return_value = True + self.dynamodb_resource.Table.return_value = mock_table + self.partition_keygen.return_value = "test_partition_key" + + test_dynamodb_adapter = DynamoDbAdapter( + table_name="test_table", partition_keygen=self.partition_keygen, + dynamodb_resource=self.dynamodb_resource, + partition_key_name="custom_key", attribute_name="custom_attr") + + try: + test_dynamodb_adapter.save_attributes( + request_envelope=self.request_envelope, + attributes=self.attributes) + except: + # Should not reach here + raise Exception( + "Save attributes failed on existing table when custom " + "partition key name and custom attribute " + "name passed") + + self.dynamodb_resource.Table.assert_called_once_with("test_table"), ( + "Existing table name passed incorrectly to dynamodb get table call") + self.partition_keygen.assert_called_once_with( + self.request_envelope), ( + "Partition Keygen provided incorrect input parameters during " + "save attributes call") + mock_table.put_item.assert_called_once_with( + Item={"custom_key": "test_partition_key", "custom_attr": + self.attributes}), ( + "Partition keygen provided incorrect partition key in item for " + "save attributes call") + + def test_save_attributes_to_existing_table_put_item_fails(self): + mock_table = mock.Mock() + mock_table.put_item.side_effect = ValueError("test exception") + self.dynamodb_resource.Table.return_value = mock_table + self.partition_keygen.return_value = "test_partition_key" + + test_dynamodb_adapter = DynamoDbAdapter( + table_name="test_table", partition_keygen=self.partition_keygen, + dynamodb_resource=self.dynamodb_resource) + + with self.assertRaises(PersistenceException) as exc: + test_dynamodb_adapter.save_attributes( + request_envelope=self.request_envelope, + attributes=self.attributes) + + assert ("Failed to save attributes to DynamoDb table. " + "Exception of type ValueError occurred: test " + "exception") in str(exc.exception), ( + "Save attributes didn't raise Persistence Exception when put item " + "failed on dynamodb resource") + mock_table.put_item.assert_called_once_with( + Item={"id": "test_partition_key", "attributes": + self.attributes}), ( + "DynamoDb Put item called with incorrect parameters") + + def test_save_attributes_fails_with_no_existing_table(self): + self.dynamodb_resource.Table.side_effect = ResourceNotExistsError( + "test", "test", "test") + self.dynamodb_resource.create_table.return_value = "test" + test_dynamodb_adapter = DynamoDbAdapter( + table_name="test_table", partition_keygen=self.partition_keygen, + dynamodb_resource=self.dynamodb_resource) + + with self.assertRaises(PersistenceException) as exc: + test_dynamodb_adapter.save_attributes( + request_envelope=self.request_envelope, attributes=self.attributes) + + assert "DynamoDb table test_table doesn't exist" in str( + exc.exception), ( + "Save attributes didn't raise Persistence Exception when no " + "existing table and create table set as false") + self.dynamodb_resource.create_table.assert_not_called(), ( + "Create table called on dynamodb resource when create_table flag " + "is set as False") + + def test_existence_check_not_done_if_flag_not_set(self): + self.dynamodb_resource.Table.side_effect = Exception( + "Invalid call to get table") + self.dynamodb_resource.create_table.side_effect = Exception( + "Invalid call to create table") + + DynamoDbAdapter( + table_name="test_table", partition_keygen=self.partition_keygen, + dynamodb_resource=self.dynamodb_resource, + create_table=False) + + self.dynamodb_resource.create_table.assert_not_called(), ( + "Create table called when create_table flag is not set, during " + "Adapter initialization") + + def test_create_table_doesnt_raise_exception_if_flag_set_existing_table(self): + self.dynamodb_resource.create_table.side_effect = \ + ResourceInUseException("Invalid call to create table") + + DynamoDbAdapter( + table_name="test_table", partition_keygen=self.partition_keygen, + dynamodb_resource=self.dynamodb_resource, + create_table=True) + + self.dynamodb_resource.create_table.assert_called_once(), ( + "Create table called when create_table flag is set " + "during Adapter initialization and resource already exists") + + def test_catch_get_table_exception_if_flag_set(self): + self.dynamodb_resource.create_table.side_effect = ValueError( + "Invalid call to create table") + + with self.assertRaises(PersistenceException) as exc: + DynamoDbAdapter( + table_name="test_table", + partition_keygen=self.partition_keygen, + dynamodb_resource=self.dynamodb_resource, create_table=True) + + assert ( + "Create table if not exists request failed: Exception of type " + "ValueError occurred: " + "Invalid call to create table") in str(exc.exception), ( + "create table if not exists didn't raise exception when " + "create_table flag is set and create table " + "resource raises exception") + + def tearDown(self): + self.dynamodb_resource = None + self.partition_keygen = None + self.request_envelope = None + self.expected_key_schema = None + self.expected_attribute_definitions = None + self.expected_provision_throughput = None + self.attributes = None diff --git a/ask-sdk-dynamodb-persistence-adapter/tests/unit/test_partition_keygen.py b/ask-sdk-dynamodb-persistence-adapter/tests/unit/test_partition_keygen.py new file mode 100644 index 0000000..79510a6 --- /dev/null +++ b/ask-sdk-dynamodb-persistence-adapter/tests/unit/test_partition_keygen.py @@ -0,0 +1,146 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 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. +# +import unittest + +from ask_sdk_model import RequestEnvelope, Context, User, Device +from ask_sdk_model.interfaces.system import SystemState +from ask_sdk_core.exceptions import PersistenceException +from ask_sdk_dynamodb.partition_keygen import ( + user_id_partition_keygen, device_id_partition_keygen) + + +class TestPartitionKeyGenerators(unittest.TestCase): + def setUp(self): + self.request_envelope = RequestEnvelope() + self.context = Context() + self.system = SystemState() + self.user = User() + self.device = Device() + + def test_valid_user_id_partition_keygen(self): + self.user.user_id = "123" + self.system.user = self.user + self.context.system = self.system + self.request_envelope.context = self.context + + assert user_id_partition_keygen(self.request_envelope) == "123", ( + "User Id Partition Key Generation retrieved wrong user id from " + "valid request envelope") + + def test_user_id_partition_keygen_raise_error_when_request_envelope_null(self): + with self.assertRaises(PersistenceException) as exc: + user_id_partition_keygen(request_envelope=None) + + assert "Couldn't retrieve user id from request envelope" in str( + exc.exception), ( + "User Id Partition Key Generation didn't throw exception when " + "null request envelope is provided") + + def test_user_id_partition_keygen_raise_error_when_context_null(self): + with self.assertRaises(PersistenceException) as exc: + user_id_partition_keygen(request_envelope=self.request_envelope) + + assert "Couldn't retrieve user id from request envelope" in str( + exc.exception), ( + "User Id Partition Key Generation didn't throw exception when " + "null context provided in request envelope") + + def test_user_id_partition_keygen_raise_error_when_system_null(self): + self.request_envelope.context = self.context + + with self.assertRaises(PersistenceException) as exc: + user_id_partition_keygen(request_envelope=self.request_envelope) + + assert "Couldn't retrieve user id from request envelope" in str( + exc.exception), ( + "User Id Partition Key Generation didn't throw exception when " + "null system provided in context of " + "request envelope") + + def test_user_id_partition_keygen_raise_error_when_user_null(self): + self.context.system = self.system + self.request_envelope.context = self.context + + with self.assertRaises(PersistenceException) as exc: + user_id_partition_keygen(request_envelope=self.request_envelope) + + assert "Couldn't retrieve user id from request envelope" in str( + exc.exception), ( + "User Id Partition Key Generation didn't throw exception when " + "null user provided in context.system of " + "request envelope") + + def test_valid_device_id_partition_keygen(self): + self.device.device_id = "123" + self.system.device = self.device + self.context.system = self.system + self.request_envelope.context = self.context + + assert device_id_partition_keygen(self.request_envelope) == "123", ( + "Device Id Partition Key Generation retrieved wrong device id " + "from valid request envelope") + + def test_device_id_partition_keygen_raise_error_when_request_envelope_null(self): + with self.assertRaises(PersistenceException) as exc: + device_id_partition_keygen(request_envelope=None) + + assert "Couldn't retrieve device id from request envelope" in str( + exc.exception), ( + "Device Id Partition Key Generation didn't throw exception when " + "null request envelope is provided") + + def test_device_id_partition_keygen_raise_error_when_context_null(self): + with self.assertRaises(PersistenceException) as exc: + device_id_partition_keygen(request_envelope=self.request_envelope) + + assert "Couldn't retrieve device id from request envelope" in str( + exc.exception), ( + "Device Id Partition Key Generation didn't throw exception when " + "null context provided in request envelope") + + def test_device_id_partition_keygen_raise_error_when_system_null(self): + self.request_envelope.context = self.context + + with self.assertRaises(PersistenceException) as exc: + device_id_partition_keygen(request_envelope=self.request_envelope) + + assert "Couldn't retrieve device id from request envelope" in str( + exc.exception), ( + "Device Id Partition Key Generation didn't throw exception when " + "null system provided in context of " + "request envelope") + + def test_device_id_partition_keygen_raise_error_when_device_null(self): + self.context.system = self.system + self.request_envelope.context = self.context + + with self.assertRaises(PersistenceException) as exc: + device_id_partition_keygen(request_envelope=self.request_envelope) + + assert "Couldn't retrieve device id from request envelope" in str( + exc.exception), ( + "Device Id Partition Key Generation didn't throw exception when " + "null device provided in context.system of " + "request envelope") + + def tearDown(self): + self.request_envelope = None + self.context = None + self.system = None + self.user = None + self.device = None diff --git a/ask-sdk-dynamodb-persistence-adapter/tox.ini b/ask-sdk-dynamodb-persistence-adapter/tox.ini new file mode 100644 index 0000000..55199a9 --- /dev/null +++ b/ask-sdk-dynamodb-persistence-adapter/tox.ini @@ -0,0 +1,11 @@ +[tox] +envlist = py27,py36 + +[testenv] +deps = + -rrequirements.txt + nose + mock + coverage +commands = + nosetests --cover-package=ask_sdk_dynamodb --with-coverage --cover-erase \ No newline at end of file diff --git a/ask-sdk/.coveragerc b/ask-sdk/.coveragerc new file mode 100644 index 0000000..58de6f4 --- /dev/null +++ b/ask-sdk/.coveragerc @@ -0,0 +1,9 @@ +[run] +branch = True + +[report] +include = + ask_sdk/* +exclude_lines = + if typing.TYPE_CHECKING: + pass \ No newline at end of file diff --git a/ask-sdk/.gitignore b/ask-sdk/.gitignore new file mode 100644 index 0000000..e057098 --- /dev/null +++ b/ask-sdk/.gitignore @@ -0,0 +1,67 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ +venv/ +.python-version + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +#Ipython Notebook +.ipynb_checkpoints + +# IntelliJ configs +*.iml diff --git a/ask-sdk/CHANGELOG.rst b/ask-sdk/CHANGELOG.rst new file mode 100644 index 0000000..12ea0a1 --- /dev/null +++ b/ask-sdk/CHANGELOG.rst @@ -0,0 +1,8 @@ +========= +CHANGELOG +========= + +0.1 +------- + +* Initial release of ASK SDK Standard package. diff --git a/ask-sdk/MANIFEST.in b/ask-sdk/MANIFEST.in new file mode 100644 index 0000000..314a533 --- /dev/null +++ b/ask-sdk/MANIFEST.in @@ -0,0 +1,5 @@ +include README.rst +include CHANGELOG.rst +include LICENSE +include requirements.txt +recursive-exclude tests * \ No newline at end of file diff --git a/ask-sdk/README.rst b/ask-sdk/README.rst new file mode 100644 index 0000000..e9368ca --- /dev/null +++ b/ask-sdk/README.rst @@ -0,0 +1,50 @@ +======================================================== +ASK SDK - Standard / Full distribution of Python ASK SDK +======================================================== + +ask-sdk is the standard SDK package for Alexa Skills Kit (ASK) by +the Software Development Kit (SDK) team for Python. It is a *all batteries included* +package for developing Alexa Skills. + + +Quick Start +----------- + +Installation +~~~~~~~~~~~~~~~ +Assuming that you have Python and ``virtualenv`` installed, you can +install the package and it's dependencies (``ask-sdk-model``, ``ask-sdk-core``, +``ask-sdk-dynamodb-persistence-adapter``) from PyPi +as follows: + +.. code-block:: sh + + >>> virtualenv venv + >>> . venv/bin/activate + >>> pip install ask-sdk + + +You can also install the whole standard package locally by following these steps: + +.. code-block:: sh + + >>> git clone https://github.com/alexalabs/alexa-skills-kit-for-python-sdk.git + >>> cd alexa-skills-kit-for-python-sdk/ask-sdk + >>> virtualenv venv + ... + >>> . venv/bin/activate + >>> python setup.py install + + +Usage and Getting Started +------------------------- +A Getting Started guide can be found `here <../docs/GETTING_STARTED.rst>`_ + + +Got Feedback? +------------- + +- We would like to hear about your bugs, feature requests, questions or quick feedback. + Please search for the `existing issues `_ before opening a new one. It would also be helpful + if you follow the templates for issue and pull request creation. Please follow the `contributing guidelines <../CONTRIBUTING.rst>`_!! +- Request and vote for `Alexa features `_! diff --git a/ask-sdk/ask_sdk/__init__.py b/ask-sdk/ask_sdk/__init__.py new file mode 100644 index 0000000..2b850df --- /dev/null +++ b/ask-sdk/ask_sdk/__init__.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 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. +# diff --git a/ask-sdk/ask_sdk/__version__.py b/ask-sdk/ask_sdk/__version__.py new file mode 100644 index 0000000..1ed7c50 --- /dev/null +++ b/ask-sdk/ask_sdk/__version__.py @@ -0,0 +1,29 @@ +# +# Copyright 2018 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. +# + +__pip_package_name__ = 'ask-sdk' +__description__ = ( + 'The ASK SDK Standard package provides a full distribution of the SDK, ' + 'all batteries included, for building Alexa Skills') +__url__ = 'http://developer.amazon.com/ask' +__version__ = '0.1' +__author__ = 'Alexa Skills Kit' +__author_email__ = 'ask-sdk-dynamic@amazon.com' +__license__ = 'Apache 2.0' +__keywords__ = ['ASK SDK', 'Alexa Skills Kit', 'Alexa', 'ASK SDK Core', + 'Persistence', 'DynamoDB', 'ASK SDK Standard'] +__install_requires__ = ["ask-sdk-core", 'ask-sdk-dynamodb-persistence-adapter'] diff --git a/ask-sdk/ask_sdk/standard.py b/ask-sdk/ask_sdk/standard.py new file mode 100644 index 0000000..1764f56 --- /dev/null +++ b/ask-sdk/ask_sdk/standard.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 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. +# +import typing + +from ask_sdk_core.skill_builder import SkillBuilder +from ask_sdk_core.api_client import DefaultApiClient +from ask_sdk_dynamodb.adapter import DynamoDbAdapter + +if typing.TYPE_CHECKING: + from typing import Callable + from ask_sdk_model import RequestEnvelope + from ask_sdk_core.skill_builder import SkillConfiguration + from boto3.resources.base import ServiceResource + + +class StandardSkillBuilder(SkillBuilder): + """Skill Builder with api client and db adapter coupling to Skill.""" + + def __init__( + self, table_name=None, auto_create_table=None, + partition_keygen=None, dynamodb_client=None): + # type: (str, bool, Callable[[RequestEnvelope], str], ServiceResource) -> None + """Skill Builder with api client and db adapter coupling to Skill. + + Standard Skill Builder is an implementation of :py:class:`SkillBuilder` + with coupling of DynamoDb Persistence Adapter settings and a Default + Api Client added to the :py:class:`Skill`. + + :param table_name: Name of the table to be created or used + :type table_name: str + :param auto_create_table: Should the adapter try to create the table if + it doesn't exist. + :type auto_create_table: bool + :param partition_keygen: Callable function that takes a request + envelope and provides a unique partition key value. + :type partition_keygen: Callable[[RequestEnvelope], str] + :param dynamodb_client: Resource to be used, to perform dynamo + operations. + :type dynamodb_client: ServiceResource + """ + super(StandardSkillBuilder, self).__init__() + self.table_name = table_name + self.auto_create_table = auto_create_table + self.partition_keygen = partition_keygen + self.dynamodb_client = dynamodb_client + + @property + def skill_configuration(self): + # type: () -> SkillConfiguration + """Create the skill configuration object using the registered + components. + """ + skill_config = super(StandardSkillBuilder, self).skill_configuration + skill_config.api_client = DefaultApiClient() + + if self.table_name is not None: + kwargs = {"table_name": self.table_name} + if self.auto_create_table: + kwargs["create_table"] = self.auto_create_table + + if self.partition_keygen: + kwargs["partition_keygen"] = self.partition_keygen + + if self.dynamodb_client: + kwargs["dynamodb_resource"] = self.dynamodb_client + + skill_config.persistence_adapter = DynamoDbAdapter(**kwargs) + return skill_config diff --git a/ask-sdk/docs/requirements-docs.txt b/ask-sdk/docs/requirements-docs.txt new file mode 100644 index 0000000..e69de29 diff --git a/ask-sdk/requirements.txt b/ask-sdk/requirements.txt new file mode 100644 index 0000000..eb2ea5b --- /dev/null +++ b/ask-sdk/requirements.txt @@ -0,0 +1,2 @@ +ask-sdk-core +ask-sdk-dynamodb-persistence-adapter diff --git a/ask-sdk/setup.cfg b/ask-sdk/setup.cfg new file mode 100644 index 0000000..7c2b287 --- /dev/null +++ b/ask-sdk/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal = 1 \ No newline at end of file diff --git a/ask-sdk/setup.py b/ask-sdk/setup.py new file mode 100644 index 0000000..4a8502b --- /dev/null +++ b/ask-sdk/setup.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 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. +# +import os +from setuptools import setup, find_packages +from codecs import open + +here = os.path.abspath(os.path.dirname(__file__)) + +about = {} +with open(os.path.join( + here, 'ask_sdk', '__version__.py'), 'r', 'utf-8') as f: + exec(f.read(), about) + +with open('README.rst', 'r', 'utf-8') as f: + readme = f.read() +with open('CHANGELOG.rst', 'r', 'utf-8') as f: + history = f.read() + +setup( + name=about['__pip_package_name__'], + version=about['__version__'], + description=about['__description__'], + long_description=readme + '\n\n' + history, + author=about['__author__'], + author_email=about['__author_email__'], + url=about['__url__'], + keywords=about['__keywords__'], + license=about['__license__'], + include_package_data=True, + install_requires=about['__install_requires__'], + extras_require={ + ':python_version == "2.7"': [ + 'enum34', + 'typing', + ], + }, + packages=find_packages( + exclude=["*.tests", "*.tests.*", "tests.*", "tests"]), + zip_safe=False, + classifiers=( + 'Development Status :: 3 - Alpha', + 'Intended Audience :: Developers', + 'Natural Language :: English', + 'License :: OSI Approved :: Apache Software License', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.6' + ), + python_requires=(">2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, " + "!=3.5.*"), +) diff --git a/ask-sdk/tests/unit/__init__.py b/ask-sdk/tests/unit/__init__.py new file mode 100644 index 0000000..5deb7d9 --- /dev/null +++ b/ask-sdk/tests/unit/__init__.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 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. +# \ No newline at end of file diff --git a/ask-sdk/tests/unit/test_standard.py b/ask-sdk/tests/unit/test_standard.py new file mode 100644 index 0000000..06aebf9 --- /dev/null +++ b/ask-sdk/tests/unit/test_standard.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 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. +# +import unittest +from six import PY3 + +from ask_sdk.standard import StandardSkillBuilder +from ask_sdk_core.api_client import DefaultApiClient +from ask_sdk_dynamodb.adapter import DynamoDbAdapter + +if PY3: + from unittest import mock +else: + import mock + + +class TestStandardSkillBuilder(unittest.TestCase): + def test_default_api_client_set(self): + test_skill_builder = StandardSkillBuilder() + + actual_skill_config = test_skill_builder.skill_configuration + + assert isinstance(actual_skill_config.api_client, DefaultApiClient), ( + "Standard Skill Builder didn't set the api client to the default" + "implementation") + + def test_persistence_adapter_default_null(self): + test_skill_builder = StandardSkillBuilder() + + actual_skill_config = test_skill_builder.skill_configuration + + assert actual_skill_config.persistence_adapter is None, ( + "Standard Skill Builder didn't set Persistence Adapter to None " + "when no table_name is provided") + + def test_persistence_adapter_set(self): + test_table_name = "TestTable" + test_dynamodb_resource = mock.Mock() + test_partition_keygen = mock.Mock() + test_auto_create_table = False + + test_skill_builder = StandardSkillBuilder( + table_name=test_table_name, + auto_create_table=test_auto_create_table, + partition_keygen=test_partition_keygen, + dynamodb_client=test_dynamodb_resource) + + actual_skill_config = test_skill_builder.skill_configuration + actual_adapter = actual_skill_config.persistence_adapter + + assert isinstance( + actual_adapter, DynamoDbAdapter), ( + "Standard Skill Builder set incorrect persistence adapter in " + "skill configuration") + + assert actual_adapter.table_name == test_table_name, ( + "Standard Skill Builder set persistence adapter with incorrect " + "table name in skill configuration") + assert actual_adapter.partition_keygen == test_partition_keygen, ( + "Standard Skill Builder set persistence adapter with incorrect " + "partition key generator function in skill configuration") + assert actual_adapter.create_table == test_auto_create_table, ( + "Standard Skill Builder set persistence adapter with incorrect " + "auto create table flag in skill configuration") + assert actual_adapter.dynamodb == test_dynamodb_resource, ( + "Standard Skill Builder set persistence adapter with incorrect " + "dynamo db resource in skill configuration") diff --git a/ask-sdk/tox.ini b/ask-sdk/tox.ini new file mode 100644 index 0000000..fae4d91 --- /dev/null +++ b/ask-sdk/tox.ini @@ -0,0 +1,11 @@ +[tox] +envlist = py27,py36 + +[testenv] +deps = + -rrequirements.txt + nose + mock + coverage +commands = + nosetests --cover-package=ask_sdk --with-coverage --cover-erase \ No newline at end of file diff --git a/docs/ATTRIBUTES.rst b/docs/ATTRIBUTES.rst new file mode 100644 index 0000000..c8587d9 --- /dev/null +++ b/docs/ATTRIBUTES.rst @@ -0,0 +1,168 @@ +================= +Skill Attributes +================= + +This guide provides information on different scopes of attributes available +to the skill developer, and how to use them in the skill. + +Attributes +========== + +The SDK allows you to store and retrieve attributes at different scopes. +For example, attributes can be used to store data that you retrieve +on subsequent requests. You can also use attributes in your handler’s +``can_handle`` logic to add conditions during request routing. + +An attribute consists of a key and a value. The key is enforced as a +``str`` type and the value is an unbounded ``object``. For session +and persistent attributes, you must ensure that value types are +serializable so they can be properly stored for subsequent retrieval. +This restriction does not apply to request-level attributes because they +do not persist outside of the request processing lifecycle. + +Attribute Scopes +================= + +Request Attributes +~~~~~~~~~~~~~~~~~~ + +Request attributes only last within a single request processing +lifecycle. Request attributes are initially empty when a request comes +in, and are discarded once a response has been produced. + +Request attributes are useful with request and response interceptors. +For example, you can inject additional data and helper methods into +request attributes through a request interceptor so they are retrievable +by request handlers. + +Session Attributes +~~~~~~~~~~~~~~~~~~ + +Session attributes persist throughout the lifespan of the current skill +session. Session attributes are available for use with any in-session +request. Any attributes set during the request processing lifecycle are +sent back to the Alexa service and provided in the next request in the +same session. + +Session attributes do not require the use of an external storage +solution. They are not available for use when handling out-of-session +requests. They are discarded once the skill session closes. + +Persistent Attributes +~~~~~~~~~~~~~~~~~~~~~ + +Persistent attributes persist beyond the lifecycle of the current +session. How these attributes are stored, including key scope (user ID +or device ID), TTL, and storage layer depends on the configuration of +the skill. + +Persistent attributes are only available when you `configure the skill +instance `_ with a ``PersistenceAdapter``. Calls to the +``AttributesManager`` to retrieve and save persistent attributes throw +an error if a ``PersistenceAdapter`` has not been configured. + +PersistenceAdapter +================== + +The ``AbstractPersistenceAdapter`` is used by ``AttributesManager`` when +retrieving and saving attributes to persistence layer (i.e. database or +local file system). The ``ask-sdk-dynamodb-persistence-adapter`` package +provides an implementation of ``AbstractPersistenceAdapter`` using `AWS +DynamoDB `_. + +All implementations of ``AbstractPersistenceAdapter`` needs to follow +the following interface. + +Interface +~~~~~~~~~ + +.. code:: python + + class AbstractPersistenceAdapter(object): + def get_attributes(self, request_envelope): + # type: (RequestEnvelope) -> Dict[str, Any] + pass + + def save_attributes(self, request_envelope, attributes): + # type: (RequestEnvelope, Dict[str, Any]) -> None + pass + +AttributesManager +================= + +The ``AttributesManager`` exposes attributes that you can retrieve and +update in your handlers. ``AttributesManager`` is available to handlers +via the `Handler Input <#REQUEST_PROCESSING.handler-input>`_ object. The ``AttributesManager`` +takes care of attributes retrieval and saving so that you can interact +directly with attributes needed by your skill. + +Interface +~~~~~~~~~ + +.. code:: python + + class AttributesManager(object): + def __init__(self, request_envelope, persistence_adapter=None): + # type: (RequestEnvelope, AbstractPersistenceAdapter) -> None + .... + + @property + def request_attributes(self): + # type: () -> Dict[str, Any] + # Request Attributes getter + .... + + @request_attributes.setter + def request_attributes(self, attributes): + # type: (Dict[str, Any]) -> None + # Request Attributes setter + .... + + @property + def session_attributes(self): + # type: () -> Dict[str, Any] + # Session Attributes getter + .... + + @session_attributes.setter + def session_attributes(self, attributes): + # type: (Dict[str, Any]) -> None + # Session Attributes setter + .... + + @property + def persistent_attributes(self): + # type: () -> Dict[str, Any] + # Persistence Attributes getter + # Uses the Persistence adapter to get the attributes + .... + + @persistent_attributes.setter + def persistent_attributes(self, attributes): + # type: (Dict[str, Any]) -> None + # Persistent Attributes setter + .... + + def save_persistent_attributes(self): + # type: () -> None + # Persistence Attributes save + # Save the Persistence adapter to save the attributes + .... + + +The following example shows how you can retrieve and save persistent +attributes. + +.. code:: python + + class PersistenceAttributesHandler(AbstractRequestHandler): + def can_handle(handler_input): + persistence_attr = handler_input.attributes_manager.persistent_attributes + return persistence_attr['foo'] == 'bar' + + def handle(handler_input): + persistence_attr = handler_input.attributes_manager.persistent_attributes + persistence_attr['foo'] = 'baz' + handler_input.attributes_manager.save_attributes() + return handler_input.response_builder.response + diff --git a/docs/DEVELOPING_YOUR_FIRST_SKILL.rst b/docs/DEVELOPING_YOUR_FIRST_SKILL.rst new file mode 100644 index 0000000..f65bfb6 --- /dev/null +++ b/docs/DEVELOPING_YOUR_FIRST_SKILL.rst @@ -0,0 +1,624 @@ +============================ +Developing Your First Skill +============================ + +The `Getting Started `_ guide showed how to set up and +install the ASK SDK for Python into a specific directory or into a virtual +environment using virtualenv. This guide walks you through developing your +first skill with the ASK SDK for Python. + +Prerequisites +------------- + +In addition to an installed version of the ASK SDK for Python you need: + +* An `Amazon Developer `_ account. This is + required to create and configure Alexa skills. +* An `Amazon Web Services (AWS) `_ account. This is + required for hosting a skill on AWS Lambda. + +Creating Hello World +-------------------- + +You'll write your Hello World in a single python file named ``hello_world.py``. +When you upload your code to AWS Lambda, you must include your skill code and +its dependencies inside a zip file as a flat file structure, so you'll place +your code in the same folder as the ASK SDK for Python. + +If you set up the SDK in a specific folder, the SDK is installed into +the ask-sdk folder within your skill folder. If you are using a virtual +environment, on Windows the SDK is installed into the ``site-packages`` folder +located inside the ``Lib`` folder. For MacOS/Linux the location depends on +the version of Python you are using, for instance *Python 3.6* users will +find site-packages inside the ``lib/Python3.6`` folder. + +Now, in the same folder where the ASK SDK for Python is installed, use your +favorite text editor or IDE to create a file named ``hello_world.py``. + +Implementing Hello World +------------------------ + +Start by creating a skill builder object. The skill builder object helps in +adding components responsible for handling input requests and generating +custom responses for your skill. + +Type or paste the following code into your ``hello_world.py`` file. + +.. code-block:: python + + from ask_sdk_core.skill_builder import SkillBuilder + + sb = SkillBuilder() + +Request handlers +~~~~~~~~~~~~~~~~ + +A custom skill needs to respond to events sent by the Alexa service. +For instance, when you ask your Alexa device (e.g. Echo, Echo Dot, Echo Show, +etc.) to 'open hello world', your skill needs to respond to the LaunchRequest +that is sent to your Hello World skill. With the ASK SDK for Python, you simply +need to write a request handler, which is code to handle incoming requests and +return a response. Your code is responsible for making sure that the right +request handler is used to process incoming requests and for providing a +response. The ASK SDK for Python provides two ways to create request handlers: + +1. Implement the ``AbstractRequestHandler`` class under +``ask_sdk_core.dispatch_components`` package. The class should contain +implementations for ``can_handle`` and ``handle`` methods. +2. Use the request_handler decorator in instantiated skill builder object to +tag functions that act as handlers for different incoming requests. + +The implementation of the Hello World skill explores using handler classes +first and then shows how to write the same skill using decorators. +The functionality of these is identical and you can use either. + +The completed source code for both options is available in the +`HelloWorld <../samples/HelloWorld>`_ sample folder. + + +Exception handlers +~~~~~~~~~~~~~~~~~~ + +Sometimes things go wrong, and your skill code needs a way to handle the +problem gracefully. The ASK SDK for Python supports exception handling in a +similar way to handling requests. As with request handlers, you have a choice +of using handler classes or decorators. The following implementation sections +explore how to implement exception handling. + +Implementation using handler classes +------------------------------------ + +To use handler classes, each request handler will be written as a class that +implements two methods of the ``AbstractRequestHandler`` class; ``can_handle`` +and ``handle``. + +The ``can_handle`` method returns a boolean value indicating +if the request handler can create an appropriate response for the request. +The ``can_handle`` method has access to the request type and additional +attributes that the skill may have set in previous requests or even saved +from a previous interaction. For our Hello World skill we only need to +reference the request information to decide if each handler can respond to +an incoming request. + +The ``handle`` method returns a Response object + +LaunchRequest Handler +~~~~~~~~~~~~~~~~~~~~~ + +The following code example shows how to configure a handler to be invoked when +the skill receives a `LaunchRequest `_. +The LaunchRequest event occurs when the skill is invoked without a specific intent. + +Type or paste the following code into your ``hello_world.py`` file, after the +previous code. + +.. code-block:: python + + from ask_sdk_core.dispatch_components import AbstractRequestHandler + from ask_sdk_model.ui import SimpleCard + + class LaunchRequestHandler(AbstractRequestHandler): + def can_handle(self, handler_input): + return handler_input.request_envelope.request.object_type == "LaunchRequest" + + def handle(self, handler_input): + speech_text = "Welcome to the Alexa Skills Kit, you can say hello!" + + handler_input.response_builder.speak(speech_text).set_card( + SimpleCard("Hello World", speech_text)).set_should_end_session( + False) + return handler_input.response_builder.response + +The can_handle function returns **True** if the incoming request is a +LaunchRequest. The handle function generates and returns a basic greeting +response. + +HelloWorldIntent Handler +~~~~~~~~~~~~~~~~~~~~~~~~ + +The following code example shows how to configure a handler to be invoked +when the skill receives an intent request with the name HelloWorldIntent. +Type or paste the following code into your ``hello_world.py`` file, after +the previous handler. + +.. code-block:: python + + class HelloWorldIntentHandler(AbstractRequestHandler): + def can_handle(self, handler_input): + return (handler_input.request_envelope.request.object_type == "IntentRequest" + and handler_input.request_envelope.request.intent.name == "HelloWorldIntent") + + def handle(self, handler_input): + speech_text = "Hello World" + + handler_input.response_builder.speak(speech_text).set_card( + SimpleCard("Hello World", speech_text)).set_should_end_session( + True) + return handler_input.response_builder.response + +The can_handle function detects if the incoming request is an +`IntentRequest `_, +and returns **True** if the intent name is HelloWorldIntent. The handle +function generates and returns a basic “Hello World” response. + +HelpIntent Handler +~~~~~~~~~~~~~~~~~~ + +The following code example shows how to configure a handler to be invoked +when the skill receives the Built-In Intent +`AMAZON.HelpIntent `_. +Type or paste the following code into your ``hello_world.py file``, after the +previous handler. + +.. code-block:: python + + class HelpIntentHandler(AbstractRequestHandler): + def can_handle(self, handler_input): + return (handler_input.request_envelope.request.object_type == "IntentRequest" + and handler_input.request_envelope.request.intent.name == "AMAZON.HelpIntent") + + def handle(self, handler_input): + speech_text = "You can say hello to me!" + + handler_input.response_builder.speak(speech_text).ask(speech_text).set_card( + SimpleCard("Hello World", speech_text)) + return handler_input.response_builder.response + +Similar to the previous handler, this handler matches an IntentRequest with +the expected intent name. Basic help instructions are returned, and the +user's microphone will open up for them to respond due to ``.ask(speech_text)``. + +CancelAndStopIntent Handler +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The CancelAndStopIntentHandler is similar to the HelpIntent handler, as it +is also triggered by Built-In +`AMAZON.CancelIntent or AMAZON.StopIntent Intents `_, +the SessionEndedRequestHandler is a good place to put your cleanup logic. +Type or paste the following code into your ``hello_world.py`` file, after the +previous handler. + +.. code-block:: python + + class SessionEndedRequestHandler(AbstractRequestHandler): + + def can_handle(self, handler_input): + return handler_input.request_envelope.request.object_type == "SessionEndedRequest" + + def handle(self, handler_input): + #any cleanup logic goes here + + return handler_input.response_builder.response + +Implementing Exception Handlers +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The following sample adds a *catch all* exception handler to your skill, to +ensure the skill returns a meaningful message in case of all exceptions. +Type or paste the following code into your ``hello_world.py`` file, after the +previous handler. + +.. code-block:: python + + from ask_sdk_core.dispatch_components import AbstractExceptionHandler + + class AllExceptionHandler(AbstractExceptionHandler): + + def can_handle(self, handler_input, exception): + return True + + def handle(self, handler_input, exception): + # Log the exception in CloudWatch Logs + print(exception) + + speech = "Sorry, I didn't get it. Can you please say it again!!" + handler_input.response_builder.speak(speech).ask(speech) + return handler_input.response_builder.response + +Creating the Lambda Handler +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The `Lambda handler `_ +is the entry point for your AWS Lambda function. The following code example +creates a Lambda Handler function to route all inbound requests to your skill. +The Lambda Handler function creates an SDK Skill instance configured with the +request handlers that you just created. Type or paste the following code into +your ``hello_world.py`` file, after the previous handler. + +.. code-block:: python + + sb.request_handlers.extend([ + LaunchRequestHandler(), + HelloWorldIntentHandler(), + HelpIntentHandler(), + CancelAndStopIntentHandler(), + SessionEndedRequestHandler()]) + + sb.add_exception_handler(AllExceptionHandler()) + + handler = sb.lambda_handler() + + +Implementation using decorators +------------------------------- + +The following code implement the same functionality as above but uses function +decorators. You can think of the decorators as a replacement to the +``can_handle`` method implemented for each request handler above. + +If you would like to try the skill using this code please make sure that +your ``hello_world.py`` file contains only the following before adding the +handler functions: + +.. code-block:: python + + from ask_sdk_core.skill_builder import SkillBuilder + + sb = SkillBuilder() + +LaunchRequest Handler +~~~~~~~~~~~~~~~~~~~~~ + +The following code example shows how to configure a handler to be invoked +when the skill receives a +`LaunchRequest `_. +The LaunchRequest event occurs when the skill is invoked without a +specific intent. + +Type or paste the following code into your ``hello_world.py`` file, after the +previous code. + +.. code-block:: python + + from ask_sdk_core.utils import is_request_type + from ask_sdk_model.ui import SimpleCard + + @sb.request_handler(can_handle_func=is_request_type("LaunchRequest")) + def launch_request_handler(handler_input): + speech_text = "Welcome to the Alexa Skills Kit, you can say hello!" + + handler_input.response_builder.speak(speech_text).set_card( + SimpleCard("Hello World", speech_text)).set_should_end_session( + False) + return handler_input.response_builder.response + + +Note: Similar to the ``can_handle`` function for the LaunchRequestHandler in +the Class pattern, the decorator returns **True** if the incoming request is +a LaunchRequest. The ``handle`` function generates and returns a basic +greeting response in the same way the handle function works for the Class +pattern. + +HelloWorldIntent Handler +~~~~~~~~~~~~~~~~~~~~~~~~ + +The following code example shows how to configure a handler to be invoked +when the skill receives an intent request with the name HelloWorldIntent. +Type or paste the following code into your ``hello_world.py`` file, after +the previous handler. + +.. code-block:: python + + from ask_sdk_core.utils import is_intent_name + + @sb.request_handler(can_handle_func=is_intent_name("HelloWorldIntent")) + def hello_world_intent_handler(handler_input): + speech_text = "Hello World!" + + handler_input.response_builder.speak(speech_text).set_card( + SimpleCard("Hello World", speech_text)).set_should_end_session( + True) + return handler_input.response_builder.response + + +HelpIntent Handler +~~~~~~~~~~~~~~~~~~ + +The following code example shows how to configure a handler to be invoked +when the skill receives the Built-In Intent +`AMAZON.HelpIntent `_. +Type or paste the following code into your ``hello_world.py file``, after the +previous handler. + +.. code-block:: python + + @sb.request_handler(can_handle_func=is_intent_name("AMAZON.HelpIntent")) + def help_intent_handler(handler_input): + speech_text = "You can say hello to me!" + + handler_input.response_builder.speak(speech_text).ask(speech_text).set_card( + SimpleCard("Hello World", speech_text)) + return handler_input.response_builder.response + +Similar to the previous handler, this handler matches an IntentRequest with +the expected intent name. Basic help instructions are returned, and the user's +microphone will open up for them to respond due to ``.ask(speech_text)``. + + +CancelAndStopIntent Handler +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The CancelAndStopIntentHandler is similar to the HelpIntent handler, as it +is also triggered by Built-In +`AMAZON.CancelIntent or AMAZON.StopIntent Intents `_, +the SessionEndedRequestHandler is a good place to put your cleanup logic. +Type or paste the following code into your ``hello_world.py`` file, after the +previous handler. + +.. code-block:: python + + @sb.request_handler(can_handle_func=is_request_type("SessionEndedRequest")) + def session_ended_request_handler(handler_input): + #any cleanup logic goes here + + return handler_input.response_builder.response + + +Implementing Exception Handlers +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The following sample adds a *catch all* exception handler to your skill, to +ensure the skill returns a meaningful message in case of all exceptions. +Type or paste the following code into your ``hello_world.py`` file, after the +previous handler. + +.. code-block:: python + + @sb.exception_handler(can_handle_func=lambda i, e: True) + def all_exception_handler(handler_input, exception): + # Log the exception in CloudWatch Logs + print(exception) + + speech = "Sorry, I didn't get it. Can you please say it again!!" + handler_input.response_builder.speak(speech).ask(speech) + return handler_input.response_builder.response + + +Creating the Lambda Handler +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The `Lambda handler `_ +is the entry point for your AWS Lambda function. The following code example +creates a Lambda Handler function to route all inbound requests to your skill. +The Lambda Handler function creates an SDK Skill instance configured with +the request handlers that you just created. + +Type or paste the following code into your ``hello_world.py`` file, after +the previous handler. + +.. code-block:: python + + handler = sb.lambda_handler() + +Preparing your code for AWS Lambda +---------------------------------- + +Your code is now complete and we need to zip the files ready to upload to +Lambda. If you followed these instructions, zip the content of the +folder (not the folder itself) where you created the ``hello_world.py`` file. +Name the file ``skill.zip``. You can check the AWS Lambda docs to get more +information on creating a +`deployment package `_ +for a walkthrough on creating an AWS Lambda function with the correct role for +your skill. When creating the function, select the *Author from scratch* option +and select the ``Python 2.7`` or ``Python 3.6`` runtime. + +Once you've created your AWS Lambda function, it's time to give the Alexa +service the ability to invoke it. To do this, navigate to the **Triggers** tabs +in your Lambda's configuration, and add **Alexa Skills Kit** as the trigger +type. Once this is done, upload the ``skill.zip`` file produced in the previous step +and fill in the *handler* information with module_name.handler which is +``hello_world.handler`` for this example. + +Configuring and Testing Your Skill +---------------------------------- + +Now that the skill code has been uploaded to AWS Lambda, you can configure +the skill with Alexa. + +* Create a new skill by following these steps: + + 1. Log in to the `Alexa Skills Kit Developer Console `_. + 2. Click the **Create Skill** button in the upper right. + 3. Enter “HelloWorld” as your skill name and click Next. + 4. For the model, select **Custom** and click **Create skill**. + +* Next, define the interaction model for the skill. Select the **Invocation** + option from the sidebar and enter "greeter" for the **Skill Invocation Name**. + +* Next, add an intent called ``HelloWorldIntent`` to the interaction model. Click + the **Add** button under the + Intents section of the Interaction Model. Leave "**Create custom intent**" + selected, enter "**HelloWorldIntent**" for the intent name, and create the + intent. On the intent detail page, add some sample utterances that users can + say to invoke the intent. For this example, we’ve provided the following + sample utterances, but feel free to add others. + + :: + + say hello + say hello world + hello + say hi + say hi world + hi + how are you + + +* Since ``AMAZON.CancelIntent``, ``AMAZON.HelpIntent``, and ``AMAZON.StopIntent`` are + built-in Alexa intents, you do not need to provide sample utterances for them. + +* The Developer Console also allows you to edit the entire skill model in JSON + format. Select **JSON Editor** from the sidebar. For this sample, you can use + the following JSON schema. + + .. code-block:: json + + { + "interactionModel": { + "languageModel": { + "invocationName": "greeter", + "intents": [ + { + "name": "AMAZON.CancelIntent", + "samples": [] + }, + { + "name": "AMAZON.HelpIntent", + "samples": [] + }, + { + "name": "AMAZON.StopIntent", + "samples": [] + }, + { + "name": "HelloWorldIntent", + "slots": [], + "samples": [ + "how are you", + "hi", + "say hi world", + "say hi", + "hello", + "say hello world", + "say hello" + ] + } + ], + "types": [] + } + } + } + + +* Once you are done editing the interaction model, be sure to save and build + the model. + +* Next, configure the endpoint for the skill. To do this, follow these steps: + + 1. Under your skill, click **Endpoint** tab, select AWS Lambda ARN radiobutton + and copy the **Skill ID** of the skill you just created. + 2. Open the AWS Developer Console in a new tab + 3. Navigate to the lambda function created in the previous step. + 4. Under the **Alexa Skills Kit** trigger, enable the + **Skill ID Verification** and provide the skill id copied previously. + Click on Add and save once done, so that Lambda function will be updated. + 5. Copy the lambda function **ARN**. ARN is the unique resource number that + helps Alexa Service identifying the lambda function it needs to call + during skill invocation. + 6. Navigate to the Alexa Skills Kit Developer Console, and click on your + **HelloWorld** skill. + 7. Under your skill, click **Endpoint** tab, select **AWS Lambda ARN** and + paste in the ARN under 'Default Region' field. + 8. The rest of the settings can be left at their default values. + Click **Save Endpoints**. + 9. Click **Invocation** tab, save and build the model. + +* At this point you can test the skill. Click **Test** in the top navigation + to go to the Test page. Make sure that the **Test is enabled for this skill** + option is enabled. You can use the Test page to simulate requests, in text + and voice form. + +* Use the invocation name along with one of the sample utterances we just + configured as a guide. For example, *tell greeter to say hello* should result + in your skill responding with “Hello World” voice and "Hello World" card on + devices with display. You should also be able to go to the Alexa App (on + your phone or at https://alexa.amazon.com) and see your skill listed under + **Your Skills**. From here, you can enable the skill on your account for + testing from an Alexa enabled device. + +* At this point, feel free to start experimenting with your intents as well as + the corresponding request handlers in your skill's code. Once you're finished + iterating, you can optionally choose to move on to the process of getting + your skill certified and published so it can be used by Alexa users worldwide. diff --git a/docs/GETTING_STARTED.rst b/docs/GETTING_STARTED.rst new file mode 100644 index 0000000..2e808b8 --- /dev/null +++ b/docs/GETTING_STARTED.rst @@ -0,0 +1,151 @@ +======================= +Setting Up The ASK SDK +======================= + +Introduction +------------ + +This guide describes how to install the ASK SDK for Python in preparation for +developing an Alexa skill. + +Prerequisites +------------- + +The ASK SDK for Python requires **Python 2 (>= 2.7)** or **Python 3 (>= 3.6)**. +Before continuing, make sure you have a supported version of Python installed. +To show the version, from a command prompt run the following command: + +.. code-block:: sh + + >>> python --version + Python 3.6.5 + +You can download the latest version of Python +`here `_. + + +Adding the ASK SDK for Python to Your Project +--------------------------------------------- + +You can download and install the ASK SDK for Python from the Python Package +Index (PyPI) using the command line tool pip. If you are using Python 2 +version 2.7.9 or later or Python 3 version 3.4 or later, pip should be +installed with Python by default. + +Many Python developers prefer to work in a virtual environment, which is an +isolated Python environment that helps manage project dependencies and package +versions. The easiest way to get started is to install the SDK in a virtual +environment. See the section +`Set up the SDK in a virtual environment <#set-up-the-sdk-in-a-virtual-environment>`_. + +Another option is to install the ASK SDK for Python to a specific folder. This +ensures that you have the required dependencies and makes it easy to locate +and deploy the required files for your finished skill. See the section +`Set up the SDK in a specific folder <#set-up-the-sdk-in-a-specific-folder>`_. + +Set up the SDK in a virtual environment +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Use the following command to create a virtual environment and change to the +created folder: + +.. code-block:: sh + + >>> virtualenv skill + +Next, activate your virtual environment. If you are using MacOS or Linux, +use the following command: + +.. code-block:: sh + + >>> source skill/bin/activate + +Windows users need to use the following command: + +.. code-block:: bat + + >>> skill\Script\activate + +The command prompt should now be prefixed with (skill) indicating that you +are working inside the virtual environment. Use the following command to +install the ASK SDK for Python: + +.. code-block:: sh + + >>> pip install ask-sdk + +On MacOS and Linux, depending on the version of Python you are using, the +SDK will be installed into the ``skill/lib/Python3.6/site-packages`` folder. +On Windows it is installed to ``skill\Lib\site-packages``. The site-packages +folder is populated with directories including: + +.. code-block:: sh + + ask_sdk + ask_sdk_core + ask_sdk_dynamodb + ask_sdk_model + boto3 + … + +Set up the SDK in a specific folder +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To get started, from a command prompt create a new folder for your Alexa skill +and navigate to the folder: + +.. code-block:: sh + + >>> mkdir skill + >>> cd skill + +Next, install the ASK SDK for Python using pip. The ``-t`` option targets a +specific folder for installation: + +.. code-block:: sh + + >>> pip install ask-sdk -t ask-sdk + +This creates a folder named ask-sdk inside your skill folder and installs +the ASK SDK for Python and its dependencies. Your skill directory should now +contain the folder ask-sdk, which is populated with directories including: + +.. code-block:: sh + + ask_sdk + ask_sdk_core + ask_sdk_dynamodb + ask_sdk_model + boto3 + … + +Note +++++ + +If using Mac OS X and you have Python installed using +`Homebrew `_, the preceding command will not work. A simple +workaround is to add a ``setup.cfg`` file in your **ask-sdk** directory with +the following content: + +.. code-block:: sh + + [install] + prefix= + +Navigate to the ask-sdk folder and run the pip install command: + +.. code-block:: sh + + >>> cd ask-sdk + >>> pip install ask-sdk -t . + +More on this can be checked on the +`homebrew docs `_ + +Next Steps +---------- + +Now that you've added the SDK to your project, you're ready to begin +developing your skill. Proceed to the next section +`Developing Your First Skill `_, for +instructions on getting started with a basic skill. diff --git a/docs/REQUEST_PROCESSING.rst b/docs/REQUEST_PROCESSING.rst new file mode 100644 index 0000000..ff3f985 --- /dev/null +++ b/docs/REQUEST_PROCESSING.rst @@ -0,0 +1,605 @@ +================== +Request Processing +================== + +This guide provides information on the following request processing components +available in the SDK, for skill development: + +- `Request Handlers <#request-handlers>`_ +- `Exception Handlers <#exception-handlers>`_ +- `Request and Response Interceptors <#request-and-response-interceptors>`_ +- `Handler Input <#handler-input>`_ + +Handler Input +============= + +Request Handlers, Request and Response Interceptors, and Exception Handlers +are all passed a global ``HandlerInput`` object during invocation. This object +exposes various entities useful in request processing, including: + +- **request_envelope**: Contains the entire `request + body `__ + sent to skill. +- **attributes_manager**: Provides access to request, session, and + persistent attributes. +- **service_client_factory**: Constructs service clients capable of + calling Alexa APIs. +- **response_builder**: Contains helper function to build responses. +- **context**: Provides an optional, context object passed in by the + host container. For example, for skills running on AWS Lambda, this + is the `context + object `__ + for the AWS Lambda function. + + +Request Handlers +================ + +Request handlers are responsible for handling one or more types of +incoming alexa requests. There are two ways of creating custom request +handlers: + +- By implementing the ``AbstractRequestHandler`` class. +- By decorating a custom handle function using the + `Skill Builder `__ ``request_handler`` + decorator. + +Interface +--------- + +AbstractRequestHandler Class +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you plan on using the ``AbstractRequestHandler`` class, you will +need to implement the following methods : + +- **can_handle**: ``can_handle`` method is called by the SDK to + determine if the given handler is capable of processing the incoming + request. This function accepts a `Handler Input <#handler-input>`__ + object and expects a boolean to be returned. If the method returns + **True**, then the handler is supposed to handle the request + successfully. If it returns **False**, the handler is not supposed + to handle the input request and hence not executed to completion. + Because of the various attributes in ``HandlerInput`` object, you + can write any condition to let SDK know whether the request can be + handled gracefully or not. +- **handle**: ``handle`` method is called by the SDK when invoking the + request handler. This function contains the handler’s request + processing logic, accepts `Handler Input <#handler-input>`__ and + returns a ``Response`` object. + +.. code:: python + + class AbstractRequestHandler(object): + @abstractmethod + def can_handle(self, handler_input): + # type: (HandlerInput) -> bool + pass + + @abstractmethod + def handle(self, handler_input): + # type: (HandlerInput) -> Response + pass + +The following example shows a request handler class that can handle the +``HelloWorldIntent``. + +.. code:: python + + from ask_sdk_core.dispatch_components import AbstractRequestHandler + from ask_sdk_model.ui import SimpleCard + + class HelloWorldIntentHandler(AbstractRequestHandler): + def can_handle(self, handler_input): + return handler_input.request_envelope.request.type == "IntentRequest" + and handler_input.request_envelope.request.intent.name == "HelloWorldIntent" + + def handle(self, handler_input): + speech_text = "Hello World"; + + return handler_input.response_builder.speak(speech_text).set_card( + SimpleCard("Hello World", speech_text)).response + +The ``can_handle`` function detects if the incoming request is an +``IntentRequest`` and returns true if the intent name is +``HelloWorldIntent``. The ``handle`` function generates and returns a +basic "Hello World" response. + +request_handler decorator from SkillBuilder +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``request_handler`` decorator from SkillBuilder class is a custom wrapper +on top of the ``AbstractRequestHandler`` class and provides the same +functionality to any custom decorated function. However, there are couple of +things to take into consideration, before using the decorator: + +- The decorator expects a ``can_handle_func`` parameter. This is similar to + the ``can_handle`` method in ``AbstractRequestHandler``. The value passed + should be a function that accepts a `Handler Input <#handler-input>`__ + object and returns a ``boolean`` value +- The decorated function should accept only one parameter, which is the + `Handler Input <#handler-input>`__ object and may return a ``Response`` + object. + +.. code:: python + + class SkillBuilder(object): + .... + def request_handler(self, can_handle_func): + def wrapper(handle_func): + # wrap the can_handle and handle into a class + # add the class into request handlers list + .... + return wrapper + +The following example shows a request handler function that can handle the +``HelloWorldIntent``. + +.. code-block:: python + + from ask_sdk_core.utils import is_intent_name + from ask_sdk_model.ui import SimpleCard + from ask_sdk_core.skill_builder import SkillBuilder + + sb = SkillBuilder() + + @sb.request_handler(can_handle_func = is_intent_name("HelloWorldIntent")) + def hello_world_intent_handler(handler_input): + speech_text = "Hello World!" + + return handler_input.response_builder.speak(speech_text).set_card( + SimpleCard("Hello World", speech_text)).response + +The ``is_intent_name`` function accepts a ``string`` parameter and returns an +anonymous function which accepts a ``HandlerInput`` as input parameter and +checks if the incoming request in ``HandlerInput`` is an ``IntentRequest`` and +returns if the intent name is the passed in ``string``, which is +``HelloWorldIntent`` in this example. The ``handle`` function generates and returns a +basic "Hello World" response. + +Registering and Processing the Request Handlers +----------------------------------------------- + + +The SDK calls the ``can_handle`` function on its request handlers in the +order in which they were provided to the ``Skill`` builder. + +If you are following the ``AbstractRequestHandler`` class approach, then +you can register the request handlers in the following way + +.. code-block:: python + + from ask_sdk_core.skill_builder import SkillBuilder + + sb = SkillBuilder() + + # Implement FooHandler, BarHandler, BazHandler classes + + sb.request_handlers.extend([ + FooHandler(), + BarHandler(), + BazHandler()]) + +If you are following the ``request_handler`` decorator approach, then +there is no need to explicitly register the handler functions, since +they are already decorated using a skill builder instance. + +.. code-block:: python + + from ask_sdk_core.skill_builder import SkillBuilder + + sb = SkillBuilder() + + # decorate foo_handler, bar_handler, baz_handler functions + +In the above example, the SDK calls request handlers in the following order: + +1. ``FooHandler`` class / ``foo_handler`` function +2. ``BarHandler`` class / ``bar_handler`` function +3. ``BazHandler`` class / ``baz_handler`` function + +The SDK always chooses the first handler that is capable of handling a +given request. In this example, if both ``FooHandler`` class /``foo_handler`` function +and ``BarHandler`` class /``bar_handler`` function are capable of handling a particular +request, ``FooHandler`` class /``foo_handler`` function is always invoked. +Keep this in mind when designing and registering request handlers. + + +Exception Handlers +============== + +Exception handlers are similar to request handlers, but are instead +responsible for handling one or more types of exceptions. They are invoked +by the SDK when an unhandled exception is thrown during the course of +request processing. + +In addition to the `Handler Input <#handler-input>`_ object, the handler +also has access to the exception raised during handling the input +request, thus making it easier for the handler to figure out how to +handle the corresponding exception. + +Similar to `Request Handlers <#request-handlers>`_, custom +request interceptors can be implemented in two ways: + +- By implementing the ``AbstractExceptionHandler`` class. +- By decorating a custom exception handling function using the + `Skill Builder `__ + ``exception_handler`` decorator. + +Interface +--------- + +AbstractExceptionHandler Class +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you plan on using the ``AbstractExceptionHandler`` class, you will +need to implement the following methods : + +- **can_handle**: ``can_handle`` method, which is called by the SDK + to determine if the given handler is capable of handling the exception. + This function returns **True** if the handler can handle the exception, + or **False** if not. Return ``True`` in all cases to create a catch-all + handler. +- **handle**: ``handle`` method, which is called by the SDK when invoking + the exception handler. This function contains all exception handling logic, + and returns a ``Response`` object. + +.. code:: python + + class AbstractExceptionHandler(object): + @abstractmethod + def can_handle(self, handler_input, exception): + # type: (HandlerInput, Exception) -> bool + pass + + @abstractmethod + def handle(self, handler_input, exception): + # type: (HandlerInput, Exception) -> Response + pass + +The following example shows an exception handler that can handle any exception +with name that contains “AskSdk”. + +.. code:: python + + class AskExceptionHandler(AbstractExceptionHandler): + def can_handle(self, handler_input, exception): + return 'AskSdk' in exception.__class__.__name__ + + def handle(self, handler_input, exception): + speech_text = "Sorry, I am unable to figure out what to do. Try again later!!"; + + return handler_input.response_builder.speak(speech_text).response + +The handler’s ``can_handle`` method returns True if the incoming exception +has a name that starts with “AskSdk”. The ``handle`` method returns a +graceful exception response to the user. + +exception_handler decorator from SkillBuilder +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``exception_handler`` decorator from SkillBuilder class is a custom wrapper +on top of the ``AbstractExceptionHandler`` class and provides the same +functionality to any custom decorated function. However, there are couple of +things to take into consideration, before using the decorator: + +- The decorator expects a ``can_handle_func`` parameter. This is similar to + the ``can_handle`` method in ``AbstractExceptionHandler``. The value passed + should be a function that accepts a `Handler Input <#handler-input>`__ + object, an ``Exception`` instance and returns a ``boolean`` value. +- The decorated function should accept only two parameters, the + `Handler Input <#handler-input>`__ object and ``Exception`` object. It may + return a ``Response`` object. + +.. code:: python + + class SkillBuilder(object): + .... + def exception_handler(self, can_handle_func): + def wrapper(handle_func): + # wrap the can_handle and handle into a class + # add the class into exception handlers list + .... + return wrapper + +The following example shows an exception handler function that can handle any exception +with name that contains “AskSdk”. + +.. code-block:: python + + from ask_sdk_core.skill_builder import SkillBuilder + + sb = SkillBuilder() + + @sb.exception_handler(can_handle_func = lambda input, e: 'AskSdk' in e.__class__.__name__) + def ask_exception_intent_handler(handler_input, exception): + speech_text = "Sorry, I am unable to figure out what to do. Try again later!!"; + + return handler_input.response_builder.speak(speech_text).response + + +Registering and Processing the Exception Handlers +------------------------------------------------- + +If you are following the ``AbstractExceptionHandler`` class approach, then +you can register the request handlers in the following way + +.. code-block:: python + + from ask_sdk_core.skill_builder import SkillBuilder + + sb = SkillBuilder() + + # Implement FooExceptionHandler, BarExceptionHandler, BazExceptionHandler classes + + sb.add_exception_handler(FooExceptionHandler()) + sb.add_exception_handler(BarExceptionHandler()) + sb.add_exception_handler(BazExceptionHandler()) + +If you are following the ``exception_handler`` decorator approach, then +there is no need to explicitly register the handler functions, since +they are already decorated using a skill builder instance. + +.. code-block:: python + + from ask_sdk_core.skill_builder import SkillBuilder + + sb = SkillBuilder() + + # decorate foo_exception_handler, bar_exception_handler, baz_exception_handler functions + + +Like request handlers, exception handlers are executed in the order in which +they were provided to the Skill. + +Request and Response Interceptors +================================= + +The SDK supports Global Request and Response Interceptors that execute +**before** and **after** matching ``RequestHandler`` execution, respectively. + +Request Interceptors +-------------------- + +The Global Request Interceptor accepts a `Handler Input `_ +object and processes it, before processing any of the registered request +handlers. Similar to `Request Handlers <#request-handlers>`_, custom +request interceptors can be implemented in two ways: + +- By implementing the ``AbstractRequestInterceptor`` class. +- By decorating a custom process function using the + `Skill Builder `__ + ``global_request_interceptor`` decorator. + +Interface +~~~~~~~~~ + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +AbstractRequestInterceptor Class +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``AbstractRequestInterceptor`` class usage needs you to implement the +``process`` method. This method takes a `Handler Input <#handler-input>`_ +instance and doesn't return anything. + +.. code:: python + + class AbstractRequestInterceptor(object): + @abstractmethod + def process(self, handler_input): + # type: (HandlerInput) -> None + pass + +The following example shows a request interceptor class that can print the +request received by Alexa service, in AWS CloudWatch logs, before handling it. + +.. code:: python + + from ask_sdk_core.dispatch_components import AbstractRequestInterceptor + + class LoggingRequestInterceptor(AbstractRequestInterceptor): + def process(self, handler_input): + print("Request received: {}".format(handler_input.request_envelope.request)) + + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +global_request_interceptor decorator from SkillBuilder +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``global_request_interceptor`` decorator from SkillBuilder class is a custom +wrapper on top of the ``AbstractRequestInterceptor`` class and provides the same +functionality to any custom decorated function. However, there are couple of +things to take into consideration, before using the decorator: + +- The decorator should be invoked as a function rather than as a function name, + since it requires the skill builder instance, to register the interceptor. +- The decorated function should accept only one parameter, which is the + `Handler Input <#handler-input>`__ object and the return value from the function + is not captured. + +.. code:: python + + class SkillBuilder(object): + .... + def global_request_interceptor(self): + def wrapper(process_func): + # wrap the process_func into a class + # add the class into request interceptors list + .... + return wrapper + +The following example shows a logging function that can be used as request interceptor. + +.. code-block:: python + + from ask_sdk_core.skill_builder import SkillBuilder + + sb = SkillBuilder() + + @sb.global_request_interceptor() + def request_logger(handler_input): + print("Request received: {}".format(handler_input.request_envelope.request)) + + +Registering and Processing the Request Interceptors +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Request interceptors are invoked immediately before execution of the request handler +for an incoming request. Request attributes in `Handler Input <#handler-input>`_'s +``Attribute Manager`` provide a way for request interceptors to pass data and entities +on to other request interceptors and request handlers. + +If you are following the ``AbstractRequestInterceptor`` class approach, then +you can register the request interceptors in the following way + +.. code-block:: python + + from ask_sdk_core.skill_builder import SkillBuilder + + sb = SkillBuilder() + + # Implement FooInterceptor, BarInterceptor, BazInterceptor classes + + sb.add_global_request_interceptor(FooInterceptor()) + sb.add_global_request_interceptor(BarInterceptor()) + sb.add_global_request_interceptor(BazInterceptor()) + +If you are following the ``global_request_interceptor`` decorator approach, then +there is no need to explicitly register the interceptor functions, since +they are already decorated using a skill builder instance. + +.. code-block:: python + + from ask_sdk_core.skill_builder import SkillBuilder + + sb = SkillBuilder() + + # decorate foo_interceptor, bar_interceptor, baz_interceptor functions + +In the above example, the SDK executes all request interceptors in the following order: + +1. ``FooInterceptor`` class / ``foo_interceptor`` function +2. ``BarInterceptor`` class / ``bar_interceptor`` function +3. ``BazInterceptor`` class / ``baz_interceptor`` function + + +Response Interceptors +--------------------- + +The Global Response Interceptor accepts a `Handler Input <#handler-input>`_ +object, a `Response` and processes them, after executing the supported request +handler. Similar to `Request Interceptors <#request-interceptors>`_, custom +response interceptors can be implemented in two ways: + +- By implementing the ``AbstractResponseInterceptor`` class. +- By decorating a custom process function using the + `Skill Builder `__ + ``global_response_interceptor`` decorator. + +Interface +~~~~~~~~~ + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +AbstractResponseInterceptor Class +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``AbstractResponseInterceptor`` class usage needs you to implement the +``process`` method. This method takes a `Handler Input <#handler-input>`_ +instance, a ``Response`` object that is returned from the previously executed +request handler. The method doesn't return anything. + +.. code:: python + + class AbstractResponseInterceptor(object): + @abstractmethod + def process(self, handler_input, response): + # type: (HandlerInput, Response) -> None + pass + +The following example shows a response interceptor class that can print the +response received from successfully handling the request, in AWS CloudWatch logs, +before returning it to the Alexa Service. + +.. code:: python + + from ask_sdk_core.dispatch_components import AbstractResponseInterceptor + + class LoggingResponseInterceptor(AbstractResponseInterceptor): + def process(handler_input, response): + print("Response generated: {}".format(response)) + + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +global_response_interceptor decorator from SkillBuilder +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``global_response_interceptor`` decorator from SkillBuilder class is a custom +wrapper on top of the ``AbstractResponseInterceptor`` class and provides the same +functionality to any custom decorated function. However, there are couple of +things to take into consideration, before using the decorator: + +- The decorator should be invoked as a function rather than as a function name, + since it requires the skill builder instance, to register the interceptor. +- The decorated function should accept two parameters, which are the + `Handler Input <#handler-input>`__ object and ``Response`` object respectively. + The return value from the function is not captured. + +.. code:: python + + class SkillBuilder(object): + .... + def global_response_interceptor(self): + def wrapper(process_func): + # wrap the process_func into a class + # add the class into response interceptors list + .... + return wrapper + +The following example shows a logging function that can be used as response interceptor. + +.. code-block:: python + + from ask_sdk_core.skill_builder import SkillBuilder + + sb = SkillBuilder() + + @sb.global_reresponse_interceptor() + def response_logger(handler_input, response): + print("Response generated: {}".format(response)) + + +Registering and Processing the Response Interceptors +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Response interceptors are invoked immediately after execution of the request handler +for an incoming request. + +If you are following the ``AbstractResponseInterceptor`` class approach, then +you can register the response interceptors in the following way + +.. code-block:: python + + from ask_sdk_core.skill_builder import SkillBuilder + + sb = SkillBuilder() + + # Implement FooInterceptor, BarInterceptor, BazInterceptor classes + + sb.add_global_response_interceptor(FooInterceptor()) + sb.add_global_response_interceptor(BarInterceptor()) + sb.add_global_response_interceptor(BazInterceptor()) + +If you are following the ``global_response_interceptor`` decorator approach, then +there is no need to explicitly register the interceptor functions, since +they are already decorated using a skill builder instance. + +.. code-block:: python + + from ask_sdk_core.skill_builder import SkillBuilder + + sb = SkillBuilder() + + # decorate foo_interceptor, bar_interceptor, baz_interceptor functions + +Similar to the processing of `Request Interceptors <#request-interceptors>`_, +all of the response interceptors are executed in the same order they are registered. diff --git a/docs/RESPONSE_BUILDING.rst b/docs/RESPONSE_BUILDING.rst new file mode 100644 index 0000000..8f8700a --- /dev/null +++ b/docs/RESPONSE_BUILDING.rst @@ -0,0 +1,88 @@ +ResponseBuilder +=============== + +The SDK includes helper functions for constructing responses. A +``Response`` may contain multiple elements, and the helper functions aid +in generating responses, reducing the need to initialize and set the +elements of each response. + +Interface +~~~~~~~~~ + +.. code:: python + + class ResponseFactory(object): + def __init__(self): + self.response = .... # Response object + + def speak(self, speech): + # type: (str) -> 'ResponseFactory' + .... + + def ask(self, speech): + # type: (str) -> 'ResponseFactory' + .... + + def set_card(self, card): + # type: (Card) -> 'ResponseFactory' + .... + + def set_directive(self, directive): + # type: (Directive) -> 'ResponseFactory' + .... + + def set_should_end_session(self, end_session): + # type: (bool) -> 'ResponseFactory' + .... + +The following example shows how to construct a response using +``ResponseFactory`` helper functions. + +.. code:: python + + def handle(handler_input): + handler_input.response_builder.speak('foo').ask('bar').set_card( + SimpleCard('title', 'content')) + return handler_input.response_builder.response + +Text Helpers +~~~~~~~~~~~~ + +The following helper functions are provided to skill developers, to +help with text content generation: + +get_plain_text_content +---------------------- + +.. code:: python + + def get_plain_text_content(primary_text, secondary_text, tertiary_text): + # type: (str, str, str) -> TextContent + # Create a text content object with text as PlainText type + .... + + +get_rich_text_content +---------------------- + +.. code:: python + + def get_rich_text_content(primary_text, secondary_text, tertiary_text): + # type: (str, str, str) -> TextContent + # Create a text content object with text as RichText type + .... + + +get_text_content +---------------------- + +.. code:: python + + def get_text_content( + primary_text, primary_text_type, + secondary_text, secondary_text_type, + tertiary_text, tertiary_text_type): + # type: (str, str, str, str, str, str) -> TextContent + # Create a text content object with text as corresponding passed-type + # Passed-in type is defaulted to PlainText + .... diff --git a/docs/SERVICE_CLIENTS.rst b/docs/SERVICE_CLIENTS.rst new file mode 100644 index 0000000..49cddcb --- /dev/null +++ b/docs/SERVICE_CLIENTS.rst @@ -0,0 +1,28 @@ +Alexa Service Clients +===================== + +The SDK includes service clients that you can use to call Alexa APIs +from within your skill logic. + +Service clients can be used in any request handler, exception handler, +and request, response interceptors. The ``service_client_factory`` +contained inside the `Handler Input `_ +allows you to retrieve client instances for every supported Alexa service. The +``service_client_factory`` is only available for use, when you +`configure the skill instance `_ +with an ``ApiClient``. + +The following example shows the ``handle`` function for a request +handler that creates an instance of the device address service client. +Creating a service client instance is as simple as calling the +appropriate factory function. + +.. code:: python + + def handle(handler_input): + device_id = handler_input.request_envelope.context.system.device.device_id + device_addr_service_client = handler_input.service_client_factory.get_device_address_service() + addr = device_addr_service_client.get_full_address(device_id) + # Other handler logic goes here + .... + diff --git a/docs/SKILL_BUILDERS.rst b/docs/SKILL_BUILDERS.rst new file mode 100644 index 0000000..2829dae --- /dev/null +++ b/docs/SKILL_BUILDERS.rst @@ -0,0 +1,113 @@ +Skill Builders +============== + +The SDK includes a ``SkillBuilder`` that provides utility methods, to +construct the ``Skill`` instance. It has the following structure: + +.. code:: python + + class SkillBuilder(object): + def __init__(self): + # Initialize empty collections for request components, + # exception handlers, interceptors. + + def add_request_handler(self, handler): + # type: (AbstractRequestHandler) -> None + .... + + def add_exception_handler(self, handler): + # type: (AbstractExceptionHandler) -> None + .... + + def add_global_request_interceptor(self, interceptor): + # type: (AbstractRequestInterceptor) -> None + .... + + def add_global_response_interceptor(self, interceptor): + # type: (AbstractResponseInterceptor) -> None + .... + + @property + def skill_configuration(self): + # type: () -> SkillConfiguration + # Build configuration object using the registered components + .... + + def create(self): + # type: () -> Skill + # Create the skill using the skill configuration + .... + + def lambda_handler(self): + # type: () -> LambdaHandler + # Create a lambda handler function that can be tagged to + # AWS Lambda handler. + # Processes the alexa request before invoking the skill, + # processes the alexa response before providing to the service + .... + + def request_handler(self, can_handle_func): + # type: (Callable[[HandlerInput], bool]) -> None + # Request Handler decorator + + def exception_handler(self, can_handle_func): + # type: (Callable[[HandlerInput, Exception], bool]) -> None + # Exception Handler decorator + + def global_request_interceptor(self): + # type: () -> None + # Global Request Interceptor decorator + + def global_response_interceptor(self): + # type: () -> None + # Global Response Interceptor decorator + +There are two extensions to ``SkillBuilder`` class, ``CustomSkillBuilder`` +and ``StandardSkillBuilder``. + +CustomSkillBuilder Class +------------------------ + +``CustomSkillBuilder`` is available in both ``ask-sdk-core`` and +``ask-sdk`` package. In addition to the common helper function above, +``CustomSkillBuilder`` also provides functions that allows you to +register custom ``AbstractPersistentAdapter`` and ``ApiClient``. + +.. code:: python + + class CustomSkillBuilder(SkillBuilder): + def __init__(self, persistence_adapter=None, api_client=None): + # type: (AbstractPersistenceAdapter, ApiClient) -> None + .... + + @property + def skill_configuration(self): + # Create skill configuration from skill builder along with + # registered persistence adapter and api client + .... + + +StandardSkillBuilder Class +-------------------------- + +``StandardSkillBuilder`` is available only in the ``ask-sdk`` package. +It uses ``DynamoDbPersistenceAdapter`` and ``DefaultApiClient`` to +provide Persistence and Service Client features. It also provides helper functions for +configuring the Dynamo DB table options. + +.. code:: python + + class StandardSkillBuilder(SkillBuilder): + def __init__(self, + table_name=None, auto_create_table=None, + partition_keygen=None, dynamodb_client=None): + # type: (str, bool, Callable[[RequestEnvelope], str], ServiceResource) -> None) + .... + + @property + def skill_configuration(self): + # Create skill configuration from skill builder along with + # default api client and dynamodb persistence adapter with + # the passed in table configuration options. + .... + diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt new file mode 100644 index 0000000..e69de29 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/samples/ColorPicker/README.rst b/samples/ColorPicker/README.rst new file mode 100644 index 0000000..357c8d5 --- /dev/null +++ b/samples/ColorPicker/README.rst @@ -0,0 +1,231 @@ +Alexa Skills Kit SDK Sample - Color Picker +========================================== + +A simple `AWS Lambda `__ function and the skill +interaction schema that demonstrates how to write a color picker skill for the +Amazon Echo using the Alexa Skill Kit Python SDK. + +**NOTE**: This sample is subject to change during the beta period. + +Concepts +-------- + +This sample shows how to create a Lambda function for handling Alexa +Skill requests that: + +- Demonstrates using custom slot types to handle a + finite set of known values. +- Use session attributes to store and pass session level attributes. + In this example, favorite color is stored and passed on the skill + session. +- Use a catch-all exception handler, for catching all exceptions + during skill invocation and return a meaningful response to the + user. More on `exception handlers here <../../docs/REQUEST_PROCESSING.rst#exception-handlers>`__ +- Custom Request and Response Interceptors: This example shows two + use cases of interceptors. More on the + `request and response interceptors here <../../docs/REQUEST_PROCESSING.rst#request-and-response-interceptors>`__ + + - Log Alexa Request and Response objects using global interceptors. + - Add card to all responses, using the SSML text in ``outputSpeech`` + in response. + +Setup +----- + +To run this example skill you need to do two things. The first is to +deploy the example code in lambda, and the second is to configure the +Alexa skill to use Lambda. + +Prerequisites +~~~~~~~~~~~~~ + +Please follow the +`prerequisites <../../docs/GETTING_STARTED.rst#prerequisites>`_ section and the +`adding ASK SDK <../../docs/GETTING_STARTED.rst#adding-the-ask-sdk-to-your-project>`_ +section in +Getting Started documentation. For this sample skill, you only need +the ``ASK SDK Core`` package. + +AWS Lambda Setup +~~~~~~~~~~~~~~~~ + +Refer to +`Hosting a Custom Skill as an AWS Lambda Function `__ +reference for a walkthrough on creating a AWS Lambda function with the +correct role for your skill. When creating the function, select the +"Author from scratch" option, and select Python 2.7 or Python 3.6 runtime. + +To prepare the skill for upload to AWS Lambda, create a zip file that +contains `color_picker.py `_, the SDK and it's dependencies. +Make sure to compress all files directly, **NOT** the project folder. You can +check the AWS Lambda docs to get more information on +`creating a deployment package `_. + +Once you’ve created your AWS Lambda function and configured "Alexa +Skills Kit" as a trigger, upload the ZIP file produced in the previous +step and set the handler to the fully qualified class name of your +handler function. In this example, it would be ``color_picker.handler``. +Finally, copy the ARN for your AWS Lambda function +because you’ll need it when configuring your skill in the Amazon +Developer console. + +Alexa Skill Setup +~~~~~~~~~~~~~~~~~ + +Now that the skill code has been uploaded to AWS Lambda we’re ready to +configure the skill with Alexa. First, navigate to the +`Alexa Skills Kit Developer Console `__. +Click the "Create Skill" button in the upper right. Enter "ColorPicker" +as your skill name. On the next page, select "Custom" and click "Create +skill". + +Now we’re ready to define the interaction model for the skill. Under +"Invocation" tab on the left side, define your Skill Invocation Name to +be ``color picker``. + +Now it’s time to add an intent to the skill. Click the "Add" button +under the Intents section of the Interaction Model. Leave "Create custom +intent" selected, enter "WhatsMyColorIntent" for the intent name, and +create the intent. Now it’s time to add some sample utterances that will +be used to invoke the intent. For this example, we’ve provided the +following sample utterances, but feel free to add others. + +:: + + whats my color + what is my color + say my color + tell me my color + whats my favorite color + what is my favorite color + say my favorite color + tell me my favorite color + tell me what my favorite color is + +Let’s add a Slot Type. You can find it below Built-In Intents.Click "Add +Slot Type" and under "Use an existing slot type from Alexa's built-in library", +search for "Color", and add "AMAZON.Color" slot type. + +Let’s add another intent to the skill that has slots, called +"MyColorIsIntent" for intent name. Skip the sample utterances part for +now and create a new slot called "Color". Select Slot Type to be +"AMAZON.Color". Now add below sample utterances that uses this slot +"Color". + +:: + + my color is {Color} + my favorite color is {Color} + +Since **AMAZON.CancelIntent**, **AMAZON.HelpIntent**, and **AMAZON.StopIntent** are +built-in Alexa intents, sample utterances do not need to be provided as +they are automatically inherited. + +The Developer Console alternately allows you to edit the entire skill +model in JSON format by selecting "JSON Editor" on the navigation bar. +For this sample, the +`interaction schema `_ can be used. + +.. code:: json + + { + "interactionModel": { + "languageModel": { + "invocationName": "my color picker", + "intents": [ + { + "name": "AMAZON.FallbackIntent", + "samples": [] + }, + { + "name": "AMAZON.CancelIntent", + "samples": [] + }, + { + "name": "AMAZON.HelpIntent", + "samples": [] + }, + { + "name": "AMAZON.StopIntent", + "samples": [] + }, + { + "name": "WhatsMyColorIntent", + "slots": [], + "samples": [ + "tell me what is my favorite color", + "whats my favorite color", + "say my color", + "say my favorite color", + "tell me my favorite color", + "what is my favorite color", + "what is my color", + "whats my color" + ] + }, + { + "name": "MyColorIsIntent", + "slots": [ + { + "name": "Color", + "type": "AMAZON.Color" + } + ], + "samples": [ + "My color is {Color}", + "my favorite color is {Color}" + ] + } + ], + "types": [] + } + } + } + +Once you’re done editing the interaction model don’t forget to save and +build the model. + +Let’s move on to the skill configuration section. Under "Endpoint" +select "AWS Lambda ARN" and paste in the ARN of the function you created +previously. The rest of the settings can be left at their default +values. Click "Save Endpoints" and proceed to the next section. + +Under the AWS lambda function "Alexa Skills Kit" trigger, enable the "Skill Id +verification" and provide the Skill Id from the skill endpoint screen. Save +the lambda function. + +Finally you’re ready to test the skill! In the "Test" tab of the +developer console you can simulate requests, in text and voice form, to +your skill. Use the invocation name along with one of the sample +utterances we just configured as a guide. You should also be able to go +to the `Echo webpage `__ and see your +skill listed under "Your Skills", where you can enable the skill on your +account for testing from an Alexa enabled device. + +At this point, feel free to start experimenting with your Intent Schema +as well as the corresponding request handlers in your skill’s +implementation. Once you’re finished iterating, you can optionally +choose to move on to the process of getting your skill certified and +published so it can be used by Alexa users worldwide. + +Additional Resources +-------------------- + +Community +~~~~~~~~~ + +- `Amazon Developer Forums `_ : Join the conversation! +- `Hackster.io `_ - See what others are building with Alexa. + +Tutorials & Guides +~~~~~~~~~~~~~~~~~~ + +- `Voice Design Guide `_ - + A great resource for learning conversational and voice user interface design. + +Documentation +~~~~~~~~~~~~~ + +- `Official Alexa Skills Kit Python SDK Docs <../../README.rst>`_ +- `Official Alexa Skills Kit Docs `_ + diff --git a/samples/ColorPicker/color_picker.py b/samples/ColorPicker/color_picker.py new file mode 100644 index 0000000..c8678d5 --- /dev/null +++ b/samples/ColorPicker/color_picker.py @@ -0,0 +1,192 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 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. +# + +from ask_sdk_core.skill_builder import SkillBuilder +from ask_sdk_core.utils import is_request_type, is_intent_name +from ask_sdk_model.ui import SimpleCard + +######## Convert SSML to Card text ############ +# This is for automatic conversion of ssml to text content on simple card +# You can create your own simple cards for each response, if this is not +# what you want to use. + +from six import PY3 +if PY3: + from html.parser import HTMLParser +else: + from HTMLParser import HTMLParser + + +class SSMLStripper(HTMLParser): + def __init__(self): + self.reset() + self.full_str_list = [] + if PY3: + self.strict = False + self.convert_charrefs = True + + def handle_data(self, d): + self.full_str_list.append(d) + + def get_data(self): + return ''.join(self.full_str_list) + +################################################ + + +skill_name = "My Color Session" +help_text = ("Please tell me your favorite color. You can say " + "my favorite color is red") + +color_slot_key = "COLOR" +color_slot = "Color" + +sb = SkillBuilder() + + +@sb.request_handler(can_handle_func=is_request_type("LaunchRequest")) +def launch_request_handler(handler_input): + # Handler for Skill Launch + speech = "Welcome to the Alexa Skills Kit color session sample." + + handler_input.response_builder.speak( + speech + " " + help_text).ask(help_text) + return handler_input.response_builder.response + + +@sb.request_handler(can_handle_func=is_intent_name("AMAZON.HelpIntent")) +def help_intent_handler(handler_input): + # Handler for Help Intent + handler_input.response_builder.speak(help_text).ask(help_text) + return handler_input.response_builder.response + + +@sb.request_handler( + can_handle_func=lambda input: + is_intent_name("AMAZON.CancelIntent")(input) or + is_intent_name("AMAZON.StopIntent")(input)) +def cancel_and_stop_intent_handler(handler_input): + # Single handler for Cancel and Stop Intent + speech_text = "Goodbye!" + + return handler_input.response_builder.speak(speech_text).response + + +@sb.request_handler(can_handle_func=is_request_type("SessionEndedRequest")) +def session_ended_request_handler(handler_input): + # Handler for Session End + return handler_input.response_builder.response + + +@sb.request_handler(can_handle_func=is_intent_name("WhatsMyColorIntent")) +def whats_my_color_handler(handler_input): + # Check if a favorite color has already been recorded in session attributes + # If yes, provide the color to the user. If not, ask for favorite color + if color_slot_key in handler_input.attributes_manager.session_attributes: + fav_color = handler_input.attributes_manager.session_attributes[ + color_slot_key] + speech = "Your favorite color is {}. Goodbye!!".format(fav_color) + handler_input.response_builder.set_should_end_session(True) + else: + speech = "I don't think I know your favorite color. " + help_text + handler_input.response_builder.ask(help_text) + + handler_input.response_builder.speak(speech) + return handler_input.response_builder.response + + +@sb.request_handler(can_handle_func=is_intent_name("MyColorIsIntent")) +def my_color_handler(handler_input): + # Check if color is provided in slot values. If provided, then + # set your favorite color from slot value into session attributes. + # If not, then it asks user to provide the color. + slots = handler_input.request_envelope.request.intent.slots + + if color_slot in slots: + fav_color = slots[color_slot].value + handler_input.attributes_manager.session_attributes[ + color_slot_key] = fav_color + speech = ("Now I know that your favorite color is {}. " + "You can ask me your favorite color by saying, " + "what's my favorite color ?".format(fav_color)) + reprompt = ("You can ask me your favorite color by saying, " + "what's my favorite color ?") + else: + speech = "I'm not sure what your favorite color is, please try again" + reprompt = ("I'm not sure what your favorite color is. " + "You can tell me your favorite color by saying, " + "my favorite color is red") + + handler_input.response_builder.speak(speech).ask(reprompt) + return handler_input.response_builder.response + + +@sb.request_handler(can_handle_func=is_intent_name("AMAZON.FallbackIntent")) +def fallback_handler(handler_input): + # AMAZON.FallbackIntent is only available in en-US locale. + # This handler will not be triggered except in that locale, + # so it is safe to deploy on any locale + speech = ( + "The {} skill can't help you with that. " + "You can tell me your favorite color by saying, " + "my favorite color is red").format(skill_name) + reprompt = ("You can tell me your favorite color by saying, " + "my favorite color is red") + handler_input.response_builder.speak(speech).ask(reprompt) + return handler_input.response_builder.response + + +def convert_speech_to_text(ssml_speech): + # convert ssml speech to text, by removing html tags + s = SSMLStripper() + s.feed(ssml_speech) + return s.get_data() + + +@sb.global_response_interceptor() +def add_card(handler_input, response): + # Add a card by translating ssml text to card content + response.card = SimpleCard( + title=skill_name, + content=convert_speech_to_text(response.output_speech.ssml)) + + +@sb.global_response_interceptor() +def log_response(handler_input, response): + # Log response from alexa service + print("Alexa Response: {}\n".format(response)) + + +@sb.global_request_interceptor() +def log_request(handler_input): + # Log request to alexa service + print("Alexa Request: {}\n".format(handler_input.request_envelope.request)) + + +@sb.exception_handler(can_handle_func=lambda i, e: True) +def all_exception_handler(handler_input, exception): + # Catch all exception handler, log exception and + # respond with custom message + print("Encountered following exception: {}".format(exception)) + + speech = "Sorry, there was some problem. Please try again!!" + handler_input.response_builder.speak(speech).ask(speech) + + return handler_input.response_builder.response + + +# Handler to be provided in lambda console. +handler = sb.lambda_handler() diff --git a/samples/ColorPicker/speech_assets/interactionSchema.json b/samples/ColorPicker/speech_assets/interactionSchema.json new file mode 100644 index 0000000..41b621f --- /dev/null +++ b/samples/ColorPicker/speech_assets/interactionSchema.json @@ -0,0 +1,53 @@ +{ + "interactionModel": { + "languageModel": { + "invocationName": "my color picker", + "intents": [ + { + "name": "AMAZON.FallbackIntent", + "samples": [] + }, + { + "name": "AMAZON.CancelIntent", + "samples": [] + }, + { + "name": "AMAZON.HelpIntent", + "samples": [] + }, + { + "name": "AMAZON.StopIntent", + "samples": [] + }, + { + "name": "WhatsMyColorIntent", + "slots": [], + "samples": [ + "tell me what is my favorite color", + "whats my favorite color", + "say my color", + "say my favorite color", + "tell me my favorite color", + "what is my favorite color", + "what is my color", + "whats my color" + ] + }, + { + "name": "MyColorIsIntent", + "slots": [ + { + "name": "Color", + "type": "AMAZON.Color" + } + ], + "samples": [ + "My color is {Color}", + "my favorite color is {Color}" + ] + } + ], + "types": [] + } + } +} \ No newline at end of file diff --git a/samples/GetDeviceAddress/README.rst b/samples/GetDeviceAddress/README.rst new file mode 100644 index 0000000..abe9b9f --- /dev/null +++ b/samples/GetDeviceAddress/README.rst @@ -0,0 +1,192 @@ +Alexa Skills Kit SDK Sample - Get Device Address +================================================ + +This Alexa sample skill is a template for a basic Alexa Device Address API use. + +The Device Address API enables skills to request and access the +configured address in the customer’s device settings. This means you can build +skills with the context to understand the customers who use the skill, then +use the data to customize the voice experience. Your skill, for example, +can deliver food and groceries to a customer’s home or provide directions to +a nearby gym. You can also see where your most active users are. +Check out our +`address information documentation `_ +to learn more. + +There are two levels of location data you can request: + +- Full address, which includes street address, city, state, zip, and country +- Country and postal code only + +This sample uses the first level of location data. + +When a user enables a skill that wants to use this location data, the user +will be prompted in the Alexa app to consent to the location data being shared +with the skill. It is important to note that when a user enables a skill +via voice, the user will not be prompted for this information and the +default choice will be "none". In this case, you can use cards to prompt +the user to provide consent using the Alexa app. The skill sample shows this +usecase with ``AskForPermissionsConsentCard`` in the response. + +**NOTE**: This sample is subject to change during the beta period. + + +Concepts +-------- + +This sample shows how to create a Lambda function for handling Alexa +Skill requests that: + +- Use service clients in SDK, to call the Alexa APIs. + More on `service clients here <../../docs/SERVICE_CLIENTS.rst>`__ +- Use ``AskForPermissionsConsentCard`` for asking for location consent + +Setup +----- + +To run this example skill you need to do two things. The first is to +deploy the example code in lambda, and the second is to configure the +Alexa skill to use Lambda. + +Prerequisites +~~~~~~~~~~~~~ + +Please follow the +`prerequisites <../../docs/GETTING_STARTED.rst#prerequisites>`_ section and the +`adding ASK SDK <../../docs/GETTING_STARTED.rst#adding-the-ask-sdk-to-your-project>`_ +section in +Getting Started documentation. For this sample skill, you need +the ``ASK SDK Core`` package. + +AWS Lambda Setup +~~~~~~~~~~~~~~~~ + +Refer to +`Hosting a Custom Skill as an AWS Lambda Function `__ +reference for a walkthrough on creating a AWS Lambda function with the +correct role for your skill. When creating the function, select the +"Author from scratch" option, and select Python 2.7 or Python 3.6 runtime. + +To prepare the skill for upload to AWS Lambda, create a zip file that +contains `device_address_api.py `_, the SDK and it's dependencies. Make sure to +compress all files directly, **NOT** the project folder. You can check the +AWS Lambda docs to get more information on +`creating a deployment package `_. + +Once you’ve created your AWS Lambda function and configured "Alexa +Skills Kit" as a trigger, upload the ZIP file produced in the previous +step and set the handler to the fully qualified class name of your +handler function. In this example, it would be ``device_address_api.handler``. +Finally, copy the ARN for your AWS Lambda function +because you’ll need it when configuring your skill in the Amazon +Developer console. + +Alexa Skill Setup +~~~~~~~~~~~~~~~~~ + +Now that the skill code has been uploaded to AWS Lambda we’re ready to +configure the skill with Alexa. First, navigate to the +`Alexa Skills Kit Developer Console `__. +Click the "Create Skill" button in the upper right. Enter "GetDeviceAddress" +as your skill name. On the next page, select "Custom" and click "Create +skill". + +Now we’re ready to define the interaction model for the skill. Under +"Invocation" tab on the left side, define your Skill Invocation Name to +be ``device address``. + +Now it’s time to add the required intents to the skill. Copy the +interactionSchema JSON provided in the `speech_assets `_ folder +and paste it under the "JSON Editor" tab. Alternatively, you can also upload +the JSON to the JSON Editor. + +.. code:: json + + { + "interactionModel": { + "languageModel": { + "invocationName": "device address", + "intents": [ + { + "name": "GetAddressIntent", + "slots": [], + "samples": [ + "where am I located", + "where do I live", + "whats my address", + "where am I", + "whats my location" + ] + }, + { + "name": "AMAZON.CancelIntent", + "samples": [] + }, + { + "name": "AMAZON.HelpIntent", + "samples": [] + }, + { + "name": "AMAZON.StopIntent", + "samples": [] + } + ], + "types": [] + } + } + } + +As can be observed from the JSON, we add a custom **GetAddressIntent** for +providing utterances for invoking the device address API call. + +Once you’re done editing the interaction model don’t forget to save and +build the model. + +Let’s move on to the skill configuration section. Under "Endpoint" +select "AWS Lambda ARN" and paste in the ARN of the function you created +previously. The rest of the settings can be left at their default +values. Click "Save Endpoints" and proceed to the next section. + +Under the AWS lambda function "Alexa Skills Kit" trigger, enable the "Skill Id +verification" and provide the Skill Id from the skill endpoint screen. Save +the lambda function. + +Since the skill needs to ask for Device Address permission from the user, this +needs to be configured in the skill. Click the "Permissions" tab on the left +navigation pane, enable the ``Device Address`` permission and select the +``Full Address`` radio button. + +Finally you’re ready to test the skill! In the "Test" tab of the +developer console you can simulate requests, in text and voice form, to +your skill. Use the invocation name along with one of the sample +utterances we just configured as a guide. You should also be able to go +to the `Echo webpage `__ and see your +skill listed under "Your Skills", where you can enable the skill on your +account for testing from an Alexa enabled device. + +At this point, feel free to start experimenting with your Intent Schema +as well as the corresponding request handlers in your skill’s +implementation. Once you’re finished iterating, you can optionally +choose to move on to the process of getting your skill certified and +published so it can be used by Alexa users worldwide. + +Additional Resources +-------------------- + +Community +~~~~~~~~~ + +- `Amazon Developer Forums `_ : Join the conversation! +- `Hackster.io `_ - See what others are building with Alexa. + +Tutorials & Guides +~~~~~~~~~~~~~~~~~~ + +- `Voice Design Guide `_ - + A great resource for learning conversational and voice user interface design. + +Documentation +~~~~~~~~~~~~~ + +- `Official Alexa Skills Kit Python SDK Docs <../../README.rst>`_ +- `Official Alexa Skills Kit Docs `_ diff --git a/samples/GetDeviceAddress/device_address_api.py b/samples/GetDeviceAddress/device_address_api.py new file mode 100644 index 0000000..433bbfd --- /dev/null +++ b/samples/GetDeviceAddress/device_address_api.py @@ -0,0 +1,183 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 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 is a skill for getting device address. +# The skill serves as a simple sample on how to use the +# service client factory and Alexa APIs through the SDK. + +from ask_sdk_core.skill_builder import CustomSkillBuilder +from ask_sdk_core.api_client import DefaultApiClient +from ask_sdk_core.dispatch_components import AbstractRequestHandler +from ask_sdk_core.dispatch_components import AbstractExceptionHandler +from ask_sdk_core.utils import is_request_type, is_intent_name +from ask_sdk_model.ui import AskForPermissionsConsentCard +from ask_sdk_model.services import ServiceException + +sb = CustomSkillBuilder(api_client=DefaultApiClient()) + +WELCOME = ("Welcome to the Sample Device Address API Skill! " + "You can ask for the device address by saying what is my " + "address. What do you want to ask?") +WHAT_DO_YOU_WANT = "What do you want to ask?" +NOTIFY_MISSING_PERMISSIONS = ("Please enable Location permissions in " + "the Amazon Alexa app.") +NO_ADDRESS = ("It looks like you don't have an address set. " + "You can set your address from the companion app.") +ADDRESS_AVAILABLE = "Here is your full address: {}, {}, {}" +ERROR = "Uh Oh. Looks like something went wrong." +LOCATION_FAILURE = ("There was an error with the Device Address API. " + "Please try again.") +GOODBYE = "Bye! Thanks for using the Sample Device Address API Skill!" +UNHANDLED = "This skill doesn't support that. Please ask something else" +HELP = ("You can use this skill by asking something like: " + "whats my address?") + +permissions = ["read::alexa:device:all:address"] +# Location Consent permission to be shown on the card. More information +# can be checked at +# https://developer.amazon.com/docs/custom-skills/device-address-api.html#sample-response-with-permission-card + + +class LaunchRequestHandler(AbstractRequestHandler): + # Handler for Skill Launch + def can_handle(self, handler_input): + return is_request_type("LaunchRequest")(handler_input) + + def handle(self, handler_input): + handler_input.response_builder.speak(WELCOME).ask(WHAT_DO_YOU_WANT) + return handler_input.response_builder.response + + +class GetAddressHandler(AbstractRequestHandler): + # Handler for Getting Device Address or asking for location consent + def can_handle(self, handler_input): + return is_intent_name("GetAddressIntent")(handler_input) + + def handle(self, handler_input): + req_envelope = handler_input.request_envelope + response_builder = handler_input.response_builder + service_client_fact = handler_input.service_client_factory + + if not (req_envelope.context.system.user.permissions and + req_envelope.context.system.user.permissions.consent_token): + response_builder.speak(NOTIFY_MISSING_PERMISSIONS) + response_builder.set_card( + AskForPermissionsConsentCard(permissions=permissions)) + return response_builder.response + + try: + device_id = req_envelope.context.system.device.device_id + device_addr_client = service_client_fact.get_device_address_service() + addr = device_addr_client.get_full_address(device_id) + + if addr.address_line1 is None and addr.state_or_region is None: + response_builder.speak(NO_ADDRESS) + else: + response_builder.speak(ADDRESS_AVAILABLE.format( + addr.address_line1, addr.state_or_region, addr.postal_code)) + return response_builder.response + except ServiceException: + response_builder.speak(ERROR) + return response_builder.response + except Exception as e: + raise e + + +class SessionEndedRequestHandler(AbstractRequestHandler): + # Handler for Session End + def can_handle(self, handler_input): + return is_request_type("SessionEndedRequest")(handler_input) + + def handle(self, handler_input): + return handler_input.response_builder.response + + +class HelpIntentHandler(AbstractRequestHandler): + # Handler for Help Intent + def can_handle(self, handler_input): + return is_intent_name("AMAZON.HelpIntent")(handler_input) + + def handle(self, handler_input): + handler_input.response_builder.speak(HELP).ask(HELP) + return handler_input.response_builder.response + + +class CancelOrStopIntentHandler(AbstractRequestHandler): + # Single handler for Cancel and Stop Intent + def can_handle(self, handler_input): + return (is_intent_name("AMAZON.CancelIntent")(handler_input) or + is_intent_name("AMAZON.StopIntent")(handler_input)) + + def handle(self, handler_input): + handler_input.response_builder.speak(GOODBYE) + return handler_input.response_builder.response + + +class FallbackIntentHandler(AbstractRequestHandler): + # AMAZON.FallbackIntent is only available in en-US locale. + # This handler will not be triggered except in that locale, + # so it is safe to deploy on any locale + def can_handle(self, handler_input): + return is_intent_name("AMAZON.FallbackIntent")(handler_input) + + def handle(self, handler_input): + handler_input.response_builder.speak(UNHANDLED).ask(HELP) + return handler_input.response_builder.response + + +class GetAddressExceptionHandler(AbstractExceptionHandler): + # Custom Exception Handler for handling device address API call exceptions + def can_handle(self, handler_input, exception): + return isinstance(exception, ServiceException) + + def handle(self, handler_input, exception): + if exception.status_code == 403: + handler_input.response_builder.speak( + NOTIFY_MISSING_PERMISSIONS).set_card( + AskForPermissionsConsentCard(permissions=permissions)) + else: + handler_input.response_builder.speak( + LOCATION_FAILURE).ask(LOCATION_FAILURE) + + return handler_input.response_builder.response + + +class CatchAllExceptionHandler(AbstractExceptionHandler): + # Catch all exception handler, log exception and + # respond with custom message + def can_handle(self, handler_input, exception): + return True + + def handle(self, handler_input, exception): + print("Encountered following exception: {}".format(exception)) + + speech = "Sorry, there was some problem. Please try again!!" + handler_input.response_builder.speak(speech).ask(speech) + + return handler_input.response_builder.response + + +sb.add_request_handler(LaunchRequestHandler()) +sb.add_request_handler(GetAddressHandler()) +sb.add_request_handler(HelpIntentHandler()) +sb.add_request_handler(CancelOrStopIntentHandler()) +sb.add_request_handler(FallbackIntentHandler()) +sb.add_request_handler(SessionEndedRequestHandler()) + +sb.add_exception_handler(GetAddressExceptionHandler()) +sb.add_exception_handler(CatchAllExceptionHandler()) + +handler = sb.lambda_handler() diff --git a/samples/GetDeviceAddress/speech_assets/interactionSchema.json b/samples/GetDeviceAddress/speech_assets/interactionSchema.json new file mode 100644 index 0000000..ee8c3de --- /dev/null +++ b/samples/GetDeviceAddress/speech_assets/interactionSchema.json @@ -0,0 +1,37 @@ +{ + "interactionModel": { + "languageModel": { + "invocationName": "device address", + "intents": [ + { + "name": "GetAddressIntent", + "slots": [], + "samples": [ + "where am I located", + "where do I live", + "whats my address", + "where am I", + "whats my location" + ] + }, + { + "name": "AMAZON.CancelIntent", + "samples": [] + }, + { + "name": "AMAZON.HelpIntent", + "samples": [] + }, + { + "name": "AMAZON.StopIntent", + "samples": [] + }, + { + "name": "AMAZON.FallbackIntent", + "samples": [] + } + ], + "types": [] + } + } +} \ No newline at end of file diff --git a/samples/HelloWorld/README.rst b/samples/HelloWorld/README.rst new file mode 100644 index 0000000..55e2701 --- /dev/null +++ b/samples/HelloWorld/README.rst @@ -0,0 +1,55 @@ +Alexa Skills Kit SDK Sample - Hello World +========================================= + +A simple `AWS Lambda `__ function that +demonstrates how to write a Hello World skill for the Amazon Echo using +the Alexa SDK. + +Concepts +-------- + +This simple sample has no external dependencies or session management, +and shows the most basic example of how to create a Lambda function for +handling Alexa Skill requests. + +Setup +----- + +To run this example skill you need to do two things. The first is to +deploy the example code in lambda, and the second is to configure the +Alexa skill to use Lambda. + +This sample skills shows two ways of developing skills: + +- Implementing ``AbstractRequestHandler`` class and registering the + handler classes explicitly in the skill builder object. The code for this + implementation is under `skill_using_classes `_ folder. +- Using the skill builder's ``request_handler`` decorator, and + decorating custom functions that responds to intents. The code for this + implementation is under `skill_using_decorators `_ + folder. + +For detailed instructions, please refer to +`Developing Your First Skill <../../docs/DEVELOPING_YOUR_FIRST_SKILL.rst>`__ + +Additional Resources +-------------------- + +Community +~~~~~~~~~ + +- `Amazon Developer Forums `_ : Join the conversation! +- `Hackster.io `_ - See what others are building with Alexa. + +Tutorials & Guides +~~~~~~~~~~~~~~~~~~ + +- `Voice Design Guide `_ - + A great resource for learning conversational and voice user interface design. + +Documentation +~~~~~~~~~~~~~ + +- `Official Alexa Skills Kit Python SDK Docs <../../README.rst>`_ +- `Official Alexa Skills Kit Docs `_ + diff --git a/samples/HelloWorld/skill_using_classes/hello_world.py b/samples/HelloWorld/skill_using_classes/hello_world.py new file mode 100644 index 0000000..9a53003 --- /dev/null +++ b/samples/HelloWorld/skill_using_classes/hello_world.py @@ -0,0 +1,134 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 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 is a simple Hello World Alexa Skill, built using +# the implementation of handler classes approach in skill builder. + +from ask_sdk_core.skill_builder import SkillBuilder +from ask_sdk_core.dispatch_components import AbstractRequestHandler +from ask_sdk_core.dispatch_components import AbstractExceptionHandler +from ask_sdk_core.utils import is_request_type, is_intent_name +from ask_sdk_model.ui import SimpleCard + +sb = SkillBuilder() + + +class LaunchRequestHandler(AbstractRequestHandler): + # Handler for Skill Launch + def can_handle(self, handler_input): + return is_request_type("LaunchRequest")(handler_input) + + def handle(self, handler_input): + speech_text = "Welcome to the Alexa Skills Kit, you can say hello!" + + handler_input.response_builder.speak(speech_text).set_card( + SimpleCard("Hello World", speech_text)).set_should_end_session( + False) + return handler_input.response_builder.response + + +class HelloWorldIntentHandler(AbstractRequestHandler): + # Handler for Hello World Intent + def can_handle(self, handler_input): + return is_intent_name("HelloWorldIntent")(handler_input) + + def handle(self, handler_input): + speech_text = "Hello Python World from Classes!" + + handler_input.response_builder.speak(speech_text).set_card( + SimpleCard("Hello World", speech_text)).set_should_end_session( + True) + return handler_input.response_builder.response + + +class HelpIntentHandler(AbstractRequestHandler): + # Handler for Help Intent + def can_handle(self, handler_input): + return is_intent_name("AMAZON.HelpIntent")(handler_input) + + def handle(self, handler_input): + speech_text = "You can say hello to me!" + + handler_input.response_builder.speak(speech_text).ask( + speech_text).set_card(SimpleCard( + "Hello World", speech_text)) + return handler_input.response_builder.response + + +class CancelOrStopIntentHandler(AbstractRequestHandler): + # Single handler for Cancel and Stop Intent + def can_handle(self, handler_input): + return (is_intent_name("AMAZON.CancelIntent")(handler_input) or + is_intent_name("AMAZON.StopIntent")(handler_input)) + + def handle(self, handler_input): + speech_text = "Goodbye!" + + handler_input.response_builder.speak(speech_text).set_card( + SimpleCard("Hello World", speech_text)) + return handler_input.response_builder.response + + +class FallbackIntentHandler(AbstractRequestHandler): + # AMAZON.FallbackIntent is only available in en-US locale. + # This handler will not be triggered except in that locale, + # so it is safe to deploy on any locale + def can_handle(self, handler_input): + return is_intent_name("AMAZON.FallbackIntent")(handler_input) + + def handle(self, handler_input): + speech_text = ( + "The Hello World skill can't help you with that. " + "You can say hello!!") + reprompt = "You can say hello!!" + handler_input.response_builder.speak(speech_text).ask(reprompt) + return handler_input.response_builder.response + + +class SessionEndedRequestHandler(AbstractRequestHandler): + # Handler for Session End + def can_handle(self, handler_input): + return is_request_type("SessionEndedRequest")(handler_input) + + def handle(self, handler_input): + return handler_input.response_builder.response + + +class CatchAllExceptionHandler(AbstractExceptionHandler): + # Catch all exception handler, log exception and + # respond with custom message + def can_handle(self, handler_input, exception): + return True + + def handle(self, handler_input, exception): + print("Encountered following exception: {}".format(exception)) + + speech = "Sorry, there was some problem. Please try again!!" + handler_input.response_builder.speak(speech).ask(speech) + + return handler_input.response_builder.response + + +sb.add_request_handler(LaunchRequestHandler()) +sb.add_request_handler(HelloWorldIntentHandler()) +sb.add_request_handler(HelpIntentHandler()) +sb.add_request_handler(CancelOrStopIntentHandler()) +sb.add_request_handler(FallbackIntentHandler()) +sb.add_request_handler(SessionEndedRequestHandler()) + +sb.add_exception_handler(CatchAllExceptionHandler()) + +handler = sb.lambda_handler() diff --git a/samples/HelloWorld/skill_using_decorators/hello_world.py b/samples/HelloWorld/skill_using_decorators/hello_world.py new file mode 100644 index 0000000..35b2c53 --- /dev/null +++ b/samples/HelloWorld/skill_using_decorators/hello_world.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 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 is a simple Hello World Alexa Skill, built using +# the decorators approach in skill builder. + +from ask_sdk_core.skill_builder import SkillBuilder +from ask_sdk_core.utils import is_request_type, is_intent_name +from ask_sdk_model.ui import SimpleCard + +sb = SkillBuilder() + + +@sb.request_handler(can_handle_func=is_request_type("LaunchRequest")) +def launch_request_handler(handler_input): + # Handler for Skill Launch + speech_text = "Welcome to the Alexa Skills Kit, you can say hello!" + + return handler_input.response_builder.speak(speech_text).set_card( + SimpleCard("Hello World", speech_text)).set_should_end_session( + False).response + + +@sb.request_handler(can_handle_func=is_intent_name("HelloWorldIntent")) +def hello_world_intent_handler(handler_input): + # Handler for Hello World Intent + speech_text = "Hello Python World from Decorators!" + + return handler_input.response_builder.speak(speech_text).set_card( + SimpleCard("Hello World", speech_text)).set_should_end_session( + True).response + + +@sb.request_handler(can_handle_func=is_intent_name("AMAZON.HelpIntent")) +def help_intent_handler(handler_input): + # Handler for Help Intent + speech_text = "You can say hello to me!" + + return handler_input.response_builder.speak(speech_text).ask( + speech_text).set_card(SimpleCard( + "Hello World", speech_text)).response + + +@sb.request_handler( + can_handle_func=lambda input: + is_intent_name("AMAZON.CancelIntent")(input) or + is_intent_name("AMAZON.StopIntent")(input)) +def cancel_and_stop_intent_handler(handler_input): + # Single handler for Cancel and Stop Intent + speech_text = "Goodbye!" + + return handler_input.response_builder.speak(speech_text).set_card( + SimpleCard("Hello World", speech_text)).response + + +@sb.request_handler(can_handle_func=is_intent_name("AMAZON.FallbackIntent")) +def fallback_handler(handler_input): + # AMAZON.FallbackIntent is only available in en-US locale. + # This handler will not be triggered except in that locale, + # so it is safe to deploy on any locale + speech = ( + "The Hello World skill can't help you with that. " + "You can say hello!!") + reprompt = "You can say hello!!" + handler_input.response_builder.speak(speech).ask(reprompt) + return handler_input.response_builder.response + + +@sb.request_handler(can_handle_func=is_request_type("SessionEndedRequest")) +def session_ended_request_handler(handler_input): + # Handler for Session End + return handler_input.response_builder.response + + +@sb.exception_handler(can_handle_func=lambda i, e: True) +def all_exception_handler(handler_input, exception): + # Catch all exception handler, log exception and + # respond with custom message + print("Encountered following exception: {}".format(exception)) + + speech = "Sorry, there was some problem. Please try again!!" + handler_input.response_builder.speak(speech).ask(speech) + + return handler_input.response_builder.response + +handler = sb.lambda_handler() diff --git a/samples/HelloWorld/speech_assets/interactionSchema.json b/samples/HelloWorld/speech_assets/interactionSchema.json new file mode 100644 index 0000000..e9bb8cc --- /dev/null +++ b/samples/HelloWorld/speech_assets/interactionSchema.json @@ -0,0 +1,39 @@ +{ + "interactionModel": { + "languageModel": { + "invocationName": "greeter", + "intents": [ + { + "name": "AMAZON.FallbackIntent", + "samples": [] + }, + { + "name": "AMAZON.CancelIntent", + "samples": [] + }, + { + "name": "AMAZON.HelpIntent", + "samples": [] + }, + { + "name": "AMAZON.StopIntent", + "samples": [] + }, + { + "name": "HelloWorldIntent", + "slots": [], + "samples": [ + "how are you", + "say hi world", + "say hi", + "hi", + "hello", + "say hello world", + "say hello" + ] + } + ], + "types": [] + } + } +} \ No newline at end of file diff --git a/samples/HighLowGame/README.rst b/samples/HighLowGame/README.rst new file mode 100644 index 0000000..cf878b1 --- /dev/null +++ b/samples/HighLowGame/README.rst @@ -0,0 +1,188 @@ +Alexa Skills Kit SDK Sample - High Low Game +=========================================== + +This Alexa sample skill is a template for a basic high-low game skill. +Guess a number, and Alexa will tell you whether the number she has in mind +is higher or lower. + +**NOTE**: This sample is subject to change during the beta period. + +Concepts +-------- + +This sample shows how to create a Lambda function for handling Alexa +Skill requests that: + +- Use Persistence attributes and Persistence adapter, to store and retrieve + attributes on AWS DynamoDB table. +- Demonstrates using a custom slot type. + +Setup +----- + +To run this example skill you need to do two things. The first is to +deploy the example code in lambda, and the second is to configure the +Alexa skill to use Lambda. + +Prerequisites +~~~~~~~~~~~~~ + +Please follow the +`prerequisites <../../docs/GETTING_STARTED.rst#prerequisites>`_ section and the +`adding ASK SDK <../../docs/GETTING_STARTED.rst#adding-the-ask-sdk-to-your-project>`_ +section in +Getting Started documentation. For this sample skill, you need +the ``ASK SDK Standard`` package. + +In addition to the SDK, the skill also needs a DynamoDb table. Go to +`AWS Console `_ +and click on the `DynamoDB `_ Service. +In the dashboard, click on 'Create table', provide 'High-Low-Game' as table name +and 'id' as partition key. Leave all settings as default and click 'Create'. + +AWS Lambda Setup +~~~~~~~~~~~~~~~~ + +Refer to +`Hosting a Custom Skill as an AWS Lambda Function `__ +reference for a walkthrough on creating a AWS Lambda function with the +correct role for your skill. When creating the function, select the +"Author from scratch" option, and select Python 2.7 or Python 3.6 runtime. + +To prepare the skill for upload to AWS Lambda, create a zip file that +contains `high_low_game.py `_, the SDK and it's dependencies. Make sure to +compress all files directly, **NOT** the project folder. You can check the +AWS Lambda docs to get more information on +`creating a deployment package `_. + +Once you’ve created your AWS Lambda function and configured "Alexa +Skills Kit" as a trigger, upload the ZIP file produced in the previous +step and set the handler to the fully qualified class name of your +handler function. In this example, it would be ``high_low_game.handler``. +Finally, copy the ARN for your AWS Lambda function +because you’ll need it when configuring your skill in the Amazon +Developer console. + +Alexa Skill Setup +~~~~~~~~~~~~~~~~~ + +Now that the skill code has been uploaded to AWS Lambda we’re ready to +configure the skill with Alexa. First, navigate to the +`Alexa Skills Kit Developer Console `__. +Click the "Create Skill" button in the upper right. Enter "HighLowGame" +as your skill name. On the next page, select "Custom" and click "Create +skill". + +Now we’re ready to define the interaction model for the skill. Under +"Invocation" tab on the left side, define your Skill Invocation Name to +be ``high low game``. + +Now it’s time to add the required intents to the skill. Copy the +interactionSchema JSON provided in the `speech_assets `_ folder +and paste it under the "JSON Editor" tab. Alternatively, you can also upload +the JSON to the JSON Editor. + +.. code:: json + + { + "interactionModel": { + "languageModel": { + "invocationName": "high low game", + "intents": [ + { + "name": "AMAZON.CancelIntent", + "samples": [] + }, + { + "name": "AMAZON.HelpIntent", + "samples": [] + }, + { + "name": "AMAZON.StopIntent", + "samples": [] + }, + { + "name": "AMAZON.YesIntent", + "samples": [] + }, + { + "name": "AMAZON.FallbackIntent", + "samples": [] + }, + { + "name": "AMAZON.NoIntent", + "samples": [] + }, + { + "name": "NumberGuessIntent", + "slots": [ + { + "name": "number", + "type": "AMAZON.NUMBER" + } + ], + "samples": [ + "{number}", + "is it {number}", + "how about {number}", + "could be {number}" + ] + } + ], + "types": [] + } + } + } + +As can be observed from the JSON, we add the built-in **AMAZON.YesIntent**, +**AMAZON.NoIntent** and a custom **NumberGuessIntent**. The +**NumberGuessIntent** is for providing utterances for guessing the number. The +"Yes" and "No" intents are for skill users to confirm if they want to play +the game. + +Once you’re done editing the interaction model don’t forget to save and +build the model. + +Let’s move on to the skill configuration section. Under "Endpoint" +select "AWS Lambda ARN" and paste in the ARN of the function you created +previously. The rest of the settings can be left at their default +values. Click "Save Endpoints" and proceed to the next section. + +Under the AWS lambda function "Alexa Skills Kit" trigger, enable the "Skill Id +verification" and provide the Skill Id from the skill endpoint screen. Save +the lambda function. + +Finally you’re ready to test the skill! In the "Test" tab of the +developer console you can simulate requests, in text and voice form, to +your skill. Use the invocation name along with one of the sample +utterances we just configured as a guide. You should also be able to go +to the `Echo webpage `__ and see your +skill listed under "Your Skills", where you can enable the skill on your +account for testing from an Alexa enabled device. + +At this point, feel free to start experimenting with your Intent Schema +as well as the corresponding request handlers in your skill’s +implementation. Once you’re finished iterating, you can optionally +choose to move on to the process of getting your skill certified and +published so it can be used by Alexa users worldwide. + +Additional Resources +-------------------- + +Community +~~~~~~~~~ + +- `Amazon Developer Forums `_ : Join the conversation! +- `Hackster.io `_ - See what others are building with Alexa. + +Tutorials & Guides +~~~~~~~~~~~~~~~~~~ + +- `Voice Design Guide `_ - + A great resource for learning conversational and voice user interface design. + +Documentation +~~~~~~~~~~~~~ + +- `Official Alexa Skills Kit Python SDK Docs <../../README.rst>`_ +- `Official Alexa Skills Kit Docs `_ diff --git a/samples/HighLowGame/high_low_game.py b/samples/HighLowGame/high_low_game.py new file mode 100644 index 0000000..51165b0 --- /dev/null +++ b/samples/HighLowGame/high_low_game.py @@ -0,0 +1,225 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 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 is a High Low Guess game Alexa Skill. +# The skill serves as a simple sample on how to use the +# persistence attributes and persistence adapter features in the SDK. +import random + +from ask_sdk.standard import StandardSkillBuilder +from ask_sdk_core.utils import is_request_type, is_intent_name + +SKILL_NAME = 'High Low Game' +sb = StandardSkillBuilder(table_name="High-Low-Game", auto_create_table=True) + + +@sb.request_handler(can_handle_func=is_request_type("LaunchRequest")) +def launch_request_handler(handler_input): + # Handler for Skill Launch + + # Get the persistence attributes, to figure out the game state + attr = handler_input.attributes_manager.persistent_attributes + if not attr: + attr['ended_session_count'] = 0 + attr['games_played'] = 0 + attr['game_state'] = 'ENDED' + + handler_input.attributes_manager.session_attributes = attr + + speech_text = ( + "Welcome to the High Low guessing game. You have played {} times. " + "Would you like to play?".format(attr["games_played"])) + reprompt = "Say yes to start the game or no to quit." + + handler_input.response_builder.speak(speech_text).ask(reprompt) + return handler_input.response_builder.response + + +@sb.request_handler(can_handle_func=is_intent_name("AMAZON.HelpIntent")) +def help_intent_handler(handler_input): + # Handler for Help Intent + speech_text = ( + "I am thinking of a number between zero and one hundred, try to " + "guess it and I will tell you if you got it or it is higher or " + "lower") + reprompt = "Try saying a number." + + handler_input.response_builder.speak(speech_text).ask(reprompt) + return handler_input.response_builder.response + + +@sb.request_handler( + can_handle_func=lambda input: + is_intent_name("AMAZON.CancelIntent")(input) or + is_intent_name("AMAZON.StopIntent")(input)) +def cancel_and_stop_intent_handler(handler_input): + # Single handler for Cancel and Stop Intent + speech_text = "Thanks for playing!!" + + handler_input.response_builder.speak( + speech_text).set_should_end_session(True) + return handler_input.response_builder.response + + +@sb.request_handler(can_handle_func=is_request_type("SessionEndedRequest")) +def session_ended_request_handler(handler_input): + # Handler for Session End + print( + "Session ended with reason: {}".format( + handler_input.request_envelope.request.reason)) + return handler_input.response_builder.response + + +def currently_playing(handler_input): + is_currently_playing = False + session_attr = handler_input.attributes_manager.session_attributes + + if ("game_state" in session_attr + and session_attr['game_state'] == "STARTED"): + is_currently_playing = True + + return is_currently_playing + + +@sb.request_handler(can_handle_func=lambda input: + not currently_playing(input) and + is_intent_name("AMAZON.YesIntent")(input)) +def yes_handler(handler_input): + # Handler for Yes Intent, only if the player said yes for + # a new game. + session_attr = handler_input.attributes_manager.session_attributes + session_attr['game_state'] = "STARTED" + session_attr['guess_number'] = random.randint(0, 100) + session_attr['no_of_guesses'] = 0 + + speech_text = "Great! Try saying a number to start the game." + reprompt = "Try saying a number." + + handler_input.response_builder.speak(speech_text).ask(reprompt) + return handler_input.response_builder.response + + +@sb.request_handler(can_handle_func=lambda input: + not currently_playing(input) and + is_intent_name("AMAZON.NoIntent")(input)) +def no_handler(handler_input): + # Handler for No Intent, only if the player said no for + # a new game. + session_attr = handler_input.attributes_manager.session_attributes + session_attr['game_state'] = "ENDED" + session_attr['ended_session_count'] += 1 + + handler_input.attributes_manager.persistent_attributes = session_attr + handler_input.attributes_manager.save_persistent_attributes() + + speech_text = "Ok. See you next time!!" + + handler_input.response_builder.speak(speech_text) + return handler_input.response_builder.response + + +@sb.request_handler(can_handle_func=lambda input: + currently_playing(input) and + is_intent_name("NumberGuessIntent")(input)) +def number_guess_handler(handler_input): + # Handler for processing guess with target + session_attr = handler_input.attributes_manager.session_attributes + target_num = session_attr["guess_number"] + guess_num = int(handler_input.request_envelope.request.intent.slots[ + "number"].value) + + session_attr["no_of_guesses"] += 1 + + if guess_num > target_num: + speech_text = ( + "{} is too high. Try saying a smaller number.".format(guess_num)) + reprompt = "Try saying a smaller number." + elif guess_num < target_num: + speech_text = ( + "{} is too low. Try saying a larger number.".format(guess_num)) + reprompt = "Try saying a larger number." + elif guess_num == target_num: + speech_text = ( + "Congratulations. {} is the correct guess. " + "You guessed the number in {} guesses. " + "Would you like to play a new game?".format( + guess_num, session_attr["no_of_guesses"])) + reprompt = "Say yes to start a new game or no to end the game" + session_attr["games_played"] += 1 + session_attr["game_state"] = "ENDED" + + handler_input.attributes_manager.persistent_attributes = session_attr + handler_input.attributes_manager.save_persistent_attributes() + else: + speech_text = "Sorry, I didn't get that. Try saying a number." + reprompt = "Try saying a number." + + handler_input.response_builder.speak(speech_text).ask(reprompt) + return handler_input.response_builder.response + + +@sb.request_handler(can_handle_func=lambda input: + is_intent_name("AMAZON.FallbackIntent")(input) or + is_intent_name("AMAZON.YesIntent")(input) or + is_intent_name("AMAZON.NoIntent")(input)) +def fallback_handler(handler_input): + # AMAZON.FallbackIntent is only available in en-US locale. + # This handler will not be triggered except in that locale, + # so it is safe to deploy on any locale + session_attr = handler_input.attributes_manager.session_attributes + + if ("game_state" in session_attr and + session_attr["game_state"]=="STARTED"): + speech_text = ( + "The {} skill can't help you with that. " + "Try guessing a number between 0 and 100. ".format(SKILL_NAME)) + reprompt = "Please guess a number between 0 and 100." + else: + speech_text = ( + "The {} skill can't help you with that. " + "It will come up with a number between 0 and 100 and " + "you try to guess it by saying a number in that range. " + "Would you like to play?".format(SKILL_NAME)) + reprompt = "Say yes to start the game or no to quit." + + handler_input.response_builder.speak(speech_text).ask(reprompt) + return handler_input.response_builder.response + + +@sb.request_handler(can_handle_func=lambda input: True) +def unhandled_intent_handler(handler_input): + # Handler for all other unhandled requests + speech = "Say yes to continue or no to end the game!!" + handler_input.response_builder.speak(speech).ask(speech) + return handler_input.response_builder.response + + +@sb.exception_handler(can_handle_func=lambda i, e: True) +def all_exception_handler(handler_input, exception): + # Catch all exception handler, log exception and + # respond with custom message + print("Encountered following exception: {}".format(exception)) + speech = "Sorry, I can't understand that. Please say again!!" + handler_input.response_builder.speak(speech).ask(speech) + return handler_input.response_builder.response + + +@sb.global_response_interceptor() +def log_response(handler_input, response): + print("Response : {}".format(response)) + + +handler = sb.lambda_handler() diff --git a/samples/HighLowGame/speech_assets/interactionSchema.json b/samples/HighLowGame/speech_assets/interactionSchema.json new file mode 100644 index 0000000..5e86142 --- /dev/null +++ b/samples/HighLowGame/speech_assets/interactionSchema.json @@ -0,0 +1,49 @@ +{ + "interactionModel": { + "languageModel": { + "invocationName": "high low game", + "intents": [ + { + "name": "AMAZON.CancelIntent", + "samples": [] + }, + { + "name": "AMAZON.HelpIntent", + "samples": [] + }, + { + "name": "AMAZON.StopIntent", + "samples": [] + }, + { + "name": "AMAZON.YesIntent", + "samples": [] + }, + { + "name": "AMAZON.FallbackIntent", + "samples": [] + }, + { + "name": "AMAZON.NoIntent", + "samples": [] + }, + { + "name": "NumberGuessIntent", + "slots": [ + { + "name": "number", + "type": "AMAZON.NUMBER" + } + ], + "samples": [ + "{number}", + "is it {number}", + "how about {number}", + "could be {number}" + ] + } + ], + "types": [] + } + } +} \ No newline at end of file diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..7c2b287 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal = 1 \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..5cfb38c --- /dev/null +++ b/setup.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 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. +# +from __future__ import print_function +import glob +import os +import sys +import copy +import runpy + +here = os.path.abspath(os.path.dirname(__file__)) + +packages = [ + os.path.dirname(p) for p in glob.glob( + os.path.join("ask-sdk*", "setup.py"))] + +for pkg in packages: + pkg_folder = os.path.join(here, pkg) + pkg_setup_path = os.path.join(pkg_folder, "setup.py") + + current_dir = os.getcwd() + current_path = sys.path + + try: + os.chdir(pkg_folder) + sys.path = [pkg_setup_path] + copy.copy(sys.path) + + print("Installing package: {}".format(pkg)) + runpy.run_path(pkg_setup_path) + except Exception as e: + print("Installation failed for package: {}".format(pkg)) + print("Exception raised: {}".format(e)) + finally: + os.chdir(current_dir) + sys.path = current_path