├── .github └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── NOTICE ├── README.md ├── client ├── sample-switch-black.json ├── sample-switch-white.json ├── sample-toaster.json └── sandbox.py ├── cloudformation ├── backend.template └── sandbox.template ├── instructions ├── 01-verify-requirements.md ├── 02-create-the-backend.md ├── 03-setup-lwa.md ├── 04-create-skill-smarthome.md ├── 05-configure-skill-smarthome.md ├── 06-link-skill-smarthome.md ├── 07-create-endpoints.md ├── 08-test-endpoints.md ├── 09-send-an-event.md ├── 10-cleanup.md ├── README.md ├── create-sandbox.md ├── img │ ├── 3.1.10-lwa-web-settings.png │ ├── 3.1.14-lwa-web-settings.png │ ├── 3.1.7-lwa-profile-configuration.png │ ├── 4.4.3-lambda-trigger.png │ ├── 4.4.4-lambda-trigger-smart-home.png │ ├── 6.2.3-smart-home-skill.png │ ├── 6.2.5-linking-dialog.png │ ├── 7.2.3.1-postman-manage-environments.png │ ├── 7.3.2-postman-collections-endpoints.png │ ├── 7.3.6-thing-sampleswitch.png │ ├── alexa-sample-smarthome-108x.png │ ├── alexa-sample-smarthome-150x.png │ └── alexa-sample-smarthome-512x.png ├── setup.txt ├── skill-sample-smarthome-iot.postman_collection.json └── skill-sample-smarthome-iot.postman_environment.json └── lambda ├── README.md ├── api ├── alexa │ ├── __init__.py │ └── skills │ │ ├── __init__.py │ │ └── smarthome │ │ ├── __init__.py │ │ ├── alexa_response.py │ │ └── alexa_utils.py ├── alexa_smart_home_message_schema.json ├── endpoint_cloud │ ├── __init__.py │ ├── api_auth.py │ ├── api_handler.py │ ├── api_handler_directive.py │ ├── api_handler_endpoint.py │ ├── api_handler_event.py │ ├── api_message.py │ ├── api_response.py │ ├── api_response_body.py │ └── api_utils.py ├── index.py └── jsonschema │ ├── __init__.py │ ├── __main__.py │ ├── _format.py │ ├── _reflect.py │ ├── _utils.py │ ├── _validators.py │ ├── _version.py │ ├── cli.py │ ├── compat.py │ ├── exceptions.py │ ├── schemas │ ├── draft3.json │ └── draft4.json │ ├── tests │ ├── __init__.py │ ├── compat.py │ ├── test_cli.py │ ├── test_exceptions.py │ ├── test_format.py │ ├── test_jsonschema_test_suite.py │ └── test_validators.py │ └── validators.py ├── setup └── index.py └── smarthome ├── __init__.py ├── index.py └── skill.json /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Issue #, if available:* 2 | 3 | *Description of changes:* 4 | 5 | 6 | By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea/ 3 | __pycache__/ 4 | output/ -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check [existing open](https://github.com/alexa/skill-sample-python-smarthome-iot/issues), or [recently closed](https://github.com/alexa/skill-sample-python-smarthome-iot/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/alexa/skill-sample-python-smarthome-iot/labels/help%20wanted) issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](https://github.com/alexa/skill-sample-python-smarthome-iot/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Amazon Software License 1.0 2 | 3 | This Amazon Software License ("License") governs your use, reproduction, and 4 | distribution of the accompanying software as specified below. 5 | 6 | 1. Definitions 7 | 8 | "Licensor" means any person or entity that distributes its Work. 9 | 10 | "Software" means the original work of authorship made available under this 11 | License. 12 | 13 | "Work" means the Software and any additions to or derivative works of the 14 | Software that are made available under this License. 15 | 16 | The terms "reproduce," "reproduction," "derivative works," and 17 | "distribution" have the meaning as provided under U.S. copyright law; 18 | provided, however, that for the purposes of this License, derivative works 19 | shall not include works that remain separable from, or merely link (or bind 20 | by name) to the interfaces of, the Work. 21 | 22 | Works, including the Software, are "made available" under this License by 23 | including in or with the Work either (a) a copyright notice referencing the 24 | applicability of this License to the Work, or (b) a copy of this License. 25 | 26 | 2. License Grants 27 | 28 | 2.1 Copyright Grant. Subject to the terms and conditions of this License, 29 | each Licensor grants to you a perpetual, worldwide, non-exclusive, 30 | royalty-free, copyright license to reproduce, prepare derivative works of, 31 | publicly display, publicly perform, sublicense and distribute its Work and 32 | any resulting derivative works in any form. 33 | 34 | 2.2 Patent Grant. Subject to the terms and conditions of this License, each 35 | Licensor grants to you a perpetual, worldwide, non-exclusive, royalty-free 36 | patent license to make, have made, use, sell, offer for sale, import, and 37 | otherwise transfer its Work, in whole or in part. The foregoing license 38 | applies only to the patent claims licensable by Licensor that would be 39 | infringed by Licensor's Work (or portion thereof) individually and 40 | excluding any combinations with any other materials or technology. 41 | 42 | 3. Limitations 43 | 44 | 3.1 Redistribution. You may reproduce or distribute the Work only if 45 | (a) you do so under this License, (b) you include a complete copy of this 46 | License with your distribution, and (c) you retain without modification 47 | any copyright, patent, trademark, or attribution notices that are present 48 | in the Work. 49 | 50 | 3.2 Derivative Works. You may specify that additional or different terms 51 | apply to the use, reproduction, and distribution of your derivative works 52 | of the Work ("Your Terms") only if (a) Your Terms provide that the use 53 | limitation in Section 3.3 applies to your derivative works, and (b) you 54 | identify the specific derivative works that are subject to Your Terms. 55 | Notwithstanding Your Terms, this License (including the redistribution 56 | requirements in Section 3.1) will continue to apply to the Work itself. 57 | 58 | 3.3 Use Limitation. The Work and any derivative works thereof only may be 59 | used or intended for use with the web services, computing platforms or 60 | applications provided by Amazon.com, Inc. or its affiliates, including 61 | Amazon Web Services, Inc. 62 | 63 | 3.4 Patent Claims. If you bring or threaten to bring a patent claim against 64 | any Licensor (including any claim, cross-claim or counterclaim in a 65 | lawsuit) to enforce any patents that you allege are infringed by any Work, 66 | then your rights under this License from such Licensor (including the 67 | grants in Sections 2.1 and 2.2) will terminate immediately. 68 | 69 | 3.5 Trademarks. This License does not grant any rights to use any 70 | Licensor's or its affiliates' names, logos, or trademarks, except as 71 | necessary to reproduce the notices described in this License. 72 | 73 | 3.6 Termination. If you violate any term of this License, then your rights 74 | under this License (including the grants in Sections 2.1 and 2.2) will 75 | terminate immediately. 76 | 77 | 4. Disclaimer of Warranty. 78 | 79 | THE WORK IS PROVIDED "AS IS" WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 80 | EITHER EXPRESS OR IMPLIED, INCLUDING WARRANTIES OR CONDITIONS OF 81 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE OR 82 | NON-INFRINGEMENT. YOU BEAR THE RISK OF UNDERTAKING ANY ACTIVITIES UNDER 83 | THIS LICENSE. SOME STATES' CONSUMER LAWS DO NOT ALLOW EXCLUSION OF AN 84 | IMPLIED WARRANTY, SO THIS DISCLAIMER MAY NOT APPLY TO YOU. 85 | 86 | 5. Limitation of Liability. 87 | 88 | EXCEPT AS PROHIBITED BY APPLICABLE LAW, IN NO EVENT AND UNDER NO LEGAL 89 | THEORY, WHETHER IN TORT (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE 90 | SHALL ANY LICENSOR BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY DIRECT, 91 | INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF OR 92 | RELATED TO THIS LICENSE, THE USE OR INABILITY TO USE THE WORK (INCLUDING 93 | BUT NOT LIMITED TO LOSS OF GOODWILL, BUSINESS INTERRUPTION, LOST PROFITS 94 | OR DATA, COMPUTER FAILURE OR MALFUNCTION, OR ANY OTHER COMM ERCIAL DAMAGES 95 | OR LOSSES), EVEN IF THE LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF 96 | SUCH DAMAGES. 97 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Skill Sample Python Smarthome Iot 2 | Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Skill Sample Python Smarthome Sandbox 2 | 3 | This is Alexa skill sample code for showing how several areas of a Smart Home model come together to define and provide endpoint devices using Lambda, IoT Core, DynamoDB, and API Gateway services. 4 | 5 | To get started, follow the [instructions](instructions/README.md). 6 | 7 | ### License 8 | 9 | This library is licensed under the Amazon Software License. 10 | 11 | -------------------------------------------------------------------------------- /client/sample-switch-black.json: -------------------------------------------------------------------------------- 1 | { 2 | "event": { 3 | "endpoint": { 4 | "userId": "INVALID", 5 | "friendlyName": "Black Sample Switch", 6 | "capabilities": [ 7 | { 8 | "type": "AlexaInterface", 9 | "interface": "Alexa", 10 | "version": "3" 11 | }, 12 | { 13 | "type": "AlexaInterface", 14 | "interface": "Alexa.PowerController", 15 | "version": "3", 16 | "properties": { 17 | "supported": [ 18 | { 19 | "name": "powerState" 20 | } 21 | ], 22 | "proactivelyReported": true, 23 | "retrievable": true 24 | } 25 | } 26 | ], 27 | "sku": "SW00-S3M-KR75", 28 | "description": "A Sample Switch", 29 | "manufacturerName": "Sample Manufacturer", 30 | "displayCategories": [ 31 | "SWITCH" 32 | ] 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /client/sample-switch-white.json: -------------------------------------------------------------------------------- 1 | { 2 | "event": { 3 | "endpoint": { 4 | "userId": "INVALID", 5 | "friendlyName": "White Sample Switch", 6 | "capabilities": [ 7 | { 8 | "type": "AlexaInterface", 9 | "interface": "Alexa", 10 | "version": "3" 11 | }, 12 | { 13 | "type": "AlexaInterface", 14 | "interface": "Alexa.PowerController", 15 | "version": "3", 16 | "properties": { 17 | "supported": [ 18 | { 19 | "name": "powerState" 20 | } 21 | ], 22 | "proactivelyReported": true, 23 | "retrievable": true 24 | } 25 | } 26 | ], 27 | "sku": "SW01-S3M-KR76", 28 | "description": "A Sample Switch", 29 | "manufacturerName": "Sample Manufacturer", 30 | "displayCategories": [ 31 | "SWITCH" 32 | ] 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /client/sample-toaster.json: -------------------------------------------------------------------------------- 1 | { 2 | "event": { 3 | "endpoint": { 4 | "userId": "INVALID", 5 | "friendlyName": "Sample Toaster", 6 | "capabilities": [ 7 | { 8 | "type": "AlexaInterface", 9 | "interface": "Alexa", 10 | "version": "3" 11 | }, 12 | { 13 | "type": "AlexaInterface", 14 | "interface": "Alexa.RangeController", 15 | "version": "3", 16 | "instance": "SampleManufacturer.Endpoint.Heat", 17 | "capabilityResources": { 18 | "friendlyNames": [ 19 | { 20 | "@type": "text", 21 | "value": { 22 | "text": "Heat", 23 | "locale": "en-US" 24 | } 25 | } 26 | ] 27 | }, 28 | "properties": { 29 | "supported": [ 30 | { 31 | "name": "rangeValue" 32 | } 33 | ], 34 | "proactivelyReported": true, 35 | "retrievable": true 36 | }, 37 | "configuration": { 38 | "supportedRange": { 39 | "minimumValue": 1, 40 | "maximumValue": 7, 41 | "precision": 1 42 | }, 43 | "presets": [ 44 | { 45 | "rangeValue": 1, 46 | "presetResources": { 47 | "friendlyNames": [ 48 | { 49 | "@type": "text", 50 | "value": { 51 | "text": "Light", 52 | "locale": "en-US" 53 | } 54 | }, 55 | { 56 | "@type": "asset", 57 | "value": { 58 | "assetId": "Alexa.Value.Minimum" 59 | } 60 | } 61 | ] 62 | } 63 | }, 64 | { 65 | "rangeValue": 4, 66 | "presetResources": { 67 | "friendlyNames": [ 68 | { 69 | "@type": "asset", 70 | "value": { 71 | "assetId": "Alexa.Value.Medium" 72 | } 73 | } 74 | ] 75 | } 76 | }, 77 | { 78 | "rangeValue": 7, 79 | "presetResources": { 80 | "friendlyNames": [ 81 | { 82 | "@type": "text", 83 | "value": { 84 | "text": "Dark", 85 | "locale": "en-US" 86 | } 87 | }, 88 | { 89 | "@type": "asset", 90 | "value": { 91 | "assetId": "Alexa.Value.Maximum" 92 | } 93 | } 94 | ] 95 | } 96 | } 97 | ] 98 | } 99 | }, 100 | { 101 | "type": "AlexaInterface", 102 | "interface": "Alexa.ToggleController", 103 | "version": "3", 104 | "instance": "SampleManufacturer.Toaster.Bagel", 105 | "capabilityResources": { 106 | "friendlyNames": [ 107 | { 108 | "@type": "text", 109 | "value": { 110 | "text": "Bagel", 111 | "locale": "en-US" 112 | } 113 | }, 114 | { 115 | "@type": "text", 116 | "value": { 117 | "text": "Bagel Mode", 118 | "locale": "en-US" 119 | } 120 | }, 121 | { 122 | "@type": "text", 123 | "value": { 124 | "text": "Bagel Setting", 125 | "locale": "en-US" 126 | } 127 | } 128 | ] 129 | }, 130 | "properties": { 131 | "supported": [ 132 | { 133 | "name": "toggleState" 134 | } 135 | ], 136 | "proactivelyReported": false, 137 | "retrievable": false 138 | } 139 | }, 140 | { 141 | "type": "AlexaInterface", 142 | "interface": "Alexa.ToggleController", 143 | "version": "3", 144 | "instance": "SampleManufacturer.Toaster.Frozen", 145 | "capabilityResources": { 146 | "friendlyNames": [ 147 | { 148 | "@type": "text", 149 | "value": { 150 | "text": "Frozen", 151 | "locale": "en-US" 152 | } 153 | }, 154 | { 155 | "@type": "text", 156 | "value": { 157 | "text": "Frozen Mode", 158 | "locale": "en-US" 159 | } 160 | }, 161 | { 162 | "@type": "text", 163 | "value": { 164 | "text": "Frozen Setting", 165 | "locale": "en-US" 166 | } 167 | } 168 | ] 169 | }, 170 | "properties": { 171 | "supported": [ 172 | { 173 | "name": "toggleState" 174 | } 175 | ], 176 | "proactivelyReported": false, 177 | "retrievable": false 178 | } 179 | } 180 | ], 181 | "sku": "TT00-AIS-K177", 182 | "description": "A Sample Toaster", 183 | "manufacturerName": "Sample Manufacturer", 184 | "displayCategories": [ 185 | "OTHER" 186 | ] 187 | } 188 | } 189 | } -------------------------------------------------------------------------------- /client/sandbox.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 5 | # 6 | # Licensed under the Amazon Software License (the "License"). You may not use this file except in 7 | # compliance with the License. A copy of the License is located at 8 | # 9 | # http://aws.amazon.com/asl/ 10 | # 11 | # or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific 13 | # language governing permissions and limitations under the License. 14 | 15 | import boto3 16 | import json 17 | import random 18 | import sys 19 | import time 20 | import webbrowser 21 | from http.server import HTTPServer, SimpleHTTPRequestHandler 22 | from threading import Thread 23 | from urllib.error import HTTPError 24 | from urllib.parse import parse_qs, urlencode, urlparse 25 | from urllib.request import build_opener, HTTPSHandler, Request, urlopen 26 | 27 | sys.path.append('../lambda/api') 28 | from endpoint_cloud import ApiAuth, ApiUtils 29 | 30 | api_auth = ApiAuth() 31 | cloudformation_aws = boto3.client('cloudformation') 32 | cloudformation_aws_resource = boto3.resource('cloudformation') 33 | 34 | # The template to use for the Sandbox 35 | template = '../cloudformation/sandbox.template' 36 | redirect_uri = 'http://127.0.0.1:9090/cb' 37 | 38 | auth_code = client_id = client_secret = access_token = refresh_token = 'INVALID' 39 | auth_code_http_server = None 40 | 41 | 42 | class AuthCodeServerHandler(SimpleHTTPRequestHandler): 43 | def do_GET(self): 44 | global auth_code, auth_code_http_server, client_id, client_secret, access_token, refresh_token 45 | 46 | path = self.path[:3] 47 | if path == '/cb': 48 | query_params = parse_qs(urlparse(self.path).query) 49 | auth_code = query_params['code'][0] 50 | print('auth_code', auth_code) 51 | self.protocol_version = 'HTTP/1.1' 52 | self.send_response(200, 'OK') 53 | self.send_header('Content-type', 'text/html') 54 | self.end_headers() 55 | self.wfile.write( 56 | bytes('' 57 | 'Smart Home Sandbox' 58 | '
' 59 | '

AuthCode: {}

' 60 | '

Starting stack creation, visit the console for status.

'.format(auth_code), 'UTF-8')) 61 | 62 | # Get access and refresh tokens 63 | response_token = api_auth.get_access_token(auth_code, client_id, client_secret, redirect_uri) 64 | response_token_string = response_token.read().decode('utf-8') 65 | # print('LOG directive.process.authorization.response_token_string:', response_token_string) 66 | response_object = json.loads(response_token_string) 67 | # print(response_object) 68 | access_token = response_object['access_token'] 69 | refresh_token = response_object['refresh_token'] 70 | 71 | # Stop the server 72 | def shutdown_server(server): 73 | server.shutdown() 74 | 75 | thread = Thread(target=shutdown_server, args=(auth_code_http_server,)) 76 | thread.start() 77 | 78 | 79 | def create_endpoint(endpoint_file_name, user_id, endpoint_api_url): 80 | with open(endpoint_file_name, 'r') as endpoint_file: 81 | event_object = json.load(endpoint_file) 82 | event_object['event']['endpoint']['userId'] = user_id 83 | 84 | url = endpoint_api_url + 'endpoints' 85 | data = bytes(json.dumps(event_object), encoding="utf-8") 86 | headers = {'Content-Type': 'application/json'} 87 | req = Request(url, data, headers) 88 | result = urlopen(req).read().decode("utf-8") 89 | response = json.loads(result) 90 | if ApiUtils.check_response(response): 91 | print('\tCreated endpoint', endpoint_file_name) 92 | endpoint_file.close() 93 | 94 | 95 | def create_stack(stack_name, vendor_id, client_id, client_secret, access_token, refresh_token): 96 | print('Creating Stack', stack_name) 97 | with open(template, 'r') as template_file: 98 | template_object = json.load(template_file) 99 | # Run the CF Template with parameters 100 | response_cloudformation = cloudformation_aws.create_stack( 101 | StackName=stack_name, 102 | TemplateBody=json.dumps(template_object), 103 | Capabilities=['CAPABILITY_NAMED_IAM'], 104 | Parameters=[ 105 | { 106 | "ParameterKey": "VendorId", 107 | "ParameterValue": vendor_id 108 | }, 109 | { 110 | "ParameterKey": "ClientId", 111 | "ParameterValue": client_id 112 | }, 113 | { 114 | "ParameterKey": "ClientSecret", 115 | "ParameterValue": client_secret 116 | }, 117 | { 118 | "ParameterKey": "AccessToken", 119 | "ParameterValue": access_token 120 | }, 121 | { 122 | "ParameterKey": "RefreshToken", 123 | "ParameterValue": refresh_token 124 | } 125 | ] 126 | ) 127 | if ApiUtils.check_response(response_cloudformation): 128 | print(stack_name, 'Stack creation started') 129 | 130 | 131 | def get_auth_code_url(redirect_uri, client_id): 132 | query_params = { 133 | 'redirect_uri': redirect_uri, 134 | 'scope': 'alexa::ask:skills:readwrite profile:user_id', 135 | 'state': 'Ask-SkillModel-ReadWrite', 136 | 'response_type': 'code', 137 | 'client_id': client_id 138 | } 139 | return 'https://www.amazon.com/ap/oa?' + urlencode(query_params) 140 | 141 | 142 | def main(): 143 | global auth_code_http_server, client_id, client_secret, access_token, refresh_token 144 | 145 | print('INSTRUCTION: Enter the Vendor ID, LWA Client ID, and LWA Client Secret') 146 | vendor_id = input('Vendor ID: ') 147 | client_id = input('Login with Amazon Client ID: ') 148 | client_secret = input('Login with Amazon Client Secret: ') 149 | 150 | print('\tAuthorizing User...') 151 | webbrowser.open(get_auth_code_url('http://127.0.0.1:9090/cb', client_id), new=2) 152 | 153 | # Start HTTP server to wait for auth code 154 | print('\tWaiting for authorization from User...') 155 | server_address = ('127.0.0.1', 9090) 156 | auth_code_http_server = httpd = HTTPServer(server_address, AuthCodeServerHandler) 157 | httpd.serve_forever() 158 | 159 | # Start the Stack Creation 160 | name_id = ''.join(random.choice('0123456789') for _ in range(6)) 161 | stack_name = 'SmartHomeSandbox-' + name_id 162 | create_stack(stack_name, vendor_id, client_id, client_secret, access_token, refresh_token) 163 | 164 | stack = cloudformation_aws_resource.Stack(stack_name) 165 | while True: 166 | status = stack.stack_status 167 | print('\tstack status:', status) 168 | 169 | if status == 'CREATE_COMPLETE': 170 | break 171 | 172 | if status == 'ROLLBACK_COMPLETE': 173 | print('Error during stack creation, check the CloudWatch logs') 174 | return 175 | 176 | time.sleep(5) 177 | stack = cloudformation_aws_resource.Stack(stack_name) 178 | 179 | # Unpack the outputs from the created stack 180 | for output in stack.outputs: 181 | 182 | if output['OutputKey'] == 'AccessToken': 183 | access_token = output['OutputValue'] 184 | print('\taccess_token:', access_token) 185 | 186 | if output['OutputKey'] == 'AlexaSkillId': 187 | alexa_skill_id = output['OutputValue'] 188 | print('\talexa_skill_id:', alexa_skill_id) 189 | 190 | if output['OutputKey'] == 'ClientId': 191 | client_id = output['OutputValue'] 192 | print('\tclient_id:', client_id) 193 | 194 | if output['OutputKey'] == 'ClientSecret': 195 | client_secret = output['OutputValue'] 196 | print('\tclient_secret:', client_secret) 197 | 198 | if output['OutputKey'] == 'EndpointApiUrl': 199 | endpoint_api_url = output['OutputValue'] 200 | print('\tendpoint_api_url:', endpoint_api_url) 201 | 202 | if output['OutputKey'] == 'SkillLambdaArn': 203 | skill_lambda_arn = output['OutputValue'] 204 | print('\tskill_lambda_arn:', skill_lambda_arn) 205 | 206 | account_linking_request = { 207 | "accountLinkingRequest": { 208 | "skipOnEnablement": "false", 209 | "type": "AUTH_CODE", 210 | "authorizationUrl": "https://www.amazon.com/ap/oa", 211 | "domains": [], 212 | "clientId": client_id, 213 | "scopes": ["profile:user_id"], 214 | "accessTokenUrl": "https://api.amazon.com/auth/o2/token", 215 | "clientSecret": client_secret, 216 | "accessTokenScheme": "HTTP_BASIC", 217 | "defaultTokenExpirationInSeconds": 20 218 | } 219 | } 220 | url_update_account_linking = \ 221 | 'https://api.amazonalexa.com/v1/skills/{0}/stages/development/accountLinkingClient'.format(alexa_skill_id) 222 | # print('url_update_account_linking:', url_update_account_linking) 223 | 224 | if access_token == 'INVALID': 225 | print('\taccess_token:', access_token) 226 | return 227 | 228 | opener = build_opener(HTTPSHandler) 229 | request = Request(url_update_account_linking, data=bytes(json.dumps(account_linking_request), 'iso-8859-1')) 230 | request.add_header('Authorization', access_token) 231 | request.get_method = lambda: 'PUT' 232 | try: 233 | response = opener.open(request) 234 | print('\tSetup Account Linking Request Response Status Code:', response.getcode()) 235 | 236 | except HTTPError as e: 237 | print(e, e.reason, e.headers) 238 | 239 | enable_url = 'https://alexa.amazon.com/spa/index.html#skills/beta/{}/?ref=skill_dsk_skb_ys'.format(alexa_skill_id) 240 | print('INSTRUCTION: Enable the Sandbox Skill at', enable_url) 241 | print('\tOpening Skill Page for Alexa Skill:', alexa_skill_id) 242 | webbrowser.open(enable_url, new=2) 243 | 244 | print('\tAdding User to SampleUsers table and Creating sample endpoint files') 245 | user_id_object = json.loads(api_auth.get_user_id(access_token).read().decode('utf-8')) 246 | user_id = user_id_object['user_id'] 247 | print('\tuser_id', user_id) 248 | 249 | # Update the stored credentials 250 | table = boto3.resource('dynamodb').Table('SampleUsers') 251 | result = table.put_item( 252 | Item={ 253 | 'UserId': user_id, 254 | 'AccessToken': access_token, 255 | 'ClientId': client_id, 256 | 'ClientSecret': client_secret, 257 | 'RefreshToken': refresh_token, 258 | 'RedirectUri': redirect_uri 259 | } 260 | ) 261 | if ApiUtils.check_response(result): 262 | print('\tCreated information for ', user_id) 263 | 264 | if use_defaults: 265 | create_endpoint('sample-switch-black.json', user_id, endpoint_api_url) 266 | create_endpoint('sample-switch-white.json', user_id, endpoint_api_url) 267 | create_endpoint('sample-toaster.json', user_id, endpoint_api_url) 268 | 269 | 270 | if __name__ == '__main__': 271 | use_defaults = True 272 | run = True 273 | # Parse the command line options 274 | if len(sys.argv) > 1: 275 | if sys.argv[1] == 'no-defaults': 276 | print('Option: Not using defaults') 277 | use_defaults = False 278 | 279 | if sys.argv[1] == 'clean-logs': 280 | print('Option: Cleaning Logs') 281 | cloudwatch_client_aws = boto3.client('logs') 282 | response = cloudwatch_client_aws.describe_log_groups( 283 | logGroupNamePrefix='/aws/lambda/SmartHomeSandbox' 284 | ) 285 | print(response) 286 | if 'logGroups' in response: 287 | for log_group in response['logGroups']: 288 | response = cloudwatch_client_aws.delete_log_group( 289 | logGroupName=log_group['logGroupName'] 290 | ) 291 | print(response) 292 | run = False 293 | 294 | if sys.argv[1] == 'clean-things': 295 | print('Option: Cleaning Things') 296 | iot_aws = boto3.client('iot') 297 | response = iot_aws.list_things_in_thing_group( 298 | thingGroupName='Samples' 299 | ) 300 | print(response) 301 | if 'things' in response: 302 | for thing in response['things']: 303 | response = iot_aws.delete_thing( 304 | thingName=thing, 305 | ) 306 | print(response) 307 | run = False 308 | 309 | if run: 310 | main() 311 | -------------------------------------------------------------------------------- /instructions/01-verify-requirements.md: -------------------------------------------------------------------------------- 1 | # Step 1: Verify Requirements 2 | These are the required accounts and resources needed for working through these instructions. 3 | 4 | #### 1.0 Verify the Required Accounts 5 | To work with these instructions, you will need both an Amazon Developer account and an Amazon Web Services account. 6 | 7 | ##### Amazon Developer Account 8 | Go to [https://developer.amazon.com](https://developer.amazon.com) and establish an account if you do not already have one. 9 | 10 | ##### Amazon Web Services Account 11 | Go to [https://aws.amazon.com](https://aws.amazon.com) and register for an Amazon Web Services (AWS) account if you do not already have one. 12 | 13 | #### 1.1 Open a Setup File 14 | This setup file is useful to store temporary IDs and other values during configuration of the environment. 15 | 16 | 1.1.1 Create a folder on your Desktop called `Alexa-SmartHome-Sample`. 17 | 18 | 1.1.2 Download the [setup.txt](https://raw.githubusercontent.com/alexa/skill-sample-python-smarthome-iot/master/instructions/setup.txt) file into the `Alexa-SmartHome-Sample` folder. 19 | 20 | 1.1.3 Open and review the `setup.txt` file: 21 | 22 | ``` 23 | Set in Step 2 - A unique API ID and name for the Alexa Smart Home Skill Lambda function 24 | [EndpointApiId] 25 | XXXXXXXXXX 26 | 27 | [SkillLambdaArn] 28 | arn:aws:lambda:us-east-1:XXXXXXXXXXXX:function:SampleSkillAdapter 29 | 30 | Set in Step 3 - The Client ID and Secret from the LWA Security Profile 31 | [Login with Amazon Client ID] 32 | amzn1.application-oa2-client.XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 33 | 34 | [Login with Amazon Client Secret] 35 | XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 36 | 37 | Set in Step 4 - A unique ID for the Alexa Smart Home Skill 38 | [Alexa Skill Application Id] 39 | amzn1.ask.skill.XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX 40 | 41 | Set in Step 5 - The Messaging Client and Secret from the Alexa Smart Home Skill 42 | [Alexa Skill Messaging Client Id] 43 | amzn1.application-oa2-client.XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 44 | 45 | [Alexa Skill Messaging Client Secret] 46 | XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 47 | 48 | Set in Step 7 - The profile user_id of the User 49 | [user_id] 50 | amzn1.account.XXXXXXXXXXXXXXXXXXXXXXXXXXXX 51 | ``` 52 | 53 | These placeholders represent the configuration entities to be collected or created for the environment. 54 | 55 | > TIP Keep secrets safe. If a Client Secret is compromised or needs to be reset, you will have to discard the secret and regenerate the Client ID and Secret again or recreate the profile. This will immediately sever the existing access relationships and customers will have to re-authenticate or re-link their account or skill. 56 | 57 |
58 | 59 | ____ 60 | Go to [Step 2: Create the Smart Home Skill Backend](02-create-the-backend.md). 61 | 62 | ____ 63 | Return to the [Instructions](README.md) 64 | -------------------------------------------------------------------------------- /instructions/02-create-the-backend.md: -------------------------------------------------------------------------------- 1 | # Step 2: Create the Smart Home Skill Backend 2 | These instructions create the backend services needed by the Smart Home Skill using a Cloud Formation stack. 3 | 4 | #### 2.1 Create the Backend Stack 5 | 6 | 2.1.1 Navigate to the Cloud Formation Console at [https://console.aws.amazon.com/cloudformation/home?region=us-east-1](https://console.aws.amazon.com/cloudformation/home?region=us-east-1) and authenticate using your AWS account. 7 | 8 | 2.1.2 Verify you are in the N. Virginia (us-east-1) region and click the **Create Stack** button. 9 | 10 | 2.1.3 In the _Specify Template_ section, select the **Amazon S3 template URL** radio button and enter the following URL `https://s3.amazonaws.com/endpoint-code-us/backend.template` into the S3 URL field. 11 | 12 | 2.1.4 Click **Next** on the bottom right of the page. 13 | 14 | 2.1.5 On the _Create Stack_ page and in the **Stack name** text input box, enter `Smart-Home-Sample-Backend` and then click **Next**. 15 | 16 | 2.1.6 When on the _Configure stack options_ page, click **Next** without making any changes. 17 | 18 | 2.1.7 On the _Review Smart-Home-Sample-Backend_ page, click the checkbox "I acknowledge that AWS CloudFormation might create IAM resources with custom names." and then click the **Create** button. 19 | 20 | > This IAM resource warning comes from the need for the Stack to create an Execution Role for the created Lambda functions. 21 | 22 | 2.1.8 In the Stacks list in the Cloud Formation console, the newly created Sample-Smart-Home-Backend stack will be created and initially have a status of _CREATE\_IN\_PROGRESS_. When the backend stack has been created and is ready, the status will change to _CREATE\_COMPLETE_. This may take 1-3 minutes. 23 | 24 | > If any issues arise, you should see a ROLLBACK\_IN\_PROGRESS message and ultimately a ROLLBACK\_COMPLETE status. 25 | 26 | #### 2.2 Locate the _Sample-Smart-Home-Backend_ Stack Outputs 27 | 28 | 2.2.1 While still in the Cloud Formation console, select the check box for _Smart-Home-Sample-Backend_ and then select the _Outputs_ details tab. 29 | 30 | 2.2.2 Locate the _EndpointApiId_ **Value** that will look something like the following example: `y053kmfr5m` and copy it to the `setup.txt` file into the [EndpointApiId] section. 31 | 32 | 2.2.3 Locate the _SkillLambdaArn_ **Value** that is formatted like the following example: `arn:aws:lambda:us-east-1:############:function:SampleSkillAdapter` and copy it into the [SkillLambdaArn] section of `setup.txt`. 33 | 34 | 35 |
36 | 37 | ____ 38 | Go to [Step 3: Set Up Login with Amazon](03-setup-lwa.md). 39 | 40 | ____ 41 | Return to the [Instructions](README.md) -------------------------------------------------------------------------------- /instructions/03-setup-lwa.md: -------------------------------------------------------------------------------- 1 | # Step 3: Set Up Login with Amazon 2 | For the sample environment, a development Login with Amazon (LWA) security profile will be used for configuring Account Linking, which is required for a Smart Home Skill. 3 | 4 | #### 3.1 Create a Login with Amazon Security Profile 5 | 6 | 3.1.1 In your web browser, go to [https://developer.amazon.com/lwa/sp/overview.html](https://developer.amazon.com/lwa/sp/overview.html) and make sure _APPS & SERVICES_ is selected in the top menu and _Login with Amazon_ is selected in the sub menu. 7 | 8 | 3.1.2 On the _Login with Amazon_ page, click the **Create a New Security Profile** button. 9 | 10 | 3.1.3 On the Security Profile Management page, enter `Sample Alexa Smart Home` for the **Security Profile Name**. 11 | 12 | 3.1.4 For the **Security Profile Description** enter `A sample security profile for Alexa Smart Home Skill development`. 13 | 14 | 3.1.5 For the **Consent Privacy Notice URL** enter `http://example.com/privacy.html` for illustrative purposes or use your own if you already have a public consent privacy policy. 15 | 16 | > For a production Smart Home Skill, a valid consent privacy notice will be required. 17 | 18 | 3.1.6 For the **Consent Logo Image** download [https://github.com/alexa/skill-sample-python-smarthome-iot/raw/master/instructions/img/alexa-sample-smarthome-150x.png](https://github.com/alexa/skill-sample-python-smarthome-iot/raw/master/instructions/img/alexa-sample-smarthome-150x.png) into the `Alexa-SmartHome-Sample` directory created on your Desktop and then click the **Upload Image** area to load the file from where you saved it. 19 | 20 | 3.1.7 If your profile configuration looks like the following, click **Save** on the Security Profile Management page. 21 | 22 | ![Security Profile Configuration](img/3.1.7-lwa-profile-configuration.png "Security Profile Configuration") 23 | 24 | 25 | > If successful, a message similar to 'Login with Amazon successfully enabled for Security Profile. Click (gear) to manage Security Profile.' will be returned. 26 | 27 | 3.1.8 From the list of Security Profiles, click the **Show Client ID and Client Secret** link for the _Sample Alexa Smart Home_ profile. 28 | 29 | 3.1.9 Copy the displayed Client ID and Client Secret values to the `setup.txt` file replacing the template entries for [Login with Amazon Client ID] and [Login with Amazon Client Secret] respectively: 30 | 31 | ``` 32 | [Login with Amazon Client ID] 33 | amzn1.application-oa2-client.XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 34 | 35 | [Login with Amazon Client Secret] 36 | XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 37 | ``` 38 | > Further configuration of the Security Profile Allowed Return URLs will be done during configuration of the Alexa Smart Home Skill. 39 | 40 |
41 | 42 | ____ 43 | Go to [Step 4: Create the Alexa Smart Home Skill](04-create-skill-smarthome.md). 44 | 45 | ____ 46 | Return to the [Instructions](README.md) -------------------------------------------------------------------------------- /instructions/04-create-skill-smarthome.md: -------------------------------------------------------------------------------- 1 | # Step 4: Create the Alexa Smart Home Skill 2 | Create an Alexa Smart Home Skill that will process the Smart Home commands. 3 | 4 | #### 4.1 Navigate to the Alexa Skills Kit developer console 5 | 6 | 4.1.1 In a web browser to the Alexa Developer Console at [https://developer.amazon.com/alexa/console/ask](https://developer.amazon.com/alexa/console/ask). If not already authenticated, you may have to _Sign In_ with your Amazon Developer Account . 7 | 8 | 4.1.2 From the _Alexa Skills_ page, click the **Create Skill** button on the top right of the page. 9 | 10 | #### 4.2 Create a New Skill 11 | 12 | 4.2.1 On the _Create a new skill_ page, enter `Sample Smart Home Skill` as the _Skill name_. 13 | 14 | 4.2.2 Leave the language as English. 15 | 16 | > For more information on adding another language to your skill, see [Develop Smart Home Skills in Multiple Languages](https://developer.amazon.com/docs/smarthome/develop-smart-home-skills-in-multiple-languages.html). 17 | 18 | 19 | 4.2.3 Under _Choose a model to add to your skill_, select **Smart Home**. 20 | 21 | 4.2.4 Click **Create a skill** 22 | 23 | #### 4.3 Collect the Application Id of _Sample Smart Home Skill_ 24 | 25 | 4.3.1 When the skill is created, the page will refresh. In the _Smart Home service endpoint_ section, copy the _Your Skill ID_ of the Alexa Skill to the `setup.txt` file into the [Alexa Skill Application Id] value. The format of the Application Id will look like following: 26 | 27 | ``` 28 | amzn1.ask.skill.xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx 29 | ``` 30 | 31 | 4.3.2 Copy the Application Id value to the clipboard. 32 | 33 | #### 4.4 Add a Smart Home Trigger to the Alexa Smart Home Skill Lambda 34 | 35 | 4.4.1 In a new tab, browse to [https://us-east-1.console.aws.amazon.com/lambda/home?region=us-east-1#/functions/SampleSkillAdapter?tab=triggers](https://us-east-1.console.aws.amazon.com/lambda/home?region=us-east-1#/functions/SampleSkillAdapter?tab=triggers). 36 | 37 | 4.4.2 Note the **Add triggers** section on the left menu. This part of the designer allows you to add triggers to your Lambda function. 38 | 39 | ![Trigger Dialog Example](img/4.4.3-lambda-trigger.png "Trigger Dialog Example") 40 | 41 | 4.4.3 In the _Configuration_ section, select **Alexa Smart Home** from the left menu and it should be added to the _SampleSkillAdapter_ as a trigger. The Alexa Smart Home trigger will report "Configuration required" until the corresponding Alexa Skill Application ID is entered as part of the configuration. 42 | 43 | ![Trigger Dialog Example - Smart Home](img/4.4.4-lambda-trigger-smart-home.png "Smart Home Trigger Dialog Example") 44 | 45 | 4.4.4 Select the _Alexa Smart Home - Configuration required_ box and locate the _Configure triggers_ section at the bottom of the page. Paste the Alexa Skill Application Id value from the clipboard into the _Application Id_ text box. If you no longer have the value on your clipboard, you can retrieve it from the [Alexa Skill Application Id] section of the `setup.txt` file. 46 | 47 | 4.4.5 Verify **Enable trigger** is checked and then click **Add**. To finish enabling the trigger, click **Save** for the function at the top right of the page. Once saved, the Alexa Smart Home trigger should report "Saved". 48 | 49 | 4.4.6 Close this tab and return to your _Sample Smart Home Skill_ in the [Alexa Developer Console](https://developer.amazon.com/alexa/console/ask). 50 | 51 |
52 | 53 | ____ 54 | Go to [Step 5: Configure the Alexa Smart Home Skill](05-configure-skill-smarthome.md). 55 | 56 | ____ 57 | Return to the [Instructions](README.md) -------------------------------------------------------------------------------- /instructions/05-configure-skill-smarthome.md: -------------------------------------------------------------------------------- 1 | # Step 5: Configure the Alexa Smart Home Skill 2 | Configure the Alexa Smart Home Skill that will process the Smart Home commands. 3 | 4 | #### 6.1 Configure the Smart Home Service Endpoint 5 | 6 | 6.1.1 Locate and copy the [SkillLambdaArn] from the `setup.txt` file. It will have the following format: 7 | 8 | ``` 9 | arn:aws:lambda:us-east-1:############:function:SampleSkillAdapter 10 | ``` 11 | 12 | 5.1.2 In the _Sample Smart Home Skill_ **Smart Home** tab and in the _Smart Home service endpoint_ section, paste the [SkillLambdaArn] value in the **Default endpoint** input box. 13 | 14 | 5.1.3 Click the **Save** button to save the configuration. 15 | 16 | #### 5.2 Configure Account Linking 17 | 18 | 5.2.1 Select the _Account Linking_ tab of the _Sample Smart Home Skill_. 19 | 20 | 5.2.2 In the _Authorization URI_ input box, enter `https://www.amazon.com/ap/oa`. 21 | 22 | 5.2.3 In the _Access Token URI_ input box, enter ``https://api.amazon.com/auth/o2/token``. 23 | 24 | 5.2.4 Set the Client Id value to the [Login with Amazon Client ID] from the `setup.txt` file. It will have the following format: 25 | 26 | ``` 27 | amzn1.application-oa2-client.XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 28 | ``` 29 | 30 | 5.2.5 Set the **Client Secret** field to the [Login with Amazon Client Secret] value from the `setup.txt` file. It will look like the following example: 31 | ``` 32 | 7ebd0170626aaa2c3df0e263bd3aa15553efe565d53d90bd88b1977387b1159c 33 | ``` 34 | 35 | 5.2.6 For the _Scope_, click the **Add scope** and then add the following scope into the text box `profile:user_id` 36 | 37 | > For more details on what customer profile information is used, visit https://developer.amazon.com/docs/login-with-amazon/customer-profile.html 38 | 39 | 5.2.7 Click the **Save** button to save the configuration. 40 | 41 | #### 5.3 Configure Permissions 42 | 43 | 5.3.1 Select the _Permissions_ tab of the _Sample Smart Home Skill_. 44 | 45 | 5.3.2 Enable the **Send Alexa Events** toggle. 46 | 47 | > TIP: This value needs to be checked and enables the operation of asynchronous messages and proactive state updates. 48 | 49 | 5.3.3 Once sending Alexa events is enabled, click the _Show_ link in the **Alexa Skill Messaging** section to expose the Client Secret and then copy the **Client ID** and **Client Secret** to the [Alexa Skill Messaging Client Id] and [Alexa Skill Messaging Client Secret] sections `setup.txt` file. The values to save will be inserted into the following sections: 50 | 51 | ``` 52 | [Alexa Skill Messaging Client Id] 53 | amzn1.application-oa2-client.XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 54 | 55 | [Alexa Skill Messaging Client Secret] 56 | XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 57 | ``` 58 | 59 | #### 5.4 Set the Allowed Return URLs 60 | Using your account-specific values from the skill configuration section, collect the Redirect URLs and set them in the _Security Profile Web Settings_ **Allowed Return URLs**. 61 | 62 | 5.4.1 Return to the _Account Linking_ tab and locate the Returned Urls section towards the bottom of the page. That section should have values that look like the following format: 63 | 64 | ``` 65 | https://pitangui.amazon.com/api/skill/link/XXXXXXXXXXXXXX 66 | https://layla.amazon.com/api/skill/link/XXXXXXXXXXXXXX 67 | https://alexa.amazon.co.jp/api/skill/link/XXXXXXXXXXXXXX 68 | ``` 69 | > These values will be copied into the previously Security Profile, so leave this tab open to coy values. 70 | 71 | 5.4.2 Open [https://developer.amazon.com/iba-sp/overview.html](https://developer.amazon.com/iba-sp/overview.html) in another browser tab and make sure _APPS & SERVICES_ is selected in the top menu and _Security Profiles_ is selected in the sub menu. 72 | 73 | 5.4.3 On the _Security Profile Management_ page, click the **Sample Alexa Smart Home** profile. 74 | 75 | 5.4.4 In the details for the _Sample Alexa Smart Home - Security Profile_ click the **Web Settings** top tab menu. 76 | 77 | 5.4.5 On the Security Profile Management page for the _Sample Alexa Smart Home_ profile, click the **Edit** button. 78 | 79 | 5.4.6 In the _Allowed Return URLs_ section click the **Add Another** link until there are 3 text input fields. 80 | 81 | 5.4.7 Copy each of the 3 URLs from the _Account Linking_ configuration page into each of the text fields. 82 | 83 | 5.4.8 When all fields are entered, click **Save**. 84 | 85 | 5.4.9 Once saved, the _Allowed Return URLs_ section should look something like the following: 86 | 87 | ![Allowed Return URLs Example](img/3.1.14-lwa-web-settings.png "Allowed Return URLs Example") 88 | 89 | 5.4.10 Close the _Security Profile Management_ tab and return to the Alexa skill configuration. 90 | 91 | 92 |
93 | 94 | ____ 95 | Go to [Step 6: Link the Alexa Smart Home Skill](06-link-skill-smarthome.md). 96 | 97 | ____ 98 | Return to the [Instructions](README.md) -------------------------------------------------------------------------------- /instructions/06-link-skill-smarthome.md: -------------------------------------------------------------------------------- 1 | # Step 6: Link the Alexa Smart Home Skill 2 | Finalize the Lambda configuration and link the Alexa Smart Home Skill to your account. 3 | 4 | #### 6.1 Update the Skill Lambda Environment variables 5 | With a completed Alexa Smart Home Skill configuration, the Alexa Skill Client ID and Client Secret will need to be passed to the backend. To accomplish this, you can set the environment variables of the Lambda that is handling the Endpoint interactions. 6 | 7 | 6.1.2 Browse to [https://us-east-1.console.aws.amazon.com/lambda/home?region=us-east-1#/functions/SampleEndpointAdapter?tab=configuration](https://us-east-1.console.aws.amazon.com/lambda/home?region=us-east-1#/functions/SampleEndpointAdapter?tab=configuration). 8 | 9 | 6.1.3 Expand the **Environment variables** section add a Key called `client_id`. 10 | 11 | 6.1.4 For the _client\_id_ value enter the value stored in [Alexa Skill Messaging Client Id] from the `setup.txt` file. 12 | 13 | 6.1.5 In the **Environment variables** section add another Key called `client_secret`. 14 | 15 | 6.1.6 For the _client\_secret_ value enter the value stored in [Alexa Messaging Skill Client Secret] from the `setup.txt` file. 16 | 17 | 6.1.7 When both the _client\_id_ and _client\_secret_ environment variables are added, click **Save** at the top of the page. 18 | 19 | #### 6.2 Link the Alexa Smart Home Skill 20 | 21 | 6.2.1 Go to [https://alexa.amazon.com/](https://alexa.amazon.com/) and select **Skills** from the left menu. 22 | 23 | > Tip: Replace the skill value at the end of https://alexa.amazon.com/spa/index.html#skills/beta/ALEXA_SKILL_ID to go directly to a skill. 24 | > For example, https://alexa.amazon.com/spa/index.html#skills/beta/amzn1.ask.skill.203e1508-e33b-4b63-8e0e-70b97e45408d 25 | 26 | 27 | 6.2.2 Click **Your Skills** from the top right of the section. 28 | 29 | 6.2.3 Locate your Sample Smart Home Skill in the list of skills in the _DEV SKILLS_ section and click on it. 30 | 31 | ![Smart Home Skill Example](img/6.2.3-smart-home-skill.png "Smart Home Skill Example") 32 | 33 | 6.2.4 On the _Sample Smart Home Skill_ page, click **Enable** in the top right and authenticate with your Amazon account. 34 | 35 | 6.2.5 On authentication, verify you are presented with the _Sample Alexa Smart Home_ authentication dialog and authenticate using your Amazon account. The transitional page will look something like the following in a browser: 36 | 37 | ![Linking Authentication](img/6.2.5-linking-dialog.png "Linking Authentication") 38 | 39 | 6.2.6 Click **Allow** to link your Account with the _Sample Alexa Smart Home_ skill. 40 | 41 | 6.2.7 On success, you should be presented with a window that instructing you to close the page and return to the _Sample Smart Home Skill_. 42 | 43 | 6.2.8 When redirected back to the Skill page, you will be prompted 'Discover Devices'. Click **Cancel** for now as no new devices from the Sample Smart Home Skill will be returned without additional configuration. 44 | 45 |
46 | 47 | ____ 48 | Go to [Step 7: Create the Endpoints](07-create-endpoints.md). 49 | 50 | ____ 51 | Return to the [Instructions](README.md) -------------------------------------------------------------------------------- /instructions/07-create-endpoints.md: -------------------------------------------------------------------------------- 1 | # Step 7: Create the Endpoints 2 | Create endpoints to be discovered during the Alexa Smart Home Skill Discovery. 3 | 4 | #### 7.1 Collect the User Id 5 | The JSON template for creating an endpoint defaults the User Id to `0`. This is useful for testing simulated calls to your AWS Lambda for a variety of directives with a known (and invalid) User Id. To associate the device with an actual Amazon account, the correct User Id obtained with the access token must be used. During skill enablement, the credentials are saved to DynamoDB into the SampleUsers table for reference. 6 | > The credentials in the SampleUsers table are for development reference only and should be stored securely in a production environment. 7 | 8 | 7.2.1 Go to [https://console.aws.amazon.com/dynamodb/home?region=us-east-1#tables:selected=SampleUsers](https://console.aws.amazon.com/dynamodb/home?region=us-east-1#tables:selected=SampleUsers) and select the **Items** tab. 9 | 10 | 7.2.2 In the list of _SampleUsers_ select the first entry UserId column that has the following format: 11 | ``` 12 | amzn1.account.XXXXXXXXXXXXXXXXXXXXXXXXXXXX 13 | ``` 14 | 15 | 7.2.3 Copy the UserId value and save it to the [user_id] section of the `setup.txt` file. The User Id is used to identify devices associated with the Amazon account. 16 | 17 | #### 7.2 Set Up Postman 18 | Postman is a tool for managing and executing HTTP requests and is very useful for API development and usage. To use it for the sample code, it must first be installed if not currently available on your system and have a sample environment configured for use. 19 | 20 | ##### 7.2.1 Install Postman 21 | 7.2.1 Go to [getpostman.com](https://www.getpostman.com) and download and install the correct Postman application for your platform. 22 | 23 | 7.2.2 Download the Postman Sample Smart Home Collection [skill-sample-smarthome-iot.postman_collection.json](https://raw.githubusercontent.com/alexa/skill-sample-python-smarthome-iot/master/instructions/skill-sample-smarthome-iot.postman_collection.json) file into the `Alexa-SmartHome-Sample` directory on your Desktop. Additionally, download the [skill-sample-smarthome-iot.postman_environment.json](https://raw.githubusercontent.com/alexa/skill-sample-python-smarthome-iot/master/instructions/skill-sample-smarthome-iot.postman_environment.json) file into the same directory. 24 | 25 | ##### 7.2.2 Import the *Alexa Smart Home (smarthome-iot)* Postman collection 26 | 27 | 7.2.2.1 Open Postman. 28 | 29 | 7.2.2.2 In Postman, click **Import** from the main menu and browse to the `skill-sample-smarthome-iot.postman_collection` file or drag it onto the _Import_ dialog. 30 | 31 | ##### 7.2.3 Import the Postman environment 32 | To fill out the variable values of the configuration use a Postman environment to store configuration-specific values. The keys defined in double curly braces like `{{endpoint_api_id}}` will be auto-expanded in the URLs for the imported collection. 33 | 34 | 7.2.3.1 In the top right of Postman, click the gear icon to open the _Environment options_ drop down menu and select **Manage Environments**. 35 | 36 | ![Postman - Manage Environments](img/7.2.3.1-postman-manage-environments.png "Postman - Manage Environments") 37 | 38 | 7.2.3.2 In opened _Manage Environments_ dialog, click the **Import** button in the bottom right. 39 | 40 | 7.2.3.3 Click the **Choose Files** button and open the `skill-sample-smarthome-iot.postman_environment.json` file downloaded into the `Alexa-SmartHome-Sample` directory. 41 | 42 | 7.2.3.4 Click the *Alexa Smart Home (smarthome-iot)* environment and then select the value of the **Variable** value called `endpoint_api_id` and set its **Current Value** to the [EndpointApiId] value from the `setup.txt` file. 43 | 44 | 7.2.3.5 Replace the `userId` current value (with a default of `0`) with your `user_id` value from the `setup.txt` file. This will associate the created thing with an Amazon account for device discovery. 45 | 46 | > Note that the `user_id` defaults to 0 because this is useful for development and identifying a device created programmatically. However, Discovery for the Smart Home Skill would not find this device since it is expecting a `user_id` in the form of a profile from Login with Amazon. 47 | 48 | 7.2.3.6 Click the **Update** button and then close the _MANAGE ENVIRONMENTS_ dialog and in the top right of Postman select the newly created *Alexa Smart Home (smarthome-iot)* environment from the environment drop down menu. 49 | 50 | 51 | #### 7.3 Create Endpoints 52 | Use Postman to generate endpoints by selecting and sending stored requests from the Alexa Smart Home (sample_backend) collection. 53 | 54 | 7.3.1 In Postman, from the left _Collections_ menu select and open the *Alexa Smart Home (smarthome-iot)* folder. In the *Endpoints* sub-folder, open the **POST** _/endpoints (Black Sample Switch)_ resource from the left menu. 55 | 56 | 7.3.2 In Postman, select the **POST** _/endpoints (Black Sample Switch)_ resource from the left menu and then select the Body tab to show the raw body that would be sent from this request. 57 | 58 | ![Postman - Collections > Endpoints > Body](img/7.3.2-postman-collections-endpoints.png "Postman - Collections > Endpoints > Body") 59 | 60 | 61 | 62 | 7.3.4 Once you have added your User Id value, click the **Send** button in the top right to send and create the endpoint. 63 | 64 | 7.3.5 Return to the [AWS IoT Things console](https://console.aws.amazon.com/iotv2/home?region=us-east-1#/thinghub) and refresh the page. A new thing of the type `SAMPLESWITCH` should be available. It's name will be a generated GUID. 65 | 66 | 7.3.6 Click on the thing identified with a thing type of `SAMPLESWITCH` to inspect its attributes. They should look something like the following: 67 | 68 | ![AWS IoT - SAMPLESWITCH](img/7.3.6-thing-sampleswitch.png "AWS IoT - SAMPLESWITCH") 69 | 70 | The Globally Unique ID (GUID) representing the name of this device will correspond to an entry in the **SampleEndpointDetails** table that holds the details of the device for discovery. You can browse to the [SampleEndpointDetails DynamoDB Table](https://console.aws.amazon.com/dynamodb/home?region=us-east-1#tables:selected=SampleEndpointDetails) and view the items entry to see the details stored in AWS. 71 | 72 | With this Sample Switch Thing defined in the account you are using for Alexa, you should now be able to discover it as a virtual device. 73 | 74 | > If you want to create other devices, look at the other options in the samples provided in the Postman collection and update the userId value in the POST body of the resource. Click **Send** to POST it to the endpoint API 75 | 76 |
77 | 78 | ____ 79 | Go to [Step 8: Test the Endpoints](08-test-endpoints.md). 80 | 81 | ____ 82 | Return to the [Instructions](README.md) -------------------------------------------------------------------------------- /instructions/08-test-endpoints.md: -------------------------------------------------------------------------------- 1 | # Step 8: Test the Endpoints 2 | Now that the environment is in place and a test device has been created, test out the endpoints. 3 | 4 | 5 | #### Step 8.1 Discover devices via a voice command 6 | 7 | 8.1.1 Return to the [Alexa Developer Console](https://developer.amazon.com/alexa/console/ask) with your Amazon account. 8 | 9 | 8.1.2 In the **Test** tab of _Sample Smart Home Skill_ skill, give the command "Discover Devices" by either entering the text into the Alexa Simulator or pressing the microphone icon to say the command. Alexa should respond with "Starting Discovery..." with a description of the discovery process. 10 | 11 | 8.1.3 Return to https://alexa.amazon.com/ and select **Smart Home** from the left menu and then select **Devices** in the main window. 12 | 13 | 8.1.4 From the list of devices, not the inclusion of a **Sample Black Switch** with a description of _A Sample Black Switch_ in the list of _Devices_. 14 | 15 | > Note If you are using an Echo device to issue a "Discover Devices" command, once discovery is complete, Alexa should respond "I found one new switch called sample black switch." or possibly with additional devices discovered. Note the addition of a _Sample Black Switch_ device to your list of devices. 16 | 17 | > Optionally, for device discovery, you can browse to https://alexa.amazon.com/spa/index.html#appliances and click the **Discover** button at the bottom of the page. 18 | 19 | #### Step 8.2 Send at Turn On voice command 20 | 21 | 8.2.1 Through the Alexa Simulator, give the command "Turn on Black Sample Switch" by either entering the text or clicking the microphone icon ans speaking the command. Alexa should respond with "OK". 22 | 23 | 8.2.2 Return to the [AWS IoT Things console](https://console.aws.amazon.com/iotv2/home?region=us-east-1#/thinghub) and locate the Thing instance with the `SAMPLESWITCH` Type to inspect its attributes. The thing will have a generated endpoint ID for an instance Name. 24 | 25 | > If you do not know what GUID maps to which device, you can lookup the reference details in The [SampleEndpointDetails Table](https://console.aws.amazon.com/dynamodb/home?region=us-east-1#tables:selected=SampleEndpointDetails;tab=items). 26 | 27 | 8.2.3 The Sample Black Switch thing Shadow state should reflect the state of the "ON" or "OFF" power voice command when the Thing is refreshed. The shadow document will look something like the following if the command was to turn on the switch: 28 | 29 | ``` 30 | { 31 | "desired": { 32 | "state": "ON" 33 | }, 34 | "delta": { 35 | "state": "ON" 36 | } 37 | } 38 | ``` 39 | 40 | The set value will reflect the command to turn on or off the switch from Alexa. 41 | 42 |
43 | 44 | ____ 45 | Go to [Step 9: Send an Event](09-send-an-event.md). 46 | 47 | ____ 48 | Return to the [Instructions](README.md) -------------------------------------------------------------------------------- /instructions/09-send-an-event.md: -------------------------------------------------------------------------------- 1 | # Step 9: Send an Event 2 | Send an external event into Alexa from the Endpoint Device backend. This simulates an external event on the endpoint that will need to be updated with Alexa. 3 | 4 | 5 | #### 9.1 Send a Proactive State Update 6 | 7 | 9.1.1 In Postman, and within the *Endpoints* sub-folder, open the **POST** _/events_ resource from the left menu. 8 | 9 | 9.1.2 Select the _Body_ tab and view the raw JSON. It should look like the following: 10 | 11 | ``` 12 | { 13 | "event": { 14 | "type": "ChangeReport", 15 | "endpoint": { 16 | "userId": "{{user_id}}", 17 | "id": "{{endpoint_id}}", 18 | "state": "OFF", 19 | "type": "SWITCH", 20 | "sku": "SW00" 21 | } 22 | } 23 | } 24 | ``` 25 | Note that the `user_id` and `endpoint_id` are variables that can be updated via the Postman environment variables. 26 | 27 | 9.1.3 Update the Postman environment variables by replacing the `user_id` "0" value with the [user_id] stored in the `config.txt` file. Additionally, replace the `endpoint_id` value with the Thing name from AWS IoT for the Sample Black Switch created. When edited, it should something like the following: 28 | 29 | ``` 30 | { 31 | "event": { 32 | "endpoint": { 33 | "userId" : "amzn1.account.XXXXXXXXXXXXXXXXXXXXXXXXXXXX", 34 | "id": "b0dcb3f0-db26-4462-8cf1-15fc97972eac", 35 | "state": "OFF", 36 | "type": "SWITCH", 37 | "sku": "SW00" 38 | } 39 | } 40 | } 41 | ``` 42 | 43 | 9.1.1 Click **Save** in the top right and then and then click the **Send** button. 44 | 45 | 9.1.2 Return to the [AWS IoT Things console](https://console.aws.amazon.com/iotv2/home?region=us-east-1#/thinghub) and note the _state_ value of the created Black Sample Switch. The state should reflect the _"state"_ value passed in the body. For instance, if set to _"OFF"_, the attribute _state_ will be set to _OFF_. 46 | 47 | Finally, while that method updates the Endpoint Cloud data, you can see the state of the response sent to the Alexa event gateway in Postman on the right of the Response section. A 202 value of `Accepted` indicates the message was received. For a full list of Success responses and errors, visit the documentation at [https://developer.amazon.com/docs/smarthome/send-events-to-the-alexa-event-gateway.html#success-response-and-errors](https://developer.amazon.com/docs/smarthome/send-events-to-the-alexa-event-gateway.html#success-response-and-errors). If ultimately successful, the state of the Black Sample Switch will change in the Alexa web and mobile applications. 48 | 49 |
50 | 51 | ____ 52 | Go to [Step 10: Clean Up](10-cleanup.md). 53 | 54 | ____ 55 | Return to the [Instructions](README.md) -------------------------------------------------------------------------------- /instructions/10-cleanup.md: -------------------------------------------------------------------------------- 1 | # Step 10: Clean Up 2 | Clean up the resources used during the workshop and tear down the Endpoint Device backend. 3 | 4 | 5 | #### 10.1 Delete the _Sample-Smart-Home-Backend_ Stack and Alexa Skill 6 | 7 | 10.1.1 Browse to Navigate to the Cloud Formation Console at [https://console.aws.amazon.com/cloudformation/home?region=us-east-1](https://console.aws.amazon.com/cloudformation/home?region=us-east-1). 8 | 9 | 10.1.2 From the list of Stacks, check the box next to only the _Sample-Smart-Home-Backend_ stack. 10 | 11 | 10.1.3 From the Action drop down menu, select **Delete Stack**. 12 | 13 | 10.1.4 From the list of Stacks, check the box next to only the _Sample-Smart-Home-Backend_ stack. 14 | 15 | 10.1.5 Go to your list of Alexa skills at [https://developer.amazon.com/edw/home.html#/skills](https://developer.amazon.com/edw/home.html#/skills) and locate the _Sample Smart Home Skill_ and click the **Delete** link. 16 | 17 | #### 10.2 Delete local files 18 | 19 | 10.2.1 Delete the _Alexa-SmartHome-Sample_ directory and its contents from the desktop. 20 | 21 | ____ 22 | Return to the [README](../README.md). 23 | 24 | ____ 25 | Return to the [Instructions](README.md) -------------------------------------------------------------------------------- /instructions/README.md: -------------------------------------------------------------------------------- 1 | # Build an Alexa Smart Home Skill Sandbox 2 | 3 | These instructions are to build a sample Alexa skill that uses AWS services to store properties of virtual devices. In the course of these instructions you will create the Alexa skill and supporting backend to handle the requests from Alexa and provide responses based on user input. 4 | 5 | ## Automated Instructions 6 | 7 | The fastest way to get up and running is to follow the automated instructions for [creating a Sandbox](create-sandbox.md). 8 | 9 | ## Manual Instructions 10 | 11 | For the manual instructions, follow these steps in order. 12 | 13 | 1. [Verify Requirements](01-verify-requirements.md) 14 | 2. [Create the Smart Home Skill Backend](02-create-the-backend.md) 15 | 3. [Set Up Login with Amazon](03-setup-lwa.md) 16 | 4. [Create the Alexa Smart Home Skill](04-create-skill-smarthome.md) 17 | 5. [Configure the Alexa Smart Home Skill](05-configure-skill-smarthome.md) 18 | 6. [Link the Alexa Smart Home Skill](06-link-skill-smarthome.md) 19 | 7. [Create the Endpoints](07-create-endpoints.md) 20 | 8. [Test the Endpoints](08-test-endpoints.md) 21 | 9. [Send an Event](09-send-an-event.md) 22 | 10. [Clean Up](10-cleanup.md) 23 | -------------------------------------------------------------------------------- /instructions/create-sandbox.md: -------------------------------------------------------------------------------- 1 | # Create the Smart Home Sample Sandbox 2 | These instructions create the backend services needed by the Smart Home Skill using a Cloud Formation stack. 3 | 4 | > You will need an Amazon Web Services account and and Amazon Developer account. For Boto3, you will need valid AWS credentials configured. Review the Boto3 Credentials documentation at https://boto3.amazonaws.com/v1/documentation/api/latest/guide/configuration.html. 5 | 6 | ### 1 Get your Vendor ID 7 | To see your Vendor ID, go to https://developer.amazon.com/mycid.html and sign in with your Amazon Developer account if prompted. 8 | 9 | ### 2 Create a Login With Amazon Profile 10 | 2.1 Go to https://developer.amazon.com/loginwithamazon/console/site/lwa/overview.html and sign in with your Amazon Developer account if prompted. 11 | 2.2 Click on **Create a New Security Profile** and enter the following values: 12 | 13 | For the Security Profile Name: `Smart Home Sandbox` 14 | 15 | For the Security Profile Description: `A sample sandbox for Alexa skill development`. 16 | 17 | For the Consent Privacy Notice URL: `https://example.com/privacy.html` 18 | 19 | 2.3 Click the **Save** button. 20 | 2.4 Click the gear icon and select Web Settings. 21 | 2.5 Click the **Edit** button. 22 | 2.6 Replace VENDOR_ID with your unique Vendor ID and add 4 Allowed Return URLs: 23 | ``` 24 | https://pitangui.amazon.com/api/skill/link/VENDOR_ID 25 | https://layla.amazon.com/api/skill/link/VENDOR_ID 26 | https://alexa.amazon.co.jp/api/skill/link/VENDOR_ID 27 | http://127.0.0.1:9090/cb 28 | ``` 29 | 2.7 Click the **Save** button. 30 | > Leave the page open to copy the Client ID and Secret 31 | 32 | ### 3 Download the Source 33 | 3.1 Clone the source into a working directory using the command: `git clone https://github.com/alexa/skill-sample-python-smarthome-sandbox` 34 | > Optionally [download the master branch as a zip](https://github.com/alexa/skill-sample-python-smarthome-sandbox/archive/master.zip) and extract it to a working directory. 35 | 36 | ### 4 Run the client script 37 | 4.1 From the command line, navigate to the cloned repository `skill-sample-python-smarthome-sandbox/client` folder. 38 | 39 | 4.2 Within the */client* folder, run `pip install boto3` 40 | 41 | 4.3 To run the sandbox, then run `python sandbox.py` 42 | 43 | 4.4 Enter the Vendor ID, LWA Client ID, and LWA Client Secret when prompted 44 | 45 | ### 5 Enable the skill 46 | 5.1 When the sandbox setup is complete, you can then enable the skill via alexa.amazon.com by going to https://alexa.amazon.com/spa/index.html#skills/your-skills/?ref-suffix=ysa_gw and selecting **DEV SKILLS** from the menu. 47 | 48 | ### 6 Test the skill 49 | 6.1 Go to https://developer.amazon.com/alexa/console/ask/ and open the **Smart Home Sandbox** skill. 50 | 51 | 6.2 Navigate to the **Test** tab and try some of the following commands with Alexa: 52 | ``` 53 | turn on black switch 54 | turn on white switch 55 | turn off black swtich 56 | turn off white switch 57 | set sample toaster heat to six 58 | turn on bagel mode of sample toaster 59 | turn off sample toaster frozen mode 60 | ``` 61 | 62 | Look through the CloudWatch logs for flow and the related AWS IoT Things for state. 63 | 64 | 65 | ____ 66 | Return to the [Instructions](README.md) 67 | -------------------------------------------------------------------------------- /instructions/img/3.1.10-lwa-web-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-python-smarthome-sandbox/8bc88c53a2e0117092b44bdbb319a1e6709c547d/instructions/img/3.1.10-lwa-web-settings.png -------------------------------------------------------------------------------- /instructions/img/3.1.14-lwa-web-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-python-smarthome-sandbox/8bc88c53a2e0117092b44bdbb319a1e6709c547d/instructions/img/3.1.14-lwa-web-settings.png -------------------------------------------------------------------------------- /instructions/img/3.1.7-lwa-profile-configuration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-python-smarthome-sandbox/8bc88c53a2e0117092b44bdbb319a1e6709c547d/instructions/img/3.1.7-lwa-profile-configuration.png -------------------------------------------------------------------------------- /instructions/img/4.4.3-lambda-trigger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-python-smarthome-sandbox/8bc88c53a2e0117092b44bdbb319a1e6709c547d/instructions/img/4.4.3-lambda-trigger.png -------------------------------------------------------------------------------- /instructions/img/4.4.4-lambda-trigger-smart-home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-python-smarthome-sandbox/8bc88c53a2e0117092b44bdbb319a1e6709c547d/instructions/img/4.4.4-lambda-trigger-smart-home.png -------------------------------------------------------------------------------- /instructions/img/6.2.3-smart-home-skill.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-python-smarthome-sandbox/8bc88c53a2e0117092b44bdbb319a1e6709c547d/instructions/img/6.2.3-smart-home-skill.png -------------------------------------------------------------------------------- /instructions/img/6.2.5-linking-dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-python-smarthome-sandbox/8bc88c53a2e0117092b44bdbb319a1e6709c547d/instructions/img/6.2.5-linking-dialog.png -------------------------------------------------------------------------------- /instructions/img/7.2.3.1-postman-manage-environments.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-python-smarthome-sandbox/8bc88c53a2e0117092b44bdbb319a1e6709c547d/instructions/img/7.2.3.1-postman-manage-environments.png -------------------------------------------------------------------------------- /instructions/img/7.3.2-postman-collections-endpoints.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-python-smarthome-sandbox/8bc88c53a2e0117092b44bdbb319a1e6709c547d/instructions/img/7.3.2-postman-collections-endpoints.png -------------------------------------------------------------------------------- /instructions/img/7.3.6-thing-sampleswitch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-python-smarthome-sandbox/8bc88c53a2e0117092b44bdbb319a1e6709c547d/instructions/img/7.3.6-thing-sampleswitch.png -------------------------------------------------------------------------------- /instructions/img/alexa-sample-smarthome-108x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-python-smarthome-sandbox/8bc88c53a2e0117092b44bdbb319a1e6709c547d/instructions/img/alexa-sample-smarthome-108x.png -------------------------------------------------------------------------------- /instructions/img/alexa-sample-smarthome-150x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-python-smarthome-sandbox/8bc88c53a2e0117092b44bdbb319a1e6709c547d/instructions/img/alexa-sample-smarthome-150x.png -------------------------------------------------------------------------------- /instructions/img/alexa-sample-smarthome-512x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-python-smarthome-sandbox/8bc88c53a2e0117092b44bdbb319a1e6709c547d/instructions/img/alexa-sample-smarthome-512x.png -------------------------------------------------------------------------------- /instructions/setup.txt: -------------------------------------------------------------------------------- 1 | Set in Step 2 - A unique API ID and name for the Alexa Smart Home Skill Lambda function 2 | [EndpointApiId] 3 | XXXXXXXXXX 4 | 5 | [SkillLambdaArn] 6 | arn:aws:lambda:us-east-1:781058041248:function:SampleSkillAdapter 7 | 8 | Set in Step 3 - The Client ID and Secret from the LWA Security Profile 9 | [Login with Amazon Client ID] 10 | amzn1.application-oa2-client.XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 11 | 12 | [Login with Amazon Client Secret] 13 | XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 14 | 15 | Set in Step 4 - A unique ID for the Alexa Smart Home Skill 16 | [Alexa Skill Application Id] 17 | amzn1.ask.skill.XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX 18 | 19 | Set in Step 5 - The Messaging Client and Secret from the Alexa Smart Home Skill 20 | [Alexa Skill Messaging Client Id] 21 | amzn1.application-oa2-client.XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 22 | 23 | [Alexa Skill Messaging Client Secret] 24 | XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 25 | 26 | Set in Step 7 - The profile user_id of the User 27 | [user_id] 28 | amzn1.account.XXXXXXXXXXXXXXXXXXXXXXXXXXXX 29 | -------------------------------------------------------------------------------- /instructions/skill-sample-smarthome-iot.postman_environment.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "bf9b00d0-3573-40ae-847c-55aa56b92df6", 3 | "name": "Alexa Smart Home Sample (smarthome-iot)", 4 | "values": [ 5 | { 6 | "key": "aws_region", 7 | "value": "us-east-1", 8 | "enabled": true 9 | }, 10 | { 11 | "key": "endpoint_api_id", 12 | "value": "", 13 | "enabled": true 14 | }, 15 | { 16 | "key": "user_id", 17 | "value": "0", 18 | "enabled": true 19 | }, 20 | { 21 | "key": "endpoint_id", 22 | "value": "", 23 | "description": "", 24 | "enabled": true 25 | } 26 | ], 27 | "_postman_variable_scope": "environment", 28 | "_postman_exported_at": "2018-11-22T12:02:41.388Z", 29 | "_postman_exported_using": "Postman/6.5.2" 30 | } 31 | -------------------------------------------------------------------------------- /lambda/README.md: -------------------------------------------------------------------------------- 1 | # Lambda Functions for Sample Smart Home Backend 2 | 3 | ### api 4 | This is written in python and functions as the device cloud for an Alexa Smart Home Skill. 5 | 6 | ### smarthome 7 | The Smart Home Skill Lambda that routes directives from Alexa to the customer endpoint. 8 | 9 | -------------------------------------------------------------------------------- /lambda/api/alexa/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-python-smarthome-sandbox/8bc88c53a2e0117092b44bdbb319a1e6709c547d/lambda/api/alexa/__init__.py -------------------------------------------------------------------------------- /lambda/api/alexa/skills/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lambda/api/alexa/skills/smarthome/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | # 5 | # Licensed under the Amazon Software License (the "License"). You may not use this file except in 6 | # compliance with the License. A copy of the License is located at 7 | # 8 | # http://aws.amazon.com/asl/ 9 | # 10 | # or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific 12 | # language governing permissions and limitations under the License. 13 | 14 | from .alexa_response import AlexaResponse 15 | from .alexa_utils import get_utc_timestamp 16 | -------------------------------------------------------------------------------- /lambda/api/alexa/skills/smarthome/alexa_response.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | # 5 | # Licensed under the Amazon Software License (the "License"). You may not use this file except in 6 | # compliance with the License. A copy of the License is located at 7 | # 8 | # http://aws.amazon.com/asl/ 9 | # 10 | # or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific 12 | # language governing permissions and limitations under the License. 13 | 14 | import random 15 | import uuid 16 | 17 | from .alexa_utils import get_utc_timestamp 18 | 19 | 20 | class AlexaResponse: 21 | 22 | def __init__(self, **kwargs): 23 | 24 | self.remove_endpoint = kwargs.get('remove_endpoint', False) 25 | 26 | self.context_properties = [] 27 | self.payload_endpoints = [] 28 | 29 | # Set up the response structure 30 | self.context = {} 31 | self.event = { 32 | 'header': { 33 | 'namespace': kwargs.get('namespace', 'Alexa'), 34 | 'name': kwargs.get('name', 'Response'), 35 | 'messageId': str(uuid.uuid4()), 36 | 'payloadVersion': kwargs.get('payload_version', '3') 37 | }, 38 | 'endpoint': { 39 | "scope": { 40 | "type": "BearerToken", 41 | "token": kwargs.get('token', 'INVALID') 42 | }, 43 | "endpointId": kwargs.get('endpoint_id', 'INVALID') 44 | }, 45 | 'payload': kwargs.get('payload', {}) 46 | } 47 | 48 | if 'correlation_token' in kwargs: 49 | self.event['header']['correlation_token'] = kwargs.get('correlation_token', 'INVALID') 50 | 51 | if 'cookie' in kwargs: 52 | self.event['endpoint']['cookie'] = kwargs.get('cookie', '{}') 53 | 54 | # No endpoint in certain types of requests 55 | if self.event['header']['name'] == 'AcceptGrant.Response' or self.event['header']['name'] == 'Discover.Response': 56 | self.remove_endpoint = True 57 | 58 | if self.remove_endpoint: 59 | self.event.pop('endpoint') 60 | 61 | def __repr__(self): 62 | return self.get() 63 | 64 | def __str__(self): 65 | return str(self.get()) 66 | 67 | def add_context_property(self, **kwargs): 68 | self.context_properties.append(self.create_context_property(**kwargs)) 69 | 70 | def add_cookie(self, key, value): 71 | endpoint = self.event['endpoint'] 72 | if 'cookie' in endpoint: 73 | endpoint['cookie'][key] = value 74 | 75 | def add_payload_endpoint(self, **kwargs): 76 | self.payload_endpoints.append(self.create_payload_endpoint(**kwargs)) 77 | 78 | @staticmethod 79 | def create_context_property(**kwargs): 80 | context_property = { 81 | 'namespace': kwargs.get('namespace', 'Alexa.EndpointHealth'), 82 | 'name': kwargs.get('name', 'connectivity'), 83 | 'value': kwargs.get('value', {'value': 'OK'}), 84 | 'timeOfSample': get_utc_timestamp(), 85 | 'uncertaintyInMilliseconds': kwargs.get('uncertainty_in_milliseconds', 0) 86 | } 87 | 88 | if 'instance' in kwargs: 89 | context_property['instance'] = kwargs.get('instance', 'UNDEFINED') 90 | 91 | return context_property 92 | 93 | @staticmethod 94 | def create_payload_endpoint(**kwargs): 95 | # Return the proper structure expected for the endpoint 96 | endpoint = { 97 | 'capabilities': kwargs.get('capabilities', []), 98 | 'description': kwargs.get('description', 'Sample Endpoint Description'), 99 | 'displayCategories': kwargs.get('display_categories', ['OTHER']), 100 | 'endpointId': kwargs.get('endpoint_id', 'endpoint_' + "%0.6d" % random.randint(0, 999999)), 101 | 'friendlyName': kwargs.get('friendly_name', 'Sample Endpoint'), 102 | 'manufacturerName': kwargs.get('manufacturer_name', 'Sample Manufacturer') 103 | } 104 | 105 | if 'cookie' in kwargs: 106 | endpoint['cookie'] = kwargs.get('cookie', {}) 107 | 108 | return endpoint 109 | 110 | @staticmethod 111 | def create_payload_endpoint_capability(**kwargs): 112 | capability = { 113 | 'type': kwargs.get('type', 'AlexaInterface'), 114 | 'interface': kwargs.get('interface', 'Alexa'), 115 | 'version': kwargs.get('version', '3') 116 | } 117 | 118 | capability_resources = kwargs.get("capabilityResources") 119 | if capability_resources: 120 | capability['capabilityResources'] = capability_resources 121 | 122 | instance = kwargs.get('instance', None) 123 | if instance: 124 | capability['instance'] = instance 125 | 126 | supported = kwargs.get('supported', None) 127 | if supported: 128 | capability['properties'] = {} 129 | capability['properties']['supported'] = supported 130 | capability['properties']['proactivelyReported'] = kwargs.get('proactively_reported', False) 131 | capability['properties']['retrievable'] = kwargs.get('retrievable', False) 132 | 133 | configuration = kwargs.get('configuration', None) 134 | if configuration: 135 | capability['configuration'] = configuration 136 | 137 | return capability 138 | 139 | def get(self, remove_empty=True): 140 | 141 | response = { 142 | 'context': self.context, 143 | 'event': self.event 144 | } 145 | 146 | if len(self.context_properties) > 0: 147 | response['context']['properties'] = self.context_properties 148 | 149 | if len(self.payload_endpoints) > 0: 150 | response['event']['payload']['endpoints'] = self.payload_endpoints 151 | 152 | if remove_empty: 153 | if len(response['context']) < 1: 154 | response.pop('context') 155 | 156 | return response 157 | 158 | def set_payload(self, payload): 159 | self.event['payload'] = payload 160 | 161 | def set_payload_endpoint(self, payload_endpoints): 162 | self.payload_endpoints = payload_endpoints 163 | 164 | def set_payload_endpoints(self, payload_endpoints): 165 | if 'endpoints' not in self.event['payload']: 166 | self.event['payload']['endpoints'] = [] 167 | 168 | self.event['payload']['endpoints'] = payload_endpoints 169 | -------------------------------------------------------------------------------- /lambda/api/alexa/skills/smarthome/alexa_utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | # 5 | # Licensed under the Amazon Software License (the "License"). You may not use this file except in 6 | # compliance with the License. A copy of the License is located at 7 | # 8 | # http://aws.amazon.com/asl/ 9 | # 10 | # or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific 12 | # language governing permissions and limitations under the License. 13 | 14 | import time 15 | 16 | 17 | def get_utc_timestamp(seconds=None): 18 | return time.strftime('%Y-%m-%dT%H:%M:%S.00Z', time.gmtime(seconds)) 19 | 20 | 21 | -------------------------------------------------------------------------------- /lambda/api/endpoint_cloud/__init__.py: -------------------------------------------------------------------------------- 1 | from .api_auth import ApiAuth 2 | from .api_handler import ApiHandler 3 | from .api_response import ApiResponse 4 | from .api_response_body import ApiResponseBody 5 | from .api_utils import ApiUtils 6 | -------------------------------------------------------------------------------- /lambda/api/endpoint_cloud/api_auth.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | # 5 | # Licensed under the Amazon Software License (the "License"). You may not use this file except in 6 | # compliance with the License. A copy of the License is located at 7 | # 8 | # http://aws.amazon.com/asl/ 9 | # 10 | # or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific 12 | # language governing permissions and limitations under the License. 13 | 14 | import http.client 15 | from urllib.parse import urlencode 16 | 17 | 18 | class ApiAuth: 19 | 20 | def post_to_api(self, payload): 21 | connection = http.client.HTTPSConnection("api.amazon.com") 22 | headers = { 23 | 'content-type': "application/x-www-form-urlencoded", 24 | 'cache-control': "no-cache" 25 | } 26 | connection.request('POST', '/auth/o2/token', urlencode(payload), headers) 27 | return connection.getresponse() 28 | 29 | def get_access_token(self, code, client_id, client_secret, redirect_uri): 30 | payload = { 31 | 'grant_type': 'authorization_code', 32 | 'code': code, 33 | 'client_id': client_id, 34 | 'client_secret': client_secret, 35 | 'redirect_uri': redirect_uri 36 | } 37 | return self.post_to_api(payload) 38 | 39 | @staticmethod 40 | def get_user_id(access_token): 41 | connection = http.client.HTTPSConnection('api.amazon.com') 42 | connection.request('GET', '/user/profile?access_token=' + access_token) 43 | return connection.getresponse() 44 | 45 | def refresh_access_token(self, refresh_token, client_id, client_secret, redirect_uri): 46 | payload = { 47 | 'grant_type': 'refresh_token', 48 | 'refresh_token': refresh_token, 49 | 'client_id': client_id, 50 | 'client_secret': client_secret, 51 | 'redirect_uri': redirect_uri 52 | } 53 | return self.post_to_api(payload) 54 | 55 | 56 | -------------------------------------------------------------------------------- /lambda/api/endpoint_cloud/api_handler.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | # 5 | # Licensed under the Amazon Software License (the "License"). You may not use this file except in 6 | # compliance with the License. A copy of the License is located at 7 | # 8 | # http://aws.amazon.com/asl/ 9 | # 10 | # or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific 12 | # language governing permissions and limitations under the License. 13 | 14 | from .api_handler_directive import ApiHandlerDirective 15 | from .api_handler_endpoint import ApiHandlerEndpoint 16 | from .api_handler_event import ApiHandlerEvent 17 | 18 | 19 | class ApiHandler: 20 | def __init__(self): 21 | self.directive = ApiHandlerDirective() 22 | self.event = ApiHandlerEvent() 23 | self.endpoint = ApiHandlerEndpoint() 24 | -------------------------------------------------------------------------------- /lambda/api/endpoint_cloud/api_handler_event.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | # 5 | # Licensed under the Amazon Software License (the "License"). You may not use this file except in 6 | # compliance with the License. A copy of the License is located at 7 | # 8 | # http://aws.amazon.com/asl/ 9 | # 10 | # or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific 12 | # language governing permissions and limitations under the License. 13 | 14 | import http.client 15 | import json 16 | from datetime import datetime, timedelta 17 | 18 | import boto3 19 | from botocore.exceptions import ClientError 20 | 21 | from alexa.skills.smarthome import AlexaResponse 22 | from .api_auth import ApiAuth 23 | 24 | dynamodb_aws = boto3.client('dynamodb') 25 | iot_aws = boto3.client('iot') 26 | iot_data_aws = boto3.client('iot-data') 27 | 28 | 29 | class ApiHandlerEvent: 30 | 31 | def create(self, request): 32 | print('LOG event.create.request -----') 33 | print(request) 34 | 35 | try: 36 | json_object = json.loads(request['body']) 37 | 38 | # Transpose the Endpoint Cloud Event into an Alexa Event Gateway Event 39 | 40 | # Get the common information from the body of the request 41 | event_type = json_object['event']['type'] # Expect AddOrUpdateReport, ChangeReport, DeleteReport 42 | endpoint_user_id = json_object['event']['endpoint']['userId'] # Expect a Profile 43 | endpoint_id = json_object['event']['endpoint']['id'] # Expect a valid AWS IoT Thing Name 44 | 45 | # Get the Access Token 46 | token = self.get_user_info(endpoint_user_id) 47 | 48 | # Build a default response 49 | response = AlexaResponse(name='ErrorResponse', message="No valid event type") 50 | 51 | if event_type == 'AddOrUpdateReport': 52 | # Get the additional information from the body of the request 53 | endpoint_friendly_name = json_object['event']['endpoint']['friendlyName'] # Expect a valid string friendly name 54 | endpoint_capabilities = json_object['event']['endpoint']['capabilities'] # Expect a valid AWS IoT Thing Name 55 | sku = json_object['event']['endpoint']['sku'] # Expect a meaningful type, ex: SW00 56 | 57 | # From the SKU, get the information for the device and combine it in the payload 58 | endpoint_sku_details = self.get_sku_details(sku) 59 | payload = { 60 | 'endpoints': [ 61 | { 62 | 'endpointId': endpoint_id, 63 | 'friendlyName': endpoint_friendly_name, 64 | 'description': endpoint_sku_details['description'], 65 | 'manufacturerName': endpoint_sku_details['manufacturer_name'], 66 | 'displayCategories': endpoint_sku_details['display_categories'], 67 | 'capabilities': endpoint_capabilities 68 | }], 69 | 'scope': { 70 | 'type': 'BearerToken', 71 | 'token': token 72 | } 73 | } 74 | 75 | # Send an event to Alexa to add/update the endpoint 76 | response = self.send_event('Alexa.Discovery', 'AddOrUpdateReport', endpoint_id, token, payload) 77 | 78 | if event_type == 'ChangeReport': 79 | try: 80 | state = json_object['event']['endpoint']['state'] # Expect a string, ex: powerState 81 | state_value = json_object['event']['endpoint']['value'] # Expect string or JSON 82 | namespace = json_object['event']['endpoint']['namespace'] 83 | instance = json_object['event']['endpoint'].get('instance', None) 84 | if instance: 85 | state = instance+'.'+state 86 | prop = AlexaResponse.create_context_property(instance=instance, namespace=namespace, name=state, value=state_value) 87 | else: 88 | prop = AlexaResponse.create_context_property(namespace=namespace, name=state, value=state_value) 89 | # Update the IoT Thing Shadow state 90 | msg = { 91 | 'state': { 92 | 'desired': 93 | { 94 | state: state_value 95 | } 96 | } 97 | } 98 | mqtt_msg = json.dumps(msg) 99 | result = iot_data_aws.update_thing_shadow( 100 | thingName=endpoint_id, 101 | payload=mqtt_msg.encode()) 102 | print('LOG event.create.iot_aws.update_thing_shadow.result -----') 103 | print(result) 104 | 105 | # Update Alexa with an Event Update 106 | if endpoint_user_id == '0': 107 | print('LOG Event: Not sent for user_id of 0') 108 | else: 109 | payload = { 110 | 'change': { 111 | 'cause': { 112 | 'type': 'PHYSICAL_INTERACTION' 113 | }, 114 | "properties": [ 115 | prop 116 | ] 117 | } 118 | } 119 | print('LOG Event: Sending event') 120 | response = self.send_event('Alexa', 'ChangeReport', endpoint_id, token, payload) 121 | 122 | except ClientError as e: 123 | alexa_response = AlexaResponse(name='ErrorResponse', message=e, payload={'type': 'INTERNAL_ERROR', 'message': e}) 124 | return alexa_response.get() 125 | 126 | if event_type == 'DeleteReport': 127 | # Send an event to Alexa to delete the endpoint 128 | payload = { 129 | 'endpoints': [ 130 | { 131 | 'endpointId': endpoint_id 132 | } 133 | ], 134 | "scope": { 135 | "type": "BearerToken", 136 | "token": token 137 | } 138 | } 139 | response = self.send_event('Alexa.Discovery', 'DeleteReport', endpoint_id, token, payload) 140 | 141 | result = response.read().decode('utf-8') 142 | print('LOG event.create.result -----') 143 | print(result) 144 | return result 145 | 146 | except KeyError as key_error: 147 | return "KeyError: " + str(key_error) 148 | 149 | # TODO Improve this with a database lookup 150 | @staticmethod 151 | def get_sku_details(sku): 152 | 153 | # Set the default at OTHER (OT00) 154 | sku_details = dict(description='A sample endpoint', manufacturer_name='Sample Manufacturer', display_categories=['OTHER']) 155 | 156 | if sku.upper().startswith('LI'): 157 | sku_details['description'] = 'A sample light endpoint' 158 | sku_details['display_categories'] = ["LIGHT"] 159 | 160 | if sku.upper().startswith('MW'): 161 | sku_details['description'] = 'A sample microwave endpoint' 162 | sku_details['display_categories'] = ["MICROWAVE"] 163 | 164 | if sku.upper().startswith('TT'): 165 | sku_details['description'] = 'A sample toaster endpoint' 166 | sku_details['display_categories'] = ["OTHER"] 167 | 168 | if sku.upper().startswith('SW'): 169 | sku_details['description'] = 'A sample switch endpoint' 170 | sku_details['display_categories'] = ["SWITCH"] 171 | 172 | return sku_details 173 | 174 | def get_user_info(self, endpoint_user_id): 175 | print('LOG event.create.get_user_info -----') 176 | table = boto3.resource('dynamodb').Table('SampleUsers') 177 | result = table.get_item( 178 | Key={ 179 | 'UserId': endpoint_user_id 180 | }, 181 | AttributesToGet=[ 182 | 'UserId', 183 | 'AccessToken', 184 | 'ClientId', 185 | 'ClientSecret', 186 | 'ExpirationUTC', 187 | 'RedirectUri', 188 | 'RefreshToken', 189 | 'TokenType' 190 | ] 191 | ) 192 | 193 | if result['ResponseMetadata']['HTTPStatusCode'] == 200: 194 | if 'Item' in result: 195 | print('LOG event.create.get_user_info.SampleUsers.get_item -----') 196 | print(str(result['Item'])) 197 | if 'ExpirationUTC' in result['Item']: 198 | expiration_utc = result['Item']['ExpirationUTC'] 199 | token_is_expired = self.is_token_expired(expiration_utc) 200 | else: 201 | token_is_expired = True 202 | print('LOG event.create.send_event.token_is_expired:', token_is_expired) 203 | if token_is_expired: 204 | # The token has expired so get a new access token using the refresh token 205 | refresh_token = result['Item']['RefreshToken'] 206 | client_id = result['Item']['ClientId'] 207 | client_secret = result['Item']['ClientSecret'] 208 | redirect_uri = result['Item']['RedirectUri'] 209 | 210 | api_auth = ApiAuth() 211 | response_refresh_token = api_auth.refresh_access_token(refresh_token, client_id, client_secret, redirect_uri) 212 | response_refresh_token_string = response_refresh_token.read().decode('utf-8') 213 | response_refresh_token_object = json.loads(response_refresh_token_string) 214 | 215 | # Store the new values from the refresh 216 | access_token = response_refresh_token_object['access_token'] 217 | refresh_token = response_refresh_token_object['refresh_token'] 218 | token_type = response_refresh_token_object['token_type'] 219 | expires_in = response_refresh_token_object['expires_in'] 220 | 221 | # Calculate expiration 222 | expiration_utc = datetime.utcnow() + timedelta(seconds=(int(expires_in) - 5)) 223 | 224 | print('access_token', access_token) 225 | print('expiration_utc', expiration_utc) 226 | 227 | result = table.update_item( 228 | Key={ 229 | 'UserId': endpoint_user_id 230 | }, 231 | UpdateExpression="set AccessToken=:a, RefreshToken=:r, TokenType=:t, ExpirationUTC=:e", 232 | ExpressionAttributeValues={ 233 | ':a': access_token, 234 | ':r': refresh_token, 235 | ':t': token_type, 236 | ':e': expiration_utc.strftime("%Y-%m-%dT%H:%M:%S.00Z") 237 | }, 238 | ReturnValues="UPDATED_NEW" 239 | ) 240 | print('LOG event.create.send_event.SampleUsers.update_item:', str(result)) 241 | 242 | # TODO Return an error here if the token could not be refreshed 243 | else: 244 | # Use the stored access token 245 | access_token = result['Item']['AccessToken'] 246 | print('LOG Using stored access_token:', access_token) 247 | 248 | return access_token 249 | 250 | @staticmethod 251 | def is_token_expired(expiration_utc): 252 | now = datetime.utcnow().replace(tzinfo=None) 253 | then = datetime.strptime(expiration_utc, "%Y-%m-%dT%H:%M:%S.00Z") 254 | is_expired = now > then 255 | if is_expired: 256 | return is_expired 257 | seconds = (now - then).seconds 258 | is_soon = seconds < 30 # Give a 30 second buffer for expiration 259 | return is_soon 260 | 261 | @staticmethod 262 | def send_event(alexa_namespace, alexa_name, endpoint_id, token, payload): 263 | 264 | remove_endpoint = alexa_name is not "ChangeReport" 265 | alexa_response = AlexaResponse(namespace=alexa_namespace, name=alexa_name, endpoint_id=endpoint_id, token=token, remove_endpoint=remove_endpoint) 266 | alexa_response.set_payload(payload) 267 | payload = json.dumps(alexa_response.get()) 268 | print('LOG api_handler_event.send_event.payload:') 269 | print(payload) 270 | 271 | # TODO Map to correct endpoint for Europe: https://api.eu.amazonalexa.com/v3/events 272 | # TODO Map to correct endpoint for Far East: https://api.fe.amazonalexa.com/v3/events 273 | alexa_event_gateway_uri = 'api.amazonalexa.com' 274 | connection = http.client.HTTPSConnection(alexa_event_gateway_uri) 275 | headers = { 276 | 'Authorization': "Bearer " + token, 277 | 'Content-Type': "application/json;charset=UTF-8", 278 | 'Cache-Control': "no-cache" 279 | } 280 | connection.request('POST', '/v3/events', payload, headers) 281 | response = connection.getresponse() 282 | print('LOG api_handler_event.send_event HTTP Status code: ' + str(response.getcode())) 283 | return response 284 | -------------------------------------------------------------------------------- /lambda/api/endpoint_cloud/api_message.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | # 5 | # Licensed under the Amazon Software License (the "License"). You may not use this file except in 6 | # compliance with the License. A copy of the License is located at 7 | # 8 | # http://aws.amazon.com/asl/ 9 | # 10 | # or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific 12 | # language governing permissions and limitations under the License. 13 | 14 | 15 | class ApiMessage: 16 | 17 | def __init__(self, **kwargs): 18 | self.context = kwargs.get('context', {}) 19 | self.header = kwargs.get('header', {}) 20 | self.endpoint = kwargs.get('endpoint', {}) 21 | self.payload = kwargs.get('payload', {}) 22 | 23 | def validate(self): 24 | 25 | return False -------------------------------------------------------------------------------- /lambda/api/endpoint_cloud/api_response.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | # 5 | # Licensed under the Amazon Software License (the "License"). You may not use this file except in 6 | # compliance with the License. A copy of the License is located at 7 | # 8 | # http://aws.amazon.com/asl/ 9 | # 10 | # or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific 12 | # language governing permissions and limitations under the License. 13 | 14 | from .api_response_body import ApiResponseBody 15 | 16 | 17 | class ApiResponse: 18 | 19 | def __init__(self, **kwargs): 20 | self.isBase64Encoded = kwargs.get('isBase64Encoded', False) 21 | self.statusCode = kwargs.get('statusCode', 200) 22 | self.headers = {} 23 | self.body = ApiResponseBody() 24 | self.response = {} 25 | 26 | def __repr__(self): 27 | return self.get() 28 | 29 | def get(self): 30 | self.headers['Content-Type'] = 'application/json' 31 | 32 | self.response['isBase64Encoded'] = str(self.isBase64Encoded) 33 | self.response['statusCode'] = str(self.statusCode) 34 | self.response['headers'] = self.headers 35 | self.response['body'] = str(self.body) 36 | 37 | return self.response 38 | -------------------------------------------------------------------------------- /lambda/api/endpoint_cloud/api_response_body.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | # 5 | # Licensed under the Amazon Software License (the "License"). You may not use this file except in 6 | # compliance with the License. A copy of the License is located at 7 | # 8 | # http://aws.amazon.com/asl/ 9 | # 10 | # or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific 12 | # language governing permissions and limitations under the License. 13 | 14 | import json 15 | 16 | 17 | class ApiResponseBody: 18 | 19 | def __init__(self, **kwargs): 20 | self.body = {} 21 | self.result = kwargs.get('result', "OK") 22 | self.message = kwargs.get('message', "") 23 | 24 | def __repr__(self): 25 | return self.create() 26 | 27 | def create(self): 28 | self.body['result'] = self.result 29 | if self.message: # Check for an empty message 30 | self.body['message'] = self.message 31 | 32 | return json.dumps(self.body) 33 | 34 | -------------------------------------------------------------------------------- /lambda/api/endpoint_cloud/api_utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | # 5 | # Licensed under the Amazon Software License (the "License"). You may not use this file except in 6 | # compliance with the License. A copy of the License is located at 7 | # 8 | # http://aws.amazon.com/asl/ 9 | # 10 | # or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific 12 | # language governing permissions and limitations under the License. 13 | 14 | import datetime 15 | import random 16 | import string 17 | 18 | 19 | class ApiUtils: 20 | 21 | @staticmethod 22 | def check_response(response): 23 | if response is None: 24 | print('ERR ApiUtils.check_response is None') 25 | return False 26 | if response['ResponseMetadata']['HTTPStatusCode'] != 200: 27 | print('ERR ApiUtils.check_response.HTTPStatusCode', response) 28 | return False 29 | else: 30 | return True 31 | 32 | @staticmethod 33 | def get_time_utc(): 34 | """ 35 | An ISO 8601 formatted string in UTC (e.g. YYYY-MM-DDThh:mm:ss.sD) 36 | :return: string date time 37 | """ 38 | return datetime.datetime.utcnow().isoformat() 39 | 40 | @staticmethod 41 | def get_code_string(size): 42 | """ 43 | A code string composed of uppercase ASCII and digits 44 | :param size: The length of the code as an int 45 | :return: string code 46 | """ 47 | return ''.join(random.choices(string.ascii_uppercase + string.digits, k=size)) 48 | 49 | @staticmethod 50 | def get_random_color_string(): 51 | """ 52 | A random color name string composed of uppercase ASCII and digits 53 | :return: string color 54 | """ 55 | return random.choice(['Beige', 'Blue', 'Brown', 'Cyan', 'Green', 'Magenta', 'Orange', 'Red', 'Violet', 'Yellow']) 56 | -------------------------------------------------------------------------------- /lambda/api/index.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | # 5 | # Licensed under the Amazon Software License (the "License"). You may not use this file except in 6 | # compliance with the License. A copy of the License is located at 7 | # 8 | # http://aws.amazon.com/asl/ 9 | # 10 | # or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific 12 | # language governing permissions and limitations under the License. 13 | 14 | import json 15 | import os 16 | import sys 17 | import traceback 18 | from endpoint_cloud import ApiHandler, ApiResponse, ApiResponseBody 19 | 20 | 21 | def get_api_url(api_id, aws_region, resource): 22 | return 'https://{0}.execute-api.{1}.amazonaws.com/prod/{2}'.format(api_id, aws_region, resource) 23 | 24 | 25 | def handler(request, context): 26 | """ 27 | Main Lambda Handler 28 | :param request Incoming API Request 29 | :param context Context for the Request 30 | """ 31 | 32 | print("LOG api.index.handler.request -----") 33 | print(json.dumps(request)) 34 | 35 | # An API Handler to handle internal operations to the endpoints 36 | api_handler = ApiHandler() 37 | 38 | # An API Response crafted to return to the caller - in this case the API Gateway 39 | # The API Gateway expects a specially formatted response 40 | api_response = ApiResponse() 41 | 42 | try: 43 | # Get the Environment Variables, these are used to dynamically compose the API URI and pass Alexa Skill Messaging credentials 44 | 45 | # Get the Region 46 | env_aws_default_region = os.environ.get('AWS_DEFAULT_REGION', None) 47 | if env_aws_default_region is None: 48 | print("ERROR skill.index.handler.aws_default_region is None default to us-east-1") 49 | env_aws_default_region = 'us-east-1' 50 | 51 | # Get the API ID, Client ID, and Client Secret 52 | env_api_id = os.environ.get('api_id', None) 53 | env_client_id = os.environ.get('client_id', None) 54 | env_client_secret = os.environ.get('client_secret', None) 55 | if env_api_id is None or env_client_id is None or env_client_secret is None: 56 | api_response.statusCode = 403 57 | api_response.body = ApiResponseBody(result="ERR", message="Environment variable is not set: api_id:{0} client_id:{1} client_secret:{2}".format(env_api_id, env_client_id, env_client_secret)) 58 | return api_response.get() 59 | 60 | # Reject the request if it isn't from our API 61 | api_id = request['requestContext']['apiId'] 62 | if api_id != env_api_id: 63 | api_response.statusCode = 403 64 | api_response.body = ApiResponseBody(result="ERR", message="api_id did not match") 65 | return api_response.get() 66 | 67 | # Route the inbound request by evaluating for the resource and HTTP method 68 | resource = request["resource"] 69 | http_method = request["httpMethod"] 70 | 71 | # POST to directives : Process an Alexa Directive - This will be used to implement Endpoint behavior and state 72 | if http_method == 'POST' and resource == '/directives': 73 | response = api_handler.directive.process(request, env_client_id, env_client_secret, get_api_url(env_api_id, env_aws_default_region, 'auth-redirect')) 74 | if response['event']['header']['name'] == 'ErrorResponse': 75 | error_message = response['event']['payload']['message']['error_description'] 76 | api_response.statusCode = 500 77 | api_response.body = ApiResponseBody(result="ERR", message=error_message) 78 | return api_response.get() 79 | else: 80 | api_response.statusCode = 200 81 | api_response.body = json.dumps(response) 82 | 83 | # POST to endpoints : Create an Endpoint 84 | if http_method == 'POST' and resource == '/endpoints': 85 | response = api_handler.endpoint.create(request) 86 | api_response.statusCode = 200 87 | api_response.body = json.dumps(response) 88 | 89 | # GET endpoints : List Endpoints 90 | if http_method == 'GET' and resource == '/endpoints': 91 | response = api_handler.endpoint.read(request) 92 | api_response.statusCode = 200 93 | api_response.body = json.dumps(response) 94 | 95 | # DELETE endpoints : Delete an Endpoint 96 | if http_method == 'DELETE' and resource == '/endpoints': 97 | response = api_handler.endpoint.delete(request) 98 | api_response.statusCode = 200 99 | api_response.body = json.dumps(response) 100 | 101 | # POST to event : Create an Event 102 | if http_method == 'POST' and resource == '/events': 103 | response = api_handler.event.create(request) 104 | print('LOG api.index.handler.request.api_handler.event.create.response:', response) 105 | api_response.statusCode = 200 106 | api_response.body = json.dumps(response) 107 | 108 | except KeyError as key_error: 109 | # For a key Error, return an error message and HTTP Status of 400 Bad Request 110 | message_string = "KeyError: " + str(key_error) 111 | 112 | # Dump a traceback to help in debugging 113 | print('TRACEBACK:START') 114 | traceback.print_tb(sys.exc_info()[2]) 115 | print('TRACEBACK:END') 116 | 117 | api_response.statusCode = 400 118 | api_response.body = ApiResponseBody(result="ERR", message=message_string) 119 | 120 | print('LOG api.index.handler.api_handler -----') 121 | print(json.dumps(api_response.get())) 122 | 123 | return api_response.get() 124 | -------------------------------------------------------------------------------- /lambda/api/jsonschema/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | An implementation of JSON Schema for Python 3 | 4 | The main functionality is provided by the validator classes for each of the 5 | supported JSON Schema versions. 6 | 7 | Most commonly, :func:`validate` is the quickest way to simply validate a given 8 | instance under a schema, and will create a validator for you. 9 | 10 | """ 11 | 12 | from jsonschema.exceptions import ( 13 | ErrorTree, FormatError, RefResolutionError, SchemaError, ValidationError 14 | ) 15 | from jsonschema._format import ( 16 | FormatChecker, draft3_format_checker, draft4_format_checker, 17 | ) 18 | from jsonschema.validators import ( 19 | Draft3Validator, Draft4Validator, RefResolver, validate 20 | ) 21 | 22 | from jsonschema._version import __version__ 23 | 24 | # flake8: noqa 25 | -------------------------------------------------------------------------------- /lambda/api/jsonschema/__main__.py: -------------------------------------------------------------------------------- 1 | from jsonschema.cli import main 2 | main() 3 | -------------------------------------------------------------------------------- /lambda/api/jsonschema/_format.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import re 3 | import socket 4 | 5 | from jsonschema.compat import str_types 6 | from jsonschema.exceptions import FormatError 7 | 8 | 9 | class FormatChecker(object): 10 | """ 11 | A ``format`` property checker. 12 | 13 | JSON Schema does not mandate that the ``format`` property actually do any 14 | validation. If validation is desired however, instances of this class can 15 | be hooked into validators to enable format validation. 16 | 17 | :class:`FormatChecker` objects always return ``True`` when asked about 18 | formats that they do not know how to validate. 19 | 20 | To check a custom format using a function that takes an instance and 21 | returns a ``bool``, use the :meth:`FormatChecker.checks` or 22 | :meth:`FormatChecker.cls_checks` decorators. 23 | 24 | Arguments: 25 | 26 | formats (iterable): 27 | 28 | The known formats to validate. This argument can be used to 29 | limit which formats will be used during validation. 30 | 31 | """ 32 | 33 | checkers = {} 34 | 35 | def __init__(self, formats=None): 36 | if formats is None: 37 | self.checkers = self.checkers.copy() 38 | else: 39 | self.checkers = dict((k, self.checkers[k]) for k in formats) 40 | 41 | def checks(self, format, raises=()): 42 | """ 43 | Register a decorated function as validating a new format. 44 | 45 | Arguments: 46 | 47 | format (str): 48 | 49 | The format that the decorated function will check. 50 | 51 | raises (Exception): 52 | 53 | The exception(s) raised by the decorated function when 54 | an invalid instance is found. 55 | 56 | The exception object will be accessible as the 57 | :attr:`ValidationError.cause` attribute of the resulting 58 | validation error. 59 | 60 | """ 61 | 62 | def _checks(func): 63 | self.checkers[format] = (func, raises) 64 | return func 65 | return _checks 66 | 67 | cls_checks = classmethod(checks) 68 | 69 | def check(self, instance, format): 70 | """ 71 | Check whether the instance conforms to the given format. 72 | 73 | Arguments: 74 | 75 | instance (any primitive type, i.e. str, number, bool): 76 | 77 | The instance to check 78 | 79 | format (str): 80 | 81 | The format that instance should conform to 82 | 83 | 84 | Raises: 85 | 86 | :exc:`FormatError` if instance does not conform to ``format`` 87 | 88 | """ 89 | 90 | if format not in self.checkers: 91 | return 92 | 93 | func, raises = self.checkers[format] 94 | result, cause = None, None 95 | try: 96 | result = func(instance) 97 | except raises as e: 98 | cause = e 99 | if not result: 100 | raise FormatError( 101 | "%r is not a %r" % (instance, format), cause=cause, 102 | ) 103 | 104 | def conforms(self, instance, format): 105 | """ 106 | Check whether the instance conforms to the given format. 107 | 108 | Arguments: 109 | 110 | instance (any primitive type, i.e. str, number, bool): 111 | 112 | The instance to check 113 | 114 | format (str): 115 | 116 | The format that instance should conform to 117 | 118 | Returns: 119 | 120 | bool: Whether it conformed 121 | 122 | """ 123 | 124 | try: 125 | self.check(instance, format) 126 | except FormatError: 127 | return False 128 | else: 129 | return True 130 | 131 | 132 | _draft_checkers = {"draft3": [], "draft4": []} 133 | 134 | 135 | def _checks_drafts(both=None, draft3=None, draft4=None, raises=()): 136 | draft3 = draft3 or both 137 | draft4 = draft4 or both 138 | 139 | def wrap(func): 140 | if draft3: 141 | _draft_checkers["draft3"].append(draft3) 142 | func = FormatChecker.cls_checks(draft3, raises)(func) 143 | if draft4: 144 | _draft_checkers["draft4"].append(draft4) 145 | func = FormatChecker.cls_checks(draft4, raises)(func) 146 | return func 147 | return wrap 148 | 149 | 150 | @_checks_drafts("email") 151 | def is_email(instance): 152 | if not isinstance(instance, str_types): 153 | return True 154 | return "@" in instance 155 | 156 | 157 | _ipv4_re = re.compile(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$") 158 | 159 | 160 | @_checks_drafts(draft3="ip-address", draft4="ipv4") 161 | def is_ipv4(instance): 162 | if not isinstance(instance, str_types): 163 | return True 164 | if not _ipv4_re.match(instance): 165 | return False 166 | return all(0 <= int(component) <= 255 for component in instance.split(".")) 167 | 168 | 169 | if hasattr(socket, "inet_pton"): 170 | @_checks_drafts("ipv6", raises=socket.error) 171 | def is_ipv6(instance): 172 | if not isinstance(instance, str_types): 173 | return True 174 | return socket.inet_pton(socket.AF_INET6, instance) 175 | 176 | 177 | _host_name_re = re.compile(r"^[A-Za-z0-9][A-Za-z0-9\.\-]{1,255}$") 178 | 179 | 180 | @_checks_drafts(draft3="host-name", draft4="hostname") 181 | def is_host_name(instance): 182 | if not isinstance(instance, str_types): 183 | return True 184 | if not _host_name_re.match(instance): 185 | return False 186 | components = instance.split(".") 187 | for component in components: 188 | if len(component) > 63: 189 | return False 190 | return True 191 | 192 | 193 | try: 194 | import rfc3987 195 | except ImportError: 196 | pass 197 | else: 198 | @_checks_drafts("uri", raises=ValueError) 199 | def is_uri(instance): 200 | if not isinstance(instance, str_types): 201 | return True 202 | return rfc3987.parse(instance, rule="URI") 203 | 204 | 205 | try: 206 | import strict_rfc3339 207 | except ImportError: 208 | try: 209 | import isodate 210 | except ImportError: 211 | pass 212 | else: 213 | @_checks_drafts("date-time", raises=(ValueError, isodate.ISO8601Error)) 214 | def is_datetime(instance): 215 | if not isinstance(instance, str_types): 216 | return True 217 | return isodate.parse_datetime(instance) 218 | else: 219 | @_checks_drafts("date-time") 220 | def is_datetime(instance): 221 | if not isinstance(instance, str_types): 222 | return True 223 | return strict_rfc3339.validate_rfc3339(instance) 224 | 225 | 226 | @_checks_drafts("regex", raises=re.error) 227 | def is_regex(instance): 228 | if not isinstance(instance, str_types): 229 | return True 230 | return re.compile(instance) 231 | 232 | 233 | @_checks_drafts(draft3="date", raises=ValueError) 234 | def is_date(instance): 235 | if not isinstance(instance, str_types): 236 | return True 237 | return datetime.datetime.strptime(instance, "%Y-%m-%d") 238 | 239 | 240 | @_checks_drafts(draft3="time", raises=ValueError) 241 | def is_time(instance): 242 | if not isinstance(instance, str_types): 243 | return True 244 | return datetime.datetime.strptime(instance, "%H:%M:%S") 245 | 246 | 247 | try: 248 | import webcolors 249 | except ImportError: 250 | pass 251 | else: 252 | def is_css_color_code(instance): 253 | return webcolors.normalize_hex(instance) 254 | 255 | @_checks_drafts(draft3="color", raises=(ValueError, TypeError)) 256 | def is_css21_color(instance): 257 | if ( 258 | not isinstance(instance, str_types) or 259 | instance.lower() in webcolors.css21_names_to_hex 260 | ): 261 | return True 262 | return is_css_color_code(instance) 263 | 264 | def is_css3_color(instance): 265 | if instance.lower() in webcolors.css3_names_to_hex: 266 | return True 267 | return is_css_color_code(instance) 268 | 269 | 270 | draft3_format_checker = FormatChecker(_draft_checkers["draft3"]) 271 | draft4_format_checker = FormatChecker(_draft_checkers["draft4"]) 272 | -------------------------------------------------------------------------------- /lambda/api/jsonschema/_reflect.py: -------------------------------------------------------------------------------- 1 | # -*- test-case-name: twisted.test.test_reflect -*- 2 | # Copyright (c) Twisted Matrix Laboratories. 3 | # See LICENSE for details. 4 | 5 | """ 6 | Standardized versions of various cool and/or strange things that you can do 7 | with Python's reflection capabilities. 8 | """ 9 | 10 | import sys 11 | 12 | from jsonschema.compat import PY3 13 | 14 | 15 | class _NoModuleFound(Exception): 16 | """ 17 | No module was found because none exists. 18 | """ 19 | 20 | 21 | 22 | class InvalidName(ValueError): 23 | """ 24 | The given name is not a dot-separated list of Python objects. 25 | """ 26 | 27 | 28 | 29 | class ModuleNotFound(InvalidName): 30 | """ 31 | The module associated with the given name doesn't exist and it can't be 32 | imported. 33 | """ 34 | 35 | 36 | 37 | class ObjectNotFound(InvalidName): 38 | """ 39 | The object associated with the given name doesn't exist and it can't be 40 | imported. 41 | """ 42 | 43 | 44 | 45 | if PY3: 46 | def reraise(exception, traceback): 47 | raise exception.with_traceback(traceback) 48 | else: 49 | exec("""def reraise(exception, traceback): 50 | raise exception.__class__, exception, traceback""") 51 | 52 | reraise.__doc__ = """ 53 | Re-raise an exception, with an optional traceback, in a way that is compatible 54 | with both Python 2 and Python 3. 55 | 56 | Note that on Python 3, re-raised exceptions will be mutated, with their 57 | C{__traceback__} attribute being set. 58 | 59 | @param exception: The exception instance. 60 | @param traceback: The traceback to use, or C{None} indicating a new traceback. 61 | """ 62 | 63 | 64 | def _importAndCheckStack(importName): 65 | """ 66 | Import the given name as a module, then walk the stack to determine whether 67 | the failure was the module not existing, or some code in the module (for 68 | example a dependent import) failing. This can be helpful to determine 69 | whether any actual application code was run. For example, to distiguish 70 | administrative error (entering the wrong module name), from programmer 71 | error (writing buggy code in a module that fails to import). 72 | 73 | @param importName: The name of the module to import. 74 | @type importName: C{str} 75 | @raise Exception: if something bad happens. This can be any type of 76 | exception, since nobody knows what loading some arbitrary code might 77 | do. 78 | @raise _NoModuleFound: if no module was found. 79 | """ 80 | try: 81 | return __import__(importName) 82 | except ImportError: 83 | excType, excValue, excTraceback = sys.exc_info() 84 | while excTraceback: 85 | execName = excTraceback.tb_frame.f_globals["__name__"] 86 | # in Python 2 execName is None when an ImportError is encountered, 87 | # where in Python 3 execName is equal to the importName. 88 | if execName is None or execName == importName: 89 | reraise(excValue, excTraceback) 90 | excTraceback = excTraceback.tb_next 91 | raise _NoModuleFound() 92 | 93 | 94 | 95 | def namedAny(name): 96 | """ 97 | Retrieve a Python object by its fully qualified name from the global Python 98 | module namespace. The first part of the name, that describes a module, 99 | will be discovered and imported. Each subsequent part of the name is 100 | treated as the name of an attribute of the object specified by all of the 101 | name which came before it. For example, the fully-qualified name of this 102 | object is 'twisted.python.reflect.namedAny'. 103 | 104 | @type name: L{str} 105 | @param name: The name of the object to return. 106 | 107 | @raise InvalidName: If the name is an empty string, starts or ends with 108 | a '.', or is otherwise syntactically incorrect. 109 | 110 | @raise ModuleNotFound: If the name is syntactically correct but the 111 | module it specifies cannot be imported because it does not appear to 112 | exist. 113 | 114 | @raise ObjectNotFound: If the name is syntactically correct, includes at 115 | least one '.', but the module it specifies cannot be imported because 116 | it does not appear to exist. 117 | 118 | @raise AttributeError: If an attribute of an object along the way cannot be 119 | accessed, or a module along the way is not found. 120 | 121 | @return: the Python object identified by 'name'. 122 | """ 123 | if not name: 124 | raise InvalidName('Empty module name') 125 | 126 | names = name.split('.') 127 | 128 | # if the name starts or ends with a '.' or contains '..', the __import__ 129 | # will raise an 'Empty module name' error. This will provide a better error 130 | # message. 131 | if '' in names: 132 | raise InvalidName( 133 | "name must be a string giving a '.'-separated list of Python " 134 | "identifiers, not %r" % (name,)) 135 | 136 | topLevelPackage = None 137 | moduleNames = names[:] 138 | while not topLevelPackage: 139 | if moduleNames: 140 | trialname = '.'.join(moduleNames) 141 | try: 142 | topLevelPackage = _importAndCheckStack(trialname) 143 | except _NoModuleFound: 144 | moduleNames.pop() 145 | else: 146 | if len(names) == 1: 147 | raise ModuleNotFound("No module named %r" % (name,)) 148 | else: 149 | raise ObjectNotFound('%r does not name an object' % (name,)) 150 | 151 | obj = topLevelPackage 152 | for n in names[1:]: 153 | obj = getattr(obj, n) 154 | 155 | return obj 156 | -------------------------------------------------------------------------------- /lambda/api/jsonschema/_utils.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import json 3 | import pkgutil 4 | import re 5 | 6 | from jsonschema.compat import str_types, MutableMapping, urlsplit 7 | 8 | 9 | class URIDict(MutableMapping): 10 | """ 11 | Dictionary which uses normalized URIs as keys. 12 | 13 | """ 14 | 15 | def normalize(self, uri): 16 | return urlsplit(uri).geturl() 17 | 18 | def __init__(self, *args, **kwargs): 19 | self.store = dict() 20 | self.store.update(*args, **kwargs) 21 | 22 | def __getitem__(self, uri): 23 | return self.store[self.normalize(uri)] 24 | 25 | def __setitem__(self, uri, value): 26 | self.store[self.normalize(uri)] = value 27 | 28 | def __delitem__(self, uri): 29 | del self.store[self.normalize(uri)] 30 | 31 | def __iter__(self): 32 | return iter(self.store) 33 | 34 | def __len__(self): 35 | return len(self.store) 36 | 37 | def __repr__(self): 38 | return repr(self.store) 39 | 40 | 41 | class Unset(object): 42 | """ 43 | An as-of-yet unset attribute or unprovided default parameter. 44 | 45 | """ 46 | 47 | def __repr__(self): 48 | return "" 49 | 50 | 51 | def load_schema(name): 52 | """ 53 | Load a schema from ./schemas/``name``.json and return it. 54 | 55 | """ 56 | 57 | data = pkgutil.get_data('jsonschema', "schemas/{0}.json".format(name)) 58 | return json.loads(data.decode("utf-8")) 59 | 60 | 61 | def indent(string, times=1): 62 | """ 63 | A dumb version of :func:`textwrap.indent` from Python 3.3. 64 | 65 | """ 66 | 67 | return "\n".join(" " * (4 * times) + line for line in string.splitlines()) 68 | 69 | 70 | def format_as_index(indices): 71 | """ 72 | Construct a single string containing indexing operations for the indices. 73 | 74 | For example, [1, 2, "foo"] -> [1][2]["foo"] 75 | 76 | Arguments: 77 | 78 | indices (sequence): 79 | 80 | The indices to format. 81 | 82 | """ 83 | 84 | if not indices: 85 | return "" 86 | return "[%s]" % "][".join(repr(index) for index in indices) 87 | 88 | 89 | def find_additional_properties(instance, schema): 90 | """ 91 | Return the set of additional properties for the given ``instance``. 92 | 93 | Weeds out properties that should have been validated by ``properties`` and 94 | / or ``patternProperties``. 95 | 96 | Assumes ``instance`` is dict-like already. 97 | 98 | """ 99 | 100 | properties = schema.get("properties", {}) 101 | patterns = "|".join(schema.get("patternProperties", {})) 102 | for property in instance: 103 | if property not in properties: 104 | if patterns and re.search(patterns, property): 105 | continue 106 | yield property 107 | 108 | 109 | def extras_msg(extras): 110 | """ 111 | Create an error message for extra items or properties. 112 | 113 | """ 114 | 115 | if len(extras) == 1: 116 | verb = "was" 117 | else: 118 | verb = "were" 119 | return ", ".join(repr(extra) for extra in extras), verb 120 | 121 | 122 | def types_msg(instance, types): 123 | """ 124 | Create an error message for a failure to match the given types. 125 | 126 | If the ``instance`` is an object and contains a ``name`` property, it will 127 | be considered to be a description of that object and used as its type. 128 | 129 | Otherwise the message is simply the reprs of the given ``types``. 130 | 131 | """ 132 | 133 | reprs = [] 134 | for type in types: 135 | try: 136 | reprs.append(repr(type["name"])) 137 | except Exception: 138 | reprs.append(repr(type)) 139 | return "%r is not of type %s" % (instance, ", ".join(reprs)) 140 | 141 | 142 | def flatten(suitable_for_isinstance): 143 | """ 144 | isinstance() can accept a bunch of really annoying different types: 145 | * a single type 146 | * a tuple of types 147 | * an arbitrary nested tree of tuples 148 | 149 | Return a flattened tuple of the given argument. 150 | 151 | """ 152 | 153 | types = set() 154 | 155 | if not isinstance(suitable_for_isinstance, tuple): 156 | suitable_for_isinstance = (suitable_for_isinstance,) 157 | for thing in suitable_for_isinstance: 158 | if isinstance(thing, tuple): 159 | types.update(flatten(thing)) 160 | else: 161 | types.add(thing) 162 | return tuple(types) 163 | 164 | 165 | def ensure_list(thing): 166 | """ 167 | Wrap ``thing`` in a list if it's a single str. 168 | 169 | Otherwise, return it unchanged. 170 | 171 | """ 172 | 173 | if isinstance(thing, str_types): 174 | return [thing] 175 | return thing 176 | 177 | 178 | def unbool(element, true=object(), false=object()): 179 | """ 180 | A hack to make True and 1 and False and 0 unique for ``uniq``. 181 | 182 | """ 183 | 184 | if element is True: 185 | return true 186 | elif element is False: 187 | return false 188 | return element 189 | 190 | 191 | def uniq(container): 192 | """ 193 | Check if all of a container's elements are unique. 194 | 195 | Successively tries first to rely that the elements are hashable, then 196 | falls back on them being sortable, and finally falls back on brute 197 | force. 198 | 199 | """ 200 | 201 | try: 202 | return len(set(unbool(i) for i in container)) == len(container) 203 | except TypeError: 204 | try: 205 | sort = sorted(unbool(i) for i in container) 206 | sliced = itertools.islice(sort, 1, None) 207 | for i, j in zip(sort, sliced): 208 | if i == j: 209 | return False 210 | except (NotImplementedError, TypeError): 211 | seen = [] 212 | for e in container: 213 | e = unbool(e) 214 | if e in seen: 215 | return False 216 | seen.append(e) 217 | return True 218 | -------------------------------------------------------------------------------- /lambda/api/jsonschema/_validators.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from jsonschema import _utils 4 | from jsonschema.exceptions import FormatError, ValidationError 5 | from jsonschema.compat import iteritems 6 | 7 | 8 | def patternProperties(validator, patternProperties, instance, schema): 9 | if not validator.is_type(instance, "object"): 10 | return 11 | 12 | for pattern, subschema in iteritems(patternProperties): 13 | for k, v in iteritems(instance): 14 | if re.search(pattern, k): 15 | for error in validator.descend( 16 | v, subschema, path=k, schema_path=pattern, 17 | ): 18 | yield error 19 | 20 | 21 | def additionalProperties(validator, aP, instance, schema): 22 | if not validator.is_type(instance, "object"): 23 | return 24 | 25 | extras = set(_utils.find_additional_properties(instance, schema)) 26 | 27 | if validator.is_type(aP, "object"): 28 | for extra in extras: 29 | for error in validator.descend(instance[extra], aP, path=extra): 30 | yield error 31 | elif not aP and extras: 32 | if "patternProperties" in schema: 33 | patterns = sorted(schema["patternProperties"]) 34 | if len(extras) == 1: 35 | verb = "does" 36 | else: 37 | verb = "do" 38 | error = "%s %s not match any of the regexes: %s" % ( 39 | ", ".join(map(repr, sorted(extras))), 40 | verb, 41 | ", ".join(map(repr, patterns)), 42 | ) 43 | yield ValidationError(error) 44 | else: 45 | error = "Additional properties are not allowed (%s %s unexpected)" 46 | yield ValidationError(error % _utils.extras_msg(extras)) 47 | 48 | 49 | def items(validator, items, instance, schema): 50 | if not validator.is_type(instance, "array"): 51 | return 52 | 53 | if validator.is_type(items, "object"): 54 | for index, item in enumerate(instance): 55 | for error in validator.descend(item, items, path=index): 56 | yield error 57 | else: 58 | for (index, item), subschema in zip(enumerate(instance), items): 59 | for error in validator.descend( 60 | item, subschema, path=index, schema_path=index, 61 | ): 62 | yield error 63 | 64 | 65 | def additionalItems(validator, aI, instance, schema): 66 | if ( 67 | not validator.is_type(instance, "array") or 68 | validator.is_type(schema.get("items", {}), "object") 69 | ): 70 | return 71 | 72 | len_items = len(schema.get("items", [])) 73 | if validator.is_type(aI, "object"): 74 | for index, item in enumerate(instance[len_items:], start=len_items): 75 | for error in validator.descend(item, aI, path=index): 76 | yield error 77 | elif not aI and len(instance) > len(schema.get("items", [])): 78 | error = "Additional items are not allowed (%s %s unexpected)" 79 | yield ValidationError( 80 | error % 81 | _utils.extras_msg(instance[len(schema.get("items", [])):]) 82 | ) 83 | 84 | 85 | def minimum(validator, minimum, instance, schema): 86 | if not validator.is_type(instance, "number"): 87 | return 88 | 89 | if schema.get("exclusiveMinimum", False): 90 | failed = instance <= minimum 91 | cmp = "less than or equal to" 92 | else: 93 | failed = instance < minimum 94 | cmp = "less than" 95 | 96 | if failed: 97 | yield ValidationError( 98 | "%r is %s the minimum of %r" % (instance, cmp, minimum) 99 | ) 100 | 101 | 102 | def maximum(validator, maximum, instance, schema): 103 | if not validator.is_type(instance, "number"): 104 | return 105 | 106 | if schema.get("exclusiveMaximum", False): 107 | failed = instance >= maximum 108 | cmp = "greater than or equal to" 109 | else: 110 | failed = instance > maximum 111 | cmp = "greater than" 112 | 113 | if failed: 114 | yield ValidationError( 115 | "%r is %s the maximum of %r" % (instance, cmp, maximum) 116 | ) 117 | 118 | 119 | def multipleOf(validator, dB, instance, schema): 120 | if not validator.is_type(instance, "number"): 121 | return 122 | 123 | if isinstance(dB, float): 124 | quotient = instance / dB 125 | failed = int(quotient) != quotient 126 | else: 127 | failed = instance % dB 128 | 129 | if failed: 130 | yield ValidationError("%r is not a multiple of %r" % (instance, dB)) 131 | 132 | 133 | def minItems(validator, mI, instance, schema): 134 | if validator.is_type(instance, "array") and len(instance) < mI: 135 | yield ValidationError("%r is too short" % (instance,)) 136 | 137 | 138 | def maxItems(validator, mI, instance, schema): 139 | if validator.is_type(instance, "array") and len(instance) > mI: 140 | yield ValidationError("%r is too long" % (instance,)) 141 | 142 | 143 | def uniqueItems(validator, uI, instance, schema): 144 | if ( 145 | uI and 146 | validator.is_type(instance, "array") and 147 | not _utils.uniq(instance) 148 | ): 149 | yield ValidationError("%r has non-unique elements" % (instance,)) 150 | 151 | 152 | def pattern(validator, patrn, instance, schema): 153 | if ( 154 | validator.is_type(instance, "string") and 155 | not re.search(patrn, instance) 156 | ): 157 | yield ValidationError("%r does not match %r" % (instance, patrn)) 158 | 159 | 160 | def format(validator, format, instance, schema): 161 | if validator.format_checker is not None: 162 | try: 163 | validator.format_checker.check(instance, format) 164 | except FormatError as error: 165 | yield ValidationError(error.message, cause=error.cause) 166 | 167 | 168 | def minLength(validator, mL, instance, schema): 169 | if validator.is_type(instance, "string") and len(instance) < mL: 170 | yield ValidationError("%r is too short" % (instance,)) 171 | 172 | 173 | def maxLength(validator, mL, instance, schema): 174 | if validator.is_type(instance, "string") and len(instance) > mL: 175 | yield ValidationError("%r is too long" % (instance,)) 176 | 177 | 178 | def dependencies(validator, dependencies, instance, schema): 179 | if not validator.is_type(instance, "object"): 180 | return 181 | 182 | for property, dependency in iteritems(dependencies): 183 | if property not in instance: 184 | continue 185 | 186 | if validator.is_type(dependency, "object"): 187 | for error in validator.descend( 188 | instance, dependency, schema_path=property, 189 | ): 190 | yield error 191 | else: 192 | dependencies = _utils.ensure_list(dependency) 193 | for dependency in dependencies: 194 | if dependency not in instance: 195 | yield ValidationError( 196 | "%r is a dependency of %r" % (dependency, property) 197 | ) 198 | 199 | 200 | def enum(validator, enums, instance, schema): 201 | if instance not in enums: 202 | yield ValidationError("%r is not one of %r" % (instance, enums)) 203 | 204 | 205 | def ref(validator, ref, instance, schema): 206 | resolve = getattr(validator.resolver, "resolve", None) 207 | if resolve is None: 208 | with validator.resolver.resolving(ref) as resolved: 209 | for error in validator.descend(instance, resolved): 210 | yield error 211 | else: 212 | scope, resolved = validator.resolver.resolve(ref) 213 | validator.resolver.push_scope(scope) 214 | 215 | try: 216 | for error in validator.descend(instance, resolved): 217 | yield error 218 | finally: 219 | validator.resolver.pop_scope() 220 | 221 | 222 | def type_draft3(validator, types, instance, schema): 223 | types = _utils.ensure_list(types) 224 | 225 | all_errors = [] 226 | for index, type in enumerate(types): 227 | if type == "any": 228 | return 229 | if validator.is_type(type, "object"): 230 | errors = list(validator.descend(instance, type, schema_path=index)) 231 | if not errors: 232 | return 233 | all_errors.extend(errors) 234 | else: 235 | if validator.is_type(instance, type): 236 | return 237 | else: 238 | yield ValidationError( 239 | _utils.types_msg(instance, types), context=all_errors, 240 | ) 241 | 242 | 243 | def properties_draft3(validator, properties, instance, schema): 244 | if not validator.is_type(instance, "object"): 245 | return 246 | 247 | for property, subschema in iteritems(properties): 248 | if property in instance: 249 | for error in validator.descend( 250 | instance[property], 251 | subschema, 252 | path=property, 253 | schema_path=property, 254 | ): 255 | yield error 256 | elif subschema.get("required", False): 257 | error = ValidationError("%r is a required property" % property) 258 | error._set( 259 | validator="required", 260 | validator_value=subschema["required"], 261 | instance=instance, 262 | schema=schema, 263 | ) 264 | error.path.appendleft(property) 265 | error.schema_path.extend([property, "required"]) 266 | yield error 267 | 268 | 269 | def disallow_draft3(validator, disallow, instance, schema): 270 | for disallowed in _utils.ensure_list(disallow): 271 | if validator.is_valid(instance, {"type": [disallowed]}): 272 | yield ValidationError( 273 | "%r is disallowed for %r" % (disallowed, instance) 274 | ) 275 | 276 | 277 | def extends_draft3(validator, extends, instance, schema): 278 | if validator.is_type(extends, "object"): 279 | for error in validator.descend(instance, extends): 280 | yield error 281 | return 282 | for index, subschema in enumerate(extends): 283 | for error in validator.descend(instance, subschema, schema_path=index): 284 | yield error 285 | 286 | 287 | def type_draft4(validator, types, instance, schema): 288 | types = _utils.ensure_list(types) 289 | 290 | if not any(validator.is_type(instance, type) for type in types): 291 | yield ValidationError(_utils.types_msg(instance, types)) 292 | 293 | 294 | def properties_draft4(validator, properties, instance, schema): 295 | if not validator.is_type(instance, "object"): 296 | return 297 | 298 | for property, subschema in iteritems(properties): 299 | if property in instance: 300 | for error in validator.descend( 301 | instance[property], 302 | subschema, 303 | path=property, 304 | schema_path=property, 305 | ): 306 | yield error 307 | 308 | 309 | def required_draft4(validator, required, instance, schema): 310 | if not validator.is_type(instance, "object"): 311 | return 312 | for property in required: 313 | if property not in instance: 314 | yield ValidationError("%r is a required property" % property) 315 | 316 | 317 | def minProperties_draft4(validator, mP, instance, schema): 318 | if validator.is_type(instance, "object") and len(instance) < mP: 319 | yield ValidationError( 320 | "%r does not have enough properties" % (instance,) 321 | ) 322 | 323 | 324 | def maxProperties_draft4(validator, mP, instance, schema): 325 | if not validator.is_type(instance, "object"): 326 | return 327 | if validator.is_type(instance, "object") and len(instance) > mP: 328 | yield ValidationError("%r has too many properties" % (instance,)) 329 | 330 | 331 | def allOf_draft4(validator, allOf, instance, schema): 332 | for index, subschema in enumerate(allOf): 333 | for error in validator.descend(instance, subschema, schema_path=index): 334 | yield error 335 | 336 | 337 | def oneOf_draft4(validator, oneOf, instance, schema): 338 | subschemas = enumerate(oneOf) 339 | all_errors = [] 340 | for index, subschema in subschemas: 341 | errs = list(validator.descend(instance, subschema, schema_path=index)) 342 | if not errs: 343 | first_valid = subschema 344 | break 345 | all_errors.extend(errs) 346 | else: 347 | yield ValidationError( 348 | "%r is not valid under any of the given schemas" % (instance,), 349 | context=all_errors, 350 | ) 351 | 352 | more_valid = [s for i, s in subschemas if validator.is_valid(instance, s)] 353 | if more_valid: 354 | more_valid.append(first_valid) 355 | reprs = ", ".join(repr(schema) for schema in more_valid) 356 | yield ValidationError( 357 | "%r is valid under each of %s" % (instance, reprs) 358 | ) 359 | 360 | 361 | def anyOf_draft4(validator, anyOf, instance, schema): 362 | all_errors = [] 363 | for index, subschema in enumerate(anyOf): 364 | errs = list(validator.descend(instance, subschema, schema_path=index)) 365 | if not errs: 366 | break 367 | all_errors.extend(errs) 368 | else: 369 | yield ValidationError( 370 | "%r is not valid under any of the given schemas" % (instance,), 371 | context=all_errors, 372 | ) 373 | 374 | 375 | def not_draft4(validator, not_schema, instance, schema): 376 | if validator.is_valid(instance, not_schema): 377 | yield ValidationError( 378 | "%r is not allowed for %r" % (not_schema, instance) 379 | ) 380 | -------------------------------------------------------------------------------- /lambda/api/jsonschema/_version.py: -------------------------------------------------------------------------------- 1 | 2 | # This file is automatically generated by setup.py. 3 | __version__ = '2.6.0' 4 | __sha__ = 'gd16713a' 5 | __revision__ = 'gd16713a' 6 | -------------------------------------------------------------------------------- /lambda/api/jsonschema/cli.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import argparse 3 | import json 4 | import sys 5 | 6 | from jsonschema._reflect import namedAny 7 | from jsonschema.validators import validator_for 8 | 9 | 10 | def _namedAnyWithDefault(name): 11 | if "." not in name: 12 | name = "jsonschema." + name 13 | return namedAny(name) 14 | 15 | 16 | def _json_file(path): 17 | with open(path) as file: 18 | return json.load(file) 19 | 20 | 21 | parser = argparse.ArgumentParser( 22 | description="JSON Schema Validation CLI", 23 | ) 24 | parser.add_argument( 25 | "-i", "--instance", 26 | action="append", 27 | dest="instances", 28 | type=_json_file, 29 | help=( 30 | "a path to a JSON instance (i.e. filename.json)" 31 | "to validate (may be specified multiple times)" 32 | ), 33 | ) 34 | parser.add_argument( 35 | "-F", "--error-format", 36 | default="{error.instance}: {error.message}\n", 37 | help=( 38 | "the format to use for each error output message, specified in " 39 | "a form suitable for passing to str.format, which will be called " 40 | "with 'error' for each error" 41 | ), 42 | ) 43 | parser.add_argument( 44 | "-V", "--validator", 45 | type=_namedAnyWithDefault, 46 | help=( 47 | "the fully qualified object name of a validator to use, or, for " 48 | "validators that are registered with jsonschema, simply the name " 49 | "of the class." 50 | ), 51 | ) 52 | parser.add_argument( 53 | "schema", 54 | help="the JSON Schema to validate with (i.e. filename.schema)", 55 | type=_json_file, 56 | ) 57 | 58 | 59 | def parse_args(args): 60 | arguments = vars(parser.parse_args(args=args or ["--help"])) 61 | if arguments["validator"] is None: 62 | arguments["validator"] = validator_for(arguments["schema"]) 63 | return arguments 64 | 65 | 66 | def main(args=sys.argv[1:]): 67 | sys.exit(run(arguments=parse_args(args=args))) 68 | 69 | 70 | def run(arguments, stdout=sys.stdout, stderr=sys.stderr): 71 | error_format = arguments["error_format"] 72 | validator = arguments["validator"](schema=arguments["schema"]) 73 | 74 | validator.check_schema(arguments["schema"]) 75 | 76 | errored = False 77 | for instance in arguments["instances"] or (): 78 | for error in validator.iter_errors(instance): 79 | stderr.write(error_format.format(error=error)) 80 | errored = True 81 | return errored 82 | -------------------------------------------------------------------------------- /lambda/api/jsonschema/compat.py: -------------------------------------------------------------------------------- 1 | import operator 2 | import sys 3 | 4 | 5 | try: 6 | from collections import MutableMapping, Sequence # noqa 7 | except ImportError: 8 | from collections.abc import MutableMapping, Sequence # noqa 9 | 10 | PY3 = sys.version_info[0] >= 3 11 | 12 | if PY3: 13 | zip = zip 14 | from functools import lru_cache 15 | from io import StringIO 16 | from urllib.parse import ( 17 | unquote, urljoin, urlunsplit, SplitResult, urlsplit as _urlsplit 18 | ) 19 | from urllib.request import urlopen 20 | str_types = str, 21 | int_types = int, 22 | iteritems = operator.methodcaller("items") 23 | else: 24 | from itertools import izip as zip # noqa 25 | from StringIO import StringIO 26 | from urlparse import ( 27 | urljoin, urlunsplit, SplitResult, urlsplit as _urlsplit # noqa 28 | ) 29 | from urllib import unquote # noqa 30 | from urllib2 import urlopen # noqa 31 | str_types = basestring 32 | int_types = int, long 33 | iteritems = operator.methodcaller("iteritems") 34 | 35 | from functools32 import lru_cache 36 | 37 | 38 | # On python < 3.3 fragments are not handled properly with unknown schemes 39 | def urlsplit(url): 40 | scheme, netloc, path, query, fragment = _urlsplit(url) 41 | if "#" in path: 42 | path, fragment = path.split("#", 1) 43 | return SplitResult(scheme, netloc, path, query, fragment) 44 | 45 | 46 | def urldefrag(url): 47 | if "#" in url: 48 | s, n, p, q, frag = urlsplit(url) 49 | defrag = urlunsplit((s, n, p, q, '')) 50 | else: 51 | defrag = url 52 | frag = '' 53 | return defrag, frag 54 | 55 | 56 | # flake8: noqa 57 | -------------------------------------------------------------------------------- /lambda/api/jsonschema/exceptions.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict, deque 2 | import itertools 3 | import pprint 4 | import textwrap 5 | 6 | from jsonschema import _utils 7 | from jsonschema.compat import PY3, iteritems 8 | 9 | 10 | WEAK_MATCHES = frozenset(["anyOf", "oneOf"]) 11 | STRONG_MATCHES = frozenset() 12 | 13 | _unset = _utils.Unset() 14 | 15 | 16 | class _Error(Exception): 17 | def __init__( 18 | self, 19 | message, 20 | validator=_unset, 21 | path=(), 22 | cause=None, 23 | context=(), 24 | validator_value=_unset, 25 | instance=_unset, 26 | schema=_unset, 27 | schema_path=(), 28 | parent=None, 29 | ): 30 | super(_Error, self).__init__( 31 | message, 32 | validator, 33 | path, 34 | cause, 35 | context, 36 | validator_value, 37 | instance, 38 | schema, 39 | schema_path, 40 | parent, 41 | ) 42 | self.message = message 43 | self.path = self.relative_path = deque(path) 44 | self.schema_path = self.relative_schema_path = deque(schema_path) 45 | self.context = list(context) 46 | self.cause = self.__cause__ = cause 47 | self.validator = validator 48 | self.validator_value = validator_value 49 | self.instance = instance 50 | self.schema = schema 51 | self.parent = parent 52 | 53 | for error in context: 54 | error.parent = self 55 | 56 | def __repr__(self): 57 | return "<%s: %r>" % (self.__class__.__name__, self.message) 58 | 59 | def __unicode__(self): 60 | essential_for_verbose = ( 61 | self.validator, self.validator_value, self.instance, self.schema, 62 | ) 63 | if any(m is _unset for m in essential_for_verbose): 64 | return self.message 65 | 66 | pschema = pprint.pformat(self.schema, width=72) 67 | pinstance = pprint.pformat(self.instance, width=72) 68 | return self.message + textwrap.dedent(""" 69 | 70 | Failed validating %r in schema%s: 71 | %s 72 | 73 | On instance%s: 74 | %s 75 | """.rstrip() 76 | ) % ( 77 | self.validator, 78 | _utils.format_as_index(list(self.relative_schema_path)[:-1]), 79 | _utils.indent(pschema), 80 | _utils.format_as_index(self.relative_path), 81 | _utils.indent(pinstance), 82 | ) 83 | 84 | if PY3: 85 | __str__ = __unicode__ 86 | else: 87 | def __str__(self): 88 | return unicode(self).encode("utf-8") 89 | 90 | @classmethod 91 | def create_from(cls, other): 92 | return cls(**other._contents()) 93 | 94 | @property 95 | def absolute_path(self): 96 | parent = self.parent 97 | if parent is None: 98 | return self.relative_path 99 | 100 | path = deque(self.relative_path) 101 | path.extendleft(reversed(parent.absolute_path)) 102 | return path 103 | 104 | @property 105 | def absolute_schema_path(self): 106 | parent = self.parent 107 | if parent is None: 108 | return self.relative_schema_path 109 | 110 | path = deque(self.relative_schema_path) 111 | path.extendleft(reversed(parent.absolute_schema_path)) 112 | return path 113 | 114 | def _set(self, **kwargs): 115 | for k, v in iteritems(kwargs): 116 | if getattr(self, k) is _unset: 117 | setattr(self, k, v) 118 | 119 | def _contents(self): 120 | attrs = ( 121 | "message", "cause", "context", "validator", "validator_value", 122 | "path", "schema_path", "instance", "schema", "parent", 123 | ) 124 | return dict((attr, getattr(self, attr)) for attr in attrs) 125 | 126 | 127 | class ValidationError(_Error): 128 | pass 129 | 130 | 131 | class SchemaError(_Error): 132 | pass 133 | 134 | 135 | class RefResolutionError(Exception): 136 | pass 137 | 138 | 139 | class UnknownType(Exception): 140 | def __init__(self, type, instance, schema): 141 | self.type = type 142 | self.instance = instance 143 | self.schema = schema 144 | 145 | def __unicode__(self): 146 | pschema = pprint.pformat(self.schema, width=72) 147 | pinstance = pprint.pformat(self.instance, width=72) 148 | return textwrap.dedent(""" 149 | Unknown type %r for validator with schema: 150 | %s 151 | 152 | While checking instance: 153 | %s 154 | """.rstrip() 155 | ) % (self.type, _utils.indent(pschema), _utils.indent(pinstance)) 156 | 157 | if PY3: 158 | __str__ = __unicode__ 159 | else: 160 | def __str__(self): 161 | return unicode(self).encode("utf-8") 162 | 163 | 164 | class FormatError(Exception): 165 | def __init__(self, message, cause=None): 166 | super(FormatError, self).__init__(message, cause) 167 | self.message = message 168 | self.cause = self.__cause__ = cause 169 | 170 | def __unicode__(self): 171 | return self.message 172 | 173 | if PY3: 174 | __str__ = __unicode__ 175 | else: 176 | def __str__(self): 177 | return self.message.encode("utf-8") 178 | 179 | 180 | class ErrorTree(object): 181 | """ 182 | ErrorTrees make it easier to check which validations failed. 183 | 184 | """ 185 | 186 | _instance = _unset 187 | 188 | def __init__(self, errors=()): 189 | self.errors = {} 190 | self._contents = defaultdict(self.__class__) 191 | 192 | for error in errors: 193 | container = self 194 | for element in error.path: 195 | container = container[element] 196 | container.errors[error.validator] = error 197 | 198 | container._instance = error.instance 199 | 200 | def __contains__(self, index): 201 | """ 202 | Check whether ``instance[index]`` has any errors. 203 | 204 | """ 205 | 206 | return index in self._contents 207 | 208 | def __getitem__(self, index): 209 | """ 210 | Retrieve the child tree one level down at the given ``index``. 211 | 212 | If the index is not in the instance that this tree corresponds to and 213 | is not known by this tree, whatever error would be raised by 214 | ``instance.__getitem__`` will be propagated (usually this is some 215 | subclass of :class:`LookupError`. 216 | 217 | """ 218 | 219 | if self._instance is not _unset and index not in self: 220 | self._instance[index] 221 | return self._contents[index] 222 | 223 | def __setitem__(self, index, value): 224 | self._contents[index] = value 225 | 226 | def __iter__(self): 227 | """ 228 | Iterate (non-recursively) over the indices in the instance with errors. 229 | 230 | """ 231 | 232 | return iter(self._contents) 233 | 234 | def __len__(self): 235 | """ 236 | Same as :attr:`total_errors`. 237 | 238 | """ 239 | 240 | return self.total_errors 241 | 242 | def __repr__(self): 243 | return "<%s (%s total errors)>" % (self.__class__.__name__, len(self)) 244 | 245 | @property 246 | def total_errors(self): 247 | """ 248 | The total number of errors in the entire tree, including children. 249 | 250 | """ 251 | 252 | child_errors = sum(len(tree) for _, tree in iteritems(self._contents)) 253 | return len(self.errors) + child_errors 254 | 255 | 256 | def by_relevance(weak=WEAK_MATCHES, strong=STRONG_MATCHES): 257 | def relevance(error): 258 | validator = error.validator 259 | return -len(error.path), validator not in weak, validator in strong 260 | return relevance 261 | 262 | 263 | relevance = by_relevance() 264 | 265 | 266 | def best_match(errors, key=relevance): 267 | errors = iter(errors) 268 | best = next(errors, None) 269 | if best is None: 270 | return 271 | best = max(itertools.chain([best], errors), key=key) 272 | 273 | while best.context: 274 | best = min(best.context, key=key) 275 | return best 276 | -------------------------------------------------------------------------------- /lambda/api/jsonschema/schemas/draft3.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-03/schema#", 3 | "dependencies": { 4 | "exclusiveMaximum": "maximum", 5 | "exclusiveMinimum": "minimum" 6 | }, 7 | "id": "http://json-schema.org/draft-03/schema#", 8 | "properties": { 9 | "$ref": { 10 | "format": "uri", 11 | "type": "string" 12 | }, 13 | "$schema": { 14 | "format": "uri", 15 | "type": "string" 16 | }, 17 | "additionalItems": { 18 | "default": {}, 19 | "type": [ 20 | { 21 | "$ref": "#" 22 | }, 23 | "boolean" 24 | ] 25 | }, 26 | "additionalProperties": { 27 | "default": {}, 28 | "type": [ 29 | { 30 | "$ref": "#" 31 | }, 32 | "boolean" 33 | ] 34 | }, 35 | "default": { 36 | "type": "any" 37 | }, 38 | "dependencies": { 39 | "additionalProperties": { 40 | "items": { 41 | "type": "string" 42 | }, 43 | "type": [ 44 | "string", 45 | "array", 46 | { 47 | "$ref": "#" 48 | } 49 | ] 50 | }, 51 | "default": {}, 52 | "type": [ 53 | "string", 54 | "array", 55 | "object" 56 | ] 57 | }, 58 | "description": { 59 | "type": "string" 60 | }, 61 | "disallow": { 62 | "items": { 63 | "type": [ 64 | "string", 65 | { 66 | "$ref": "#" 67 | } 68 | ] 69 | }, 70 | "type": [ 71 | "string", 72 | "array" 73 | ], 74 | "uniqueItems": true 75 | }, 76 | "divisibleBy": { 77 | "default": 1, 78 | "exclusiveMinimum": true, 79 | "minimum": 0, 80 | "type": "number" 81 | }, 82 | "enum": { 83 | "minItems": 1, 84 | "type": "array", 85 | "uniqueItems": true 86 | }, 87 | "exclusiveMaximum": { 88 | "default": false, 89 | "type": "boolean" 90 | }, 91 | "exclusiveMinimum": { 92 | "default": false, 93 | "type": "boolean" 94 | }, 95 | "extends": { 96 | "default": {}, 97 | "items": { 98 | "$ref": "#" 99 | }, 100 | "type": [ 101 | { 102 | "$ref": "#" 103 | }, 104 | "array" 105 | ] 106 | }, 107 | "format": { 108 | "type": "string" 109 | }, 110 | "id": { 111 | "format": "uri", 112 | "type": "string" 113 | }, 114 | "items": { 115 | "default": {}, 116 | "items": { 117 | "$ref": "#" 118 | }, 119 | "type": [ 120 | { 121 | "$ref": "#" 122 | }, 123 | "array" 124 | ] 125 | }, 126 | "maxDecimal": { 127 | "minimum": 0, 128 | "type": "number" 129 | }, 130 | "maxItems": { 131 | "minimum": 0, 132 | "type": "integer" 133 | }, 134 | "maxLength": { 135 | "type": "integer" 136 | }, 137 | "maximum": { 138 | "type": "number" 139 | }, 140 | "minItems": { 141 | "default": 0, 142 | "minimum": 0, 143 | "type": "integer" 144 | }, 145 | "minLength": { 146 | "default": 0, 147 | "minimum": 0, 148 | "type": "integer" 149 | }, 150 | "minimum": { 151 | "type": "number" 152 | }, 153 | "pattern": { 154 | "format": "regex", 155 | "type": "string" 156 | }, 157 | "patternProperties": { 158 | "additionalProperties": { 159 | "$ref": "#" 160 | }, 161 | "default": {}, 162 | "type": "object" 163 | }, 164 | "properties": { 165 | "additionalProperties": { 166 | "$ref": "#", 167 | "type": "object" 168 | }, 169 | "default": {}, 170 | "type": "object" 171 | }, 172 | "required": { 173 | "default": false, 174 | "type": "boolean" 175 | }, 176 | "title": { 177 | "type": "string" 178 | }, 179 | "type": { 180 | "default": "any", 181 | "items": { 182 | "type": [ 183 | "string", 184 | { 185 | "$ref": "#" 186 | } 187 | ] 188 | }, 189 | "type": [ 190 | "string", 191 | "array" 192 | ], 193 | "uniqueItems": true 194 | }, 195 | "uniqueItems": { 196 | "default": false, 197 | "type": "boolean" 198 | } 199 | }, 200 | "type": "object" 201 | } 202 | -------------------------------------------------------------------------------- /lambda/api/jsonschema/schemas/draft4.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "default": {}, 4 | "definitions": { 5 | "positiveInteger": { 6 | "minimum": 0, 7 | "type": "integer" 8 | }, 9 | "positiveIntegerDefault0": { 10 | "allOf": [ 11 | { 12 | "$ref": "#/definitions/positiveInteger" 13 | }, 14 | { 15 | "default": 0 16 | } 17 | ] 18 | }, 19 | "schemaArray": { 20 | "items": { 21 | "$ref": "#" 22 | }, 23 | "minItems": 1, 24 | "type": "array" 25 | }, 26 | "simpleTypes": { 27 | "enum": [ 28 | "array", 29 | "boolean", 30 | "integer", 31 | "null", 32 | "number", 33 | "object", 34 | "string" 35 | ] 36 | }, 37 | "stringArray": { 38 | "items": { 39 | "type": "string" 40 | }, 41 | "minItems": 1, 42 | "type": "array", 43 | "uniqueItems": true 44 | } 45 | }, 46 | "dependencies": { 47 | "exclusiveMaximum": [ 48 | "maximum" 49 | ], 50 | "exclusiveMinimum": [ 51 | "minimum" 52 | ] 53 | }, 54 | "description": "Core schema meta-schema", 55 | "id": "http://json-schema.org/draft-04/schema#", 56 | "properties": { 57 | "$schema": { 58 | "format": "uri", 59 | "type": "string" 60 | }, 61 | "additionalItems": { 62 | "anyOf": [ 63 | { 64 | "type": "boolean" 65 | }, 66 | { 67 | "$ref": "#" 68 | } 69 | ], 70 | "default": {} 71 | }, 72 | "additionalProperties": { 73 | "anyOf": [ 74 | { 75 | "type": "boolean" 76 | }, 77 | { 78 | "$ref": "#" 79 | } 80 | ], 81 | "default": {} 82 | }, 83 | "allOf": { 84 | "$ref": "#/definitions/schemaArray" 85 | }, 86 | "anyOf": { 87 | "$ref": "#/definitions/schemaArray" 88 | }, 89 | "default": {}, 90 | "definitions": { 91 | "additionalProperties": { 92 | "$ref": "#" 93 | }, 94 | "default": {}, 95 | "type": "object" 96 | }, 97 | "dependencies": { 98 | "additionalProperties": { 99 | "anyOf": [ 100 | { 101 | "$ref": "#" 102 | }, 103 | { 104 | "$ref": "#/definitions/stringArray" 105 | } 106 | ] 107 | }, 108 | "type": "object" 109 | }, 110 | "description": { 111 | "type": "string" 112 | }, 113 | "enum": { 114 | "minItems": 1, 115 | "type": "array", 116 | "uniqueItems": true 117 | }, 118 | "exclusiveMaximum": { 119 | "default": false, 120 | "type": "boolean" 121 | }, 122 | "exclusiveMinimum": { 123 | "default": false, 124 | "type": "boolean" 125 | }, 126 | "format": { 127 | "type": "string" 128 | }, 129 | "id": { 130 | "format": "uri", 131 | "type": "string" 132 | }, 133 | "items": { 134 | "anyOf": [ 135 | { 136 | "$ref": "#" 137 | }, 138 | { 139 | "$ref": "#/definitions/schemaArray" 140 | } 141 | ], 142 | "default": {} 143 | }, 144 | "maxItems": { 145 | "$ref": "#/definitions/positiveInteger" 146 | }, 147 | "maxLength": { 148 | "$ref": "#/definitions/positiveInteger" 149 | }, 150 | "maxProperties": { 151 | "$ref": "#/definitions/positiveInteger" 152 | }, 153 | "maximum": { 154 | "type": "number" 155 | }, 156 | "minItems": { 157 | "$ref": "#/definitions/positiveIntegerDefault0" 158 | }, 159 | "minLength": { 160 | "$ref": "#/definitions/positiveIntegerDefault0" 161 | }, 162 | "minProperties": { 163 | "$ref": "#/definitions/positiveIntegerDefault0" 164 | }, 165 | "minimum": { 166 | "type": "number" 167 | }, 168 | "multipleOf": { 169 | "exclusiveMinimum": true, 170 | "minimum": 0, 171 | "type": "number" 172 | }, 173 | "not": { 174 | "$ref": "#" 175 | }, 176 | "oneOf": { 177 | "$ref": "#/definitions/schemaArray" 178 | }, 179 | "pattern": { 180 | "format": "regex", 181 | "type": "string" 182 | }, 183 | "patternProperties": { 184 | "additionalProperties": { 185 | "$ref": "#" 186 | }, 187 | "default": {}, 188 | "type": "object" 189 | }, 190 | "properties": { 191 | "additionalProperties": { 192 | "$ref": "#" 193 | }, 194 | "default": {}, 195 | "type": "object" 196 | }, 197 | "required": { 198 | "$ref": "#/definitions/stringArray" 199 | }, 200 | "title": { 201 | "type": "string" 202 | }, 203 | "type": { 204 | "anyOf": [ 205 | { 206 | "$ref": "#/definitions/simpleTypes" 207 | }, 208 | { 209 | "items": { 210 | "$ref": "#/definitions/simpleTypes" 211 | }, 212 | "minItems": 1, 213 | "type": "array", 214 | "uniqueItems": true 215 | } 216 | ] 217 | }, 218 | "uniqueItems": { 219 | "default": false, 220 | "type": "boolean" 221 | } 222 | }, 223 | "type": "object" 224 | } 225 | -------------------------------------------------------------------------------- /lambda/api/jsonschema/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-python-smarthome-sandbox/8bc88c53a2e0117092b44bdbb319a1e6709c547d/lambda/api/jsonschema/tests/__init__.py -------------------------------------------------------------------------------- /lambda/api/jsonschema/tests/compat.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | 4 | if sys.version_info[:2] < (2, 7): # pragma: no cover 5 | import unittest2 as unittest 6 | else: 7 | import unittest 8 | 9 | try: 10 | from unittest import mock 11 | except ImportError: 12 | import mock 13 | 14 | 15 | # flake8: noqa 16 | -------------------------------------------------------------------------------- /lambda/api/jsonschema/tests/test_cli.py: -------------------------------------------------------------------------------- 1 | from jsonschema import Draft4Validator, ValidationError, cli 2 | from jsonschema.compat import StringIO 3 | from jsonschema.exceptions import SchemaError 4 | from jsonschema.tests.compat import mock, unittest 5 | 6 | 7 | def fake_validator(*errors): 8 | errors = list(reversed(errors)) 9 | 10 | class FakeValidator(object): 11 | def __init__(self, *args, **kwargs): 12 | pass 13 | 14 | def iter_errors(self, instance): 15 | if errors: 16 | return errors.pop() 17 | return [] 18 | 19 | def check_schema(self, schema): 20 | pass 21 | 22 | return FakeValidator 23 | 24 | 25 | class TestParser(unittest.TestCase): 26 | FakeValidator = fake_validator() 27 | 28 | def setUp(self): 29 | mock_open = mock.mock_open() 30 | patch_open = mock.patch.object(cli, "open", mock_open, create=True) 31 | patch_open.start() 32 | self.addCleanup(patch_open.stop) 33 | 34 | mock_json_load = mock.Mock() 35 | mock_json_load.return_value = {} 36 | patch_json_load = mock.patch("json.load") 37 | patch_json_load.start() 38 | self.addCleanup(patch_json_load.stop) 39 | 40 | def test_find_validator_by_fully_qualified_object_name(self): 41 | arguments = cli.parse_args( 42 | [ 43 | "--validator", 44 | "jsonschema.tests.test_cli.TestParser.FakeValidator", 45 | "--instance", "foo.json", 46 | "schema.json", 47 | ] 48 | ) 49 | self.assertIs(arguments["validator"], self.FakeValidator) 50 | 51 | def test_find_validator_in_jsonschema(self): 52 | arguments = cli.parse_args( 53 | [ 54 | "--validator", "Draft4Validator", 55 | "--instance", "foo.json", 56 | "schema.json", 57 | ] 58 | ) 59 | self.assertIs(arguments["validator"], Draft4Validator) 60 | 61 | 62 | class TestCLI(unittest.TestCase): 63 | def test_draft3_schema_draft4_validator(self): 64 | stdout, stderr = StringIO(), StringIO() 65 | with self.assertRaises(SchemaError): 66 | cli.run( 67 | { 68 | "validator": Draft4Validator, 69 | "schema": { 70 | "anyOf": [ 71 | {"minimum": 20}, 72 | {"type": "string"}, 73 | {"required": True}, 74 | ], 75 | }, 76 | "instances": [1], 77 | "error_format": "{error.message}", 78 | }, 79 | stdout=stdout, 80 | stderr=stderr, 81 | ) 82 | 83 | def test_successful_validation(self): 84 | stdout, stderr = StringIO(), StringIO() 85 | exit_code = cli.run( 86 | { 87 | "validator": fake_validator(), 88 | "schema": {}, 89 | "instances": [1], 90 | "error_format": "{error.message}", 91 | }, 92 | stdout=stdout, 93 | stderr=stderr, 94 | ) 95 | self.assertFalse(stdout.getvalue()) 96 | self.assertFalse(stderr.getvalue()) 97 | self.assertEqual(exit_code, 0) 98 | 99 | def test_unsuccessful_validation(self): 100 | error = ValidationError("I am an error!", instance=1) 101 | stdout, stderr = StringIO(), StringIO() 102 | exit_code = cli.run( 103 | { 104 | "validator": fake_validator([error]), 105 | "schema": {}, 106 | "instances": [1], 107 | "error_format": "{error.instance} - {error.message}", 108 | }, 109 | stdout=stdout, 110 | stderr=stderr, 111 | ) 112 | self.assertFalse(stdout.getvalue()) 113 | self.assertEqual(stderr.getvalue(), "1 - I am an error!") 114 | self.assertEqual(exit_code, 1) 115 | 116 | def test_unsuccessful_validation_multiple_instances(self): 117 | first_errors = [ 118 | ValidationError("9", instance=1), 119 | ValidationError("8", instance=1), 120 | ] 121 | second_errors = [ValidationError("7", instance=2)] 122 | stdout, stderr = StringIO(), StringIO() 123 | exit_code = cli.run( 124 | { 125 | "validator": fake_validator(first_errors, second_errors), 126 | "schema": {}, 127 | "instances": [1, 2], 128 | "error_format": "{error.instance} - {error.message}\t", 129 | }, 130 | stdout=stdout, 131 | stderr=stderr, 132 | ) 133 | self.assertFalse(stdout.getvalue()) 134 | self.assertEqual(stderr.getvalue(), "1 - 9\t1 - 8\t2 - 7\t") 135 | self.assertEqual(exit_code, 1) 136 | -------------------------------------------------------------------------------- /lambda/api/jsonschema/tests/test_exceptions.py: -------------------------------------------------------------------------------- 1 | import textwrap 2 | 3 | from jsonschema import Draft4Validator, exceptions 4 | from jsonschema.compat import PY3 5 | from jsonschema.tests.compat import mock, unittest 6 | 7 | 8 | class TestBestMatch(unittest.TestCase): 9 | def best_match(self, errors): 10 | errors = list(errors) 11 | best = exceptions.best_match(errors) 12 | reversed_best = exceptions.best_match(reversed(errors)) 13 | msg = "Didn't return a consistent best match!\nGot: {0}\n\nThen: {1}" 14 | self.assertEqual( 15 | best, reversed_best, msg=msg.format(best, reversed_best), 16 | ) 17 | return best 18 | 19 | def test_shallower_errors_are_better_matches(self): 20 | validator = Draft4Validator( 21 | { 22 | "properties": { 23 | "foo": { 24 | "minProperties": 2, 25 | "properties": {"bar": {"type": "object"}}, 26 | }, 27 | }, 28 | }, 29 | ) 30 | best = self.best_match(validator.iter_errors({"foo": {"bar": []}})) 31 | self.assertEqual(best.validator, "minProperties") 32 | 33 | def test_oneOf_and_anyOf_are_weak_matches(self): 34 | """ 35 | A property you *must* match is probably better than one you have to 36 | match a part of. 37 | 38 | """ 39 | 40 | validator = Draft4Validator( 41 | { 42 | "minProperties": 2, 43 | "anyOf": [{"type": "string"}, {"type": "number"}], 44 | "oneOf": [{"type": "string"}, {"type": "number"}], 45 | } 46 | ) 47 | best = self.best_match(validator.iter_errors({})) 48 | self.assertEqual(best.validator, "minProperties") 49 | 50 | def test_if_the_most_relevant_error_is_anyOf_it_is_traversed(self): 51 | """ 52 | If the most relevant error is an anyOf, then we traverse its context 53 | and select the otherwise *least* relevant error, since in this case 54 | that means the most specific, deep, error inside the instance. 55 | 56 | I.e. since only one of the schemas must match, we look for the most 57 | relevant one. 58 | 59 | """ 60 | 61 | validator = Draft4Validator( 62 | { 63 | "properties": { 64 | "foo": { 65 | "anyOf": [ 66 | {"type": "string"}, 67 | {"properties": {"bar": {"type": "array"}}}, 68 | ], 69 | }, 70 | }, 71 | }, 72 | ) 73 | best = self.best_match(validator.iter_errors({"foo": {"bar": 12}})) 74 | self.assertEqual(best.validator_value, "array") 75 | 76 | def test_if_the_most_relevant_error_is_oneOf_it_is_traversed(self): 77 | """ 78 | If the most relevant error is an oneOf, then we traverse its context 79 | and select the otherwise *least* relevant error, since in this case 80 | that means the most specific, deep, error inside the instance. 81 | 82 | I.e. since only one of the schemas must match, we look for the most 83 | relevant one. 84 | 85 | """ 86 | 87 | validator = Draft4Validator( 88 | { 89 | "properties": { 90 | "foo": { 91 | "oneOf": [ 92 | {"type": "string"}, 93 | {"properties": {"bar": {"type": "array"}}}, 94 | ], 95 | }, 96 | }, 97 | }, 98 | ) 99 | best = self.best_match(validator.iter_errors({"foo": {"bar": 12}})) 100 | self.assertEqual(best.validator_value, "array") 101 | 102 | def test_if_the_most_relevant_error_is_allOf_it_is_traversed(self): 103 | """ 104 | Now, if the error is allOf, we traverse but select the *most* relevant 105 | error from the context, because all schemas here must match anyways. 106 | 107 | """ 108 | 109 | validator = Draft4Validator( 110 | { 111 | "properties": { 112 | "foo": { 113 | "allOf": [ 114 | {"type": "string"}, 115 | {"properties": {"bar": {"type": "array"}}}, 116 | ], 117 | }, 118 | }, 119 | }, 120 | ) 121 | best = self.best_match(validator.iter_errors({"foo": {"bar": 12}})) 122 | self.assertEqual(best.validator_value, "string") 123 | 124 | def test_nested_context_for_oneOf(self): 125 | validator = Draft4Validator( 126 | { 127 | "properties": { 128 | "foo": { 129 | "oneOf": [ 130 | {"type": "string"}, 131 | { 132 | "oneOf": [ 133 | {"type": "string"}, 134 | { 135 | "properties": { 136 | "bar": {"type": "array"}, 137 | }, 138 | }, 139 | ], 140 | }, 141 | ], 142 | }, 143 | }, 144 | }, 145 | ) 146 | best = self.best_match(validator.iter_errors({"foo": {"bar": 12}})) 147 | self.assertEqual(best.validator_value, "array") 148 | 149 | def test_one_error(self): 150 | validator = Draft4Validator({"minProperties": 2}) 151 | error, = validator.iter_errors({}) 152 | self.assertEqual( 153 | exceptions.best_match(validator.iter_errors({})).validator, 154 | "minProperties", 155 | ) 156 | 157 | def test_no_errors(self): 158 | validator = Draft4Validator({}) 159 | self.assertIsNone(exceptions.best_match(validator.iter_errors({}))) 160 | 161 | 162 | class TestByRelevance(unittest.TestCase): 163 | def test_short_paths_are_better_matches(self): 164 | shallow = exceptions.ValidationError("Oh no!", path=["baz"]) 165 | deep = exceptions.ValidationError("Oh yes!", path=["foo", "bar"]) 166 | match = max([shallow, deep], key=exceptions.relevance) 167 | self.assertIs(match, shallow) 168 | 169 | match = max([deep, shallow], key=exceptions.relevance) 170 | self.assertIs(match, shallow) 171 | 172 | def test_global_errors_are_even_better_matches(self): 173 | shallow = exceptions.ValidationError("Oh no!", path=[]) 174 | deep = exceptions.ValidationError("Oh yes!", path=["foo"]) 175 | 176 | errors = sorted([shallow, deep], key=exceptions.relevance) 177 | self.assertEqual( 178 | [list(error.path) for error in errors], 179 | [["foo"], []], 180 | ) 181 | 182 | errors = sorted([deep, shallow], key=exceptions.relevance) 183 | self.assertEqual( 184 | [list(error.path) for error in errors], 185 | [["foo"], []], 186 | ) 187 | 188 | def test_weak_validators_are_lower_priority(self): 189 | weak = exceptions.ValidationError("Oh no!", path=[], validator="a") 190 | normal = exceptions.ValidationError("Oh yes!", path=[], validator="b") 191 | 192 | best_match = exceptions.by_relevance(weak="a") 193 | 194 | match = max([weak, normal], key=best_match) 195 | self.assertIs(match, normal) 196 | 197 | match = max([normal, weak], key=best_match) 198 | self.assertIs(match, normal) 199 | 200 | def test_strong_validators_are_higher_priority(self): 201 | weak = exceptions.ValidationError("Oh no!", path=[], validator="a") 202 | normal = exceptions.ValidationError("Oh yes!", path=[], validator="b") 203 | strong = exceptions.ValidationError("Oh fine!", path=[], validator="c") 204 | 205 | best_match = exceptions.by_relevance(weak="a", strong="c") 206 | 207 | match = max([weak, normal, strong], key=best_match) 208 | self.assertIs(match, strong) 209 | 210 | match = max([strong, normal, weak], key=best_match) 211 | self.assertIs(match, strong) 212 | 213 | 214 | class TestErrorTree(unittest.TestCase): 215 | def test_it_knows_how_many_total_errors_it_contains(self): 216 | errors = [mock.MagicMock() for _ in range(8)] 217 | tree = exceptions.ErrorTree(errors) 218 | self.assertEqual(tree.total_errors, 8) 219 | 220 | def test_it_contains_an_item_if_the_item_had_an_error(self): 221 | errors = [exceptions.ValidationError("a message", path=["bar"])] 222 | tree = exceptions.ErrorTree(errors) 223 | self.assertIn("bar", tree) 224 | 225 | def test_it_does_not_contain_an_item_if_the_item_had_no_error(self): 226 | errors = [exceptions.ValidationError("a message", path=["bar"])] 227 | tree = exceptions.ErrorTree(errors) 228 | self.assertNotIn("foo", tree) 229 | 230 | def test_validators_that_failed_appear_in_errors_dict(self): 231 | error = exceptions.ValidationError("a message", validator="foo") 232 | tree = exceptions.ErrorTree([error]) 233 | self.assertEqual(tree.errors, {"foo": error}) 234 | 235 | def test_it_creates_a_child_tree_for_each_nested_path(self): 236 | errors = [ 237 | exceptions.ValidationError("a bar message", path=["bar"]), 238 | exceptions.ValidationError("a bar -> 0 message", path=["bar", 0]), 239 | ] 240 | tree = exceptions.ErrorTree(errors) 241 | self.assertIn(0, tree["bar"]) 242 | self.assertNotIn(1, tree["bar"]) 243 | 244 | def test_children_have_their_errors_dicts_built(self): 245 | e1, e2 = ( 246 | exceptions.ValidationError("1", validator="foo", path=["bar", 0]), 247 | exceptions.ValidationError("2", validator="quux", path=["bar", 0]), 248 | ) 249 | tree = exceptions.ErrorTree([e1, e2]) 250 | self.assertEqual(tree["bar"][0].errors, {"foo": e1, "quux": e2}) 251 | 252 | def test_regression_multiple_errors_with_instance(self): 253 | e1, e2 = ( 254 | exceptions.ValidationError( 255 | "1", 256 | validator="foo", 257 | path=["bar", "bar2"], 258 | instance="i1"), 259 | exceptions.ValidationError( 260 | "2", 261 | validator="quux", 262 | path=["foobar", 2], 263 | instance="i2"), 264 | ) 265 | # Will raise an exception if the bug is still there. 266 | exceptions.ErrorTree([e1, e2]) 267 | 268 | def test_it_does_not_contain_subtrees_that_are_not_in_the_instance(self): 269 | error = exceptions.ValidationError("123", validator="foo", instance=[]) 270 | tree = exceptions.ErrorTree([error]) 271 | 272 | with self.assertRaises(IndexError): 273 | tree[0] 274 | 275 | def test_if_its_in_the_tree_anyhow_it_does_not_raise_an_error(self): 276 | """ 277 | If a validator is dumb (like :validator:`required` in draft 3) and 278 | refers to a path that isn't in the instance, the tree still properly 279 | returns a subtree for that path. 280 | 281 | """ 282 | 283 | error = exceptions.ValidationError( 284 | "a message", validator="foo", instance={}, path=["foo"], 285 | ) 286 | tree = exceptions.ErrorTree([error]) 287 | self.assertIsInstance(tree["foo"], exceptions.ErrorTree) 288 | 289 | 290 | class TestErrorInitReprStr(unittest.TestCase): 291 | def make_error(self, **kwargs): 292 | defaults = dict( 293 | message=u"hello", 294 | validator=u"type", 295 | validator_value=u"string", 296 | instance=5, 297 | schema={u"type": u"string"}, 298 | ) 299 | defaults.update(kwargs) 300 | return exceptions.ValidationError(**defaults) 301 | 302 | def assertShows(self, expected, **kwargs): 303 | if PY3: 304 | expected = expected.replace("u'", "'") 305 | expected = textwrap.dedent(expected).rstrip("\n") 306 | 307 | error = self.make_error(**kwargs) 308 | message_line, _, rest = str(error).partition("\n") 309 | self.assertEqual(message_line, error.message) 310 | self.assertEqual(rest, expected) 311 | 312 | def test_it_calls_super_and_sets_args(self): 313 | error = self.make_error() 314 | self.assertGreater(len(error.args), 1) 315 | 316 | def test_repr(self): 317 | self.assertEqual( 318 | repr(exceptions.ValidationError(message="Hello!")), 319 | "" % "Hello!", 320 | ) 321 | 322 | def test_unset_error(self): 323 | error = exceptions.ValidationError("message") 324 | self.assertEqual(str(error), "message") 325 | 326 | kwargs = { 327 | "validator": "type", 328 | "validator_value": "string", 329 | "instance": 5, 330 | "schema": {"type": "string"}, 331 | } 332 | # Just the message should show if any of the attributes are unset 333 | for attr in kwargs: 334 | k = dict(kwargs) 335 | del k[attr] 336 | error = exceptions.ValidationError("message", **k) 337 | self.assertEqual(str(error), "message") 338 | 339 | def test_empty_paths(self): 340 | self.assertShows( 341 | """ 342 | Failed validating u'type' in schema: 343 | {u'type': u'string'} 344 | 345 | On instance: 346 | 5 347 | """, 348 | path=[], 349 | schema_path=[], 350 | ) 351 | 352 | def test_one_item_paths(self): 353 | self.assertShows( 354 | """ 355 | Failed validating u'type' in schema: 356 | {u'type': u'string'} 357 | 358 | On instance[0]: 359 | 5 360 | """, 361 | path=[0], 362 | schema_path=["items"], 363 | ) 364 | 365 | def test_multiple_item_paths(self): 366 | self.assertShows( 367 | """ 368 | Failed validating u'type' in schema[u'items'][0]: 369 | {u'type': u'string'} 370 | 371 | On instance[0][u'a']: 372 | 5 373 | """, 374 | path=[0, u"a"], 375 | schema_path=[u"items", 0, 1], 376 | ) 377 | 378 | def test_uses_pprint(self): 379 | with mock.patch("pprint.pformat") as pformat: 380 | str(self.make_error()) 381 | self.assertEqual(pformat.call_count, 2) # schema + instance 382 | 383 | def test_str_works_with_instances_having_overriden_eq_operator(self): 384 | """ 385 | Check for https://github.com/Julian/jsonschema/issues/164 which 386 | rendered exceptions unusable when a `ValidationError` involved 387 | instances with an `__eq__` method that returned truthy values. 388 | 389 | """ 390 | 391 | instance = mock.MagicMock() 392 | error = exceptions.ValidationError( 393 | "a message", 394 | validator="foo", 395 | instance=instance, 396 | validator_value="some", 397 | schema="schema", 398 | ) 399 | str(error) 400 | self.assertFalse(instance.__eq__.called) 401 | -------------------------------------------------------------------------------- /lambda/api/jsonschema/tests/test_format.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for the parts of jsonschema related to the :validator:`format` property. 3 | 4 | """ 5 | 6 | from jsonschema.tests.compat import mock, unittest 7 | 8 | from jsonschema import FormatError, ValidationError, FormatChecker 9 | from jsonschema.validators import Draft4Validator 10 | 11 | 12 | class TestFormatChecker(unittest.TestCase): 13 | def setUp(self): 14 | self.fn = mock.Mock() 15 | 16 | def test_it_can_validate_no_formats(self): 17 | checker = FormatChecker(formats=()) 18 | self.assertFalse(checker.checkers) 19 | 20 | def test_it_raises_a_key_error_for_unknown_formats(self): 21 | with self.assertRaises(KeyError): 22 | FormatChecker(formats=["o noes"]) 23 | 24 | def test_it_can_register_cls_checkers(self): 25 | with mock.patch.dict(FormatChecker.checkers, clear=True): 26 | FormatChecker.cls_checks("new")(self.fn) 27 | self.assertEqual(FormatChecker.checkers, {"new": (self.fn, ())}) 28 | 29 | def test_it_can_register_checkers(self): 30 | checker = FormatChecker() 31 | checker.checks("new")(self.fn) 32 | self.assertEqual( 33 | checker.checkers, 34 | dict(FormatChecker.checkers, new=(self.fn, ())) 35 | ) 36 | 37 | def test_it_catches_registered_errors(self): 38 | checker = FormatChecker() 39 | cause = self.fn.side_effect = ValueError() 40 | 41 | checker.checks("foo", raises=ValueError)(self.fn) 42 | 43 | with self.assertRaises(FormatError) as cm: 44 | checker.check("bar", "foo") 45 | 46 | self.assertIs(cm.exception.cause, cause) 47 | self.assertIs(cm.exception.__cause__, cause) 48 | 49 | # Unregistered errors should not be caught 50 | self.fn.side_effect = AttributeError 51 | with self.assertRaises(AttributeError): 52 | checker.check("bar", "foo") 53 | 54 | def test_format_error_causes_become_validation_error_causes(self): 55 | checker = FormatChecker() 56 | checker.checks("foo", raises=ValueError)(self.fn) 57 | cause = self.fn.side_effect = ValueError() 58 | validator = Draft4Validator({"format": "foo"}, format_checker=checker) 59 | 60 | with self.assertRaises(ValidationError) as cm: 61 | validator.validate("bar") 62 | 63 | self.assertIs(cm.exception.__cause__, cause) 64 | -------------------------------------------------------------------------------- /lambda/api/jsonschema/tests/test_jsonschema_test_suite.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test runner for the JSON Schema official test suite 3 | 4 | Tests comprehensive correctness of each draft's validator. 5 | 6 | See https://github.com/json-schema/JSON-Schema-Test-Suite for details. 7 | 8 | """ 9 | 10 | from contextlib import closing 11 | from decimal import Decimal 12 | import glob 13 | import json 14 | import io 15 | import itertools 16 | import os 17 | import re 18 | import subprocess 19 | import sys 20 | 21 | try: 22 | from sys import pypy_version_info 23 | except ImportError: 24 | pypy_version_info = None 25 | 26 | from jsonschema import ( 27 | FormatError, SchemaError, ValidationError, Draft3Validator, 28 | Draft4Validator, FormatChecker, draft3_format_checker, 29 | draft4_format_checker, validate, 30 | ) 31 | from jsonschema.compat import PY3 32 | from jsonschema.tests.compat import mock, unittest 33 | import jsonschema 34 | 35 | 36 | REPO_ROOT = os.path.join(os.path.dirname(jsonschema.__file__), os.path.pardir) 37 | SUITE = os.getenv("JSON_SCHEMA_TEST_SUITE", os.path.join(REPO_ROOT, "json")) 38 | 39 | if not os.path.isdir(SUITE): 40 | raise ValueError( 41 | "Can't find the JSON-Schema-Test-Suite directory. Set the " 42 | "'JSON_SCHEMA_TEST_SUITE' environment variable or run the tests from " 43 | "alongside a checkout of the suite." 44 | ) 45 | 46 | TESTS_DIR = os.path.join(SUITE, "tests") 47 | JSONSCHEMA_SUITE = os.path.join(SUITE, "bin", "jsonschema_suite") 48 | 49 | remotes_stdout = subprocess.Popen( 50 | ["python", JSONSCHEMA_SUITE, "remotes"], stdout=subprocess.PIPE, 51 | ).stdout 52 | 53 | with closing(remotes_stdout): 54 | if PY3: 55 | remotes_stdout = io.TextIOWrapper(remotes_stdout) 56 | REMOTES = json.load(remotes_stdout) 57 | 58 | 59 | def make_case(schema, data, valid, name): 60 | if valid: 61 | def test_case(self): 62 | kwargs = getattr(self, "validator_kwargs", {}) 63 | validate(data, schema, cls=self.validator_class, **kwargs) 64 | else: 65 | def test_case(self): 66 | kwargs = getattr(self, "validator_kwargs", {}) 67 | with self.assertRaises(ValidationError): 68 | validate(data, schema, cls=self.validator_class, **kwargs) 69 | 70 | if not PY3: 71 | name = name.encode("utf-8") 72 | test_case.__name__ = name 73 | 74 | return test_case 75 | 76 | 77 | def maybe_skip(skip, test_case, case, test): 78 | if skip is not None: 79 | reason = skip(case, test) 80 | if reason is not None: 81 | test_case = unittest.skip(reason)(test_case) 82 | return test_case 83 | 84 | 85 | def load_json_cases(tests_glob, ignore_glob="", basedir=TESTS_DIR, skip=None): 86 | if ignore_glob: 87 | ignore_glob = os.path.join(basedir, ignore_glob) 88 | 89 | def add_test_methods(test_class): 90 | ignored = set(glob.iglob(ignore_glob)) 91 | 92 | for filename in glob.iglob(os.path.join(basedir, tests_glob)): 93 | if filename in ignored: 94 | continue 95 | 96 | validating, _ = os.path.splitext(os.path.basename(filename)) 97 | id = itertools.count(1) 98 | 99 | with open(filename) as test_file: 100 | for case in json.load(test_file): 101 | for test in case["tests"]: 102 | name = "test_%s_%s_%s" % ( 103 | validating, 104 | next(id), 105 | re.sub(r"[\W ]+", "_", test["description"]), 106 | ) 107 | assert not hasattr(test_class, name), name 108 | 109 | test_case = make_case( 110 | data=test["data"], 111 | schema=case["schema"], 112 | valid=test["valid"], 113 | name=name, 114 | ) 115 | test_case = maybe_skip(skip, test_case, case, test) 116 | setattr(test_class, name, test_case) 117 | 118 | return test_class 119 | return add_test_methods 120 | 121 | 122 | class TypesMixin(object): 123 | @unittest.skipIf(PY3, "In Python 3 json.load always produces unicode") 124 | def test_string_a_bytestring_is_a_string(self): 125 | self.validator_class({"type": "string"}).validate(b"foo") 126 | 127 | 128 | class DecimalMixin(object): 129 | def test_it_can_validate_with_decimals(self): 130 | schema = {"type": "number"} 131 | validator = self.validator_class( 132 | schema, types={"number": (int, float, Decimal)} 133 | ) 134 | 135 | for valid in [1, 1.1, Decimal(1) / Decimal(8)]: 136 | validator.validate(valid) 137 | 138 | for invalid in ["foo", {}, [], True, None]: 139 | with self.assertRaises(ValidationError): 140 | validator.validate(invalid) 141 | 142 | 143 | def missing_format(checker): 144 | def missing_format(case, test): 145 | format = case["schema"].get("format") 146 | if format not in checker.checkers: 147 | return "Format checker {0!r} not found.".format(format) 148 | elif ( 149 | format == "date-time" and 150 | pypy_version_info is not None and 151 | pypy_version_info[:2] <= (1, 9) 152 | ): 153 | # datetime.datetime is overzealous about typechecking in <=1.9 154 | return "datetime.datetime is broken on this version of PyPy." 155 | return missing_format 156 | 157 | 158 | class FormatMixin(object): 159 | def test_it_returns_true_for_formats_it_does_not_know_about(self): 160 | validator = self.validator_class( 161 | {"format": "carrot"}, format_checker=FormatChecker(), 162 | ) 163 | validator.validate("bugs") 164 | 165 | def test_it_does_not_validate_formats_by_default(self): 166 | validator = self.validator_class({}) 167 | self.assertIsNone(validator.format_checker) 168 | 169 | def test_it_validates_formats_if_a_checker_is_provided(self): 170 | checker = mock.Mock(spec=FormatChecker) 171 | validator = self.validator_class( 172 | {"format": "foo"}, format_checker=checker, 173 | ) 174 | 175 | validator.validate("bar") 176 | 177 | checker.check.assert_called_once_with("bar", "foo") 178 | 179 | cause = ValueError() 180 | checker.check.side_effect = FormatError('aoeu', cause=cause) 181 | 182 | with self.assertRaises(ValidationError) as cm: 183 | validator.validate("bar") 184 | # Make sure original cause is attached 185 | self.assertIs(cm.exception.cause, cause) 186 | 187 | def test_it_validates_formats_of_any_type(self): 188 | checker = mock.Mock(spec=FormatChecker) 189 | validator = self.validator_class( 190 | {"format": "foo"}, format_checker=checker, 191 | ) 192 | 193 | validator.validate([1, 2, 3]) 194 | 195 | checker.check.assert_called_once_with([1, 2, 3], "foo") 196 | 197 | cause = ValueError() 198 | checker.check.side_effect = FormatError('aoeu', cause=cause) 199 | 200 | with self.assertRaises(ValidationError) as cm: 201 | validator.validate([1, 2, 3]) 202 | # Make sure original cause is attached 203 | self.assertIs(cm.exception.cause, cause) 204 | 205 | 206 | if sys.maxunicode == 2 ** 16 - 1: # This is a narrow build. 207 | def narrow_unicode_build(case, test): 208 | if "supplementary Unicode" in test["description"]: 209 | return "Not running surrogate Unicode case, this Python is narrow." 210 | else: 211 | def narrow_unicode_build(case, test): # This isn't, skip nothing. 212 | return 213 | 214 | 215 | @load_json_cases( 216 | "draft3/*.json", 217 | skip=narrow_unicode_build, 218 | ignore_glob="draft3/refRemote.json", 219 | ) 220 | @load_json_cases( 221 | "draft3/optional/format.json", skip=missing_format(draft3_format_checker) 222 | ) 223 | @load_json_cases("draft3/optional/bignum.json") 224 | @load_json_cases("draft3/optional/zeroTerminatedFloats.json") 225 | class TestDraft3(unittest.TestCase, TypesMixin, DecimalMixin, FormatMixin): 226 | validator_class = Draft3Validator 227 | validator_kwargs = {"format_checker": draft3_format_checker} 228 | 229 | def test_any_type_is_valid_for_type_any(self): 230 | validator = self.validator_class({"type": "any"}) 231 | validator.validate(mock.Mock()) 232 | 233 | # TODO: we're in need of more meta schema tests 234 | def test_invalid_properties(self): 235 | with self.assertRaises(SchemaError): 236 | validate({}, {"properties": {"test": True}}, 237 | cls=self.validator_class) 238 | 239 | def test_minItems_invalid_string(self): 240 | with self.assertRaises(SchemaError): 241 | # needs to be an integer 242 | validate([1], {"minItems": "1"}, cls=self.validator_class) 243 | 244 | 245 | @load_json_cases( 246 | "draft4/*.json", 247 | skip=narrow_unicode_build, 248 | ignore_glob="draft4/refRemote.json", 249 | ) 250 | @load_json_cases( 251 | "draft4/optional/format.json", skip=missing_format(draft4_format_checker) 252 | ) 253 | @load_json_cases("draft4/optional/bignum.json") 254 | @load_json_cases("draft4/optional/zeroTerminatedFloats.json") 255 | class TestDraft4(unittest.TestCase, TypesMixin, DecimalMixin, FormatMixin): 256 | validator_class = Draft4Validator 257 | validator_kwargs = {"format_checker": draft4_format_checker} 258 | 259 | # TODO: we're in need of more meta schema tests 260 | def test_invalid_properties(self): 261 | with self.assertRaises(SchemaError): 262 | validate({}, {"properties": {"test": True}}, 263 | cls=self.validator_class) 264 | 265 | def test_minItems_invalid_string(self): 266 | with self.assertRaises(SchemaError): 267 | # needs to be an integer 268 | validate([1], {"minItems": "1"}, cls=self.validator_class) 269 | 270 | 271 | class RemoteRefResolutionMixin(object): 272 | def setUp(self): 273 | patch = mock.patch("jsonschema.validators.requests") 274 | requests = patch.start() 275 | requests.get.side_effect = self.resolve 276 | self.addCleanup(patch.stop) 277 | 278 | def resolve(self, reference): 279 | _, _, reference = reference.partition("http://localhost:1234/") 280 | return mock.Mock(**{"json.return_value": REMOTES.get(reference)}) 281 | 282 | 283 | @load_json_cases("draft3/refRemote.json") 284 | class Draft3RemoteResolution(RemoteRefResolutionMixin, unittest.TestCase): 285 | validator_class = Draft3Validator 286 | 287 | 288 | @load_json_cases("draft4/refRemote.json") 289 | class Draft4RemoteResolution(RemoteRefResolutionMixin, unittest.TestCase): 290 | validator_class = Draft4Validator 291 | -------------------------------------------------------------------------------- /lambda/setup/index.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | # 5 | # Licensed under the Amazon Software License (the "License"). You may not use this file except in 6 | # compliance with the License. A copy of the License is located at 7 | # 8 | # http://aws.amazon.com/asl/ 9 | # 10 | # or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific 12 | # language governing permissions and limitations under the License. 13 | 14 | import boto3 15 | import json 16 | from urllib.request import build_opener, HTTPError, HTTPHandler, Request, urlopen 17 | 18 | lambda_aws = boto3.client('lambda') 19 | 20 | 21 | def handler(event, context): 22 | print("LOG setup.index.handler.event -----\n", json.dumps(event)) 23 | 24 | if event['RequestType'] == "Create": 25 | 26 | resource_properties = event['ResourceProperties'] 27 | 28 | access_token = resource_properties['AccessToken'] 29 | print("LOG setup.index.handler.access_token:", access_token) 30 | 31 | endpoint_apid_id = event['ResourceProperties']['EndpointApiId'] 32 | print("LOG setup.index.handler.endpoint_apid_id:", endpoint_apid_id) 33 | 34 | endpoint_lambda = event['ResourceProperties']['EndpointLambda'] 35 | print("LOG setup.index.handler.endpoint_lambda:", endpoint_lambda) 36 | 37 | skill_id = resource_properties['SkillId'] 38 | print("LOG setup.index.handler.skill_id:", skill_id) 39 | 40 | skill_lambda = event['ResourceProperties']['SkillLambda'] 41 | print("LOG setup.index.handler.skill_lambda:", skill_lambda) 42 | 43 | alexa_skill_lambda_permission_statement_id = event['ResourceProperties']['AlexaSkillLambdaPermissionStatementId'] 44 | print("LOG setup.index.handler.alexa_skill_lambda_permission_statement_id:", alexa_skill_lambda_permission_statement_id) 45 | 46 | response = lambda_aws.remove_permission( 47 | FunctionName=skill_lambda, 48 | StatementId=alexa_skill_lambda_permission_statement_id 49 | ) 50 | print(response) 51 | 52 | response = lambda_aws.add_permission( 53 | FunctionName=skill_lambda, 54 | StatementId=alexa_skill_lambda_permission_statement_id, 55 | Action='lambda:InvokeFunction', 56 | Principal='alexa-connectedhome.amazon.com', 57 | EventSourceToken=skill_id 58 | ) 59 | print(response) 60 | 61 | # Get the messaging credentials 62 | url = 'https://api.amazonalexa.com/v1/skills/{}/credentials'.format(skill_id) 63 | headers = { 64 | 'Content-Type': 'application/json', 65 | 'Accept': 'application/json', 66 | 'Authorization': access_token 67 | } 68 | request = Request(url=url, headers=headers) 69 | result = urlopen(request).read().decode("utf-8") 70 | response = json.loads(result) 71 | if 'skillMessagingCredentials' in response: 72 | client_id = response['skillMessagingCredentials']['clientId'] 73 | client_secret = response['skillMessagingCredentials']['clientSecret'] 74 | 75 | # Update the Lambda 76 | response = lambda_aws.update_function_configuration( 77 | FunctionName=endpoint_lambda, 78 | Environment={ 79 | 'Variables': { 80 | 'api_id': endpoint_apid_id, 81 | 'client_id': client_id, 82 | 'client_secret': client_secret 83 | } 84 | } 85 | ) 86 | print(response) 87 | else: 88 | print('ERR setup.index.handler: Invalid SMAPI response') 89 | 90 | # if event['RequestType'] == "Delete": 91 | # print("LOG setup.index.handler:Delete") 92 | # return send_response(event, context, "SUCCESS", {}) 93 | 94 | response_status = 'SUCCESS' 95 | response_data = {'event': event} 96 | 97 | response_body = json.dumps( 98 | { 99 | 'Status': response_status, 100 | 'Reason': "CloudWatch Log Stream: " + context.log_stream_name, 101 | 'PhysicalResourceId': context.log_stream_name, 102 | 'StackId': event['StackId'], 103 | 'RequestId': event['RequestId'], 104 | 'LogicalResourceId': event['LogicalResourceId'], 105 | 'Data': response_data 106 | } 107 | ) 108 | 109 | opener = build_opener(HTTPHandler) 110 | request = Request(event['ResponseURL'], data=bytes(response_body, 'utf-8')) 111 | request.add_header('Content-Type', '') # NOTE This has to be empty 112 | request.add_header('Content-Length', len(response_body)) 113 | request.get_method = lambda: 'PUT' 114 | 115 | try: 116 | print("LOG setup.index.send:opener.open -----") 117 | response = opener.open(request, timeout=7) 118 | print("Response Status Code: {0} Message: {1}".format(response.getcode(), response.msg)) 119 | return True 120 | 121 | except HTTPError as e: 122 | print("HTTPError: {}".format(e)) 123 | return False 124 | -------------------------------------------------------------------------------- /lambda/smarthome/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-python-smarthome-sandbox/8bc88c53a2e0117092b44bdbb319a1e6709c547d/lambda/smarthome/__init__.py -------------------------------------------------------------------------------- /lambda/smarthome/index.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | # 5 | # Licensed under the Amazon Software License (the "License"). You may not use this file except in 6 | # compliance with the License. A copy of the License is located at 7 | # 8 | # http://aws.amazon.com/asl/ 9 | # 10 | # or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific 12 | # language governing permissions and limitations under the License. 13 | 14 | import json 15 | import os 16 | import urllib.request 17 | from urllib.request import HTTPError 18 | 19 | 20 | def get_api_url(api_id, aws_region, resource): 21 | return 'https://{0}.execute-api.{1}.amazonaws.com/prod/{2}'.format(api_id, aws_region, resource) 22 | 23 | 24 | def handler(request, context): 25 | try: 26 | print("LOG skill.index.handler.request -----") 27 | print(json.dumps(request)) 28 | 29 | # Get the Environment Variables, these are used to dynamically compose the API URI 30 | 31 | # Get the Region 32 | env_aws_default_region = os.environ.get('AWS_DEFAULT_REGION', None) 33 | if env_aws_default_region is None: 34 | print("ERROR skill.index.handler.aws_default_region is None default to us-east-1") 35 | env_aws_default_region = 'us-east-1' 36 | 37 | # Get the API ID 38 | env_api_id = os.environ.get('api_id', None) 39 | if env_api_id is None: 40 | print("ERROR skill.index.handler.env_api_id is None") 41 | return '{}' 42 | 43 | # Pass the requested directive to the backend Directive API 44 | url = get_api_url(env_api_id, env_aws_default_region, 'directives') 45 | data = bytes(json.dumps(request), encoding="utf-8") 46 | headers = {'Content-Type': 'application/json'} 47 | req = urllib.request.Request(url, data, headers) 48 | result = urllib.request.urlopen(req).read().decode("utf-8") 49 | response = json.loads(result) 50 | print("LOG skill.index.handler.response -----") 51 | print(json.dumps(response)) 52 | return response 53 | 54 | except HTTPError as error: 55 | error_output = {'code': error.code, 'msg': 'An error occurred while handling a request to /directives. Also review the Endpoint API logs.'} 56 | print("ERROR skill.index.handler.error:", error_output) 57 | return error_output 58 | 59 | except ValueError as error: 60 | print("ERROR skill.index.handler.error:", error) 61 | return {'error': error} 62 | -------------------------------------------------------------------------------- /lambda/smarthome/skill.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest": { 3 | "publishingInformation": { 4 | "locales": { 5 | "en-US": { 6 | "summary": "A sample smart home sandbox Alexa skill.", 7 | "examplePhrases": [ 8 | "Alexa turn on white sample switch", 9 | "Alexa turn off black sample switch", 10 | "Alexa set sample toaster heat to six" 11 | ], 12 | "name": "Smart Home Sandbox", 13 | "description": "This is a Sample Smart Home Skill Sandbox." 14 | } 15 | }, 16 | "isAvailableWorldwide": false, 17 | "testingInstructions": "Interact with the virtual device templates.", 18 | "category": "SMART_HOME", 19 | "distributionCountries": [ 20 | "US" 21 | ] 22 | }, 23 | "apis": { 24 | "smartHome": { 25 | "endpoint": { 26 | "uri": "SkillLambda" 27 | }, 28 | "regions": { 29 | "NA": { 30 | "endpoint": { 31 | "uri": "SkillLambda" 32 | } 33 | } 34 | }, 35 | "protocolVersion": "3" 36 | } 37 | }, 38 | "manifestVersion": "1.0", 39 | "permissions": [ 40 | { 41 | "name": "alexa::async_event:write" 42 | } 43 | ], 44 | "privacyAndCompliance": { 45 | "allowsPurchases": false, 46 | "locales": { 47 | "en-US": { 48 | "privacyPolicyUrl": "https://www.amazon.com/gp/help/customer/display.html?nodeId=468496" 49 | } 50 | }, 51 | "isExportCompliant": true, 52 | "containsAds": false, 53 | "isChildDirected": false, 54 | "usesPersonalInfo": false 55 | } 56 | } 57 | } 58 | --------------------------------------------------------------------------------