├── .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 | ''.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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------