From acba478dd60b6493decbf3458bd20834e8beb108 Mon Sep 17 00:00:00 2001 From: Nikhil Yogendra Murali Date: Thu, 14 Mar 2019 03:56:33 -0700 Subject: [PATCH] Add request utility methods This commit introduces some commonly used request utility methods like get_locale, get_intent_name etc. --- ask-sdk-core/ask_sdk_core/utils/__init__.py | 7 +- .../ask_sdk_core/utils/request_util.py | 301 ++++++++++++++++++ ask-sdk-core/ask_sdk_core/utils/viewport.py | 10 +- ask-sdk-core/tests/unit/test_utils.py | 246 +++++++++++++- docs/en/api/core.rst | 16 +- 5 files changed, 570 insertions(+), 10 deletions(-) create mode 100644 ask-sdk-core/ask_sdk_core/utils/request_util.py diff --git a/ask-sdk-core/ask_sdk_core/utils/__init__.py b/ask-sdk-core/ask_sdk_core/utils/__init__.py index 2bd1ca7..78d8540 100644 --- a/ask-sdk-core/ask_sdk_core/utils/__init__.py +++ b/ask-sdk-core/ask_sdk_core/utils/__init__.py @@ -18,8 +18,13 @@ import sys from ..__version__ import __version__ -from .predicate import is_canfulfill_intent_name, is_intent_name, is_request_type +from .predicate import ( + is_canfulfill_intent_name, is_intent_name, is_request_type) from ask_sdk_runtime.utils import user_agent_info +from .request_util import ( + get_slot, get_slot_value, get_account_linking_access_token, + get_api_access_token, get_device_id, get_dialog_state, get_intent_name, + get_locale, get_request_type, is_new_session, get_supported_interfaces) SDK_VERSION = __version__ diff --git a/ask-sdk-core/ask_sdk_core/utils/request_util.py b/ask-sdk-core/ask_sdk_core/utils/request_util.py new file mode 100644 index 0000000..1080a2d --- /dev/null +++ b/ask-sdk-core/ask_sdk_core/utils/request_util.py @@ -0,0 +1,301 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2019 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.intent_request import IntentRequest +from ask_sdk_model.supported_interfaces import SupportedInterfaces + +if typing.TYPE_CHECKING: + from ..handler_input import HandlerInput + from typing import Optional, AnyStr + from ask_sdk_model.slot import Slot + from ask_sdk_model.dialog_state import DialogState + + +def get_locale(handler_input): + # type: (HandlerInput) -> AnyStr + """Return locale value from input request. + + The method returns the ``locale`` value present in the request. More + information about the locale can be found here : + https://developer.amazon.com/docs/custom-skills/request-and-response-json-reference.html#request-locale + + :param handler_input: The handler input instance that is generally + passed in the sdk's request and exception components + :type handler_input: ask_sdk_core.handler_input.HandlerInput + :return: Locale value from the request + :rtype: str + """ + return handler_input.request_envelope.request.locale + + +def get_request_type(handler_input): + # type: (HandlerInput) -> AnyStr + """Return the type of the input request. + + The method retrieves the request ``type`` of the input request. More + information about the different request types are mentioned here : + https://developer.amazon.com/docs/custom-skills/request-and-response-json-reference.html#request-body-parameters + + :param handler_input: The handler input instance that is generally + passed in the sdk's request and exception components + :type handler_input: ask_sdk_core.handler_input.HandlerInput + :return: Type value of the input request + :rtype: str + """ + return handler_input.request_envelope.request.object_type + + +def get_intent_name(handler_input): + # type: (HandlerInput) -> AnyStr + """Return the name of the intent request. + + The method retrieves the intent ``name`` from the input request, only if + the input request is an + :py:class:`ask_sdk_model.intent_request.IntentRequest`. If the input + is not an IntentRequest, a :py:class:`TypeError` is raised. + + :param handler_input: The handler input instance that is generally + passed in the sdk's request and exception components + :type handler_input: ask_sdk_core.handler_input.HandlerInput + :return: Name of the intent request + :rtype: str + :raises: TypeError + """ + request = handler_input.request_envelope.request + if isinstance(request, IntentRequest): + return request.intent.name + + raise TypeError("The provided request is not an IntentRequest") + + +def get_account_linking_access_token(handler_input): + # type: (HandlerInput) -> Optional[AnyStr] + """Return the access token in the request. + + The method retrieves the user's ``accessToken`` from the input request. + Once a user successfully enables a skill and links their Alexa + account to the skill, the input request will have the user's + access token. A `None` value is returned if there is no access token + in the input request. More information on this can be found here : + https://developer.amazon.com/docs/account-linking/add-account-linking-logic-custom-skill.html + + :param handler_input: The handler input instance that is generally + passed in the sdk's request and exception components + :type handler_input: ask_sdk_core.handler_input.HandlerInput + :return: User account linked access token if available. None if not + available + :rtype: Optional[str] + """ + return handler_input.request_envelope.context.system.user.access_token + + +def get_api_access_token(handler_input): + # type: (HandlerInput) -> AnyStr + """Return the api access token in the request. + + The method retrieves the ``apiAccessToken`` from the input request, + which has the encapsulated information of permissions granted by the + user. This token can be used to call Alexa-specific APIs. More information + about this can be found here : + https://developer.amazon.com/docs/custom-skills/request-and-response-json-reference.html#system-object + + The SDK already includes this token in the API calls done through the + `service_client_factory` in + :py:class:`ask_sdk_core.handler_input.HandlerInput`. + + :param handler_input: The handler input instance that is generally + passed in the sdk's request and exception components + :type handler_input: ask_sdk_core.handler_input.HandlerInput + :return: Api access token from the input request, which encapsulates any + permissions consented by the user + :rtype: str + """ + return handler_input.request_envelope.context.system.api_access_token + + +def get_device_id(handler_input): + # type: (HandlerInput) -> AnyStr + """Return the device id from the input request. + + The method retrieves the `deviceId` property from the input request. + This value uniquely identifies the device and is generally used as + input for some Alexa-specific API calls. More information about this + can be found here : + https://developer.amazon.com/docs/custom-skills/request-and-response-json-reference.html#system-object + + :param handler_input: The handler input instance that is generally + passed in the sdk's request and exception components + :type handler_input: ask_sdk_core.handler_input.HandlerInput + :return: Unique device id of the device used to send the alexa request + :rtype: str + """ + return handler_input.request_envelope.context.system.device.device_id + + +def get_dialog_state(handler_input): + # type: (HandlerInput) -> Optional[DialogState] + """Return the dialog state enum from the intent request. + + The method retrieves the `dialogState` from the intent request, if + the skill's interaction model includes a dialog model. This can be + used to determine the current status of user conversation and return + the appropriate dialog directives if the conversation is not yet complete. + More information on dialog management can be found here : + https://developer.amazon.com/docs/custom-skills/define-the-dialog-to-collect-and-confirm-required-information.html + + The method returns a ``None`` if there is no dialog model added or + if the intent doesn't have dialog management. The method raises a + :py:class:`TypeError` if the input is not an `IntentRequest`. + + :param handler_input: The handler input instance that is generally + passed in the sdk's request and exception components. + :type handler_input: ask_sdk_core.handler_input.HandlerInput + :return: State of the dialog model from the intent request. + :rtype: Optional[ask_sdk_model.dialog_state.DialogState] + :raises: TypeError if the input is not an IntentRequest + """ + request = handler_input.request_envelope.request + if isinstance(request, IntentRequest): + return request.dialog_state + + raise TypeError("The provided request is not an IntentRequest") + + +def get_slot(handler_input, slot_name): + # type: (HandlerInput, AnyStr) -> Optional[Slot] + """Return the slot information from intent request. + + The method retrieves the slot information + :py:class:`ask_sdk_model.slot.Slot` from the input intent request + for the given ``slot_name``. More information on the slots can be + found here : + https://developer.amazon.com/docs/custom-skills/request-types-reference.html#slot-object + + If there is no such slot, then a ``None`` + is returned. If the input request is not an + :py:class:`ask_sdk_model.intent_request.IntentRequest`, a + :py:class:`TypeError` is raised. + + :param handler_input: The handler input instance that is generally + passed in the sdk's request and exception components + :type handler_input: ask_sdk_core.handler_input.HandlerInput + :param slot_name: Name of the slot that needs to be retrieved + :type slot_name: str + :return: Slot information for the provided slot name if it exists, + or a `None` value + :rtype: Optional[ask_sdk_model.slot.Slot] + :raises: TypeError if the input is not an IntentRequest + """ + request = handler_input.request_envelope.request + if isinstance(request, IntentRequest): + if request.intent.slots is not None: + return request.intent.slots.get(slot_name, None) + else: + return None + + raise TypeError("The provided request is not an IntentRequest") + + +def get_slot_value(handler_input, slot_name): + # type: (HandlerInput, AnyStr) -> AnyStr + """Return the slot value from intent request. + + The method retrieves the slot value from the input intent request + for the given ``slot_name``. More information on the slots can be + found here : + https://developer.amazon.com/docs/custom-skills/request-types-reference.html#slot-object + + If there is no such slot, then a :py:class:`ValueError` is raised. + If the input request is not an + :py:class:`ask_sdk_model.intent_request.IntentRequest`, a + :py:class:`TypeError` is raised. + + :param handler_input: The handler input instance that is generally + passed in the sdk's request and exception components + :type handler_input: ask_sdk_core.handler_input.HandlerInput + :param slot_name: Name of the slot for which the value has to be retrieved + :type slot_name: str + :return: Slot value for the provided slot if it exists + :rtype: str + :raises: TypeError if the input is not an IntentRequest. ValueError is + slot doesn't exist + """ + slot = get_slot(handler_input=handler_input, slot_name=slot_name) + + if slot is not None: + return slot.value + + raise ValueError( + "Provided slot {} doesn't exist in the input request".format( + slot_name)) + + +def get_supported_interfaces(handler_input): + # type: (HandlerInput) -> SupportedInterfaces + """Retrieves the supported interfaces from input request. + + The method returns an + :py:class:`ask_sdk_model.supported_interfaces.SupportedInterfaces` + object instance listing each interface that the device + supports. For example, if ``supported_interfaces`` includes + ``audio_player``, then you know that the device supports streaming + audio using the AudioPlayer interface. More information on + `supportedInterfaces` can be found here : + https://developer.amazon.com/docs/custom-skills/request-and-response-json-reference.html#system-object + + :param handler_input: The handler input instance that is generally + passed in the sdk's request and exception components + :type handler_input: ask_sdk_core.handler_input.HandlerInput + :return: Instance of + :py:class:`ask_sdk_model.supported_interfaces.SupportedInterfaces` + mentioning which all interfaces the device supports + :rtype: ask_sdk_model.supported_interfaces.SupportedInterfaces + """ + return ( + handler_input.request_envelope.context.system.device. + supported_interfaces) + + +def is_new_session(handler_input): + # type: (HandlerInput) -> bool + """Return if the session is new for the input request. + + The method retrieves the ``new`` value from the input request's + session, which indicates if it's a new session or not. The + :py:class:`ask_sdk_model.session.Session` is only included on all + standard requests except ``AudioPlayer``, ``VideoApp`` and + ``PlaybackController`` requests. More information can be found here : + https://developer.amazon.com/docs/custom-skills/request-and-response-json-reference.html#session-object + + A :py:class:`TypeError` is raised if the input request doesn't have + the ``session`` information. + + :param handler_input: The handler input instance that is generally + passed in the sdk's request and exception components + :type handler_input: ask_sdk_core.handler_input.HandlerInput + :return: Boolean if the session is new for the input request + :rtype: bool + :raises: TypeError if the input request doesn't have a session + """ + session = handler_input.request_envelope.session + + if session is not None: + return session.new + + raise TypeError("The provided request doesn't have a session") diff --git a/ask-sdk-core/ask_sdk_core/utils/viewport.py b/ask-sdk-core/ask_sdk_core/utils/viewport.py index f09ddc0..befbf10 100644 --- a/ask-sdk-core/ask_sdk_core/utils/viewport.py +++ b/ask-sdk-core/ask_sdk_core/utils/viewport.py @@ -87,7 +87,7 @@ def get_orientation(width, height): :type width: int :type height: int - :return viewport orientation enum + :return: viewport orientation enum :rtype: Orientation """ if width > height: @@ -103,7 +103,7 @@ def get_size(size): """Get viewport size from given size. :type size: int - :return viewport size enum + :return: viewport size enum :rtype: Size """ if size in range(0, 600): @@ -125,7 +125,7 @@ def get_dpi_group(dpi): """Get viewport density group from given dpi. :type dpi: int - :return viewport density group enum + :return: viewport density group enum :rtype: Density """ if dpi in range(0, 121): @@ -157,8 +157,8 @@ def get_viewport_profile(request_envelope): :param request_envelope: The alexa request envelope object :type request_envelope: ask_sdk_model.request_envelope.RequestEnvelope - :return Calculated Viewport Profile enum - :rtype ViewportProfile + :return: Calculated Viewport Profile enum + :rtype: ViewportProfile """ viewport_state = request_envelope.context.viewport if viewport_state: diff --git a/ask-sdk-core/tests/unit/test_utils.py b/ask-sdk-core/tests/unit/test_utils.py index fa99f49..c7984db 100644 --- a/ask-sdk-core/tests/unit/test_utils.py +++ b/ask-sdk-core/tests/unit/test_utils.py @@ -19,13 +19,20 @@ import random from ask_sdk_model import ( - IntentRequest, RequestEnvelope, Intent, SessionEndedRequest, Context) + IntentRequest, RequestEnvelope, Intent, SessionEndedRequest, Context, + LaunchRequest, DialogState, Slot, Session, User, Device, + SupportedInterfaces) from ask_sdk_model.canfulfill import CanFulfillIntentRequest +from ask_sdk_model.interfaces.viewport import ViewportState, Shape +from ask_sdk_model.interfaces.system import SystemState +from ask_sdk_model.interfaces.display import DisplayInterface from ask_sdk_core.utils import ( - is_canfulfill_intent_name, is_intent_name, is_request_type, viewport) + is_canfulfill_intent_name, is_intent_name, is_request_type, viewport, + get_slot, get_slot_value, get_account_linking_access_token, + get_api_access_token, get_device_id, get_dialog_state, get_intent_name, + get_locale, get_request_type, is_new_session, get_supported_interfaces) from ask_sdk_core.handler_input import HandlerInput from ask_sdk_core.exceptions import AskSdkException -from ask_sdk_model.interfaces.viewport import ViewportState, Shape def test_is_canfulfill_intent_name_match(): @@ -393,3 +400,236 @@ def test_viewport_map_to_unknown_for_no_viewport(self): assert (viewport.get_viewport_profile(test_request_env) is viewport.ViewportProfile.UNKNOWN_VIEWPORT_PROFILE), ( "Viewport profile couldn't resolve UNKNOWN_VIEWPORT_PROFILE") + + +class TestRequestUtils(unittest.TestCase): + + def setUp(self): + self.test_locale = "foo_locale" + self.test_request_type = "LaunchRequest" + self.test_dialog_state = DialogState.COMPLETED + self.test_intent_name = "foo_intent" + self.test_slot_name = "foo_slot" + self.test_slot_value = "foo_slot_value" + self.test_slot = Slot( + name=self.test_slot_name, value=self.test_slot_value) + self.test_api_access_token = "foo_api_access_token" + self.test_access_token = "foo_account_linking_access_token" + self.test_device_id = "foo_device_id" + self.test_supported_interfaces = SupportedInterfaces( + display=DisplayInterface( + template_version="test_template", markup_version="test_markup") + ) + self.test_new_session = False + + self.test_launch_request = LaunchRequest(locale=self.test_locale) + self.test_intent_request = IntentRequest( + dialog_state=self.test_dialog_state, + intent=Intent( + name=self.test_intent_name, + slots={ + self.test_slot_name: Slot( + name=self.test_slot_name, + value=self.test_slot_value) + })) + self.test_request_envelope = RequestEnvelope( + session=Session(new=self.test_new_session), + context=Context( + system=SystemState( + user=User(access_token=self.test_access_token), + api_access_token=self.test_api_access_token, + device=Device( + device_id=self.test_device_id, + supported_interfaces=self.test_supported_interfaces)))) + + def _create_handler_input(self, request): + self.test_request_envelope.request = request + return HandlerInput(request_envelope=self.test_request_envelope) + + def test_get_locale(self): + test_input = self._create_handler_input( + request=self.test_launch_request) + + self.assertEqual( + get_locale(handler_input=test_input), self.test_locale, + "get_locale method returned incorrect locale for input request") + + def test_get_request_type(self): + test_input = self._create_handler_input( + request=self.test_launch_request) + + self.assertEqual( + get_request_type(handler_input=test_input), self.test_request_type, + "get_request_type method returned incorrect request type for " + "input request") + + def test_get_intent_name_throws_exception_for_non_intent_request(self): + test_input = self._create_handler_input( + request=self.test_launch_request) + + with self.assertRaises( + TypeError, + msg="get_intent_name method didn't throw TypeError when an " + "invalid request type is passed"): + get_intent_name(handler_input=test_input) + + def test_get_intent_name(self): + test_input = self._create_handler_input( + request=self.test_intent_request) + + self.assertEqual( + get_intent_name(handler_input=test_input), self.test_intent_name, + "get_intent_name method returned incorrect intent name for " + "input request") + + def test_get_account_linking_access_token(self): + test_input = self._create_handler_input( + request=self.test_launch_request) + + self.assertEqual( + get_account_linking_access_token(handler_input=test_input), + self.test_access_token, + "get_account_linking_access_token method returned incorrect " + "access token from input request") + + def test_get_api_access_token(self): + test_input = self._create_handler_input( + request=self.test_launch_request) + + self.assertEqual( + get_api_access_token(handler_input=test_input), + self.test_api_access_token, + "get_api_access_token method returned incorrect " + "api access token from input request") + + def test_get_device_id(self): + test_input = self._create_handler_input( + request=self.test_launch_request) + + self.assertEqual( + get_device_id(handler_input=test_input), + self.test_device_id, + "get_device_id method returned incorrect " + "device id from input request") + + def test_get_dialog_state_throws_exception_for_non_intent_request(self): + test_input = self._create_handler_input( + request=self.test_launch_request) + + with self.assertRaises( + TypeError, + msg="get_dialog_state method didn't throw TypeError when an " + "invalid request type is passed"): + get_dialog_state(handler_input=test_input) + + def test_get_dialog_state(self): + test_input = self._create_handler_input( + request=self.test_intent_request) + + self.assertEqual( + get_dialog_state(handler_input=test_input), + self.test_dialog_state, + "get_dialog_state method returned incorrect " + "dialog state from input request") + + def test_get_slot_throws_exception_for_non_intent_request(self): + test_input = self._create_handler_input( + request=self.test_launch_request) + + with self.assertRaises( + TypeError, + msg="get_slot method didn't throw TypeError when an " + "invalid request type is passed"): + get_slot(handler_input=test_input, slot_name=self.test_slot_name) + + def test_get_slot_returns_none_for_no_slots(self): + self.test_intent_request.intent.slots = None + test_input = self._create_handler_input( + request=self.test_intent_request) + + self.assertEqual( + get_slot(handler_input=test_input, slot_name="some_slot"), + None, + "get_slot method didn't return None from input request " + "when intent request has no slots") + + def test_get_slot_returns_none_for_non_existent_slot(self): + test_input = self._create_handler_input( + request=self.test_intent_request) + + self.assertEqual( + get_slot(handler_input=test_input, slot_name="some_slot"), + None, + "get_slot method didn't return None from input request " + "when a non-existent slot name is passed") + + def test_get_slot(self): + test_input = self._create_handler_input( + request=self.test_intent_request) + + self.assertEqual( + get_slot(handler_input=test_input, slot_name=self.test_slot_name), + self.test_slot, + "get_slot method returned incorrect slot from input request " + "when a valid slot name is passed") + + def test_get_slot_value_throws_exception_for_non_intent_request(self): + test_input = self._create_handler_input( + request=self.test_launch_request) + + with self.assertRaises( + TypeError, + msg="get_slot_value method didn't throw TypeError when an " + "invalid request type is passed"): + get_slot_value( + handler_input=test_input, slot_name=self.test_slot_name) + + def test_get_slot_value_throws_exception_for_non_existent_slot(self): + test_input = self._create_handler_input( + request=self.test_intent_request) + + with self.assertRaises( + ValueError, + msg="get_slot_value method didn't throw ValueError when an " + "invalid slot name is passed"): + get_slot_value(handler_input=test_input, slot_name="some_slot") + + def test_get_slot_value(self): + test_input = self._create_handler_input( + request=self.test_intent_request) + + self.assertEqual( + get_slot_value( + handler_input=test_input, slot_name=self.test_slot_name), + self.test_slot_value, + "get_slot_value method returned incorrect slot value from " + "input request when a valid slot name is passed") + + def test_get_supported_interfaces(self): + test_input = self._create_handler_input( + request=self.test_launch_request) + + self.assertEqual( + get_supported_interfaces(handler_input=test_input), + self.test_supported_interfaces, + "get_supported_interfaces method returned incorrect " + "supported interfaces from input request") + + def test_is_new_session_throws_exception_if_session_not_exists(self): + test_input = HandlerInput(request_envelope=RequestEnvelope()) + + with self.assertRaises( + TypeError, + msg="is_new_session method didn't throw TypeError when an " + "input request without session is passed"): + is_new_session(handler_input=test_input) + + def test_is_new_session(self): + test_input = self._create_handler_input( + request=self.test_launch_request) + + self.assertEqual( + is_new_session(handler_input=test_input), + self.test_new_session, + "is_new_session method returned incorrect session information " + "from input request when a session exists") diff --git a/docs/en/api/core.rst b/docs/en/api/core.rst index 9e25879..40fb066 100644 --- a/docs/en/api/core.rst +++ b/docs/en/api/core.rst @@ -118,7 +118,21 @@ Default Serializer General Utilities ~~~~~~~~~~~~~~~~~ -.. automodule:: ask_sdk_core.utils +.. automodule:: ask_sdk_core.utils.predicate + :members: + :undoc-members: + :inherited-members: + :show-inheritance: + :member-order: bysource + +.. automodule:: ask_sdk_core.utils.viewport + :members: + :undoc-members: + :inherited-members: + :show-inheritance: + :member-order: bysource + +.. automodule:: ask_sdk_core.utils.request_util :members: :undoc-members: :inherited-members: