├── .gitignore ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── docs ├── Makefile ├── _static │ ├── logo-full.png │ └── logo-sm.png ├── _templates │ ├── links.html │ ├── sidebarlogo.html │ └── stayinformed.html ├── _themes │ ├── LICENSE │ ├── README │ ├── flask │ │ ├── layout.html │ │ ├── relations.html │ │ ├── static │ │ │ └── flasky.css_t │ │ └── theme.conf │ └── flask_theme_support.py ├── conf.py ├── configuration.rst ├── contents.rst.inc ├── flaskdocext.py ├── getting_started.rst ├── index.rst ├── make.bat ├── requests.rst ├── responses.rst └── user_contributions.rst ├── flask_ask ├── __init__.py ├── cache.py ├── convert.py ├── core.py ├── models.py └── verifier.py ├── requirements-dev.txt ├── requirements.txt ├── samples ├── audio │ ├── playlist_demo │ │ ├── playlist.py │ │ └── speech_assets │ │ │ ├── IntentSchema.json │ │ │ └── SampleUtterances.txt │ └── simple_demo │ │ ├── ask_audio.py │ │ └── speech_assets │ │ ├── IntentSchema.json │ │ └── SampleUtterances.txt ├── blueprint_demo │ ├── demo.py │ ├── helloworld.py │ ├── speech_assets │ │ ├── IntentSchema.json │ │ └── SampleUtterances.txt │ └── templates.yaml ├── helloworld │ ├── helloworld.py │ └── speech_assets │ │ ├── IntentSchema.json │ │ └── SampleUtterances.txt ├── historybuff │ ├── historybuff.py │ └── speech_assets │ │ ├── IntentSchema.json │ │ └── SampleUtterances.txt ├── purchase │ ├── IntentSchema.json │ ├── model.py │ ├── purchase.py │ └── templates.yaml ├── session │ ├── session.py │ ├── speech_assets │ │ ├── IntentSchema.json │ │ ├── SampleUtterances.txt │ │ └── customSlotTypes │ │ │ └── LIST_OF_COLORS │ └── templates.yaml ├── spacegeek │ ├── spacegeek.py │ ├── speech_assets │ │ ├── IntentSchema.json │ │ └── SampleUtterances.txt │ └── templates.yaml └── tidepooler │ ├── speech_assets │ ├── IntentSchema.json │ ├── SampleUtterances.txt │ └── customSlotTypes │ │ ├── LIST_OF_CITIES │ │ └── LIST_OF_STATES │ ├── templates.yaml │ └── tidepooler.py ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── test_audio.py ├── test_cache.py ├── test_core.py ├── test_integration.py ├── test_integration_support_entity_resolution.py ├── test_samples.py └── test_unicode.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | 91 | # Vagrant 92 | .vagrant 93 | 94 | # Misc 95 | .DS_Store 96 | temp 97 | .idea/ 98 | 99 | # Project 100 | backups 101 | settings.cfg 102 | fabfile/settings.py 103 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2016 John Wheeler 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.rst *.txt LICENSE tox.ini .travis.yml docs/Makefile .coveragerc conftest.py 2 | recursive-include tests *.py 3 | recursive-include docs *.rst 4 | recursive-include docs *.py 5 | prune docs/_build 6 | 7 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | 2 | .. image:: http://flask-ask.readthedocs.io/en/latest/_images/logo-full.png 3 | 4 | =================================== 5 | Program the Amazon Echo with Python 6 | =================================== 7 | 8 | Flask-Ask is a `Flask extension `_ that makes building Alexa skills for the Amazon Echo easier and much more fun. 9 | 10 | * `Flask-Ask quickstart on Amazon's Developer Blog `_. 11 | * `Level Up with our Alexa Skills Kit Video Tutorial `_ 12 | * `Chat on Gitter.im `_ 13 | 14 | The Basics 15 | =============== 16 | 17 | A Flask-Ask application looks like this: 18 | 19 | .. code-block:: python 20 | 21 | from flask import Flask 22 | from flask_ask import Ask, statement 23 | 24 | app = Flask(__name__) 25 | ask = Ask(app, '/') 26 | 27 | @ask.intent('HelloIntent') 28 | def hello(firstname): 29 | speech_text = "Hello %s" % firstname 30 | return statement(speech_text).simple_card('Hello', speech_text) 31 | 32 | if __name__ == '__main__': 33 | app.run() 34 | 35 | In the code above: 36 | 37 | #. The ``Ask`` object is created by passing in the Flask application and a route to forward Alexa requests to. 38 | #. The ``intent`` decorator maps ``HelloIntent`` to a view function ``hello``. 39 | #. The intent's ``firstname`` slot is implicitly mapped to ``hello``'s ``firstname`` parameter. 40 | #. Lastly, a builder constructs a spoken response and displays a contextual card in the Alexa smartphone/tablet app. 41 | 42 | More code examples are in the `samples `_ directory. 43 | 44 | Jinja Templates 45 | --------------- 46 | 47 | Since Alexa responses are usually short phrases, you might find it convenient to put them in the same file. 48 | Flask-Ask has a `Jinja template loader `_ that loads 49 | multiple templates from a single YAML file. For example, here's a template that supports the minimal voice interface 50 | above: 51 | 52 | .. code-block:: yaml 53 | 54 | hello: Hello, {{ firstname }} 55 | 56 | Templates are stored in a file called `templates.yaml` located in the application root. Checkout the `Tidepooler example `_ to see why it makes sense to extract speech out of the code and into templates as the number of spoken phrases grow. 57 | 58 | Features 59 | =============== 60 | 61 | Flask-Ask handles the boilerplate, so you can focus on writing clean code. Flask-Ask: 62 | 63 | * Has decorators to map Alexa requests and intent slots to view functions 64 | * Helps construct ask and tell responses, reprompts and cards 65 | * Makes session management easy 66 | * Allows for the separation of code and speech through Jinja templates 67 | * Verifies Alexa request signatures 68 | 69 | Installation 70 | =============== 71 | 72 | To install Flask-Ask:: 73 | 74 | pip install flask-ask 75 | 76 | Documentation 77 | =============== 78 | 79 | These resources will get you up and running quickly: 80 | 81 | * `5-minute quickstart `_ 82 | * `Full online documentation `_ 83 | 84 | Fantastic 3-part tutorial series by Harrison Kinsley 85 | 86 | * `Intro and Skill Logic - Alexa Skills w/ Python and Flask-Ask Part 1 `_ 87 | * `Headlines Function - Alexa Skills w/ Python and Flask-Ask Part 2 `_ 88 | * `Testing our Skill - Alexa Skills w/ Python and Flask-Ask Part 3 `_ 89 | 90 | Deployment 91 | =============== 92 | 93 | You can deploy using any WSGI compliant framework (uWSGI, Gunicorn). If you haven't deployed a Flask app to production, `checkout flask-live-starter `_. 94 | 95 | To deploy on AWS Lambda, you have two options. Use `Zappa `_ to automate the deployment of an AWS Lambda function and an AWS API Gateway to provide a public facing endpoint for your Lambda function. This `blog post `_ shows how to deploy Flask-Ask with Zappa from scratch. Note: When deploying to AWS Lambda with Zappa, make sure you point the Alexa skill to the HTTPS API gateway that Zappa creates, not the Lambda function's ARN. 96 | 97 | Alternatively, you can use AWS Lambda directly without the need for an AWS API Gateway endpoint. In this case you will need to `deploy `_ your Lambda function yourself and use `virtualenv `_ to create a deployment package that contains your Flask-Ask application along with its dependencies, which can be uploaded to Lambda. If your Lambda handler is configured as `lambda_function.lambda_handler`, then you would save the full application example above in a file called `lambda_function.py` and add the following two lines to it: 98 | 99 | .. code-block:: python 100 | 101 | def lambda_handler(event, _context): 102 | return ask.run_aws_lambda(event) 103 | 104 | 105 | Development 106 | =============== 107 | 108 | If you'd like to work from the Flask-Ask source, clone the project and run:: 109 | 110 | pip install -r requirements-dev.txt 111 | 112 | This will install all base requirements from `requirements.txt` as well as requirements needed for running tests from the `tests` directory. 113 | 114 | Tests can be run with:: 115 | 116 | python setup.py test 117 | 118 | Or:: 119 | 120 | python -m unittest 121 | 122 | To install from your local clone or fork of the project, run:: 123 | 124 | python setup.py install 125 | 126 | Related projects 127 | =============== 128 | 129 | `cookiecutter-flask-ask `_ is a Cookiecutter to easily bootstrap a Flask-Ask project, including documentation, speech assets and basic built-in intents. 130 | 131 | Have a Google Home? Checkout `Flask-Assistant `_ (early alpha) 132 | 133 | 134 | Thank You 135 | =============== 136 | 137 | Thanks for checking this library out! I hope you find it useful. 138 | 139 | Of course, there's always room for improvement. 140 | Feel free to `open an issue `_ so we can make Flask-Ask better. 141 | 142 | Special thanks to `@kennethreitz `_ for his `sense `_ of `style `_, and of course, `@mitsuhiko `_ for `Flask `_ 143 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don\'t have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help 23 | help: 24 | @echo "Please use \`make ' where is one of" 25 | @echo " html to make standalone HTML files" 26 | @echo " dirhtml to make HTML files named index.html in directories" 27 | @echo " singlehtml to make a single large HTML file" 28 | @echo " pickle to make pickle files" 29 | @echo " json to make JSON files" 30 | @echo " htmlhelp to make HTML files and a HTML help project" 31 | @echo " qthelp to make HTML files and a qthelp project" 32 | @echo " applehelp to make an Apple Help Book" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " epub3 to make an epub3" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | @echo " dummy to check syntax errors of document sources" 51 | 52 | .PHONY: clean 53 | clean: 54 | rm -rf $(BUILDDIR)/* 55 | 56 | .PHONY: html 57 | html: 58 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 61 | 62 | .PHONY: dirhtml 63 | dirhtml: 64 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 65 | @echo 66 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 67 | 68 | .PHONY: singlehtml 69 | singlehtml: 70 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 71 | @echo 72 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 73 | 74 | .PHONY: pickle 75 | pickle: 76 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 77 | @echo 78 | @echo "Build finished; now you can process the pickle files." 79 | 80 | .PHONY: json 81 | json: 82 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 83 | @echo 84 | @echo "Build finished; now you can process the JSON files." 85 | 86 | .PHONY: htmlhelp 87 | htmlhelp: 88 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 89 | @echo 90 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 91 | ".hhp project file in $(BUILDDIR)/htmlhelp." 92 | 93 | .PHONY: qthelp 94 | qthelp: 95 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 96 | @echo 97 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 98 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 99 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Flask-Ask.qhcp" 100 | @echo "To view the help file:" 101 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Flask-Ask.qhc" 102 | 103 | .PHONY: applehelp 104 | applehelp: 105 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 106 | @echo 107 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 108 | @echo "N.B. You won't be able to view it unless you put it in" \ 109 | "~/Library/Documentation/Help or install it in your application" \ 110 | "bundle." 111 | 112 | .PHONY: devhelp 113 | devhelp: 114 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 115 | @echo 116 | @echo "Build finished." 117 | @echo "To view the help file:" 118 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Flask-Ask" 119 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Flask-Ask" 120 | @echo "# devhelp" 121 | 122 | .PHONY: epub 123 | epub: 124 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 125 | @echo 126 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 127 | 128 | .PHONY: epub3 129 | epub3: 130 | $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 131 | @echo 132 | @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." 133 | 134 | .PHONY: latex 135 | latex: 136 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 137 | @echo 138 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 139 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 140 | "(use \`make latexpdf' here to do that automatically)." 141 | 142 | .PHONY: latexpdf 143 | latexpdf: 144 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 145 | @echo "Running LaTeX files through pdflatex..." 146 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 147 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 148 | 149 | .PHONY: latexpdfja 150 | latexpdfja: 151 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 152 | @echo "Running LaTeX files through platex and dvipdfmx..." 153 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 154 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 155 | 156 | .PHONY: text 157 | text: 158 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 159 | @echo 160 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 161 | 162 | .PHONY: man 163 | man: 164 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 165 | @echo 166 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 167 | 168 | .PHONY: texinfo 169 | texinfo: 170 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 171 | @echo 172 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 173 | @echo "Run \`make' in that directory to run these through makeinfo" \ 174 | "(use \`make info' here to do that automatically)." 175 | 176 | .PHONY: info 177 | info: 178 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 179 | @echo "Running Texinfo files through makeinfo..." 180 | make -C $(BUILDDIR)/texinfo info 181 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 182 | 183 | .PHONY: gettext 184 | gettext: 185 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 186 | @echo 187 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 188 | 189 | .PHONY: changes 190 | changes: 191 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 192 | @echo 193 | @echo "The overview file is in $(BUILDDIR)/changes." 194 | 195 | .PHONY: linkcheck 196 | linkcheck: 197 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 198 | @echo 199 | @echo "Link check complete; look for any errors in the above output " \ 200 | "or in $(BUILDDIR)/linkcheck/output.txt." 201 | 202 | .PHONY: doctest 203 | doctest: 204 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 205 | @echo "Testing of doctests in the sources finished, look at the " \ 206 | "results in $(BUILDDIR)/doctest/output.txt." 207 | 208 | .PHONY: coverage 209 | coverage: 210 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 211 | @echo "Testing of coverage in the sources finished, look at the " \ 212 | "results in $(BUILDDIR)/coverage/python.txt." 213 | 214 | .PHONY: xml 215 | xml: 216 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 217 | @echo 218 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 219 | 220 | .PHONY: pseudoxml 221 | pseudoxml: 222 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 223 | @echo 224 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 225 | 226 | .PHONY: dummy 227 | dummy: 228 | $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy 229 | @echo 230 | @echo "Build finished. Dummy builder generates no files." 231 | -------------------------------------------------------------------------------- /docs/_static/logo-full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnwheeler/flask-ask/a93488b700479a3b2d80eb54d0f6585caae15ef3/docs/_static/logo-full.png -------------------------------------------------------------------------------- /docs/_static/logo-sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnwheeler/flask-ask/a93488b700479a3b2d80eb54d0f6585caae15ef3/docs/_static/logo-sm.png -------------------------------------------------------------------------------- /docs/_templates/links.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |

Resources

4 | 15 | 16 | 20 | 21 |
22 | 23 |

Project Links

24 | 25 | 30 | 31 |
32 | -------------------------------------------------------------------------------- /docs/_templates/sidebarlogo.html: -------------------------------------------------------------------------------- 1 |

6 | 7 | 8 | Alexa Skills Kit Development for Amazon Echo Devices with Python 9 | 10 | -------------------------------------------------------------------------------- /docs/_templates/stayinformed.html: -------------------------------------------------------------------------------- 1 | 2 |

Stay Informed

3 | 4 | Star 5 | 6 |
7 | 8 |

9 | Receive updates on new releases and upcoming projects. 10 |

11 | 12 |

13 | Follow @johnwheeler 14 |

15 | 16 |

17 | 18 |

19 | 20 |
21 | -------------------------------------------------------------------------------- /docs/_themes/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 by Armin Ronacher. 2 | 3 | Some rights reserved. 4 | 5 | Redistribution and use in source and binary forms of the theme, with or 6 | without modification, are permitted provided that the following conditions 7 | are met: 8 | 9 | * Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above 13 | copyright notice, this list of conditions and the following 14 | disclaimer in the documentation and/or other materials provided 15 | with the distribution. 16 | 17 | * The names of the contributors may not be used to endorse or 18 | promote products derived from this software without specific 19 | prior written permission. 20 | 21 | We kindly ask you to only use these themes in an unmodified manner just 22 | for Flask and Flask-related products, not for unrelated projects. If you 23 | like the visual style and want to use it for your own projects, please 24 | consider making some larger changes to the themes (such as changing 25 | font faces, sizes, colors or margins). 26 | 27 | THIS THEME IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 28 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 29 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 30 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 31 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 32 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 33 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 34 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 35 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 36 | ARISING IN ANY WAY OUT OF THE USE OF THIS THEME, EVEN IF ADVISED OF THE 37 | POSSIBILITY OF SUCH DAMAGE. 38 | -------------------------------------------------------------------------------- /docs/_themes/README: -------------------------------------------------------------------------------- 1 | Flask Sphinx Styles 2 | =================== 3 | 4 | This repository contains sphinx styles for Flask and Flask related 5 | projects. To use this style in your Sphinx documentation, follow 6 | this guide: 7 | 8 | 1. put this folder as _themes into your docs folder. Alternatively 9 | you can also use git submodules to check out the contents there. 10 | 2. add this to your conf.py: 11 | 12 | sys.path.append(os.path.abspath('_themes')) 13 | html_theme_path = ['_themes'] 14 | html_theme = 'flask' 15 | 16 | The following themes exist: 17 | 18 | - 'flask' - the standard flask documentation theme for large 19 | projects 20 | - 'flask_small' - small one-page theme. Intended to be used by 21 | very small addon libraries for flask. 22 | 23 | The following options exist for the flask_small theme: 24 | 25 | [options] 26 | index_logo = '' filename of a picture in _static 27 | to be used as replacement for the 28 | h1 in the index.rst file. 29 | index_logo_height = 120px height of the index logo 30 | github_fork = '' repository name on github for the 31 | "fork me" badge 32 | -------------------------------------------------------------------------------- /docs/_themes/flask/layout.html: -------------------------------------------------------------------------------- 1 | {%- extends "basic/layout.html" %} {%- block extrahead %} {{ super() }} 2 | 3 | 4 | 5 | 23 | 24 | 25 | 26 | 27 | {% if theme_touch_icon %} 28 | {% endif %} {% endblock %} {%- block relbar2 %} {% if theme_github_fork %} 29 | 30 | Fork me on GitHub 31 | 32 | {% endif %} {% endblock %} {%- block footer %} 33 | 36 | {%- endblock %} 37 | -------------------------------------------------------------------------------- /docs/_themes/flask/relations.html: -------------------------------------------------------------------------------- 1 |

Related Topics

2 | 20 | -------------------------------------------------------------------------------- /docs/_themes/flask/static/flasky.css_t: -------------------------------------------------------------------------------- 1 | /* 2 | * flasky.css_t 3 | * ~~~~~~~~~~~~ 4 | * 5 | * :copyright: Copyright 2010 by Armin Ronacher. 6 | * :license: Flask Design License, see LICENSE for details. 7 | */ 8 | 9 | {% set page_width = '940px' %} 10 | {% set sidebar_width = '220px' %} 11 | 12 | @import url("basic.css"); 13 | 14 | /* -- page layout ----------------------------------------------------------- */ 15 | 16 | body { 17 | font-family: 'Georgia', serif; 18 | font-size: 17px; 19 | background-color: white; 20 | color: #000; 21 | margin: 0; 22 | padding: 0; 23 | } 24 | 25 | div.document { 26 | width: {{ page_width }}; 27 | margin: 30px auto 0 auto; 28 | } 29 | 30 | div.documentwrapper { 31 | float: left; 32 | width: 100%; 33 | } 34 | 35 | div.bodywrapper { 36 | margin: 0 0 0 {{ sidebar_width }}; 37 | } 38 | 39 | div.sphinxsidebar { 40 | width: {{ sidebar_width }}; 41 | } 42 | 43 | hr { 44 | border: 1px solid #B1B4B6; 45 | } 46 | 47 | div.body { 48 | background-color: #ffffff; 49 | color: #3E4349; 50 | padding: 0 30px 0 30px; 51 | } 52 | 53 | img.floatingflask { 54 | padding: 0 0 10px 10px; 55 | float: right; 56 | } 57 | 58 | div.footer { 59 | width: {{ page_width }}; 60 | margin: 20px auto 30px auto; 61 | font-size: 14px; 62 | color: #888; 63 | text-align: right; 64 | } 65 | 66 | div.footer a { 67 | color: #888; 68 | } 69 | 70 | div.related { 71 | display: none; 72 | } 73 | 74 | div.sphinxsidebar a { 75 | color: #444; 76 | text-decoration: none; 77 | border-bottom: 1px dotted #999; 78 | } 79 | 80 | div.sphinxsidebar a:hover { 81 | border-bottom: 1px solid #999; 82 | } 83 | 84 | div.sphinxsidebar { 85 | font-size: 14px; 86 | line-height: 1.5; 87 | } 88 | 89 | div.sphinxsidebarwrapper { 90 | padding: 18px 10px; 91 | } 92 | 93 | div.sphinxsidebarwrapper p.logo { 94 | padding: 0 0 6px 0; 95 | margin: 0; 96 | } 97 | 98 | div.sphinxsidebar h3, 99 | div.sphinxsidebar h4 { 100 | font-family: 'Garamond', 'Georgia', serif; 101 | color: #444; 102 | font-size: 24px; 103 | font-weight: normal; 104 | margin: 0 0 5px 0; 105 | padding: 0; 106 | } 107 | 108 | div.sphinxsidebar h4 { 109 | font-size: 20px; 110 | } 111 | 112 | div.sphinxsidebar h3 a { 113 | color: #444; 114 | } 115 | 116 | div.sphinxsidebar p.logo a, 117 | div.sphinxsidebar h3 a, 118 | div.sphinxsidebar p.logo a:hover, 119 | div.sphinxsidebar h3 a:hover { 120 | border: none; 121 | } 122 | 123 | div.sphinxsidebar p { 124 | color: #555; 125 | margin: 10px 0; 126 | } 127 | 128 | div.sphinxsidebar ul { 129 | margin: 10px 0; 130 | padding: 0; 131 | color: #000; 132 | } 133 | 134 | div.sphinxsidebar input { 135 | border: 1px solid #ccc; 136 | font-family: 'Georgia', serif; 137 | font-size: 1em; 138 | } 139 | 140 | /* -- body styles ----------------------------------------------------------- */ 141 | 142 | a { 143 | color: #004B6B; 144 | text-decoration: underline; 145 | } 146 | 147 | a:hover { 148 | color: #6D4100; 149 | text-decoration: underline; 150 | } 151 | 152 | div.body h1, 153 | div.body h2, 154 | div.body h3, 155 | div.body h4, 156 | div.body h5, 157 | div.body h6 { 158 | font-family: 'Garamond', 'Georgia', serif; 159 | font-weight: normal; 160 | margin: 30px 0px 10px 0px; 161 | padding: 0; 162 | } 163 | 164 | {% if theme_index_logo %} 165 | div.indexwrapper h1 { 166 | text-indent: -999999px; 167 | background: url({{ theme_index_logo }}) no-repeat center center; 168 | height: {{ theme_index_logo_height }}; 169 | } 170 | {% endif %} 171 | div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; } 172 | div.body h2 { font-size: 180%; } 173 | div.body h3 { font-size: 150%; } 174 | div.body h4 { font-size: 130%; } 175 | div.body h5 { font-size: 100%; } 176 | div.body h6 { font-size: 100%; } 177 | 178 | a.headerlink { 179 | color: #ddd; 180 | padding: 0 4px; 181 | text-decoration: none; 182 | } 183 | 184 | a.headerlink:hover { 185 | color: #444; 186 | background: #eaeaea; 187 | } 188 | 189 | div.body p, div.body dd, div.body li { 190 | line-height: 1.4em; 191 | } 192 | 193 | div.admonition { 194 | background: #fafafa; 195 | margin: 20px -30px; 196 | padding: 10px 30px; 197 | border-top: 1px solid #ccc; 198 | border-bottom: 1px solid #ccc; 199 | } 200 | 201 | div.admonition tt.xref, div.admonition a tt { 202 | border-bottom: 1px solid #fafafa; 203 | } 204 | 205 | dd div.admonition { 206 | margin-left: -60px; 207 | padding-left: 60px; 208 | } 209 | 210 | div.admonition p.admonition-title { 211 | font-family: 'Garamond', 'Georgia', serif; 212 | font-weight: normal; 213 | font-size: 24px; 214 | margin: 0 0 10px 0; 215 | padding: 0; 216 | line-height: 1; 217 | } 218 | 219 | div.admonition p.last { 220 | margin-bottom: 0; 221 | } 222 | 223 | div.highlight { 224 | background-color: white; 225 | } 226 | 227 | dt:target, .highlight { 228 | background: #FAF3E8; 229 | } 230 | 231 | div.note { 232 | background-color: #eee; 233 | border: 1px solid #ccc; 234 | } 235 | 236 | div.seealso { 237 | background-color: #ffc; 238 | border: 1px solid #ff6; 239 | } 240 | 241 | div.topic { 242 | background-color: #eee; 243 | padding: 0 7px 7px 7px; 244 | } 245 | 246 | p.admonition-title { 247 | display: inline; 248 | } 249 | 250 | p.admonition-title:after { 251 | content: ":"; 252 | } 253 | 254 | pre, tt { 255 | font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; 256 | font-size: 0.9em; 257 | } 258 | 259 | img.screenshot { 260 | } 261 | 262 | tt.descname, tt.descclassname { 263 | font-size: 0.95em; 264 | } 265 | 266 | tt.descname { 267 | padding-right: 0.08em; 268 | } 269 | 270 | img.screenshot { 271 | -moz-box-shadow: 2px 2px 4px #eee; 272 | -webkit-box-shadow: 2px 2px 4px #eee; 273 | box-shadow: 2px 2px 4px #eee; 274 | } 275 | 276 | table.docutils { 277 | border: 1px solid #888; 278 | -moz-box-shadow: 2px 2px 4px #eee; 279 | -webkit-box-shadow: 2px 2px 4px #eee; 280 | box-shadow: 2px 2px 4px #eee; 281 | } 282 | 283 | table.docutils td, table.docutils th { 284 | border: 1px solid #888; 285 | padding: 0.25em 0.7em; 286 | } 287 | 288 | table.field-list, table.footnote { 289 | border: none; 290 | -moz-box-shadow: none; 291 | -webkit-box-shadow: none; 292 | box-shadow: none; 293 | } 294 | 295 | table.footnote { 296 | margin: 15px 0; 297 | width: 100%; 298 | border: 1px solid #eee; 299 | background: #fdfdfd; 300 | font-size: 0.9em; 301 | } 302 | 303 | table.footnote + table.footnote { 304 | margin-top: -15px; 305 | border-top: none; 306 | } 307 | 308 | table.field-list th { 309 | padding: 0 0.8em 0 0; 310 | } 311 | 312 | table.field-list td { 313 | padding: 0; 314 | } 315 | 316 | table.footnote td.label { 317 | width: 0px; 318 | padding: 0.3em 0 0.3em 0.5em; 319 | } 320 | 321 | table.footnote td { 322 | padding: 0.3em 0.5em; 323 | } 324 | 325 | dl { 326 | margin: 0; 327 | padding: 0; 328 | } 329 | 330 | dl dd { 331 | margin-left: 30px; 332 | } 333 | 334 | blockquote { 335 | margin: 0 0 0 30px; 336 | padding: 0; 337 | } 338 | 339 | ul, ol { 340 | margin: 10px 0 10px 30px; 341 | padding: 0; 342 | } 343 | 344 | pre { 345 | background: #eee; 346 | margin: 12px 0px; 347 | padding: 11px 14px; 348 | line-height: 1.3em; 349 | } 350 | 351 | dl pre, blockquote pre, li pre { 352 | margin-left: -60px; 353 | padding-left: 60px; 354 | } 355 | 356 | dl dl pre { 357 | margin-left: -90px; 358 | padding-left: 90px; 359 | } 360 | 361 | tt { 362 | background-color: #ecf0f3; 363 | color: #222; 364 | /* padding: 1px 2px; */ 365 | } 366 | 367 | tt.xref, a tt { 368 | background-color: #FBFBFB; 369 | border-bottom: 1px solid white; 370 | } 371 | 372 | a.reference { 373 | text-decoration: none; 374 | border-bottom: 1px dotted #004B6B; 375 | } 376 | 377 | a.reference:hover { 378 | border-bottom: 1px solid #6D4100; 379 | } 380 | 381 | a.footnote-reference { 382 | text-decoration: none; 383 | font-size: 0.7em; 384 | vertical-align: top; 385 | border-bottom: 1px dotted #004B6B; 386 | } 387 | 388 | a.footnote-reference:hover { 389 | border-bottom: 1px solid #6D4100; 390 | } 391 | 392 | a:hover tt { 393 | background: #EEE; 394 | } 395 | 396 | small { 397 | font-size: 0.9em; 398 | } 399 | 400 | 401 | @media screen and (max-width: 870px) { 402 | 403 | div.sphinxsidebar { 404 | display: none; 405 | } 406 | 407 | div.document { 408 | width: 100%; 409 | 410 | } 411 | 412 | div.documentwrapper { 413 | margin-left: 0; 414 | margin-top: 0; 415 | margin-right: 0; 416 | margin-bottom: 0; 417 | } 418 | 419 | div.bodywrapper { 420 | margin-top: 0; 421 | margin-right: 0; 422 | margin-bottom: 0; 423 | margin-left: 0; 424 | } 425 | 426 | ul { 427 | margin-left: 0; 428 | } 429 | 430 | .document { 431 | width: auto; 432 | } 433 | 434 | .footer { 435 | width: auto; 436 | } 437 | 438 | .bodywrapper { 439 | margin: 0; 440 | } 441 | 442 | .footer { 443 | width: auto; 444 | } 445 | 446 | .github { 447 | display: none; 448 | } 449 | 450 | 451 | 452 | } 453 | 454 | 455 | 456 | @media screen and (max-width: 875px) { 457 | 458 | body { 459 | margin: 0; 460 | padding: 20px 30px; 461 | } 462 | 463 | div.documentwrapper { 464 | float: none; 465 | background: white; 466 | } 467 | 468 | div.sphinxsidebar { 469 | display: block; 470 | float: none; 471 | width: 102.5%; 472 | margin: 50px -30px -20px -30px; 473 | padding: 10px 20px; 474 | background: #333; 475 | color: white; 476 | } 477 | 478 | div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p, 479 | div.sphinxsidebar h3 a { 480 | color: white; 481 | } 482 | 483 | div.sphinxsidebar a { 484 | color: #aaa; 485 | } 486 | 487 | div.sphinxsidebar p.logo { 488 | display: none; 489 | } 490 | 491 | div.document { 492 | width: 100%; 493 | margin: 0; 494 | } 495 | 496 | div.related { 497 | display: block; 498 | margin: 0; 499 | padding: 10px 0 20px 0; 500 | } 501 | 502 | div.related ul, 503 | div.related ul li { 504 | margin: 0; 505 | padding: 0; 506 | } 507 | 508 | div.footer { 509 | display: none; 510 | } 511 | 512 | div.bodywrapper { 513 | margin: 0; 514 | } 515 | 516 | div.body { 517 | min-height: 0; 518 | padding: 0; 519 | } 520 | 521 | .rtd_doc_footer { 522 | display: none; 523 | } 524 | 525 | .document { 526 | width: auto; 527 | } 528 | 529 | .footer { 530 | width: auto; 531 | } 532 | 533 | .footer { 534 | width: auto; 535 | } 536 | 537 | .github { 538 | display: none; 539 | } 540 | } 541 | 542 | 543 | /* scrollbars */ 544 | 545 | ::-webkit-scrollbar { 546 | width: 6px; 547 | height: 6px; 548 | } 549 | 550 | ::-webkit-scrollbar-button:start:decrement, 551 | ::-webkit-scrollbar-button:end:increment { 552 | display: block; 553 | height: 10px; 554 | } 555 | 556 | ::-webkit-scrollbar-button:vertical:increment { 557 | background-color: #fff; 558 | } 559 | 560 | ::-webkit-scrollbar-track-piece { 561 | background-color: #eee; 562 | -webkit-border-radius: 3px; 563 | } 564 | 565 | ::-webkit-scrollbar-thumb:vertical { 566 | height: 50px; 567 | background-color: #ccc; 568 | -webkit-border-radius: 3px; 569 | } 570 | 571 | ::-webkit-scrollbar-thumb:horizontal { 572 | width: 50px; 573 | background-color: #ccc; 574 | -webkit-border-radius: 3px; 575 | } 576 | 577 | /* misc. */ 578 | 579 | .revsys-inline { 580 | display: none!important; 581 | } 582 | -------------------------------------------------------------------------------- /docs/_themes/flask/theme.conf: -------------------------------------------------------------------------------- 1 | [theme] 2 | inherit = basic 3 | stylesheet = flasky.css 4 | pygments_style = flask_theme_support.FlaskyStyle 5 | 6 | [options] 7 | index_logo = '' 8 | index_logo_height = 120px 9 | touch_icon = 10 | github_fork = '' 11 | -------------------------------------------------------------------------------- /docs/_themes/flask_theme_support.py: -------------------------------------------------------------------------------- 1 | # flasky extensions. flasky pygments style based on tango style 2 | from pygments.style import Style 3 | from pygments.token import Keyword, Name, Comment, String, Error, \ 4 | Number, Operator, Generic, Whitespace, Punctuation, Other, Literal 5 | 6 | 7 | class FlaskyStyle(Style): 8 | background_color = "#f8f8f8" 9 | default_style = "" 10 | 11 | styles = { 12 | # No corresponding class for the following: 13 | #Text: "", # class: '' 14 | Whitespace: "underline #f8f8f8", # class: 'w' 15 | Error: "#a40000 border:#ef2929", # class: 'err' 16 | Other: "#000000", # class 'x' 17 | 18 | Comment: "italic #8f5902", # class: 'c' 19 | Comment.Preproc: "noitalic", # class: 'cp' 20 | 21 | Keyword: "bold #004461", # class: 'k' 22 | Keyword.Constant: "bold #004461", # class: 'kc' 23 | Keyword.Declaration: "bold #004461", # class: 'kd' 24 | Keyword.Namespace: "bold #004461", # class: 'kn' 25 | Keyword.Pseudo: "bold #004461", # class: 'kp' 26 | Keyword.Reserved: "bold #004461", # class: 'kr' 27 | Keyword.Type: "bold #004461", # class: 'kt' 28 | 29 | Operator: "#582800", # class: 'o' 30 | Operator.Word: "bold #004461", # class: 'ow' - like keywords 31 | 32 | Punctuation: "bold #000000", # class: 'p' 33 | 34 | # because special names such as Name.Class, Name.Function, etc. 35 | # are not recognized as such later in the parsing, we choose them 36 | # to look the same as ordinary variables. 37 | Name: "#000000", # class: 'n' 38 | Name.Attribute: "#c4a000", # class: 'na' - to be revised 39 | Name.Builtin: "#004461", # class: 'nb' 40 | Name.Builtin.Pseudo: "#3465a4", # class: 'bp' 41 | Name.Class: "#000000", # class: 'nc' - to be revised 42 | Name.Constant: "#000000", # class: 'no' - to be revised 43 | Name.Decorator: "#888", # class: 'nd' - to be revised 44 | Name.Entity: "#ce5c00", # class: 'ni' 45 | Name.Exception: "bold #cc0000", # class: 'ne' 46 | Name.Function: "#000000", # class: 'nf' 47 | Name.Property: "#000000", # class: 'py' 48 | Name.Label: "#f57900", # class: 'nl' 49 | Name.Namespace: "#000000", # class: 'nn' - to be revised 50 | Name.Other: "#000000", # class: 'nx' 51 | Name.Tag: "bold #004461", # class: 'nt' - like a keyword 52 | Name.Variable: "#000000", # class: 'nv' - to be revised 53 | Name.Variable.Class: "#000000", # class: 'vc' - to be revised 54 | Name.Variable.Global: "#000000", # class: 'vg' - to be revised 55 | Name.Variable.Instance: "#000000", # class: 'vi' - to be revised 56 | 57 | Number: "#990000", # class: 'm' 58 | 59 | Literal: "#000000", # class: 'l' 60 | Literal.Date: "#000000", # class: 'ld' 61 | 62 | String: "#4e9a06", # class: 's' 63 | String.Backtick: "#4e9a06", # class: 'sb' 64 | String.Char: "#4e9a06", # class: 'sc' 65 | String.Doc: "italic #8f5902", # class: 'sd' - like a comment 66 | String.Double: "#4e9a06", # class: 's2' 67 | String.Escape: "#4e9a06", # class: 'se' 68 | String.Heredoc: "#4e9a06", # class: 'sh' 69 | String.Interpol: "#4e9a06", # class: 'si' 70 | String.Other: "#4e9a06", # class: 'sx' 71 | String.Regex: "#4e9a06", # class: 'sr' 72 | String.Single: "#4e9a06", # class: 's1' 73 | String.Symbol: "#4e9a06", # class: 'ss' 74 | 75 | Generic: "#000000", # class: 'g' 76 | Generic.Deleted: "#a40000", # class: 'gd' 77 | Generic.Emph: "italic #000000", # class: 'ge' 78 | Generic.Error: "#ef2929", # class: 'gr' 79 | Generic.Heading: "bold #000080", # class: 'gh' 80 | Generic.Inserted: "#00A000", # class: 'gi' 81 | Generic.Output: "#888", # class: 'go' 82 | Generic.Prompt: "#745334", # class: 'gp' 83 | Generic.Strong: "bold #000000", # class: 'gs' 84 | Generic.Subheading: "bold #800080", # class: 'gu' 85 | Generic.Traceback: "bold #a40000", # class: 'gt' 86 | } 87 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Flask documentation build configuration file, created by 4 | # sphinx-quickstart on Tue Apr 6 15:24:58 2010. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | from __future__ import print_function 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | sys.path.append(os.path.abspath('_themes')) 20 | sys.path.append(os.path.abspath('.')) 21 | 22 | # -- General configuration ----------------------------------------------------- 23 | 24 | # If your documentation needs a minimal Sphinx version, state it here. 25 | #needs_sphinx = '1.0' 26 | 27 | # Add any Sphinx extension module names here, as strings. They can be extensions 28 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 29 | extensions = [ 30 | 'sphinx.ext.autodoc', 31 | 'sphinx.ext.intersphinx', 32 | 'flaskdocext' 33 | ] 34 | 35 | # Add any paths that contain templates here, relative to this directory. 36 | templates_path = ['_templates'] 37 | 38 | # The suffix of source filenames. 39 | source_suffix = '.rst' 40 | 41 | # The encoding of source files. 42 | #source_encoding = 'utf-8-sig' 43 | 44 | # The master toctree document. 45 | master_doc = 'index' 46 | 47 | # General information about the project. 48 | project = u'Flask-Ask' 49 | copyright = u'2016, John Wheeler' 50 | 51 | # The language for content autogenerated by Sphinx. Refer to documentation 52 | # for a list of supported languages. 53 | #language = None 54 | 55 | # There are two options for replacing |today|: either, you set today to some 56 | # non-false value, then it is used: 57 | #today = '' 58 | # Else, today_fmt is used as the format for a strftime call. 59 | #today_fmt = '%B %d, %Y' 60 | 61 | # List of patterns, relative to source directory, that match files and 62 | # directories to ignore when looking for source files. 63 | exclude_patterns = ['_build'] 64 | 65 | # The reST default role (used for this markup: `text`) to use for all documents. 66 | #default_role = None 67 | 68 | # If true, '()' will be appended to :func: etc. cross-reference text. 69 | #add_function_parentheses = True 70 | 71 | # If true, the current module name will be prepended to all description 72 | # unit titles (such as .. function::). 73 | #add_module_names = True 74 | 75 | # If true, sectionauthor and moduleauthor directives will be shown in the 76 | # output. They are ignored by default. 77 | #show_authors = False 78 | 79 | # A list of ignored prefixes for module index sorting. 80 | #modindex_common_prefix = [] 81 | 82 | 83 | # -- Options for HTML output --------------------------------------------------- 84 | 85 | # The theme to use for HTML and HTML Help pages. Major themes that come with 86 | # Sphinx are currently 'default' and 'sphinxdoc'. 87 | html_theme = 'flask' 88 | 89 | # Theme options are theme-specific and customize the look and feel of a theme 90 | # further. For a list of options available for each theme, see the 91 | # documentation. 92 | html_theme_options = { 93 | 'github_fork': 'johnwheeler/flask-ask' 94 | } 95 | 96 | html_sidebars = { 97 | 'index': ['globaltoc.html', 'links.html', 'stayinformed.html'], 98 | '**': ['sidebarlogo.html', 'globaltoc.html', 'links.html', 'stayinformed.html'] 99 | } 100 | 101 | # Add any paths that contain custom themes here, relative to this directory. 102 | html_theme_path = ['_themes'] 103 | 104 | # The name for this set of Sphinx documents. If None, it defaults to 105 | # " v documentation". 106 | #html_title = None 107 | 108 | # A shorter title for the navigation bar. Default is the same as html_title. 109 | #html_short_title = None 110 | 111 | # The name of an image file (relative to this directory) to place at the top 112 | # of the sidebar. Do not set, template magic! 113 | #html_logo = None 114 | 115 | # The name of an image file (within the static path) to use as favicon of the 116 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 117 | # pixels large. 118 | #html_favicon = "flask-favicon.ico" 119 | 120 | # Add any paths that contain custom static files (such as style sheets) here, 121 | # relative to this directory. They are copied after the builtin static files, 122 | # so a file named "default.css" will overwrite the builtin "default.css". 123 | html_static_path = ['_static'] 124 | 125 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 126 | # using the given strftime format. 127 | #html_last_updated_fmt = '%b %d, %Y' 128 | 129 | # If true, SmartyPants will be used to convert quotes and dashes to 130 | # typographically correct entities. 131 | #html_use_smartypants = True 132 | 133 | # Custom sidebar templates, maps document names to template names. 134 | # html_sidebars = { 135 | # 'index': ['sidebarintro.html', 'sourcelink.html', 'searchbox.html'], 136 | # '**': ['sidebarlogo.html', 'localtoc.html', 'relations.html', 137 | # 'sourcelink.html', 'searchbox.html'] 138 | # } 139 | 140 | # Additional templates that should be rendered to pages, maps page names to 141 | # template names. 142 | #html_additional_pages = {} 143 | 144 | # If false, no module index is generated. 145 | html_use_modindex = False 146 | 147 | # If false, no index is generated. 148 | #html_use_index = True 149 | 150 | # If true, the index is split into individual pages for each letter. 151 | #html_split_index = False 152 | 153 | # If true, links to the reST sources are added to the pages. 154 | #html_show_sourcelink = True 155 | 156 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 157 | html_show_sphinx = False 158 | 159 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 160 | #html_show_copyright = True 161 | 162 | # If true, an OpenSearch description file will be output, and all pages will 163 | # contain a tag referring to it. The value of this option must be the 164 | # base URL from which the finished HTML is served. 165 | #html_use_opensearch = '' 166 | 167 | # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). 168 | #html_file_suffix = '' 169 | 170 | # -- Options for Epub output --------------------------------------------------- 171 | 172 | # Bibliographic Dublin Core info. 173 | #epub_title = '' 174 | #epub_author = '' 175 | #epub_publisher = '' 176 | #epub_copyright = '' 177 | 178 | # The language of the text. It defaults to the language option 179 | # or en if the language is not set. 180 | #epub_language = '' 181 | 182 | # The scheme of the identifier. Typical schemes are ISBN or URL. 183 | #epub_scheme = '' 184 | 185 | # The unique identifier of the text. This can be a ISBN number 186 | # or the project homepage. 187 | #epub_identifier = '' 188 | 189 | # A unique identification for the text. 190 | #epub_uid = '' 191 | 192 | # HTML files that should be inserted before the pages created by sphinx. 193 | # The format is a list of tuples containing the path and title. 194 | #epub_pre_files = [] 195 | 196 | # HTML files shat should be inserted after the pages created by sphinx. 197 | # The format is a list of tuples containing the path and title. 198 | #epub_post_files = [] 199 | 200 | # A list of files that should not be packed into the epub file. 201 | #epub_exclude_files = [] 202 | 203 | # The depth of the table of contents in toc.ncx. 204 | #epub_tocdepth = 3 205 | -------------------------------------------------------------------------------- /docs/configuration.rst: -------------------------------------------------------------------------------- 1 | Configuration 2 | ============= 3 | 4 | Configuration 5 | ------------- 6 | 7 | Flask-Ask exposes the following configuration variables: 8 | 9 | ============================ ============================================================================================ 10 | `ASK_APPLICATION_ID` Turn on application ID verification by setting this variable to an application ID or a 11 | list of allowed application IDs. By default, application ID verification is disabled and a 12 | warning is logged. This variable should be set in production to ensure 13 | requests are being sent by the applications you specify. **Default:** ``None`` 14 | `ASK_VERIFY_REQUESTS` Enables or disables 15 | `Alexa request verification `_, 16 | which ensures requests sent to your skill are 17 | from Amazon's Alexa service. This setting should not be disabled in production. It is 18 | useful for mocking JSON requests in automated tests. **Default:** ``True`` 19 | `ASK_VERIFY_TIMESTAMP_DEBUG` Turn on request timestamp verification while debugging by setting this to ``True``. 20 | Timestamp verification helps mitigate against 21 | `replay attacks `_. It 22 | relies on the system clock being synchronized with an NTP server. This setting should not 23 | be enabled in production. **Default:** ``False`` 24 | ============================ ============================================================================================ 25 | 26 | Logging 27 | ------- 28 | 29 | To see the JSON request / response structures pretty printed in the logs, turn on ``DEBUG``-level logging:: 30 | 31 | import logging 32 | 33 | logging.getLogger('flask_ask').setLevel(logging.DEBUG) 34 | -------------------------------------------------------------------------------- /docs/contents.rst.inc: -------------------------------------------------------------------------------- 1 | Table Of Contents 2 | ----------------- 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | getting_started 8 | requests 9 | responses 10 | configuration 11 | user_contributions 12 | -------------------------------------------------------------------------------- /docs/flaskdocext.py: -------------------------------------------------------------------------------- 1 | import re 2 | import inspect 3 | 4 | 5 | _internal_mark_re = re.compile(r'^\s*:internal:\s*$(?m)') 6 | 7 | 8 | def skip_member(app, what, name, obj, skip, options): 9 | docstring = inspect.getdoc(obj) 10 | if skip: 11 | return True 12 | return _internal_mark_re.search(docstring or '') is not None 13 | 14 | 15 | def setup(app): 16 | app.connect('autodoc-skip-member', skip_member) 17 | -------------------------------------------------------------------------------- /docs/getting_started.rst: -------------------------------------------------------------------------------- 1 | Getting Started 2 | =============== 3 | 4 | Installation 5 | ------------ 6 | To install Flask-Ask:: 7 | 8 | pip install flask-ask 9 | 10 | 11 | A Minimal Voice User Interface 12 | ------------------------------ 13 | A Flask-Ask application looks like this: 14 | 15 | .. code-block:: python 16 | 17 | from flask import Flask, render_template 18 | from flask_ask import Ask, statement 19 | 20 | app = Flask(__name__) 21 | ask = Ask(app, '/') 22 | 23 | @ask.intent('HelloIntent') 24 | def hello(firstname): 25 | text = render_template('hello', firstname=firstname) 26 | return statement(text).simple_card('Hello', text) 27 | 28 | if __name__ == '__main__': 29 | app.run(debug=True) 30 | 31 | In the code above: 32 | 33 | #. The ``Ask`` object is created by passing in the Flask application and a route to forward Alexa requests to. 34 | #. The ``intent`` decorator maps ``HelloIntent`` to a view function ``hello``. 35 | #. The intent's ``firstname`` slot is implicitly mapped to ``hello``'s ``firstname`` parameter. 36 | #. Jinja templates are supported. Internally, templates are loaded from a YAML file (discussed further below). 37 | #. Lastly, a builder constructs a spoken response and displays a contextual card in the Alexa smartphone/tablet app. 38 | 39 | Since Alexa responses are usually short phrases, it's convenient to put them in the same file. 40 | Flask-Ask has a `Jinja template loader `_ that loads 41 | multiple templates from a single YAML file. For example, here's a template that supports the minimal voice interface 42 | above.Templates are stored in a file called `templates.yaml` located in the application root: 43 | 44 | .. code-block:: yaml 45 | 46 | hello: Hello, {{ firstname }} 47 | 48 | For more information about how the Alexa Skills Kit works, see `Understanding Custom Skills `_ in the Alexa Skills Kit documentation. 49 | 50 | Additionally, more code and template examples are in the `samples `_ directory. 51 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | :orphan: 2 | 3 | .. image:: _static/logo-full.png 4 | :alt: Flask-Ask: Alexa Skills Kit Development for Amazon Echo Devices with Python 5 | 6 | 7 | 😎 `Lighten your cognitive load. Level up with the Alexa Skills Kit Video Tutorial `_. 8 | 9 | 10 | .. raw:: html 11 | 12 |

13 | Star 14 |

15 | 16 | 17 | Welcome to Flask-Ask 18 | ==================== 19 | 20 | Building high-quality Alexa skills for Amazon Echo Devices takes time. Flask-Ask makes it easier and much more fun. 21 | Use Flask-Ask with `ngrok `_ to eliminate the deploy-to-test step and get work done faster. 22 | 23 | Flask-Ask: 24 | 25 | * Has decorators to map Alexa requests and intent slots to view functions 26 | * Helps construct ask and tell responses, reprompts and cards 27 | * Makes session management easy 28 | * Allows for the separation of code and speech through Jinja templates 29 | * Verifies Alexa request signatures 30 | 31 | .. raw:: html 32 | 33 |
34 | 35 | 36 | Follow along with this quickstart on `Amazon 37 | `_. 38 | 39 | .. include:: contents.rst.inc 40 | 41 | .. raw:: html 42 | 43 |
44 | 45 | 46 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. epub3 to make an epub3 31 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 32 | echo. text to make text files 33 | echo. man to make manual pages 34 | echo. texinfo to make Texinfo files 35 | echo. gettext to make PO message catalogs 36 | echo. changes to make an overview over all changed/added/deprecated items 37 | echo. xml to make Docutils-native XML files 38 | echo. pseudoxml to make pseudoxml-XML files for display purposes 39 | echo. linkcheck to check all external links for integrity 40 | echo. doctest to run all doctests embedded in the documentation if enabled 41 | echo. coverage to run coverage check of the documentation if enabled 42 | echo. dummy to check syntax errors of document sources 43 | goto end 44 | ) 45 | 46 | if "%1" == "clean" ( 47 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 48 | del /q /s %BUILDDIR%\* 49 | goto end 50 | ) 51 | 52 | 53 | REM Check if sphinx-build is available and fallback to Python version if any 54 | %SPHINXBUILD% 1>NUL 2>NUL 55 | if errorlevel 9009 goto sphinx_python 56 | goto sphinx_ok 57 | 58 | :sphinx_python 59 | 60 | set SPHINXBUILD=python -m sphinx.__init__ 61 | %SPHINXBUILD% 2> nul 62 | if errorlevel 9009 ( 63 | echo. 64 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 65 | echo.installed, then set the SPHINXBUILD environment variable to point 66 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 67 | echo.may add the Sphinx directory to PATH. 68 | echo. 69 | echo.If you don't have Sphinx installed, grab it from 70 | echo.http://sphinx-doc.org/ 71 | exit /b 1 72 | ) 73 | 74 | :sphinx_ok 75 | 76 | 77 | if "%1" == "html" ( 78 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 79 | if errorlevel 1 exit /b 1 80 | echo. 81 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 82 | goto end 83 | ) 84 | 85 | if "%1" == "dirhtml" ( 86 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 87 | if errorlevel 1 exit /b 1 88 | echo. 89 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 90 | goto end 91 | ) 92 | 93 | if "%1" == "singlehtml" ( 94 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 95 | if errorlevel 1 exit /b 1 96 | echo. 97 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 98 | goto end 99 | ) 100 | 101 | if "%1" == "pickle" ( 102 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 103 | if errorlevel 1 exit /b 1 104 | echo. 105 | echo.Build finished; now you can process the pickle files. 106 | goto end 107 | ) 108 | 109 | if "%1" == "json" ( 110 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 111 | if errorlevel 1 exit /b 1 112 | echo. 113 | echo.Build finished; now you can process the JSON files. 114 | goto end 115 | ) 116 | 117 | if "%1" == "htmlhelp" ( 118 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 119 | if errorlevel 1 exit /b 1 120 | echo. 121 | echo.Build finished; now you can run HTML Help Workshop with the ^ 122 | .hhp project file in %BUILDDIR%/htmlhelp. 123 | goto end 124 | ) 125 | 126 | if "%1" == "qthelp" ( 127 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 128 | if errorlevel 1 exit /b 1 129 | echo. 130 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 131 | .qhcp project file in %BUILDDIR%/qthelp, like this: 132 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Flask-Ask.qhcp 133 | echo.To view the help file: 134 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Flask-Ask.ghc 135 | goto end 136 | ) 137 | 138 | if "%1" == "devhelp" ( 139 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 140 | if errorlevel 1 exit /b 1 141 | echo. 142 | echo.Build finished. 143 | goto end 144 | ) 145 | 146 | if "%1" == "epub" ( 147 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 148 | if errorlevel 1 exit /b 1 149 | echo. 150 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 151 | goto end 152 | ) 153 | 154 | if "%1" == "epub3" ( 155 | %SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3 156 | if errorlevel 1 exit /b 1 157 | echo. 158 | echo.Build finished. The epub3 file is in %BUILDDIR%/epub3. 159 | goto end 160 | ) 161 | 162 | if "%1" == "latex" ( 163 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 164 | if errorlevel 1 exit /b 1 165 | echo. 166 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdf" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "latexpdfja" ( 181 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 182 | cd %BUILDDIR%/latex 183 | make all-pdf-ja 184 | cd %~dp0 185 | echo. 186 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 187 | goto end 188 | ) 189 | 190 | if "%1" == "text" ( 191 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 192 | if errorlevel 1 exit /b 1 193 | echo. 194 | echo.Build finished. The text files are in %BUILDDIR%/text. 195 | goto end 196 | ) 197 | 198 | if "%1" == "man" ( 199 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 200 | if errorlevel 1 exit /b 1 201 | echo. 202 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 203 | goto end 204 | ) 205 | 206 | if "%1" == "texinfo" ( 207 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 208 | if errorlevel 1 exit /b 1 209 | echo. 210 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 211 | goto end 212 | ) 213 | 214 | if "%1" == "gettext" ( 215 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 216 | if errorlevel 1 exit /b 1 217 | echo. 218 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 219 | goto end 220 | ) 221 | 222 | if "%1" == "changes" ( 223 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 224 | if errorlevel 1 exit /b 1 225 | echo. 226 | echo.The overview file is in %BUILDDIR%/changes. 227 | goto end 228 | ) 229 | 230 | if "%1" == "linkcheck" ( 231 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 232 | if errorlevel 1 exit /b 1 233 | echo. 234 | echo.Link check complete; look for any errors in the above output ^ 235 | or in %BUILDDIR%/linkcheck/output.txt. 236 | goto end 237 | ) 238 | 239 | if "%1" == "doctest" ( 240 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 241 | if errorlevel 1 exit /b 1 242 | echo. 243 | echo.Testing of doctests in the sources finished, look at the ^ 244 | results in %BUILDDIR%/doctest/output.txt. 245 | goto end 246 | ) 247 | 248 | if "%1" == "coverage" ( 249 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 250 | if errorlevel 1 exit /b 1 251 | echo. 252 | echo.Testing of coverage in the sources finished, look at the ^ 253 | results in %BUILDDIR%/coverage/python.txt. 254 | goto end 255 | ) 256 | 257 | if "%1" == "xml" ( 258 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 259 | if errorlevel 1 exit /b 1 260 | echo. 261 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 262 | goto end 263 | ) 264 | 265 | if "%1" == "pseudoxml" ( 266 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 267 | if errorlevel 1 exit /b 1 268 | echo. 269 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 270 | goto end 271 | ) 272 | 273 | if "%1" == "dummy" ( 274 | %SPHINXBUILD% -b dummy %ALLSPHINXOPTS% %BUILDDIR%/dummy 275 | if errorlevel 1 exit /b 1 276 | echo. 277 | echo.Build finished. Dummy builder generates no files. 278 | goto end 279 | ) 280 | 281 | :end 282 | -------------------------------------------------------------------------------- /docs/requests.rst: -------------------------------------------------------------------------------- 1 | Handling Requests 2 | ================= 3 | 4 | With the Alexa Skills Kit, spoken phrases are mapped to actions executed on a server. Alexa converts 5 | speech into JSON and delivers the JSON to your application. 6 | For example, the phrase: 7 | 8 | "Alexa, Tell HelloApp to say hi to John" 9 | 10 | produces JSON like the following: 11 | 12 | .. code-block:: javascript 13 | 14 | "request": { 15 | "intent": { 16 | "name": "HelloIntent", 17 | "slots": { 18 | "firstname": { 19 | "name": "firstname", 20 | "value": "John" 21 | } 22 | } 23 | } 24 | ... 25 | } 26 | 27 | Parameters called 'slots' are defined and parsed out of speech at runtime. 28 | For example, the spoken word 'John' above is parsed into the slot named ``firstname`` with the ``AMAZON.US_FIRST_NAME`` 29 | data type. 30 | 31 | For detailed information, see 32 | `Handling Requests Sent by Alexa `_ 33 | on the Amazon developer website. 34 | 35 | This section shows how to process Alexa requests with Flask-Ask. It contains the following subsections: 36 | 37 | .. contents:: 38 | :local: 39 | :backlinks: none 40 | 41 | Mapping Alexa Requests to View Functions 42 | ---------------------------------------- 43 | 44 | 📼 Here is a video demo on `Handling Requests with Flask-Ask video `_. 45 | 46 | Flask-Ask has decorators to map Alexa requests to view functions. 47 | 48 | The ``launch`` decorator handles launch requests:: 49 | 50 | @ask.launch 51 | def launched(): 52 | return question('Welcome to Foo') 53 | 54 | The ``intent`` decorator handles intent requests:: 55 | 56 | @ask.intent('HelloWorldIntent') 57 | def hello(): 58 | return statement('Hello, world') 59 | 60 | The ``session_ended`` decorator is for the session ended request:: 61 | 62 | @ask.session_ended 63 | def session_ended(): 64 | return "{}", 200 65 | 66 | Launch and intent requests can both start sessions. Avoid duplicate code with the ``on_session_started`` callback:: 67 | 68 | @ask.on_session_started 69 | def new_session(): 70 | log.info('new session started') 71 | 72 | 73 | Mapping Intent Slots to View Function Parameters 74 | ------------------------------------------------ 75 | 76 | 📼 Here is a video demo on `Intent Slots with Flask-Ask video `_. 77 | 78 | 79 | When Parameter and Slot Names Differ 80 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 81 | 82 | Tell Flask-Ask when slot and view function parameter names differ with ``mapping``:: 83 | 84 | @ask.intent('WeatherIntent', mapping={'city': 'City'}) 85 | def weather(city): 86 | return statement('I predict great weather for {}'.format(city)) 87 | 88 | Above, the parameter ``city`` is mapped to the slot ``City``. 89 | 90 | 91 | Assigning Default Values when Slots are Empty 92 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 93 | 94 | Parameters are assigned a value of ``None`` if the Alexa service: 95 | 96 | * Does not return a corresponding slot in the request 97 | * Includes a corresponding slot without its ``value`` attribute 98 | * Includes a corresponding slot with an empty ``value`` attribute (e.g. ``""``) 99 | 100 | Use the ``default`` parameter for default values instead of ``None``. The default itself should be a 101 | literal or a callable that resolves to a value. The next example shows the literal ``'World'``:: 102 | 103 | @ask.intent('HelloIntent', default={'name': 'World'}) 104 | def hello(name): 105 | return statement('Hello, {}'.format(name)) 106 | 107 | 108 | Converting Slots Values to Python Data Types 109 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 110 | 111 | 📼 Here is a video demo on `Slot Conversions with Flask-Ask video `_. 112 | 113 | When slot values are available, they're always assigned to parameters as strings. Convert to other Python 114 | data types with ``convert``. ``convert`` is a ``dict`` that maps parameter names to callables:: 115 | 116 | @ask.intent('AddIntent', convert={'x': int, 'y': int}) 117 | def add(x, y): 118 | z = x + y 119 | return statement('{} plus {} equals {}'.format(x, y, z)) 120 | 121 | 122 | Above, ``x`` and ``y`` will both be passed to ``int()`` and thus converted to ``int`` instances. 123 | 124 | Flask-Ask provides convenient API constants for Amazon ``AMAZON.DATE``, ``AMAZON.TIME``, and ``AMAZON.DURATION`` 125 | types exist since those are harder to build callables against. Instead of trying to define functions that work with 126 | inputs like those in Amazon's 127 | `documentation `_, 128 | just pass the strings in the second column below: 129 | 130 | 📼 Here is a video demo on `Slot Conversion Helpers with Flask-Ask video `_. 131 | 132 | =================== =============== ====================== 133 | Amazon Data Type String Python Data Type 134 | =================== =============== ====================== 135 | ``AMAZON.DATE`` ``'date'`` ``datetime.date`` 136 | ``AMAZON.TIME`` ``'time'`` ``datetime.time`` 137 | ``AMAZON.DURATION`` ``'timedelta'`` ``datetime.timedelta`` 138 | =================== =============== ====================== 139 | 140 | **Examples** 141 | 142 | .. code-block:: python 143 | 144 | convert={'the_date': 'date'} 145 | 146 | converts ``'2015-11-24'``, ``'2015-W48-WE'``, or ``'201X'`` into a ``datetime.date`` 147 | 148 | .. code-block:: python 149 | 150 | convert={'appointment_time': 'time'} 151 | 152 | converts ``'06:00'``, ``'14:15'``, or ``'23:59'`` into a ``datetime.time``. 153 | 154 | .. code-block:: python 155 | 156 | convert={'ago': 'timedelta'} 157 | 158 | converts ``'PT10M'``, ``'PT45S'``, or ``'P2YT3H10M'`` into a ``datetime.timedelta``. 159 | 160 | 161 | Handling Conversion Errors 162 | ^^^^^^^^^^^^^^^^^^^^^^^^^^ 163 | 164 | Sometimes Alexa doesn't understand what's said, and slots come in with question marks: 165 | 166 | .. code-block:: javascript 167 | 168 | "slots": { 169 | "age": { 170 | "name": "age", 171 | "value": "?" 172 | } 173 | } 174 | 175 | Recover gracefully with the ``convert_errors`` context local. Import it to use it: 176 | 177 | .. code-block:: python 178 | 179 | ... 180 | from flask_ask import statement, question, convert_errors 181 | 182 | 183 | @ask.intent('AgeIntent', convert={'age': int}) 184 | def say_age(age): 185 | if 'age' in convert_errors: 186 | # since age failed to convert, it keeps its string 187 | # value (e.g. "?") for later interrogation. 188 | return question("Can you please repeat your age?") 189 | 190 | # conversion guaranteed to have succeeded 191 | # age is an int 192 | return statement("Your age is {}".format(age)) 193 | 194 | 195 | ``convert_errors`` is a ``dict`` that maps parameter names to the ``Exceptions`` raised during 196 | conversion. When writing your own converters, raise ``Exceptions`` on failure, so 197 | they work with ``convert_errors``:: 198 | 199 | def to_direction_const(s): 200 | if s.lower() not in ['left', 'right'] 201 | raise Exception("must be left or right") 202 | return LEFT if s == 'left' else RIGHT 203 | 204 | @ask.intent('TurnIntent', convert={'direction': to_direction_const}) 205 | def turn(direction): 206 | # do something with direction 207 | ... 208 | 209 | 210 | That ``convert_errors`` is a ``dict`` allows for granular error recovery:: 211 | 212 | if 'something' in convert_errors: 213 | # Did something fail? 214 | 215 | or:: 216 | 217 | if convert_errors: 218 | # Did anything fail? 219 | 220 | 221 | 222 | ``session``, ``context``, ``request`` and ``version`` Context Locals 223 | --------------------------------------------------------------------- 224 | An Alexa 225 | `request payload `_ 226 | has four top-level elements: ``session``, ``context``, ``request`` and ``version``. Like Flask, Flask-Ask provides `context 227 | locals `_ that spare you from having to add these as extra parameters to 228 | your functions. However, the ``request`` and ``session`` objects are distinct from Flask's ``request`` and ``session``. 229 | Flask-Ask's ``request``, ``context`` and ``session`` correspond to the Alexa request payload components while Flask's correspond 230 | to lower-level HTTP constructs. 231 | 232 | To use Flask-Ask's context locals, just import them:: 233 | 234 | from flask import App 235 | from flask_ask import Ask, request, context, session, version 236 | 237 | app = Flask(__name__) 238 | ask = Ask(app) 239 | log = logging.getLogger() 240 | 241 | @ask.intent('ExampleIntent') 242 | def example(): 243 | log.info("Request ID: {}".format(request.requestId)) 244 | log.info("Request Type: {}".format(request.type)) 245 | log.info("Request Timestamp: {}".format(request.timestamp)) 246 | log.info("Session New?: {}".format(session.new)) 247 | log.info("User ID: {}".format(session.user.userId)) 248 | log.info("Alexa Version: {}".format(version)) 249 | log.info("Device ID: {}".format(context.System.device.deviceId)) 250 | log.info("Consent Token: {}".format(context.System.user.permissions.consentToken)) 251 | ... 252 | 253 | If you want to use both Flask and Flask-Ask context locals in the same module, use ``import as``:: 254 | 255 | from flask import App, request, session 256 | from flask_ask import ( 257 | Ask, 258 | request as ask_request, 259 | session as ask_session, 260 | version 261 | ) 262 | 263 | For a complete reference on ``request``, ``context`` and ``session`` fields, see the 264 | `JSON Interface Reference for Custom Skills `_ 265 | in the Alexa Skills Kit documentation. 266 | -------------------------------------------------------------------------------- /docs/responses.rst: -------------------------------------------------------------------------------- 1 | Building Responses 2 | ================== 3 | 4 | 📼 Here is a video demo on `Building Responses with Flask-Ask video `_ . 5 | 6 | The two primary constructs in Flask-Ask for creating responses are ``statement`` and ``question``. 7 | 8 | Statements terminate Echo sessions. The user is free to start another session, but Alexa will have no memory of it 9 | (unless persistence is programmed separately on the server with a database or the like). 10 | 11 | A ``question``, on the other hand, prompts the user for additional speech and keeps a session open. 12 | This session is similar to an HTTP session but the implementation is different. Since your application is 13 | communicating with the Alexa service instead of a browser, there are no cookies or local storage. Instead, the 14 | session is maintained in both the request and response JSON structures. In addition to the session component of 15 | questions, questions also allow a ``reprompt``, which is typically a rephrasing of the question if user did not answer 16 | the first time. 17 | 18 | This sections shows how to build responses with Flask-Ask. It contains the following subsections: 19 | 20 | .. contents:: 21 | :local: 22 | :backlinks: none 23 | 24 | Telling with ``statement`` 25 | -------------------------- 26 | ``statement`` closes the session:: 27 | 28 | @ask.intent('AllYourBaseIntent') 29 | def all_your_base(): 30 | return statement('All your base are belong to us') 31 | 32 | 33 | Asking with ``question`` 34 | ------------------------ 35 | Asking with ``question`` prompts the user for a response while keeping the session open:: 36 | 37 | @ask.intent('AppointmentIntent') 38 | def make_appointment(): 39 | return question("What day would you like to make an appointment for?") 40 | 41 | If the user doesn't respond, encourage them by rephrasing the question with ``reprompt``:: 42 | 43 | @ask.intent('AppointmentIntent') 44 | def make_appointment(): 45 | return question("What day would you like to make an appointment for?") \ 46 | .reprompt("I didn't get that. When would you like to be seen?") 47 | 48 | 49 | Session Management 50 | ------------------ 51 | 52 | The ``session`` context local has an ``attributes`` dictionary for persisting information across requests:: 53 | 54 | session.attributes['city'] = "San Francisco" 55 | 56 | When the response is rendered, the session attributes are automatically copied over into 57 | the response's ``sessionAttributes`` structure. 58 | 59 | The renderer looks for an ``attribute_encoder`` on the session. If the renderer finds one, it will pass it to 60 | ``json.dumps`` as either that function's ``cls`` or ``default`` keyword parameters depending on whether 61 | a ``json.JSONEncoder`` or a function is used, respectively. 62 | 63 | Here's an example that uses a function:: 64 | 65 | def _json_date_handler(obj): 66 | if isinstance(obj, datetime.date): 67 | return obj.isoformat() 68 | 69 | session.attributes['date'] = date 70 | session.attributes_encoder = _json_date_handler 71 | 72 | See the `json.dump documentation `_ for for details about 73 | that method's ``cls`` and ``default`` parameters. 74 | 75 | 76 | Automatic Handling of Plaintext and SSML 77 | ---------------------------------------- 78 | The Alexa Skills Kit supports plain text or 79 | `SSML `_ outputs. Flask-Ask automatically 80 | detects if your speech text contains SSML by attempting to parse it into XML, and checking 81 | if the root element is ``speak``:: 82 | 83 | try: 84 | xmldoc = ElementTree.fromstring(text) 85 | if xmldoc.tag == 'speak': 86 | # output type is 'SSML' 87 | except ElementTree.ParseError: 88 | pass 89 | # output type is 'PlainText' 90 | 91 | 92 | Displaying Cards in the Alexa Smartphone/Tablet App 93 | --------------------------------------------------- 94 | In addition to speaking back, Flask-Ask can display contextual cards in the Alexa smartphone/tablet app. All four 95 | of the Alexa Skills Kit card types are supported. 96 | 97 | Simple cards display a title and message:: 98 | 99 | @ask.intent('AllYourBaseIntent') 100 | def all_your_base(): 101 | return statement('All your base are belong to us') \ 102 | .simple_card(title='CATS says...', content='Make your time') 103 | 104 | Standard cards are like simple cards but they also support small and large image URLs:: 105 | 106 | @ask.intent('AllYourBaseIntent') 107 | def all_your_base(): 108 | return statement('All your base are belong to us') \ 109 | .standard_card(title='CATS says...', 110 | text='Make your time', 111 | small_image_url='https://example.com/small.png', 112 | large_image_url='https://example.com/large.png') 113 | 114 | Link account cards display a link to authorize the Alexa user with a user account in your system. The link displayed is the auhorization URL you configure in the amazon skill developer portal:: 115 | 116 | @ask.intent('AllYourBaseIntent') 117 | def all_your_base(): 118 | return statement('Please link your account in the Alexa app') \ 119 | .link_account_card() 120 | 121 | Consent cards ask for the permission to access the device's address. You can either ask for the country and postal code (`read::alexa:device:all:address:country_and_postal_code`) or for the full address (`read::alexa:device:all:address`). The permission you ask for has to match what you've specified in the amazon skill developer portal:: 122 | 123 | @ask.intent('AllYourBaseIntent') 124 | def all_your_base(): 125 | return statement('Please allow access to your location') \ 126 | .consent_card("read::alexa:device:all:address") 127 | 128 | 129 | Jinja Templates 130 | --------------- 131 | You can also use Jinja templates. Define them in a YAML file named `templates.yaml` inside your application root:: 132 | 133 | @ask.intent('RBelongToUsIntent') 134 | def all_your_base(): 135 | notice = render_template('all_your_base_msg', who='us') 136 | return statement(notice) 137 | 138 | .. code-block:: yaml 139 | 140 | all_your_base_msg: All your base are belong to {{ who }} 141 | 142 | multiple_line_example: | 143 | 144 | I am a multi-line SSML template. My content spans more than one line, 145 | so there's a pipe and a newline that separates my name and value. 146 | Enjoy the sounds of the ocean. 147 | 149 | 150 | You can also use a custom templates file passed into the Ask object:: 151 | 152 | ask = Ask(app, '/', None, 'custom-templates.yml') 153 | -------------------------------------------------------------------------------- /docs/user_contributions.rst: -------------------------------------------------------------------------------- 1 | User Contributions 2 | ================== 3 | 4 | Have an article or video to submit? Please send it to john@johnwheeler.org 5 | 6 | `Flask-Ask: A New Python Framework for Rapid Alexa Skills Kit Development `_ 7 | 8 | by John Wheeler 9 | 10 | `Running with Alexa Part I. `_ 11 | 12 | by Tim Kjær Lange 13 | 14 | `Intro and Skill Logic - Alexa Skills w/ Python and Flask-Ask Part 1 `_ 15 | 16 | by Harrison Kinsley 17 | 18 | `Headlines Function - Alexa Skills w/ Python and Flask-Ask Part 2 `_ 19 | 20 | by Harrison Kinsley 21 | 22 | `Testing our Skill - Alexa Skills w/ Python and Flask-Ask Part 3 `_ 23 | 24 | by Harrison Kinsley 25 | 26 | `Flask-Ask — A tutorial on a simple and easy way to build complex Alexa Skills `_ 27 | 28 | by Bjorn Vuylsteker 29 | -------------------------------------------------------------------------------- /flask_ask/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | logger = logging.getLogger('flask_ask') 4 | logger.addHandler(logging.StreamHandler()) 5 | if logger.level == logging.NOTSET: 6 | logger.setLevel(logging.WARN) 7 | 8 | 9 | from .core import ( 10 | Ask, 11 | request, 12 | session, 13 | version, 14 | context, 15 | current_stream, 16 | convert_errors 17 | ) 18 | 19 | from .models import ( 20 | question, 21 | statement, 22 | audio, 23 | delegate, 24 | elicit_slot, 25 | confirm_slot, 26 | confirm_intent, 27 | buy, 28 | upsell, 29 | refund 30 | ) 31 | -------------------------------------------------------------------------------- /flask_ask/cache.py: -------------------------------------------------------------------------------- 1 | """ 2 | Stream cache functions 3 | """ 4 | 5 | 6 | def push_stream(cache, user_id, stream): 7 | """ 8 | Push a stream onto the stream stack in cache. 9 | 10 | :param cache: werkzeug BasicCache-like object 11 | :param user_id: id of user, used as key in cache 12 | :param stream: stream object to push onto stack 13 | 14 | :return: True on successful update, 15 | False if failed to update, 16 | None if invalid input was given 17 | """ 18 | stack = cache.get(user_id) 19 | if stack is None: 20 | stack = [] 21 | if stream: 22 | stack.append(stream) 23 | return cache.set(user_id, stack) 24 | return None 25 | 26 | 27 | def pop_stream(cache, user_id): 28 | """ 29 | Pop an item off the stack in the cache. If stack 30 | is empty after pop, it deletes the stack. 31 | 32 | :param cache: werkzeug BasicCache-like object 33 | :param user_id: id of user, used as key in cache 34 | 35 | :return: top item from stack, otherwise None 36 | """ 37 | stack = cache.get(user_id) 38 | if stack is None: 39 | return None 40 | 41 | result = stack.pop() 42 | 43 | if len(stack) == 0: 44 | cache.delete(user_id) 45 | else: 46 | cache.set(user_id, stack) 47 | 48 | return result 49 | 50 | 51 | def set_stream(cache, user_id, stream): 52 | """ 53 | Overwrite stack in the cache. 54 | 55 | :param cache: werkzeug BasicCache-liek object 56 | :param user_id: id of user, used as key in cache 57 | :param stream: value to initialize new stack with 58 | 59 | :return: None 60 | """ 61 | if stream: 62 | return cache.set(user_id, [stream]) 63 | 64 | 65 | def top_stream(cache, user_id): 66 | """ 67 | Peek at the top of the stack in the cache. 68 | 69 | :param cache: werkzeug BasicCache-like object 70 | :param user_id: id of user, used as key in cache 71 | 72 | :return: top item in user's cached stack, otherwise None 73 | """ 74 | if not user_id: 75 | return None 76 | 77 | stack = cache.get(user_id) 78 | if stack is None: 79 | return None 80 | return stack.pop() 81 | -------------------------------------------------------------------------------- /flask_ask/convert.py: -------------------------------------------------------------------------------- 1 | import re 2 | from datetime import datetime, time 3 | 4 | import aniso8601 5 | 6 | from . import logger 7 | 8 | 9 | _DATE_PATTERNS = { 10 | # "today", "tomorrow", "november twenty-fifth": 2015-11-25 11 | '^\d{4}-\d{2}-\d{2}$': '%Y-%m-%d', 12 | # "this week", "next week": 2015-W48 13 | '^\d{4}-W\d{2}$': '%Y-W%U-%w', 14 | # "this weekend": 2015-W48-WE 15 | '^\d{4}-W\d{2}-WE$': '%Y-W%U-WE-%w', 16 | # "this month": 2015-11 17 | '^\d{4}-\d{2}$': '%Y-%m', 18 | # "next year": 2016 19 | '^\d{4}$': '%Y', 20 | } 21 | 22 | 23 | def to_date(amazon_date): 24 | # make so 'next decade' matches work against 'next year' regex 25 | amazon_date = re.sub('X$', '0', amazon_date) 26 | for re_pattern, format_pattern in list(_DATE_PATTERNS.items()): 27 | if re.match(re_pattern, amazon_date): 28 | if '%U' in format_pattern: 29 | # http://stackoverflow.com/a/17087427/1163855 30 | amazon_date += '-0' 31 | return datetime.strptime(amazon_date, format_pattern).date() 32 | return None 33 | 34 | 35 | def to_time(amazon_time): 36 | if amazon_time == "AM": 37 | return time(hour=0) 38 | if amazon_time == "PM": 39 | return time(hour=12) 40 | if amazon_time == "MO": 41 | return time(hour=5) 42 | if amazon_time == "AF": 43 | return time(hour=12) 44 | if amazon_time == "EV": 45 | return time(hour=17) 46 | if amazon_time == "NI": 47 | return time(hour=21) 48 | try: 49 | return aniso8601.parse_time(amazon_time) 50 | except ValueError as e: 51 | logger.warn("ValueError for amazon_time '{}'.".format(amazon_time)) 52 | logger.warn("ValueError message: {}".format(e.message)) 53 | return None 54 | 55 | 56 | def to_timedelta(amazon_duration): 57 | return aniso8601.parse_duration(amazon_duration) 58 | -------------------------------------------------------------------------------- /flask_ask/models.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | from flask import json 3 | from xml.etree import ElementTree 4 | import aniso8601 5 | from .core import session, context, current_stream, stream_cache, dbgdump 6 | from .cache import push_stream 7 | import uuid 8 | 9 | 10 | class _Field(dict): 11 | """Container to represent Alexa Request Data. 12 | 13 | Initialized with request_json and creates a dict object with attributes 14 | to be accessed via dot notation or as a dict key-value. 15 | 16 | Parameters within the request_json that contain their data as a json object 17 | are also represented as a _Field object. 18 | 19 | Example: 20 | 21 | payload_object = _Field(alexa_json_payload) 22 | 23 | request_type_from_keys = payload_object['request']['type'] 24 | request_type_from_attrs = payload_object.request.type 25 | 26 | assert request_type_from_keys == request_type_from_attrs 27 | """ 28 | 29 | def __init__(self, request_json={}): 30 | super(_Field, self).__init__(request_json) 31 | for key, value in request_json.items(): 32 | if isinstance(value, dict): 33 | value = _Field(value) 34 | self[key] = value 35 | 36 | def __getattr__(self, attr): 37 | # converts timestamp str to datetime.datetime object 38 | if 'timestamp' in attr: 39 | return aniso8601.parse_datetime(self.get(attr)) 40 | return self.get(attr) 41 | 42 | def __setattr__(self, key, value): 43 | self.__setitem__(key, value) 44 | 45 | 46 | class _Response(object): 47 | 48 | def __init__(self, speech): 49 | self._json_default = None 50 | self._response = { 51 | 'outputSpeech': _output_speech(speech) 52 | } 53 | 54 | def simple_card(self, title=None, content=None): 55 | card = { 56 | 'type': 'Simple', 57 | 'title': title, 58 | 'content': content 59 | } 60 | self._response['card'] = card 61 | return self 62 | 63 | def standard_card(self, title=None, text=None, small_image_url=None, large_image_url=None): 64 | card = { 65 | 'type': 'Standard', 66 | 'title': title, 67 | 'text': text 68 | } 69 | 70 | if any((small_image_url, large_image_url)): 71 | card['image'] = {} 72 | if small_image_url is not None: 73 | card['image']['smallImageUrl'] = small_image_url 74 | if large_image_url is not None: 75 | card['image']['largeImageUrl'] = large_image_url 76 | 77 | self._response['card'] = card 78 | return self 79 | 80 | def list_display_render(self, template=None, title=None, backButton='HIDDEN', token=None, background_image_url=None, image=None, listItems=None, hintText=None): 81 | directive = [ 82 | { 83 | 'type': 'Display.RenderTemplate', 84 | 'template': { 85 | 'type': template, 86 | 'backButton': backButton, 87 | 'title': title, 88 | 'listItems': listItems 89 | } 90 | } 91 | ] 92 | 93 | if background_image_url is not None: 94 | directive[0]['template']['backgroundImage'] = { 95 | 'sources': [ 96 | {'url': background_image_url} 97 | ] 98 | } 99 | 100 | if hintText is not None: 101 | hint = { 102 | 'type':'Hint', 103 | 'hint': { 104 | 'type':"PlainText", 105 | 'text': hintText 106 | } 107 | } 108 | directive.append(hint) 109 | self._response['directives'] = directive 110 | return self 111 | 112 | def display_render(self, template=None, title=None, backButton='HIDDEN', token=None, background_image_url=None, image=None, text=None, hintText=None): 113 | directive = [ 114 | { 115 | 'type': 'Display.RenderTemplate', 116 | 'template': { 117 | 'type': template, 118 | 'backButton': backButton, 119 | 'title': title, 120 | 'textContent': text 121 | } 122 | } 123 | ] 124 | 125 | if background_image_url is not None: 126 | directive[0]['template']['backgroundImage'] = { 127 | 'sources': [ 128 | {'url': background_image_url} 129 | ] 130 | } 131 | 132 | if image is not None: 133 | directive[0]['template']['image'] = { 134 | 'sources': [ 135 | {'url': image} 136 | ] 137 | } 138 | 139 | if token is not None: 140 | directive[0]['template']['token'] = token 141 | 142 | if hintText is not None: 143 | hint = { 144 | 'type':'Hint', 145 | 'hint': { 146 | 'type':"PlainText", 147 | 'text': hintText 148 | } 149 | } 150 | directive.append(hint) 151 | 152 | self._response['directives'] = directive 153 | return self 154 | 155 | def link_account_card(self): 156 | card = {'type': 'LinkAccount'} 157 | self._response['card'] = card 158 | return self 159 | 160 | def consent_card(self, permissions): 161 | card = { 162 | 'type': 'AskForPermissionsConsent', 163 | 'permissions': [permissions] 164 | } 165 | self._response['card'] = card 166 | return self 167 | 168 | def render_response(self): 169 | response_wrapper = { 170 | 'version': '1.0', 171 | 'response': self._response, 172 | 'sessionAttributes': session.attributes 173 | } 174 | 175 | kw = {} 176 | if hasattr(session, 'attributes_encoder'): 177 | json_encoder = session.attributes_encoder 178 | kwargname = 'cls' if inspect.isclass(json_encoder) else 'default' 179 | kw[kwargname] = json_encoder 180 | dbgdump(response_wrapper, **kw) 181 | 182 | return json.dumps(response_wrapper, **kw) 183 | 184 | 185 | class statement(_Response): 186 | 187 | def __init__(self, speech): 188 | super(statement, self).__init__(speech) 189 | self._response['shouldEndSession'] = True 190 | 191 | 192 | class question(_Response): 193 | 194 | def __init__(self, speech): 195 | super(question, self).__init__(speech) 196 | self._response['shouldEndSession'] = False 197 | 198 | def reprompt(self, reprompt): 199 | reprompt = {'outputSpeech': _output_speech(reprompt)} 200 | self._response['reprompt'] = reprompt 201 | return self 202 | 203 | 204 | class buy(_Response): 205 | 206 | def __init__(self, productId=None): 207 | self._response = { 208 | 'shouldEndSession': True, 209 | 'directives': [{ 210 | 'type': 'Connections.SendRequest', 211 | 'name': 'Buy', 212 | 'payload': { 213 | 'InSkillProduct': { 214 | 'productId': productId 215 | } 216 | }, 217 | 'token': 'correlationToken' 218 | }] 219 | } 220 | 221 | 222 | class refund(_Response): 223 | 224 | def __init__(self, productId=None): 225 | self._response = { 226 | 'shouldEndSession': True, 227 | 'directives': [{ 228 | 'type': 'Connections.SendRequest', 229 | 'name': 'Cancel', 230 | 'payload': { 231 | 'InSkillProduct': { 232 | 'productId': productId 233 | } 234 | }, 235 | 'token': 'correlationToken' 236 | }] 237 | } 238 | 239 | class upsell(_Response): 240 | 241 | def __init__(self, productId=None, msg=None): 242 | self._response = { 243 | 'shouldEndSession': True, 244 | 'directives': [{ 245 | 'type': 'Connections.SendRequest', 246 | 'name': 'Upsell', 247 | 'payload': { 248 | 'InSkillProduct': { 249 | 'productId': productId 250 | }, 251 | 'upsellMessage': msg 252 | }, 253 | 'token': 'correlationToken' 254 | }] 255 | } 256 | 257 | class delegate(_Response): 258 | 259 | def __init__(self, updated_intent=None): 260 | self._response = { 261 | 'shouldEndSession': False, 262 | 'directives': [{'type': 'Dialog.Delegate'}] 263 | } 264 | 265 | if updated_intent: 266 | self._response['directives'][0]['updatedIntent'] = updated_intent 267 | 268 | 269 | class elicit_slot(_Response): 270 | """ 271 | Sends an ElicitSlot directive. 272 | slot - The slot name to elicit 273 | speech - The output speech 274 | updated_intent - Optional updated intent 275 | """ 276 | 277 | def __init__(self, slot, speech, updated_intent=None): 278 | self._response = { 279 | 'shouldEndSession': False, 280 | 'directives': [{ 281 | 'type': 'Dialog.ElicitSlot', 282 | 'slotToElicit': slot, 283 | }], 284 | 'outputSpeech': _output_speech(speech), 285 | } 286 | 287 | if updated_intent: 288 | self._response['directives'][0]['updatedIntent'] = updated_intent 289 | 290 | class confirm_slot(_Response): 291 | """ 292 | Sends a ConfirmSlot directive. 293 | slot - The slot name to confirm 294 | speech - The output speech 295 | updated_intent - Optional updated intent 296 | """ 297 | 298 | def __init__(self, slot, speech, updated_intent=None): 299 | self._response = { 300 | 'shouldEndSession': False, 301 | 'directives': [{ 302 | 'type': 'Dialog.ConfirmSlot', 303 | 'slotToConfirm': slot, 304 | }], 305 | 'outputSpeech': _output_speech(speech), 306 | } 307 | 308 | if updated_intent: 309 | self._response['directives'][0]['updatedIntent'] = updated_intent 310 | 311 | class confirm_intent(_Response): 312 | """ 313 | Sends a ConfirmIntent directive. 314 | 315 | """ 316 | def __init__(self, speech, updated_intent=None): 317 | self._response = { 318 | 'shouldEndSession': False, 319 | 'directives': [{ 320 | 'type': 'Dialog.ConfirmIntent', 321 | }], 322 | 'outputSpeech': _output_speech(speech), 323 | } 324 | 325 | if updated_intent: 326 | self._response['directives'][0]['updatedIntent'] = updated_intent 327 | 328 | 329 | class audio(_Response): 330 | """Returns a response object with an Amazon AudioPlayer Directive. 331 | 332 | Responses for LaunchRequests and IntentRequests may include outputSpeech in addition to an audio directive 333 | 334 | Note that responses to AudioPlayer requests do not allow outputSpeech. 335 | These must only include AudioPlayer Directives. 336 | 337 | @ask.intent('PlayFooAudioIntent') 338 | def play_foo_audio(): 339 | speech = 'playing from foo' 340 | stream_url = www.foo.com 341 | return audio(speech).play(stream_url) 342 | 343 | 344 | @ask.intent('AMAZON.PauseIntent') 345 | def stop_audio(): 346 | return audio('Ok, stopping the audio').stop() 347 | """ 348 | 349 | def __init__(self, speech=''): 350 | super(audio, self).__init__(speech) 351 | if not speech: 352 | self._response = {} 353 | self._response['directives'] = [] 354 | 355 | def play(self, stream_url, offset=0, opaque_token=None): 356 | """Sends a Play Directive to begin playback and replace current and enqueued streams.""" 357 | 358 | self._response['shouldEndSession'] = True 359 | directive = self._play_directive('REPLACE_ALL') 360 | directive['audioItem'] = self._audio_item(stream_url=stream_url, offset=offset, opaque_token=opaque_token) 361 | self._response['directives'].append(directive) 362 | return self 363 | 364 | def enqueue(self, stream_url, offset=0, opaque_token=None): 365 | """Adds stream to the queue. Does not impact the currently playing stream.""" 366 | directive = self._play_directive('ENQUEUE') 367 | audio_item = self._audio_item(stream_url=stream_url, 368 | offset=offset, 369 | push_buffer=False, 370 | opaque_token=opaque_token) 371 | audio_item['stream']['expectedPreviousToken'] = current_stream.token 372 | 373 | directive['audioItem'] = audio_item 374 | self._response['directives'].append(directive) 375 | return self 376 | 377 | def play_next(self, stream_url=None, offset=0, opaque_token=None): 378 | """Replace all streams in the queue but does not impact the currently playing stream.""" 379 | 380 | directive = self._play_directive('REPLACE_ENQUEUED') 381 | directive['audioItem'] = self._audio_item(stream_url=stream_url, offset=offset, opaque_token=opaque_token) 382 | self._response['directives'].append(directive) 383 | return self 384 | 385 | def resume(self): 386 | """Sends Play Directive to resume playback at the paused offset""" 387 | directive = self._play_directive('REPLACE_ALL') 388 | directive['audioItem'] = self._audio_item() 389 | self._response['directives'].append(directive) 390 | return self 391 | 392 | def _play_directive(self, behavior): 393 | directive = {} 394 | directive['type'] = 'AudioPlayer.Play' 395 | directive['playBehavior'] = behavior 396 | return directive 397 | 398 | def _audio_item(self, stream_url=None, offset=0, push_buffer=True, opaque_token=None): 399 | """Builds an AudioPlayer Directive's audioItem and updates current_stream""" 400 | audio_item = {'stream': {}} 401 | stream = audio_item['stream'] 402 | 403 | # existing stream 404 | if not stream_url: 405 | # stream.update(current_stream.__dict__) 406 | stream['url'] = current_stream.url 407 | stream['token'] = current_stream.token 408 | stream['offsetInMilliseconds'] = current_stream.offsetInMilliseconds 409 | 410 | # new stream 411 | else: 412 | stream['url'] = stream_url 413 | stream['token'] = opaque_token or str(uuid.uuid4()) 414 | stream['offsetInMilliseconds'] = offset 415 | 416 | if push_buffer: # prevents enqueued streams from becoming current_stream 417 | push_stream(stream_cache, context['System']['user']['userId'], stream) 418 | return audio_item 419 | 420 | def stop(self): 421 | """Sends AudioPlayer.Stop Directive to stop the current stream playback""" 422 | self._response['directives'].append({'type': 'AudioPlayer.Stop'}) 423 | return self 424 | 425 | def clear_queue(self, stop=False): 426 | """Clears queued streams and optionally stops current stream. 427 | 428 | Keyword Arguments: 429 | stop {bool} set True to stop current current stream and clear queued streams. 430 | set False to clear queued streams and allow current stream to finish 431 | default: {False} 432 | """ 433 | 434 | directive = {} 435 | directive['type'] = 'AudioPlayer.ClearQueue' 436 | if stop: 437 | directive['clearBehavior'] = 'CLEAR_ALL' 438 | else: 439 | directive['clearBehavior'] = 'CLEAR_ENQUEUED' 440 | 441 | self._response['directives'].append(directive) 442 | return self 443 | 444 | 445 | def _copyattr(src, dest, attr, convert=None): 446 | if attr in src: 447 | value = src[attr] 448 | if convert is not None: 449 | value = convert(value) 450 | setattr(dest, attr, value) 451 | 452 | 453 | def _output_speech(speech): 454 | try: 455 | xmldoc = ElementTree.fromstring(speech) 456 | if xmldoc.tag == 'speak': 457 | return {'type': 'SSML', 'ssml': speech} 458 | except (UnicodeEncodeError, ElementTree.ParseError) as e: 459 | pass 460 | return {'type': 'PlainText', 'text': speech} 461 | -------------------------------------------------------------------------------- /flask_ask/verifier.py: -------------------------------------------------------------------------------- 1 | import os 2 | import base64 3 | import posixpath 4 | from datetime import datetime 5 | from six.moves.urllib.parse import urlparse 6 | from six.moves.urllib.request import urlopen 7 | 8 | from OpenSSL import crypto 9 | 10 | from . import logger 11 | 12 | 13 | class VerificationError(Exception): pass 14 | 15 | 16 | def load_certificate(cert_url): 17 | if not _valid_certificate_url(cert_url): 18 | raise VerificationError("Certificate URL verification failed") 19 | cert_data = urlopen(cert_url).read() 20 | cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert_data) 21 | if not _valid_certificate(cert): 22 | raise VerificationError("Certificate verification failed") 23 | return cert 24 | 25 | 26 | def verify_signature(cert, signature, signed_data): 27 | try: 28 | signature = base64.b64decode(signature) 29 | crypto.verify(cert, signature, signed_data, 'sha1') 30 | except crypto.Error as e: 31 | raise VerificationError(e) 32 | 33 | 34 | def verify_timestamp(timestamp): 35 | dt = datetime.utcnow() - timestamp.replace(tzinfo=None) 36 | if abs(dt.total_seconds()) > 150: 37 | raise VerificationError("Timestamp verification failed") 38 | 39 | 40 | def verify_application_id(candidate, records): 41 | if candidate not in records: 42 | raise VerificationError("Application ID verification failed") 43 | 44 | 45 | def _valid_certificate_url(cert_url): 46 | parsed_url = urlparse(cert_url) 47 | if parsed_url.scheme == 'https': 48 | if parsed_url.hostname == "s3.amazonaws.com": 49 | if posixpath.normpath(parsed_url.path).startswith("/echo.api/"): 50 | return True 51 | return False 52 | 53 | 54 | def _valid_certificate(cert): 55 | not_after = cert.get_notAfter().decode('utf-8') 56 | not_after = datetime.strptime(not_after, '%Y%m%d%H%M%SZ') 57 | if datetime.utcnow() >= not_after: 58 | return False 59 | found = False 60 | for i in range(0, cert.get_extension_count()): 61 | extension = cert.get_extension(i) 62 | short_name = extension.get_short_name().decode('utf-8') 63 | value = str(extension) 64 | if 'subjectAltName' == short_name and 'DNS:echo-api.amazon.com' == value: 65 | found = True 66 | break 67 | if not found: 68 | return False 69 | return True 70 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | mock==2.0.0 3 | requests==2.13.0 4 | tox==2.7.0 5 | 6 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aniso8601==1.2.0 2 | Flask==1.1.1 3 | cryptography==2.1.4 4 | pyOpenSSL==17.0.0 5 | PyYAML==5.4 6 | six==1.11.0 7 | Werkzeug==0.16.1 8 | -------------------------------------------------------------------------------- /samples/audio/playlist_demo/playlist.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import logging 3 | import os 4 | from copy import copy 5 | 6 | from flask import Flask, json 7 | from flask_ask import Ask, question, statement, audio, current_stream, logger 8 | 9 | app = Flask(__name__) 10 | ask = Ask(app, "/") 11 | logging.getLogger('flask_ask').setLevel(logging.INFO) 12 | 13 | 14 | playlist = [ 15 | # 'https://www.freesound.org/data/previews/367/367142_2188-lq.mp3', 16 | 'https://archive.org/download/mailboxbadgerdrumsamplesvolume2/Ringing.mp3', 17 | 'https://archive.org/download/petescott20160927/20160927%20RC300-53-127.0bpm.mp3', 18 | 'https://archive.org/download/plpl011/plpl011_05-johnny_ripper-rain.mp3', 19 | 'https://archive.org/download/piano_by_maxmsp/beats107.mp3', 20 | 'https://archive.org/download/petescott20160927/20160927%20RC300-58-115.1bpm.mp3', 21 | 'https://archive.org/download/PianoScale/PianoScale.mp3', 22 | # 'https://archive.org/download/FemaleVoiceSample/Female_VoiceTalent_demo.mp4', 23 | 'https://archive.org/download/mailboxbadgerdrumsamplesvolume2/Risset%20Drum%201.mp3', 24 | 'https://archive.org/download/mailboxbadgerdrumsamplesvolume2/Submarine.mp3', 25 | # 'https://ia800203.us.archive.org/27/items/CarelessWhisper_435/CarelessWhisper.ogg' 26 | ] 27 | 28 | 29 | class QueueManager(object): 30 | """Manages queue data in a seperate context from current_stream. 31 | 32 | The flask-ask Local current_stream refers only to the current data from Alexa requests and Skill Responses. 33 | Alexa Skills Kit does not provide enqueued or stream-histroy data and does not provide a session attribute 34 | when delivering AudioPlayer Requests. 35 | 36 | This class is used to maintain accurate control of multiple streams, 37 | so that the user may send Intents to move throughout a queue. 38 | """ 39 | 40 | def __init__(self, urls): 41 | self._urls = urls 42 | self._queued = collections.deque(urls) 43 | self._history = collections.deque() 44 | self._current = None 45 | 46 | @property 47 | def status(self): 48 | status = { 49 | 'Current Position': self.current_position, 50 | 'Current URL': self.current, 51 | 'Next URL': self.up_next, 52 | 'Previous': self.previous, 53 | 'History': list(self.history) 54 | } 55 | return status 56 | 57 | @property 58 | def up_next(self): 59 | """Returns the url at the front of the queue""" 60 | qcopy = copy(self._queued) 61 | try: 62 | return qcopy.popleft() 63 | except IndexError: 64 | return None 65 | 66 | @property 67 | def current(self): 68 | return self._current 69 | 70 | @current.setter 71 | def current(self, url): 72 | self._save_to_history() 73 | self._current = url 74 | 75 | @property 76 | def history(self): 77 | return self._history 78 | 79 | @property 80 | def previous(self): 81 | history = copy(self.history) 82 | try: 83 | return history.pop() 84 | except IndexError: 85 | return None 86 | 87 | def add(self, url): 88 | self._urls.append(url) 89 | self._queued.append(url) 90 | 91 | def extend(self, urls): 92 | self._urls.extend(urls) 93 | self._queued.extend(urls) 94 | 95 | def _save_to_history(self): 96 | if self._current: 97 | self._history.append(self._current) 98 | 99 | def end_current(self): 100 | self._save_to_history() 101 | self._current = None 102 | 103 | def step(self): 104 | self.end_current() 105 | self._current = self._queued.popleft() 106 | return self._current 107 | 108 | def step_back(self): 109 | self._queued.appendleft(self._current) 110 | self._current = self._history.pop() 111 | return self._current 112 | 113 | def reset(self): 114 | self._queued = collections.deque(self._urls) 115 | self._history = [] 116 | 117 | def start(self): 118 | self.__init__(self._urls) 119 | return self.step() 120 | 121 | @property 122 | def current_position(self): 123 | return len(self._history) + 1 124 | 125 | 126 | queue = QueueManager(playlist) 127 | 128 | 129 | @ask.launch 130 | def launch(): 131 | card_title = 'Playlist Example' 132 | text = 'Welcome to an example for playing a playlist. You can ask me to start the playlist.' 133 | prompt = 'You can ask start playlist.' 134 | return question(text).reprompt(prompt).simple_card(card_title, text) 135 | 136 | 137 | @ask.intent('PlaylistDemoIntent') 138 | def start_playlist(): 139 | speech = 'Heres a playlist of some sounds. You can ask me Next, Previous, or Start Over' 140 | stream_url = queue.start() 141 | return audio(speech).play(stream_url) 142 | 143 | 144 | # QueueManager object is not stepped forward here. 145 | # This allows for Next Intents and on_playback_finished requests to trigger the step 146 | @ask.on_playback_nearly_finished() 147 | def nearly_finished(): 148 | if queue.up_next: 149 | _infodump('Alexa is now ready for a Next or Previous Intent') 150 | # dump_stream_info() 151 | next_stream = queue.up_next 152 | _infodump('Enqueueing {}'.format(next_stream)) 153 | return audio().enqueue(next_stream) 154 | else: 155 | _infodump('Nearly finished with last song in playlist') 156 | 157 | 158 | @ask.on_playback_finished() 159 | def play_back_finished(): 160 | _infodump('Finished Audio stream for track {}'.format(queue.current_position)) 161 | if queue.up_next: 162 | queue.step() 163 | _infodump('stepped queue forward') 164 | dump_stream_info() 165 | else: 166 | return statement('You have reached the end of the playlist!') 167 | 168 | 169 | # NextIntent steps queue forward and clears enqueued streams that were already sent to Alexa 170 | # next_stream will match queue.up_next and enqueue Alexa with the correct subsequent stream. 171 | @ask.intent('AMAZON.NextIntent') 172 | def next_song(): 173 | if queue.up_next: 174 | speech = 'playing next queued song' 175 | next_stream = queue.step() 176 | _infodump('Stepped queue forward to {}'.format(next_stream)) 177 | dump_stream_info() 178 | return audio(speech).play(next_stream) 179 | else: 180 | return audio('There are no more songs in the queue') 181 | 182 | 183 | @ask.intent('AMAZON.PreviousIntent') 184 | def previous_song(): 185 | if queue.previous: 186 | speech = 'playing previously played song' 187 | prev_stream = queue.step_back() 188 | dump_stream_info() 189 | return audio(speech).play(prev_stream) 190 | 191 | else: 192 | return audio('There are no songs in your playlist history.') 193 | 194 | 195 | @ask.intent('AMAZON.StartOverIntent') 196 | def restart_track(): 197 | if queue.current: 198 | speech = 'Restarting current track' 199 | dump_stream_info() 200 | return audio(speech).play(queue.current, offset=0) 201 | else: 202 | return statement('There is no current song') 203 | 204 | 205 | @ask.on_playback_started() 206 | def started(offset, token, url): 207 | _infodump('Started audio stream for track {}'.format(queue.current_position)) 208 | dump_stream_info() 209 | 210 | 211 | @ask.on_playback_stopped() 212 | def stopped(offset, token): 213 | _infodump('Stopped audio stream for track {}'.format(queue.current_position)) 214 | 215 | @ask.intent('AMAZON.PauseIntent') 216 | def pause(): 217 | seconds = current_stream.offsetInMilliseconds / 1000 218 | msg = 'Paused the Playlist on track {}, offset at {} seconds'.format( 219 | queue.current_position, seconds) 220 | _infodump(msg) 221 | dump_stream_info() 222 | return audio(msg).stop().simple_card(msg) 223 | 224 | 225 | @ask.intent('AMAZON.ResumeIntent') 226 | def resume(): 227 | seconds = current_stream.offsetInMilliseconds / 1000 228 | msg = 'Resuming the Playlist on track {}, offset at {} seconds'.format(queue.current_position, seconds) 229 | _infodump(msg) 230 | dump_stream_info() 231 | return audio(msg).resume().simple_card(msg) 232 | 233 | 234 | @ask.session_ended 235 | def session_ended(): 236 | return "{}", 200 237 | 238 | def dump_stream_info(): 239 | status = { 240 | 'Current Stream Status': current_stream.__dict__, 241 | 'Queue status': queue.status 242 | } 243 | _infodump(status) 244 | 245 | 246 | def _infodump(obj, indent=2): 247 | msg = json.dumps(obj, indent=indent) 248 | logger.info(msg) 249 | 250 | 251 | if __name__ == '__main__': 252 | if 'ASK_VERIFY_REQUESTS' in os.environ: 253 | verify = str(os.environ.get('ASK_VERIFY_REQUESTS', '')).lower() 254 | if verify == 'false': 255 | app.config['ASK_VERIFY_REQUESTS'] = False 256 | app.run(debug=True) 257 | -------------------------------------------------------------------------------- /samples/audio/playlist_demo/speech_assets/IntentSchema.json: -------------------------------------------------------------------------------- 1 | { 2 | "intents": [ 3 | { 4 | "intent": "AMAZON.PauseIntent" 5 | }, 6 | { 7 | "intent": "PlaylistDemoIntent" 8 | }, 9 | { 10 | "intent": "AMAZON.StopIntent" 11 | }, 12 | { 13 | "intent": "AMAZON.ResumeIntent" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /samples/audio/playlist_demo/speech_assets/SampleUtterances.txt: -------------------------------------------------------------------------------- 1 | PlaylistDemoIntent start the playlist -------------------------------------------------------------------------------- /samples/audio/simple_demo/ask_audio.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | from flask import Flask, json, render_template 5 | from flask_ask import Ask, request, session, question, statement, context, audio, current_stream 6 | 7 | app = Flask(__name__) 8 | ask = Ask(app, "/") 9 | logger = logging.getLogger() 10 | logging.getLogger('flask_ask').setLevel(logging.INFO) 11 | 12 | 13 | @ask.launch 14 | def launch(): 15 | card_title = 'Audio Example' 16 | text = 'Welcome to an audio example. You can ask to begin demo, or try asking me to play the sax.' 17 | prompt = 'You can ask to begin demo, or try asking me to play the sax.' 18 | return question(text).reprompt(prompt).simple_card(card_title, text) 19 | 20 | 21 | @ask.intent('DemoIntent') 22 | def demo(): 23 | speech = "Here's one of my favorites" 24 | stream_url = 'https://www.vintagecomputermusic.com/mp3/s2t9_Computer_Speech_Demonstration.mp3' 25 | return audio(speech).play(stream_url, offset=93000) 26 | 27 | 28 | # 'ask audio_skil Play the sax 29 | @ask.intent('SaxIntent') 30 | def george_michael(): 31 | speech = 'yeah you got it!' 32 | stream_url = 'https://ia800203.us.archive.org/27/items/CarelessWhisper_435/CarelessWhisper.ogg' 33 | return audio(speech).play(stream_url) 34 | 35 | 36 | @ask.intent('AMAZON.PauseIntent') 37 | def pause(): 38 | return audio('Paused the stream.').stop() 39 | 40 | 41 | @ask.intent('AMAZON.ResumeIntent') 42 | def resume(): 43 | return audio('Resuming.').resume() 44 | 45 | @ask.intent('AMAZON.StopIntent') 46 | def stop(): 47 | return audio('stopping').clear_queue(stop=True) 48 | 49 | 50 | 51 | # optional callbacks 52 | @ask.on_playback_started() 53 | def started(offset, token): 54 | _infodump('STARTED Audio Stream at {} ms'.format(offset)) 55 | _infodump('Stream holds the token {}'.format(token)) 56 | _infodump('STARTED Audio stream from {}'.format(current_stream.url)) 57 | 58 | 59 | @ask.on_playback_stopped() 60 | def stopped(offset, token): 61 | _infodump('STOPPED Audio Stream at {} ms'.format(offset)) 62 | _infodump('Stream holds the token {}'.format(token)) 63 | _infodump('Stream stopped playing from {}'.format(current_stream.url)) 64 | 65 | 66 | @ask.on_playback_nearly_finished() 67 | def nearly_finished(): 68 | _infodump('Stream nearly finished from {}'.format(current_stream.url)) 69 | 70 | @ask.on_playback_finished() 71 | def stream_finished(token): 72 | _infodump('Playback has finished for stream with token {}'.format(token)) 73 | 74 | @ask.session_ended 75 | def session_ended(): 76 | return "{}", 200 77 | 78 | def _infodump(obj, indent=2): 79 | msg = json.dumps(obj, indent=indent) 80 | logger.info(msg) 81 | 82 | 83 | if __name__ == '__main__': 84 | if 'ASK_VERIFY_REQUESTS' in os.environ: 85 | verify = str(os.environ.get('ASK_VERIFY_REQUESTS', '')).lower() 86 | if verify == 'false': 87 | app.config['ASK_VERIFY_REQUESTS'] = False 88 | app.run(debug=True) 89 | 90 | -------------------------------------------------------------------------------- /samples/audio/simple_demo/speech_assets/IntentSchema.json: -------------------------------------------------------------------------------- 1 | { 2 | "intents": [ 3 | { 4 | "intent": "AMAZON.PauseIntent" 5 | }, 6 | { 7 | "intent": "DemoIntent" 8 | }, 9 | { 10 | "intent": "SaxIntent" 11 | }, 12 | { 13 | "intent": "AMAZON.StopIntent" 14 | }, 15 | { 16 | "intent": "AMAZON.ResumeIntent" 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /samples/audio/simple_demo/speech_assets/SampleUtterances.txt: -------------------------------------------------------------------------------- 1 | DemoIntent begin demo 2 | SaxIntent play the sax 3 | SaxIntent play sax -------------------------------------------------------------------------------- /samples/blueprint_demo/demo.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | from flask import Flask 5 | from helloworld import blueprint 6 | 7 | app = Flask(__name__) 8 | app.register_blueprint(blueprint) 9 | 10 | logging.getLogger('flask_app').setLevel(logging.DEBUG) 11 | 12 | 13 | if __name__ == '__main__': 14 | if 'ASK_VERIFY_REQUESTS' in os.environ: 15 | verify = str(os.environ.get('ASK_VERIFY_REQUESTS', '')).lower() 16 | if verify == 'false': 17 | app.config['ASK_VERIFY_REQUESTS'] = False 18 | app.run(debug=True) 19 | -------------------------------------------------------------------------------- /samples/blueprint_demo/helloworld.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from flask import Blueprint, render_template 4 | from flask_ask import Ask, question, statement 5 | 6 | 7 | blueprint = Blueprint('blueprint_api', __name__, url_prefix="/ask") 8 | ask = Ask(blueprint=blueprint) 9 | 10 | logging.getLogger('flask_ask').setLevel(logging.DEBUG) 11 | 12 | 13 | @ask.launch 14 | def launch(): 15 | speech_text = render_template('welcome') 16 | return question(speech_text).reprompt(speech_text).simple_card('HelloWorld', speech_text) 17 | 18 | 19 | @ask.intent('HelloWorldIntent') 20 | def hello_world(): 21 | speech_text = render_template('hello') 22 | return statement(speech_text).simple_card('HelloWorld', speech_text) 23 | 24 | 25 | @ask.intent('AMAZON.HelpIntent') 26 | def help(): 27 | speech_text = render_template('help') 28 | return question(speech_text).reprompt(speech_text).simple_card('HelloWorld', speech_text) 29 | 30 | 31 | @ask.session_ended 32 | def session_ended(): 33 | return "{}", 200 34 | 35 | -------------------------------------------------------------------------------- /samples/blueprint_demo/speech_assets/IntentSchema.json: -------------------------------------------------------------------------------- 1 | { 2 | "intents": [ 3 | { 4 | "intent": "HelloWorldIntent" 5 | }, 6 | { 7 | "intent": "AMAZON.HelpIntent" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /samples/blueprint_demo/speech_assets/SampleUtterances.txt: -------------------------------------------------------------------------------- 1 | HelloWorldIntent say hello 2 | HelloWorldIntent say hello world 3 | HelloWorldIntent hello 4 | HelloWorldIntent say hi 5 | HelloWorldIntent say hi world 6 | HelloWorldIntent hi 7 | HelloWorldIntent how are you 8 | -------------------------------------------------------------------------------- /samples/blueprint_demo/templates.yaml: -------------------------------------------------------------------------------- 1 | welcome: "Welcome to the Alexa Skills Kit, you can say hello" 2 | hello: "Hello world!" 3 | help: "You can say hello to me!" -------------------------------------------------------------------------------- /samples/helloworld/helloworld.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | from flask import Flask 5 | from flask_ask import Ask, request, session, question, statement 6 | 7 | 8 | app = Flask(__name__) 9 | ask = Ask(app, "/") 10 | logging.getLogger('flask_ask').setLevel(logging.DEBUG) 11 | 12 | 13 | @ask.launch 14 | def launch(): 15 | speech_text = 'Welcome to the Alexa Skills Kit, you can say hello' 16 | return question(speech_text).reprompt(speech_text).simple_card('HelloWorld', speech_text) 17 | 18 | 19 | @ask.intent('HelloWorldIntent') 20 | def hello_world(): 21 | speech_text = 'Hello world' 22 | return statement(speech_text).simple_card('HelloWorld', speech_text) 23 | 24 | 25 | @ask.intent('AMAZON.HelpIntent') 26 | def help(): 27 | speech_text = 'You can say hello to me!' 28 | return question(speech_text).reprompt(speech_text).simple_card('HelloWorld', speech_text) 29 | 30 | 31 | @ask.session_ended 32 | def session_ended(): 33 | return "{}", 200 34 | 35 | 36 | if __name__ == '__main__': 37 | if 'ASK_VERIFY_REQUESTS' in os.environ: 38 | verify = str(os.environ.get('ASK_VERIFY_REQUESTS', '')).lower() 39 | if verify == 'false': 40 | app.config['ASK_VERIFY_REQUESTS'] = False 41 | app.run(debug=True) 42 | -------------------------------------------------------------------------------- /samples/helloworld/speech_assets/IntentSchema.json: -------------------------------------------------------------------------------- 1 | { 2 | "intents": [ 3 | { 4 | "intent": "HelloWorldIntent" 5 | }, 6 | { 7 | "intent": "AMAZON.HelpIntent" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /samples/helloworld/speech_assets/SampleUtterances.txt: -------------------------------------------------------------------------------- 1 | HelloWorldIntent say hello 2 | HelloWorldIntent say hello world 3 | HelloWorldIntent hello 4 | HelloWorldIntent say hi 5 | HelloWorldIntent say hi world 6 | HelloWorldIntent hi 7 | HelloWorldIntent how are you 8 | -------------------------------------------------------------------------------- /samples/historybuff/historybuff.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import re 4 | from six.moves.urllib.request import urlopen 5 | 6 | 7 | from flask import Flask 8 | from flask_ask import Ask, request, session, question, statement 9 | 10 | 11 | app = Flask(__name__) 12 | ask = Ask(app, "/") 13 | logging.getLogger('flask_ask').setLevel(logging.DEBUG) 14 | 15 | 16 | # URL prefix to download history content from Wikipedia. 17 | URL_PREFIX = 'https://en.wikipedia.org/w/api.php?action=query&prop=extracts' + \ 18 | '&format=json&explaintext=&exsectionformat=plain&redirects=&titles=' 19 | 20 | # Constant defining number of events to be read at one time. 21 | PAGINATION_SIZE = 3 22 | 23 | # Length of the delimiter between individual events. 24 | DELIMITER_SIZE = 2 25 | 26 | # Size of events from Wikipedia response. 27 | SIZE_OF_EVENTS = 10 28 | 29 | # Constant defining session attribute key for the event index 30 | SESSION_INDEX = 'index' 31 | 32 | # Constant defining session attribute key for the event text key for date of events. 33 | SESSION_TEXT = 'text' 34 | 35 | 36 | @ask.launch 37 | def launch(): 38 | speech_output = 'History buff. What day do you want events for?' 39 | reprompt_text = "With History Buff, you can get historical events for any day of the year. " + \ 40 | "For example, you could say today, or August thirtieth. " + \ 41 | "Now, which day do you want?" 42 | return question(speech_output).reprompt(reprompt_text) 43 | 44 | 45 | @ask.intent('GetFirstEventIntent', convert={ 'day': 'date' }) 46 | def get_first_event(day): 47 | month_name = day.strftime('%B') 48 | day_number = day.day 49 | events = _get_json_events_from_wikipedia(month_name, day_number) 50 | if not events: 51 | speech_output = "There is a problem connecting to Wikipedia at this time. Please try again later." 52 | return statement('{}'.format(speech_output)) 53 | else: 54 | card_title = "Events on {} {}".format(month_name, day_number) 55 | speech_output = "

For {} {}

".format(month_name, day_number) 56 | card_output = "" 57 | for i in range(PAGINATION_SIZE): 58 | speech_output += "

{}

".format(events[i]) 59 | card_output += "{}\n".format(events[i]) 60 | speech_output += " Wanna go deeper into history?" 61 | card_output += " Wanna go deeper into history?" 62 | reprompt_text = "With History Buff, you can get historical events for any day of the year. " + \ 63 | "For example, you could say today, or August thirtieth. " + \ 64 | "Now, which day do you want?" 65 | session.attributes[SESSION_INDEX] = PAGINATION_SIZE 66 | session.attributes[SESSION_TEXT] = events 67 | speech_output = '{}'.format(speech_output) 68 | return question(speech_output).reprompt(reprompt_text).simple_card(card_title, card_output) 69 | 70 | 71 | @ask.intent('GetNextEventIntent') 72 | def get_next_event(): 73 | events = session.attributes[SESSION_TEXT] 74 | index = session.attributes[SESSION_INDEX] 75 | card_title = "More events on this day in history" 76 | speech_output = "" 77 | card_output = "" 78 | i = 0 79 | while i < PAGINATION_SIZE and index < len(events): 80 | speech_output += "

{}

".format(events[index]) 81 | card_output += "{}\n".format(events[index]) 82 | i += 1 83 | index += 1 84 | speech_output += " Wanna go deeper into history?" 85 | reprompt_text = "Do you want to know more about what happened on this date?" 86 | session.attributes[SESSION_INDEX] = index 87 | speech_output = '{}'.format(speech_output) 88 | return question(speech_output).reprompt(reprompt_text).simple_card(card_title, card_output) 89 | 90 | 91 | @ask.intent('AMAZON.StopIntent') 92 | def stop(): 93 | return statement("Goodbye") 94 | 95 | 96 | @ask.intent('AMAZON.CancelIntent') 97 | def cancel(): 98 | return statement("Goodbye") 99 | 100 | 101 | @ask.session_ended 102 | def session_ended(): 103 | return "{}", 200 104 | 105 | 106 | def _get_json_events_from_wikipedia(month, date): 107 | url = "{}{}_{}".format(URL_PREFIX, month, date) 108 | data = urlopen(url).read().decode('utf-8') 109 | return _parse_json(data) 110 | 111 | 112 | def _parse_json(text): 113 | events = [] 114 | try: 115 | slice_start = text.index("\\nEvents\\n") + SIZE_OF_EVENTS 116 | slice_end = text.index("\\n\\n\\nBirths") 117 | text = text[slice_start:slice_end]; 118 | except ValueError: 119 | return events 120 | start_index = end_index = 0 121 | done = False 122 | while not done: 123 | try: 124 | end_index = text.index('\\n', start_index + DELIMITER_SIZE) 125 | event_text = text[start_index:end_index] 126 | start_index = end_index + 2 127 | except ValueError: 128 | event_text = text[start_index:] 129 | done = True 130 | # replace dashes returned in text from Wikipedia's API 131 | event_text = event_text.replace('\\u2013', '') 132 | # add comma after year so Alexa pauses before continuing with the sentence 133 | event_text = re.sub('^\d+', r'\g<0>,', event_text) 134 | events.append(event_text) 135 | events.reverse() 136 | return events 137 | 138 | 139 | if __name__ == '__main__': 140 | if 'ASK_VERIFY_REQUESTS' in os.environ: 141 | verify = str(os.environ.get('ASK_VERIFY_REQUESTS', '')).lower() 142 | if verify == 'false': 143 | app.config['ASK_VERIFY_REQUESTS'] = False 144 | app.run(debug=True) 145 | 146 | -------------------------------------------------------------------------------- /samples/historybuff/speech_assets/IntentSchema.json: -------------------------------------------------------------------------------- 1 | { 2 | "intents": [ 3 | { 4 | "intent": "GetFirstEventIntent", 5 | "slots": [ 6 | { 7 | "name": "day", 8 | "type": "AMAZON.DATE" 9 | } 10 | ] 11 | }, 12 | { 13 | "intent": "GetNextEventIntent" 14 | }, 15 | { 16 | "intent": "AMAZON.HelpIntent" 17 | }, 18 | { 19 | "intent": "AMAZON.StopIntent" 20 | }, 21 | { 22 | "intent": "AMAZON.CancelIntent" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /samples/historybuff/speech_assets/SampleUtterances.txt: -------------------------------------------------------------------------------- 1 | GetFirstEventIntent get events for {day} 2 | GetFirstEventIntent give me events for {day} 3 | GetFirstEventIntent what happened on {day} 4 | GetFirstEventIntent what happened 5 | GetFirstEventIntent {day} 6 | 7 | GetNextEventIntent yes 8 | GetNextEventIntent yup 9 | GetNextEventIntent sure 10 | GetNextEventIntent yes please 11 | 12 | AMAZON.StopIntent no 13 | AMAZON.StopIntent nope 14 | AMAZON.StopIntent no thanks 15 | AMAZON.StopIntent no thank you 16 | -------------------------------------------------------------------------------- /samples/purchase/IntentSchema.json: -------------------------------------------------------------------------------- 1 | { 2 | "interactionModel": { 3 | "languageModel": { 4 | "invocationName": "demo", 5 | "intents": [ 6 | { 7 | "name": "AMAZON.FallbackIntent", 8 | "samples": [] 9 | }, 10 | { 11 | "name": "AMAZON.CancelIntent", 12 | "samples": [] 13 | }, 14 | { 15 | "name": "AMAZON.HelpIntent", 16 | "samples": [] 17 | }, 18 | { 19 | "name": "AMAZON.StopIntent", 20 | "samples": [] 21 | }, 22 | { 23 | "name": "BuySkillItemIntent", 24 | "slots": [ 25 | { 26 | "name": "ProductName", 27 | "type": "LIST_OF_PRODUCT_NAMES" 28 | } 29 | ], 30 | "samples": [ 31 | "{ProductName}", 32 | "buy", 33 | "shop", 34 | "buy {ProductName}", 35 | "purchase {ProductName}", 36 | "want {ProductName}", 37 | "would like {ProductName}" 38 | ] 39 | }, 40 | { 41 | "name": "RefundSkillItemIntent", 42 | "slots": [ 43 | { 44 | "name": "ProductName", 45 | "type": "LIST_OF_PRODUCT_NAMES" 46 | } 47 | ], 48 | "samples": [ 49 | "cancel {ProductName}", 50 | "return {ProductName}", 51 | "refund {ProductName}", 52 | "want a refund for {ProductName}", 53 | "would like to return {ProductName}" 54 | ] 55 | } 56 | ], 57 | "types": [ 58 | { 59 | "name": "LIST_OF_PRODUCT_NAMES", 60 | "values": [ 61 | { 62 | "name": { 63 | "value": "monthly subscription" 64 | } 65 | }, 66 | { 67 | "name": { 68 | "value": "start smoking" 69 | } 70 | }, 71 | { 72 | "name": { 73 | "value": "stop smoking" 74 | } 75 | } 76 | ] 77 | } 78 | ] 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /samples/purchase/model.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from flask import json 3 | from flask_ask import logger 4 | 5 | class Product(): 6 | ''' 7 | Object model for inSkillProducts and methods to access products. 8 | 9 | {"inSkillProducts":[ 10 | {"productId":"amzn1.adg.product.your_product_id", 11 | "referenceName":"product_name", 12 | "type":"ENTITLEMENT", 13 | "name":"product name", 14 | "summary":"This product has helped many people.", 15 | "entitled":"NOT_ENTITLED", 16 | "purchasable":"NOT_PURCHASABLE"}], 17 | "nextToken":null, 18 | "truncated":false} 19 | 20 | ''' 21 | 22 | def __init__(self, apiAccessToken): 23 | self.token = apiAccessToken 24 | self.product_list = self.query() 25 | 26 | 27 | def query(self): 28 | # Information required to invoke the API is available in the session 29 | apiEndpoint = "https://api.amazonalexa.com" 30 | apiPath = "/v1/users/~current/skills/~current/inSkillProducts" 31 | token = "bearer " + self.token 32 | language = "en-US" #self.event.request.locale 33 | 34 | url = apiEndpoint + apiPath 35 | headers = { 36 | "Content-Type" : 'application/json', 37 | "Accept-Language" : language, 38 | "Authorization" : token 39 | } 40 | #Call the API 41 | res = requests.get(url, headers=headers) 42 | logger.info('PRODUCTS:' + '*' * 80) 43 | logger.info(res.status_code) 44 | logger.info(res.text) 45 | if res.status_code == 200: 46 | data = json.loads(res.text) 47 | return data['inSkillProducts'] 48 | else: 49 | return None 50 | 51 | def list(self): 52 | """ return list of purchasable and not entitled products""" 53 | mylist = [] 54 | for prod in self.product_list: 55 | if self.purchasable(prod) and not self.entitled(prod): 56 | mylist.append(prod) 57 | return mylist 58 | 59 | def purchasable(self, product): 60 | """ return True if purchasable product""" 61 | return 'PURCHASABLE' == product['purchasable'] 62 | 63 | def entitled(self, product): 64 | """ return True if entitled product""" 65 | return 'ENTITLED' == product['entitled'] 66 | 67 | 68 | def productId(self, name): 69 | print(self.product_list) 70 | for prod in self.product_list: 71 | if name == prod['name'].lower(): 72 | return prod['productId'] 73 | return None 74 | 75 | def productName(self, id): 76 | for prod in self.product_list: 77 | if id == prod['productId']: 78 | return prod['name'] 79 | return None 80 | -------------------------------------------------------------------------------- /samples/purchase/purchase.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import requests 4 | 5 | from flask import Flask, json, render_template 6 | from flask_ask import Ask, request, session, question, statement, context, buy, upsell, refund, logger 7 | from model import Product 8 | 9 | app = Flask(__name__) 10 | ask = Ask(app, "/") 11 | logging.getLogger('flask_ask').setLevel(logging.DEBUG) 12 | 13 | 14 | PRODUCT_KEY = "PRODUCT" 15 | 16 | 17 | 18 | @ask.on_purchase_completed( mapping={'payload': 'payload','name':'name','status':'status','token':'token'}) 19 | def completed(payload, name, status, token): 20 | products = Product(context.System.apiAccessToken) 21 | logger.info('on-purchase-completed {}'.format( request)) 22 | logger.info('payload: {} {}'.format(payload.purchaseResult, payload.productId)) 23 | logger.info('name: {}'.format(name)) 24 | logger.info('token: {}'.format(token)) 25 | logger.info('status: {}'.format( status.code == 200)) 26 | product_name = products.productName(payload.productId) 27 | logger.info('Product name'.format(product_name)) 28 | if status.code == '200' and ('ACCEPTED' in payload.purchaseResult): 29 | return question('To listen it just say - play {} '.format(product_name)) 30 | else: 31 | return question('Do you want to buy another product?') 32 | 33 | @ask.launch 34 | def launch(): 35 | products = Product(context.System.apiAccessToken) 36 | question_text = render_template('welcome', products=products.list()) 37 | reprompt_text = render_template('welcome_reprompt') 38 | return question(question_text).reprompt(reprompt_text).simple_card('Welcome', question_text) 39 | 40 | 41 | @ask.intent('BuySkillItemIntent', mapping={'product_name': 'ProductName'}) 42 | def buy_intent(product_name): 43 | products = Product(context.System.apiAccessToken) 44 | logger.info("PRODUCT: {}".format(product_name)) 45 | buy_card = render_template('buy_card', product=product_name) 46 | productId = products.productId(product_name) 47 | if productId is not None: 48 | session.attributes[PRODUCT_KEY] = productId 49 | else: 50 | return statement("I didn't find a product {}".format(product_name)) 51 | raise NotImplementedError() 52 | return buy(productId).simple_card('Welcome', question_text) 53 | 54 | #return upsell(product,'get this great product') 55 | 56 | 57 | @ask.intent('RefundSkillItemIntent', mapping={'product_name': 'ProductName'}) 58 | def refund_intent(product_name): 59 | refund_card = render_template('refund_card') 60 | logger.info("PRODUCT: {}".format(product_name)) 61 | 62 | products = Product(context.System.apiAccessToken) 63 | productId = products.productId(product_name) 64 | 65 | if productId is not None: 66 | session.attributes[PRODUCT_KEY] = productId 67 | else: 68 | raise NotImplementedError() 69 | return refund(productId) 70 | 71 | 72 | @ask.intent('AMAZON.FallbackIntent') 73 | def fallback_intent(): 74 | return statement("FallbackIntent") 75 | 76 | 77 | @ask.session_ended 78 | def session_ended(): 79 | return "{}", 200 80 | 81 | 82 | if __name__ == '__main__': 83 | if 'ASK_VERIFY_REQUESTS' in os.environ: 84 | verify = str(os.environ.get('ASK_VERIFY_REQUESTS', '')).lower() 85 | if verify == 'false': 86 | app.config['ASK_VERIFY_REQUESTS'] = False 87 | app.run(debug=True) 88 | 89 | -------------------------------------------------------------------------------- /samples/purchase/templates.yaml: -------------------------------------------------------------------------------- 1 | welcome: | 2 | Welcome to the Flask-ask purchase demo. 3 | {% if products %} 4 | Here is a list of products available: 5 | {%for product in products%} 6 | {{ product.name}}, 7 | {%endfor %} 8 | Please tell me the product name you want to buy. 9 | {%else%} 10 | You have no products configured. Please configure products using ASK CLI. 11 | {%endif%} 12 | 13 | 14 | welcome_reprompt: Please tell me the product name you want to buy. 15 | 16 | refund_card: | 17 | Refund Intent for {{product}} 18 | 19 | 20 | buy_card: | 21 | Buy Intent for {{product}} 22 | -------------------------------------------------------------------------------- /samples/session/session.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | from flask import Flask, json, render_template 5 | from flask_ask import Ask, request, session, question, statement 6 | 7 | 8 | app = Flask(__name__) 9 | ask = Ask(app, "/") 10 | logging.getLogger('flask_ask').setLevel(logging.DEBUG) 11 | 12 | 13 | COLOR_KEY = "COLOR" 14 | 15 | 16 | @ask.launch 17 | def launch(): 18 | card_title = render_template('card_title') 19 | question_text = render_template('welcome') 20 | reprompt_text = render_template('welcome_reprompt') 21 | return question(question_text).reprompt(reprompt_text).simple_card(card_title, question_text) 22 | 23 | 24 | @ask.intent('MyColorIsIntent', mapping={'color': 'Color'}) 25 | def my_color_is(color): 26 | card_title = render_template('card_title') 27 | if color is not None: 28 | session.attributes[COLOR_KEY] = color 29 | question_text = render_template('known_color', color=color) 30 | reprompt_text = render_template('known_color_reprompt') 31 | else: 32 | question_text = render_template('unknown_color') 33 | reprompt_text = render_template('unknown_color_reprompt') 34 | return question(question_text).reprompt(reprompt_text).simple_card(card_title, question_text) 35 | 36 | 37 | @ask.intent('WhatsMyColorIntent') 38 | def whats_my_color(): 39 | card_title = render_template('card_title') 40 | color = session.attributes.get(COLOR_KEY) 41 | if color is not None: 42 | statement_text = render_template('known_color_bye', color=color) 43 | return statement(statement_text).simple_card(card_title, statement_text) 44 | else: 45 | question_text = render_template('unknown_color_reprompt') 46 | return question(question_text).reprompt(question_text).simple_card(card_title, question_text) 47 | 48 | 49 | @ask.session_ended 50 | def session_ended(): 51 | return "{}", 200 52 | 53 | 54 | if __name__ == '__main__': 55 | if 'ASK_VERIFY_REQUESTS' in os.environ: 56 | verify = str(os.environ.get('ASK_VERIFY_REQUESTS', '')).lower() 57 | if verify == 'false': 58 | app.config['ASK_VERIFY_REQUESTS'] = False 59 | app.run(debug=True) 60 | 61 | -------------------------------------------------------------------------------- /samples/session/speech_assets/IntentSchema.json: -------------------------------------------------------------------------------- 1 | { 2 | "intents": [ 3 | { 4 | "intent": "MyColorIsIntent", 5 | "slots": [ 6 | { 7 | "name": "Color", 8 | "type": "LIST_OF_COLORS" 9 | } 10 | ] 11 | }, 12 | { 13 | "intent": "WhatsMyColorIntent" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /samples/session/speech_assets/SampleUtterances.txt: -------------------------------------------------------------------------------- 1 | MyColorIsIntent my color is {Color} 2 | MyColorIsIntent my favorite color is {Color} 3 | WhatsMyColorIntent whats my color 4 | WhatsMyColorIntent what is my color 5 | WhatsMyColorIntent say my color 6 | WhatsMyColorIntent tell me my color 7 | WhatsMyColorIntent whats my favorite color 8 | WhatsMyColorIntent what is my favorite color 9 | WhatsMyColorIntent say my favorite color 10 | WhatsMyColorIntent tell me my favorite color 11 | WhatsMyColorIntent tell me what my favorite color is 12 | -------------------------------------------------------------------------------- /samples/session/speech_assets/customSlotTypes/LIST_OF_COLORS: -------------------------------------------------------------------------------- 1 | green 2 | blue 3 | purple 4 | red 5 | orange 6 | yellow 7 | -------------------------------------------------------------------------------- /samples/session/templates.yaml: -------------------------------------------------------------------------------- 1 | welcome: | 2 | Welcome to the Alexa Skills Kit sample. Please tell me your favorite color by 3 | saying, my favorite color is red 4 | 5 | welcome_reprompt: Please tell me your favorite color by saying, my favorite color is red 6 | 7 | known_color: | 8 | I now know that your favorite color is {{ color }}. You can ask me your favorite color 9 | by saying, what's my favorite color? 10 | 11 | known_color_reprompt: You can ask me your favorite color by saying, what's my favorite color? 12 | 13 | known_color_bye: Your favorite color is {{ color }}. Goodbye 14 | 15 | unknown_color: I'm not sure what your favorite color is, please try again 16 | 17 | unknown_color_reprompt: | 18 | I'm not sure what your favorite color is. You can tell me your favorite color by saying, 19 | my favorite color is red 20 | 21 | card_title: Session 22 | -------------------------------------------------------------------------------- /samples/spacegeek/spacegeek.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | from random import randint 4 | 5 | from flask import Flask, render_template 6 | from flask_ask import Ask, request, session, question, statement 7 | 8 | 9 | app = Flask(__name__) 10 | ask = Ask(app, "/") 11 | logging.getLogger('flask_ask').setLevel(logging.DEBUG) 12 | 13 | 14 | @ask.launch 15 | def launch(): 16 | return get_new_fact() 17 | 18 | 19 | @ask.intent('GetNewFactIntent') 20 | def get_new_fact(): 21 | num_facts = 13 # increment this when adding a new fact template 22 | fact_index = randint(0, num_facts-1) 23 | fact_text = render_template('space_fact_{}'.format(fact_index)) 24 | card_title = render_template('card_title') 25 | return statement(fact_text).simple_card(card_title, fact_text) 26 | 27 | 28 | @ask.intent('AMAZON.HelpIntent') 29 | def help(): 30 | help_text = render_template('help') 31 | return question(help_text).reprompt(help_text) 32 | 33 | 34 | @ask.intent('AMAZON.StopIntent') 35 | def stop(): 36 | bye_text = render_template('bye') 37 | return statement(bye_text) 38 | 39 | 40 | @ask.intent('AMAZON.CancelIntent') 41 | def cancel(): 42 | bye_text = render_template('bye') 43 | return statement(bye_text) 44 | 45 | 46 | @ask.session_ended 47 | def session_ended(): 48 | return "{}", 200 49 | 50 | 51 | if __name__ == '__main__': 52 | if 'ASK_VERIFY_REQUESTS' in os.environ: 53 | verify = str(os.environ.get('ASK_VERIFY_REQUESTS', '')).lower() 54 | if verify == 'false': 55 | app.config['ASK_VERIFY_REQUESTS'] = False 56 | app.run(debug=True) 57 | -------------------------------------------------------------------------------- /samples/spacegeek/speech_assets/IntentSchema.json: -------------------------------------------------------------------------------- 1 | { 2 | "intents": [ 3 | { 4 | "intent": "GetNewFactIntent" 5 | }, 6 | { 7 | "intent": "AMAZON.HelpIntent" 8 | }, 9 | { 10 | "intent": "AMAZON.StopIntent" 11 | }, 12 | { 13 | "intent": "AMAZON.CancelIntent" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /samples/spacegeek/speech_assets/SampleUtterances.txt: -------------------------------------------------------------------------------- 1 | GetNewFactIntent a fact 2 | GetNewFactIntent a space fact 3 | GetNewFactIntent tell me a fact 4 | GetNewFactIntent tell me a space fact 5 | GetNewFactIntent give me a fact 6 | GetNewFactIntent give me a space fact 7 | GetNewFactIntent tell me trivia 8 | GetNewFactIntent tell me a space trivia 9 | GetNewFactIntent give me trivia 10 | GetNewFactIntent give me a space trivia 11 | GetNewFactIntent give me some information 12 | GetNewFactIntent give me some space information 13 | GetNewFactIntent tell me something 14 | GetNewFactIntent give me something 15 | -------------------------------------------------------------------------------- /samples/spacegeek/templates.yaml: -------------------------------------------------------------------------------- 1 | space_fact_0: A year on Mercury is just 88 days long. 2 | space_fact_1: Despite being farther from the Sun, Venus experiences higher temperatures than Mercury. 3 | space_fact_2: Venus rotates counter-clockwise, possibly because of a collision in the past with an asteroid. 4 | space_fact_3: On Mars, the Sun appears about half the size as it does on Earth. 5 | space_fact_4: Earth is the only planet not named after a god. 6 | space_fact_5: Jupiter has the shortest day of all the planets. 7 | space_fact_6: The Milky Way galaxy will collide with the Andromeda Galaxy in about 5 billion years. 8 | space_fact_7: The Sun contains 99.86% of the mass in the Solar System. 9 | space_fact_8: The Sun is an almost perfect sphere. 10 | space_fact_9: A total solar eclipse can happen once every 1 to 2 years. This makes them a rare event. 11 | space_fact_10: Saturn radiates two and a half times more energy into space than it receives from the sun. 12 | space_fact_11: The temperature inside the Sun can reach 15 million degrees Celsius. 13 | space_fact_12: The Moon is moving approximately 3.8 cm away from our planet every year. 14 | card_title: SpaceGeek 15 | help: You can ask Space Geek tell me a space fact, or, you can say exit. What can I help you with? 16 | bye: Goodbye 17 | -------------------------------------------------------------------------------- /samples/tidepooler/speech_assets/IntentSchema.json: -------------------------------------------------------------------------------- 1 | { 2 | "intents": [ 3 | { 4 | "intent": "OneshotTideIntent", 5 | "slots": [ 6 | { 7 | "name": "City", 8 | "type": "LIST_OF_CITIES" 9 | }, 10 | { 11 | "name": "State", 12 | "type": "LIST_OF_STATES" 13 | }, 14 | { 15 | "name": "Date", 16 | "type": "AMAZON.DATE" 17 | } 18 | ] 19 | }, 20 | { 21 | "intent": "DialogTideIntent", 22 | "slots": [ 23 | { 24 | "name": "City", 25 | "type": "LIST_OF_CITIES" 26 | }, 27 | { 28 | "name": "State", 29 | "type": "LIST_OF_STATES" 30 | }, 31 | { 32 | "name": "Date", 33 | "type": "AMAZON.DATE" 34 | } 35 | ] 36 | }, 37 | { 38 | "intent": "SupportedCitiesIntent" 39 | }, 40 | { 41 | "intent": "AMAZON.HelpIntent" 42 | }, 43 | { 44 | "intent": "AMAZON.StopIntent" 45 | }, 46 | { 47 | "intent": "AMAZON.CancelIntent" 48 | } 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /samples/tidepooler/speech_assets/SampleUtterances.txt: -------------------------------------------------------------------------------- 1 | DialogTideIntent {City} 2 | DialogTideIntent {City} {State} 3 | DialogTideIntent {Date} 4 | 5 | OneshotTideIntent get high tide 6 | OneshotTideIntent get high tide for {City} {State} 7 | OneshotTideIntent get high tide for {City} {State} {Date} 8 | OneshotTideIntent get high tide for {City} {Date} 9 | OneshotTideIntent get high tide for {Date} 10 | OneshotTideIntent get high tide {Date} 11 | OneshotTideIntent get the high tide for {City} {Date} 12 | OneshotTideIntent get the next tide for {City} for {Date} 13 | OneshotTideIntent get the tides for {Date} 14 | OneshotTideIntent get tide information for {City} 15 | OneshotTideIntent get tide information for {City} {State} 16 | OneshotTideIntent get tide information for {City} {State} on {Date} 17 | OneshotTideIntent get tide information for {City} city 18 | OneshotTideIntent get tide information for {City} for {Date} 19 | OneshotTideIntent get tide information for {City} on {Date} 20 | OneshotTideIntent get tide information for {City} {Date} 21 | OneshotTideIntent get tides for {City} 22 | OneshotTideIntent get tides for {City} {State} 23 | OneshotTideIntent get tides for {City} {State} {Date} 24 | OneshotTideIntent get tides for {City} {Date} 25 | OneshotTideIntent tide information 26 | OneshotTideIntent tide information for {City} 27 | OneshotTideIntent tide information for {City} {State} 28 | OneshotTideIntent tide information for {City} on {Date} 29 | OneshotTideIntent tide information for {Date} 30 | OneshotTideIntent when high tide is 31 | OneshotTideIntent when is high tide 32 | OneshotTideIntent when is high tide for {City} {Date} 33 | OneshotTideIntent when is high tide in {City} 34 | OneshotTideIntent when is high tide in {City} {State} 35 | OneshotTideIntent when is high tide in {City} city 36 | OneshotTideIntent when is high tide in {City} on {Date} 37 | OneshotTideIntent when is high tide on {Date} 38 | OneshotTideIntent when is high tide {Date} 39 | OneshotTideIntent when is next tide 40 | OneshotTideIntent when is next tide in {City} {State} 41 | OneshotTideIntent when is next tide in {City} {State} on {Date} 42 | OneshotTideIntent when is next tide on {Date} 43 | OneshotTideIntent when is the highest tide in {City} 44 | OneshotTideIntent when is the highest tide in {City} {Date} 45 | OneshotTideIntent when is the highest tide {Date} 46 | OneshotTideIntent when is the next high tide {Date} 47 | OneshotTideIntent when is the next highest water 48 | OneshotTideIntent when is the next highest water for {City} 49 | OneshotTideIntent when is the next highest water for {City} {State} for {Date} 50 | OneshotTideIntent when is the next highest water for {City} {State} 51 | OneshotTideIntent when is the next highest water for {Date} 52 | OneshotTideIntent when is the next tide for {City} 53 | OneshotTideIntent when is the next tide for {City} {State} 54 | OneshotTideIntent when is the next tide for {City} city 55 | OneshotTideIntent when is the next tide for {City} for {Date} 56 | OneshotTideIntent when is the next tide for {Date} 57 | OneshotTideIntent when is today's high tide 58 | OneshotTideIntent when is today's highest tide {Date} 59 | OneshotTideIntent when will the water be highest for {City} 60 | OneshotTideIntent when will the water be highest for {City} for {Date} 61 | OneshotTideIntent when will the water be highest for {Date} 62 | OneshotTideIntent when will the water be highest {Date} 63 | OneshotTideIntent when is high tide on {Date} 64 | OneshotTideIntent when is the next tide for {Date} 65 | OneshotTideIntent when is next tide on {Date} 66 | OneshotTideIntent get the tides for {Date} 67 | OneshotTideIntent when is the highest tide {Date} 68 | OneshotTideIntent when is the next highest water for {Date} 69 | OneshotTideIntent when is high tide on {Date} 70 | OneshotTideIntent get high tide for {Date} 71 | OneshotTideIntent get high tide {Date} 72 | OneshotTideIntent when is high tide {Date} 73 | OneshotTideIntent tide information for {Date} 74 | OneshotTideIntent when is high tide in {City} on {Date} 75 | OneshotTideIntent get the next tide for {City} for {Date} 76 | OneshotTideIntent get high tide for {City} California {Date} 77 | OneshotTideIntent when is high tide for {City} {Date} 78 | OneshotTideIntent tide information for {City} on {Date} 79 | OneshotTideIntent when is high tide in {City} on {Date} 80 | OneshotTideIntent when is the next tide for {City} for {Date} 81 | OneshotTideIntent when is next tide in {City} {State} on {Date} 82 | OneshotTideIntent get high tide for {City} {Date} 83 | OneshotTideIntent get the high tide for {City} {Date} 84 | OneshotTideIntent tide information for {City} on {Date} 85 | OneshotTideIntent get tide information for {City} on {Date} 86 | OneshotTideIntent get tides for {City} {Date} 87 | 88 | SupportedCitiesIntent what cities 89 | SupportedCitiesIntent what cities are supported 90 | SupportedCitiesIntent which cities are supported 91 | SupportedCitiesIntent which cities 92 | SupportedCitiesIntent which cities do you know -------------------------------------------------------------------------------- /samples/tidepooler/speech_assets/customSlotTypes/LIST_OF_CITIES: -------------------------------------------------------------------------------- 1 | seattle 2 | los angeles 3 | monterey 4 | san diego 5 | san francisco 6 | boston 7 | new york 8 | miami 9 | wilmington 10 | tampa 11 | galveston 12 | morehead 13 | new orleans 14 | beaufort 15 | myrtle beach 16 | virginia beach 17 | charleston -------------------------------------------------------------------------------- /samples/tidepooler/speech_assets/customSlotTypes/LIST_OF_STATES: -------------------------------------------------------------------------------- 1 | california 2 | florida 3 | louisiana 4 | massachusetts 5 | new york 6 | north carolina 7 | south carolina 8 | texas 9 | virginia 10 | washington -------------------------------------------------------------------------------- /samples/tidepooler/templates.yaml: -------------------------------------------------------------------------------- 1 | welcome: | 2 | 3 | Welcome to Tide Pooler. 4 | 7 | 8 | tide_info: | 9 | {{ date | humanize_date }} in {{ city }}, the first high tide will be around 10 | {{ tideinfo.first_high_tide_time | humanize_time }}, and will peak at about 11 | {{ tideinfo.first_high_tide_height | humanize_height }}, followed by a low tide around 12 | {{ tideinfo.low_tide_time | humanize_time }}, that will be about 13 | {{ tideinfo.low_tide_height | humanize_height }}. 14 | 15 | The second high tide will be around {{ tideinfo.second_high_tide_time | humanize_time }}, 16 | and will peak at about {{ tideinfo.second_high_tide_height | humanize_height }} 17 | 18 | help: | 19 | I can lead you through providing a city and day of the week to get tide information, or you can simply open 20 | Tide Pooler and ask a question like, get tide information for Seattle on Saturday. For a list of supported 21 | cities, ask what cities are supported. Which city would you like tide information for? 22 | 23 | list_cities: | 24 | Currently, I know tide information for these coastal cities: {{ cities }} 25 | Which city would you like tide information for? 26 | 27 | list_cities_reprompt: Which city would you like tide information for? 28 | 29 | city_dialog: For which city would you like tide information for {{ date | humanize_date }} 30 | 31 | city_dialog_reprompt: For which city? 32 | 33 | date_dialog: For which date would you like tide information for {{ city }}? 34 | 35 | date_dialog_reprompt: For which date? 36 | 37 | date_dialog2: Please try again saying a day of the week, for example, Saturday 38 | 39 | noaa_problem: Sorry, the National Oceanic tide service is experiencing a problem. Please try again later. 40 | 41 | bye: Goodbye 42 | -------------------------------------------------------------------------------- /samples/tidepooler/tidepooler.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | import datetime 4 | import math 5 | import re 6 | from six.moves.urllib.request import urlopen 7 | from six.moves.urllib.parse import urlencode 8 | 9 | import aniso8601 10 | from flask import Flask, json, render_template 11 | from flask_ask import Ask, request, session, question, statement 12 | 13 | 14 | ENDPOINT = "https://tidesandcurrents.noaa.gov/api/datagetter" 15 | SESSION_CITY = "city" 16 | SESSION_DATE = "date" 17 | 18 | # NOAA station codes 19 | STATION_CODE_SEATTLE = "9447130" 20 | STATION_CODE_SAN_FRANCISCO = "9414290" 21 | STATION_CODE_MONTEREY = "9413450" 22 | STATION_CODE_LOS_ANGELES = "9410660" 23 | STATION_CODE_SAN_DIEGO = "9410170" 24 | STATION_CODE_BOSTON = "8443970" 25 | STATION_CODE_NEW_YORK = "8518750" 26 | STATION_CODE_VIRGINIA_BEACH = "8638863" 27 | STATION_CODE_WILMINGTON = "8658163" 28 | STATION_CODE_CHARLESTON = "8665530" 29 | STATION_CODE_BEAUFORT = "8656483" 30 | STATION_CODE_MYRTLE_BEACH = "8661070" 31 | STATION_CODE_MIAMI = "8723214" 32 | STATION_CODE_TAMPA = "8726667" 33 | STATION_CODE_NEW_ORLEANS = "8761927" 34 | STATION_CODE_GALVESTON = "8771341" 35 | 36 | STATIONS = {} 37 | STATIONS["seattle"] = STATION_CODE_SEATTLE 38 | STATIONS["san francisco"] = STATION_CODE_SAN_FRANCISCO 39 | STATIONS["monterey"] = STATION_CODE_MONTEREY 40 | STATIONS["los angeles"] = STATION_CODE_LOS_ANGELES 41 | STATIONS["san diego"] = STATION_CODE_SAN_DIEGO 42 | STATIONS["boston"] = STATION_CODE_BOSTON 43 | STATIONS["new york"] = STATION_CODE_NEW_YORK 44 | STATIONS["virginia beach"] = STATION_CODE_VIRGINIA_BEACH 45 | STATIONS["wilmington"] = STATION_CODE_WILMINGTON 46 | STATIONS["charleston"] = STATION_CODE_CHARLESTON 47 | STATIONS["beaufort"] = STATION_CODE_BEAUFORT 48 | STATIONS["myrtle beach"] = STATION_CODE_MYRTLE_BEACH 49 | STATIONS["miami"] = STATION_CODE_MIAMI 50 | STATIONS["tampa"] = STATION_CODE_TAMPA 51 | STATIONS["new orleans"] = STATION_CODE_NEW_ORLEANS 52 | STATIONS["galveston"] = STATION_CODE_GALVESTON 53 | 54 | 55 | app = Flask(__name__) 56 | ask = Ask(app, "/") 57 | logging.getLogger('flask_ask').setLevel(logging.DEBUG) 58 | 59 | 60 | class TideInfo(object): 61 | 62 | def __init__(self): 63 | self.first_high_tide_time = None 64 | self.first_high_tide_height = None 65 | self.low_tide_time = None 66 | self.low_tide_height = None 67 | self.second_high_tide_time = None 68 | self.second_high_tide_height = None 69 | 70 | 71 | @ask.launch 72 | def launch(): 73 | welcome_text = render_template('welcome') 74 | help_text = render_template('help') 75 | return question(welcome_text).reprompt(help_text) 76 | 77 | 78 | @ask.intent('OneshotTideIntent', 79 | mapping={'city': 'City', 'date': 'Date'}, 80 | convert={'date': 'date'}, 81 | default={'city': 'seattle', 'date': datetime.date.today }) 82 | def one_shot_tide(city, date): 83 | if city.lower() not in STATIONS: 84 | return supported_cities() 85 | return _make_tide_request(city, date) 86 | 87 | 88 | @ask.intent('DialogTideIntent', 89 | mapping={'city': 'City', 'date': 'Date'}, 90 | convert={'date': 'date'}) 91 | def dialog_tide(city, date): 92 | if city is not None: 93 | if city.lower() not in STATIONS: 94 | return supported_cities() 95 | if SESSION_DATE not in session.attributes: 96 | session.attributes[SESSION_CITY] = city 97 | return _dialog_date(city) 98 | date = aniso8601.parse_date(session.attributes[SESSION_DATE]) 99 | return _make_tide_request(city, date) 100 | elif date is not None: 101 | if SESSION_CITY not in session.attributes: 102 | session.attributes[SESSION_DATE] = date.isoformat() 103 | return _dialog_city(date) 104 | city = session.attributes[SESSION_CITY] 105 | return _make_tide_request(city, date) 106 | else: 107 | return _dialog_no_slot() 108 | 109 | 110 | @ask.intent('SupportedCitiesIntent') 111 | def supported_cities(): 112 | cities = ", ".join(sorted(STATIONS.keys())) 113 | list_cities_text = render_template('list_cities', cities=cities) 114 | list_cities_reprompt_text = render_template('list_cities_reprompt') 115 | return question(list_cities_text).reprompt(list_cities_reprompt_text) 116 | 117 | 118 | @ask.intent('AMAZON.HelpIntent') 119 | def help(): 120 | help_text = render_template('help') 121 | list_cities_reprompt_text = render_template('list_cities_reprompt') 122 | return question(help_text).reprompt(list_cities_reprompt_text) 123 | 124 | 125 | @ask.intent('AMAZON.StopIntent') 126 | def stop(): 127 | bye_text = render_template('bye') 128 | return statement(bye_text) 129 | 130 | 131 | @ask.intent('AMAZON.CancelIntent') 132 | def cancel(): 133 | bye_text = render_template('bye') 134 | return statement(bye_text) 135 | 136 | 137 | @ask.session_ended 138 | def session_ended(): 139 | return "{}", 200 140 | 141 | 142 | @app.template_filter() 143 | def humanize_date(dt): 144 | # http://stackoverflow.com/a/20007730/1163855 145 | ordinal = lambda n: "%d%s" % (n,"tsnrhtdd"[(n/10%10!=1)*(n%10<4)*n%10::4]) 146 | month_and_day_of_week = dt.strftime('%A %B') 147 | day_of_month = ordinal(dt.day) 148 | year = dt.year if dt.year != datetime.datetime.now().year else "" 149 | formatted_date = "{} {} {}".format(month_and_day_of_week, day_of_month, year) 150 | formatted_date = re.sub('\s+', ' ', formatted_date) 151 | return formatted_date 152 | 153 | 154 | @app.template_filter() 155 | def humanize_time(dt): 156 | morning_threshold = 12 157 | afternoon_threshold = 17 158 | evening_threshold = 20 159 | hour_24 = dt.hour 160 | if hour_24 < morning_threshold: 161 | period_of_day = "in the morning" 162 | elif hour_24 < afternoon_threshold: 163 | period_of_day = "in the afternoon" 164 | elif hour_24 < evening_threshold: 165 | period_of_day = "in the evening" 166 | else: 167 | period_of_day = " at night" 168 | the_time = dt.strftime('%I:%M') 169 | formatted_time = "{} {}".format(the_time, period_of_day) 170 | return formatted_time 171 | 172 | 173 | @app.template_filter() 174 | def humanize_height(height): 175 | round_down_threshold = 0.25 176 | round_to_half_threshold = 0.75 177 | is_negative = False 178 | if height < 0: 179 | height = abs(height) 180 | is_negative = True 181 | remainder = height % 1 182 | if remainder < round_down_threshold: 183 | remainder_text = "" 184 | feet = int(math.floor(height)) 185 | elif remainder < round_to_half_threshold: 186 | remainder_text = "and a half" 187 | feet = int(math.floor(height)) 188 | else: 189 | remainder_text = "" 190 | feet = int(math.floor(height)) 191 | if is_negative: 192 | feet *= -1 193 | formatted_height = "{} {} feet".format(feet, remainder_text) 194 | formatted_height = re.sub('\s+', ' ', formatted_height) 195 | return formatted_height 196 | 197 | 198 | def _dialog_no_slot(): 199 | if SESSION_CITY in session.attributes: 200 | date_dialog2_text = render_template('date_dialog2') 201 | return question(date_dialog2_text).reprompt(date_dialog2_text) 202 | else: 203 | return supported_cities() 204 | 205 | 206 | def _dialog_date(city): 207 | date_dialog_text = render_template('date_dialog', city=city) 208 | date_dialog_reprompt_text = render_template('date_dialog_reprompt') 209 | return question(date_dialog_text).reprompt(date_dialog_reprompt_text) 210 | 211 | 212 | def _dialog_city(date): 213 | session.attributes[SESSION_DATE] = date 214 | session.attributes_encoder = _json_date_handler 215 | city_dialog_text = render_template('city_dialog', date=date) 216 | city_dialog_reprompt_text = render_template('city_dialog_reprompt') 217 | return question(city_dialog_text).reprompt(city_dialog_reprompt_text) 218 | 219 | 220 | def _json_date_handler(obj): 221 | if isinstance(obj, datetime.date): 222 | return obj.isoformat() 223 | 224 | 225 | def _make_tide_request(city, date): 226 | station = STATIONS.get(city.lower()) 227 | noaa_api_params = { 228 | 'station': station, 229 | 'product': 'predictions', 230 | 'datum': 'MLLW', 231 | 'units': 'english', 232 | 'time_zone': 'lst_ldt', 233 | 'format': 'json' 234 | } 235 | if date == datetime.date.today(): 236 | noaa_api_params['date'] = 'today' 237 | else: 238 | noaa_api_params['begin_date'] = date.strftime('%Y%m%d') 239 | noaa_api_params['range'] = 24 240 | url = ENDPOINT + "?" + urlencode(noaa_api_params) 241 | resp_body = urlopen(url).read() 242 | if len(resp_body) == 0: 243 | statement_text = render_template('noaa_problem') 244 | else: 245 | noaa_response_obj = json.loads(resp_body) 246 | predictions = noaa_response_obj['predictions'] 247 | tideinfo = _find_tide_info(predictions) 248 | statement_text = render_template('tide_info', date=date, city=city, tideinfo=tideinfo) 249 | return statement(statement_text).simple_card("Tide Pooler", statement_text) 250 | 251 | 252 | def _find_tide_info(predictions): 253 | """ 254 | Algorithm to find the 2 high tides for the day, the first of which is smaller and occurs 255 | mid-day, the second of which is larger and typically in the evening. 256 | """ 257 | 258 | last_prediction = None 259 | first_high_tide = None 260 | second_high_tide = None 261 | low_tide = None 262 | first_tide_done = False 263 | for prediction in predictions: 264 | if last_prediction is None: 265 | last_prediction = prediction 266 | continue 267 | if last_prediction['v'] < prediction['v']: 268 | if not first_tide_done: 269 | first_high_tide = prediction 270 | else: 271 | second_high_tide = prediction 272 | else: # we're decreasing 273 | if not first_tide_done and first_high_tide is not None: 274 | first_tide_done = True 275 | elif second_high_tide is not None: 276 | break # we're decreasing after having found the 2nd tide. We're done. 277 | if first_tide_done: 278 | low_tide = prediction 279 | last_prediction = prediction 280 | 281 | fmt = '%Y-%m-%d %H:%M' 282 | parse = datetime.datetime.strptime 283 | tideinfo = TideInfo() 284 | tideinfo.first_high_tide_time = parse(first_high_tide['t'], fmt) 285 | tideinfo.first_high_tide_height = float(first_high_tide['v']) 286 | tideinfo.second_high_tide_time = parse(second_high_tide['t'], fmt) 287 | tideinfo.second_high_tide_height = float(second_high_tide['v']) 288 | tideinfo.low_tide_time = parse(low_tide['t'], fmt) 289 | tideinfo.low_tide_height = float(low_tide['v']) 290 | return tideinfo 291 | 292 | 293 | if __name__ == '__main__': 294 | if 'ASK_VERIFY_REQUESTS' in os.environ: 295 | verify = str(os.environ.get('ASK_VERIFY_REQUESTS', '')).lower() 296 | if verify == 'false': 297 | app.config['ASK_VERIFY_REQUESTS'] = False 298 | app.run(debug=True) 299 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | 4 | [metadata] 5 | description-file = README.md 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Flask-Ask 3 | ------------- 4 | 5 | Easy Alexa Skills Kit integration for Flask 6 | """ 7 | from setuptools import setup 8 | 9 | def parse_requirements(filename): 10 | """ load requirements from a pip requirements file """ 11 | lineiter = (line.strip() for line in open(filename)) 12 | return [line for line in lineiter if line and not line.startswith("#")] 13 | 14 | setup( 15 | name='Flask-Ask', 16 | version='0.9.7', 17 | url='https://github.com/johnwheeler/flask-ask', 18 | license='Apache 2.0', 19 | author='John Wheeler', 20 | author_email='john@johnwheeler.org', 21 | description='Rapid Alexa Skills Kit Development for Amazon Echo Devices in Python', 22 | long_description=__doc__, 23 | packages=['flask_ask'], 24 | zip_safe=False, 25 | include_package_data=True, 26 | platforms='any', 27 | install_requires=parse_requirements('requirements.txt'), 28 | test_requires=[ 29 | 'mock', 30 | 'requests' 31 | ], 32 | test_suite='tests', 33 | classifiers=[ 34 | 'License :: OSI Approved :: Apache Software License', 35 | 'Framework :: Flask', 36 | 'Programming Language :: Python', 37 | 'Environment :: Web Environment', 38 | 'Intended Audience :: Developers', 39 | 'Operating System :: OS Independent', 40 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 41 | 'Topic :: Software Development :: Libraries :: Python Modules' 42 | ] 43 | ) 44 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnwheeler/flask-ask/a93488b700479a3b2d80eb54d0f6585caae15ef3/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_audio.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from mock import patch, MagicMock 3 | from flask import Flask 4 | from flask_ask import Ask, audio 5 | from flask_ask.models import _Field 6 | 7 | 8 | class AudioUnitTests(unittest.TestCase): 9 | 10 | def setUp(self): 11 | self.ask_patcher = patch('flask_ask.core.find_ask', return_value=Ask()) 12 | self.ask_patcher.start() 13 | self.context_patcher = patch('flask_ask.models.context', return_value=MagicMock()) 14 | self.context_patcher.start() 15 | 16 | def tearDown(self): 17 | self.ask_patcher.stop() 18 | self.context_patcher.stop() 19 | 20 | def test_token_generation(self): 21 | """ Confirm we get a new token when setting a stream url """ 22 | audio_item = audio()._audio_item(stream_url='https://fakestream', offset=123) 23 | self.assertEqual(36, len(audio_item['stream']['token'])) 24 | self.assertEqual(123, audio_item['stream']['offsetInMilliseconds']) 25 | 26 | def test_custom_token(self): 27 | """ Check to see that the provided opaque token remains constant""" 28 | token = "hello_world" 29 | audio_item = audio()._audio_item(stream_url='https://fakestream', offset=10, opaque_token=token) 30 | self.assertEqual(token, audio_item['stream']['token']) 31 | self.assertEqual(10, audio_item['stream']['offsetInMilliseconds']) 32 | 33 | 34 | class AskStreamHandlingTests(unittest.TestCase): 35 | 36 | def setUp(self): 37 | fake_context = {'System': {'user': {'userId': 'dave'}}} 38 | self.context_patcher = patch.object(Ask, 'context', return_value=fake_context) 39 | self.context_patcher.start() 40 | self.request_patcher = patch.object(Ask, 'request', return_value=MagicMock()) 41 | self.request_patcher.start() 42 | 43 | def tearDown(self): 44 | self.context_patcher.stop() 45 | self.request_patcher.stop() 46 | 47 | def test_setting_and_getting_current_stream(self): 48 | ask = Ask() 49 | with patch('flask_ask.core.find_ask', return_value=ask): 50 | self.assertEqual(_Field(), ask.current_stream) 51 | 52 | stream = _Field() 53 | stream.__dict__.update({'token': 'asdf', 'offsetInMilliseconds': 123, 'url': 'junk'}) 54 | with patch('flask_ask.core.top_stream', return_value=stream): 55 | self.assertEqual(stream, ask.current_stream) 56 | 57 | def test_from_directive_call(self): 58 | ask = Ask() 59 | fake_stream = _Field() 60 | fake_stream.__dict__.update({'token':'fake'}) 61 | with patch('flask_ask.core.top_stream', return_value=fake_stream): 62 | from_buffer = ask._from_directive() 63 | self.assertEqual(fake_stream, from_buffer) 64 | 65 | 66 | if __name__ == '__main__': 67 | unittest.main() 68 | -------------------------------------------------------------------------------- /tests/test_cache.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from mock import patch, Mock 3 | from werkzeug.contrib.cache import SimpleCache 4 | from flask_ask.core import Ask 5 | from flask_ask.cache import push_stream, pop_stream, top_stream, set_stream 6 | 7 | 8 | class CacheTests(unittest.TestCase): 9 | 10 | def setUp(self): 11 | self.patcher = patch('flask_ask.core.find_ask', return_value=Ask()) 12 | self.ask = self.patcher.start() 13 | self.user_id = 'dave' 14 | self.token = '123-abc' 15 | self.cache = SimpleCache() 16 | 17 | def tearDown(self): 18 | self.patcher.stop() 19 | 20 | def test_adding_removing_stream(self): 21 | self.assertTrue(push_stream(self.cache, self.user_id, self.token)) 22 | 23 | # peak at the top 24 | self.assertEqual(self.token, top_stream(self.cache, self.user_id)) 25 | self.assertIsNone(top_stream(self.cache, 'not dave')) 26 | 27 | # pop it off 28 | self.assertEqual(self.token, pop_stream(self.cache, self.user_id)) 29 | self.assertIsNone(top_stream(self.cache, self.user_id)) 30 | 31 | def test_pushing_works_like_a_stack(self): 32 | push_stream(self.cache, self.user_id, 'junk') 33 | push_stream(self.cache, self.user_id, self.token) 34 | 35 | self.assertEqual(self.token, pop_stream(self.cache, self.user_id)) 36 | self.assertEqual('junk', pop_stream(self.cache, self.user_id)) 37 | self.assertIsNone(pop_stream(self.cache, self.user_id)) 38 | 39 | def test_cannot_push_nones_into_stack(self): 40 | self.assertIsNone(push_stream(self.cache, self.user_id, None)) 41 | 42 | def test_set_overrides_stack(self): 43 | push_stream(self.cache, self.user_id, '1') 44 | push_stream(self.cache, self.user_id, '2') 45 | self.assertEqual('2', top_stream(self.cache, self.user_id)) 46 | 47 | set_stream(self.cache, self.user_id, '3') 48 | self.assertEqual('3', pop_stream(self.cache, self.user_id)) 49 | self.assertIsNone(pop_stream(self.cache, self.user_id)) 50 | 51 | def test_calls_to_top_with_no_user_return_none(self): 52 | """ RedisCache implementation doesn't like None key values. """ 53 | mock = Mock() 54 | result = top_stream(mock, None) 55 | self.assertFalse(mock.get.called) 56 | self.assertIsNone(result) 57 | 58 | 59 | if __name__ == '__main__': 60 | unittest.main() 61 | -------------------------------------------------------------------------------- /tests/test_core.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import unittest 3 | from aniso8601.timezone import UTCOffset, build_utcoffset 4 | from flask_ask.core import Ask 5 | 6 | from datetime import datetime, timedelta 7 | from mock import patch, MagicMock 8 | import json 9 | 10 | 11 | class FakeRequest(object): 12 | """ Fake out a Flask request for testing purposes for now """ 13 | 14 | headers = {'Signaturecertchainurl': None, 'Signature': None} 15 | 16 | def __init__(self, data): 17 | self.data = json.dumps(data) 18 | 19 | 20 | class TestCoreRoutines(unittest.TestCase): 21 | """ Tests for core Flask Ask functionality """ 22 | 23 | 24 | def setUp(self): 25 | self.mock_app = MagicMock() 26 | self.mock_app.debug = True 27 | self.mock_app.config = {'ASK_VERIFY_TIMESTAMP_DEBUG': False} 28 | 29 | # XXX: this mess implies we should think about tidying up Ask._alexa_request 30 | self.patch_current_app = patch('flask_ask.core.current_app', new=self.mock_app) 31 | self.patch_load_cert = patch('flask_ask.core.verifier.load_certificate') 32 | self.patch_verify_sig = patch('flask_ask.core.verifier.verify_signature') 33 | self.patch_current_app.start() 34 | self.patch_load_cert.start() 35 | self.patch_verify_sig.start() 36 | 37 | @patch('flask_ask.core.flask_request', 38 | new=FakeRequest({'request': {'timestamp': 1234}, 39 | 'session': {'application': {'applicationId': 1}}})) 40 | def test_alexa_request_parsing(self): 41 | ask = Ask() 42 | ask._alexa_request() 43 | 44 | 45 | def test_parse_timestamp(self): 46 | utc = build_utcoffset('UTC', timedelta(hours=0)) 47 | result = Ask._parse_timestamp('2017-07-08T07:38:00Z') 48 | self.assertEqual(datetime(2017, 7, 8, 7, 38, 0, 0, utc), result) 49 | 50 | result = Ask._parse_timestamp(1234567890) 51 | self.assertEqual(datetime(2009, 2, 13, 23, 31, 30), result) 52 | 53 | with self.assertRaises(ValueError): 54 | Ask._parse_timestamp(None) 55 | 56 | def test_tries_parsing_on_valueerror(self): 57 | max_timestamp = 253402300800 58 | 59 | # should cause a ValueError normally 60 | with self.assertRaises(ValueError): 61 | datetime.utcfromtimestamp(max_timestamp) 62 | 63 | # should safely parse, assuming scale change needed 64 | # note: this assert looks odd, but Py2 handles the parsing 65 | # differently, resulting in a differing timestamp 66 | # due to more granularity of microseconds 67 | result = Ask._parse_timestamp(max_timestamp) 68 | self.assertEqual(datetime(1978, 1, 11, 21, 31, 40).timetuple()[0:6], 69 | result.timetuple()[0:6]) 70 | 71 | with self.assertRaises(ValueError): 72 | # still raise an error if too large 73 | Ask._parse_timestamp(max_timestamp * 1000) 74 | 75 | def tearDown(self): 76 | self.patch_current_app.stop() 77 | self.patch_load_cert.stop() 78 | self.patch_verify_sig.stop() 79 | -------------------------------------------------------------------------------- /tests/test_integration.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import json 3 | import uuid 4 | 5 | from flask_ask import Ask, audio 6 | from flask import Flask 7 | 8 | 9 | play_request = { 10 | "version": "1.0", 11 | "session": { 12 | "new": True, 13 | "sessionId": "amzn1.echo-api.session.0000000-0000-0000-0000-00000000000", 14 | "application": { 15 | "applicationId": "fake-application-id" 16 | }, 17 | "attributes": {}, 18 | "user": { 19 | "userId": "amzn1.account.AM3B00000000000000000000000" 20 | } 21 | }, 22 | "context": { 23 | "System": { 24 | "application": { 25 | "applicationId": "fake-application-id" 26 | }, 27 | "user": { 28 | "userId": "amzn1.account.AM3B00000000000000000000000" 29 | }, 30 | "device": { 31 | "supportedInterfaces": { 32 | "AudioPlayer": {} 33 | } 34 | } 35 | }, 36 | "AudioPlayer": { 37 | "offsetInMilliseconds": 0, 38 | "playerActivity": "IDLE" 39 | } 40 | }, 41 | "request": { 42 | "type": "IntentRequest", 43 | "requestId": "string", 44 | "timestamp": "string", 45 | "locale": "string", 46 | "intent": { 47 | "name": "TestPlay", 48 | "slots": { 49 | } 50 | } 51 | } 52 | } 53 | 54 | 55 | class AudioIntegrationTests(unittest.TestCase): 56 | """ Integration tests of the Audio Directives """ 57 | 58 | def setUp(self): 59 | self.app = Flask(__name__) 60 | self.app.config['ASK_VERIFY_REQUESTS'] = False 61 | self.ask = Ask(app=self.app, route='/ask') 62 | self.client = self.app.test_client() 63 | self.stream_url = 'https://fakestream' 64 | self.custom_token = 'custom_uuid_{0}'.format(str(uuid.uuid4())) 65 | 66 | @self.ask.intent('TestPlay') 67 | def play(): 68 | return audio('playing').play(self.stream_url) 69 | 70 | @self.ask.intent('TestCustomTokenIntents') 71 | def custom_token_intents(): 72 | return audio('playing with custom token').play(self.stream_url, 73 | opaque_token=self.custom_token) 74 | 75 | def tearDown(self): 76 | pass 77 | 78 | def test_play_intent(self): 79 | """ Test to see if we can properly play a stream """ 80 | response = self.client.post('/ask', data=json.dumps(play_request)) 81 | self.assertEqual(200, response.status_code) 82 | 83 | data = json.loads(response.data.decode('utf-8')) 84 | self.assertEqual('playing', 85 | data['response']['outputSpeech']['text']) 86 | 87 | directive = data['response']['directives'][0] 88 | self.assertEqual('AudioPlayer.Play', directive['type']) 89 | 90 | stream = directive['audioItem']['stream'] 91 | self.assertIsNotNone(stream['token']) 92 | self.assertEqual(self.stream_url, stream['url']) 93 | self.assertEqual(0, stream['offsetInMilliseconds']) 94 | 95 | def test_play_intent_with_custom_token(self): 96 | """ Test to check that custom token supplied is returned """ 97 | 98 | # change the intent name to route to our custom token for play_request 99 | original_intent_name = play_request['request']['intent']['name'] 100 | play_request['request']['intent']['name'] = 'TestCustomTokenIntents' 101 | 102 | response = self.client.post('/ask', data=json.dumps(play_request)) 103 | self.assertEqual(200, response.status_code) 104 | 105 | data = json.loads(response.data.decode('utf-8')) 106 | self.assertEqual('playing with custom token', 107 | data['response']['outputSpeech']['text']) 108 | 109 | directive = data['response']['directives'][0] 110 | self.assertEqual('AudioPlayer.Play', directive['type']) 111 | 112 | stream = directive['audioItem']['stream'] 113 | self.assertEqual(stream['token'], self.custom_token) 114 | self.assertEqual(self.stream_url, stream['url']) 115 | self.assertEqual(0, stream['offsetInMilliseconds']) 116 | 117 | # reset our play_request 118 | play_request['request']['intent']['name'] = original_intent_name 119 | 120 | 121 | if __name__ == '__main__': 122 | unittest.main() 123 | -------------------------------------------------------------------------------- /tests/test_integration_support_entity_resolution.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import json 3 | import uuid 4 | 5 | from flask_ask import Ask, statement 6 | from flask import Flask 7 | 8 | 9 | play_request = { 10 | "version": "1.0", 11 | "session": { 12 | "new": False, 13 | "sessionId": "amzn1.echo-api.session.f6ebc0ba-9d7a-4c3f-b056-b6c3f9da0713", 14 | "application": { 15 | "applicationId": "amzn1.ask.skill.26338c44-65da-4d58-aa75-c86b21271eb7" 16 | }, 17 | "user": { 18 | "userId": "amzn1.ask.account.AHR7KBC3MFCX7LYT6HJBGDLIGQUU3FLANWCZ", 19 | } 20 | }, 21 | "context": { 22 | "AudioPlayer": { 23 | "playerActivity": "IDLE" 24 | }, 25 | "Display": { 26 | "token": "" 27 | }, 28 | "System": { 29 | "application": { 30 | "applicationId": "amzn1.ask.skill.26338c44-65da-4d58-aa75-c86b21271eb7" 31 | }, 32 | "user": { 33 | "userId": "amzn1.ask.account.AHR7KBC3MFCX7LYT6HJBGDLIGQUU3FLANWCZ", 34 | }, 35 | "device": { 36 | "deviceId": "amzn1.ask.device.AELNXV4JQJMF5QALYUQXHOZJ", 37 | "supportedInterfaces": { 38 | "AudioPlayer": {}, 39 | "Display": { 40 | "templateVersion": "1.0", 41 | "markupVersion": "1.0" 42 | } 43 | } 44 | }, 45 | "apiEndpoint": "https://api.amazonalexa.com", 46 | } 47 | }, 48 | "request": { 49 | "type": "IntentRequest", 50 | "requestId": "amzn1.echo-api.request.4859a7e3-1960-4ed9-ac7b-854309346916", 51 | "timestamp": "2018-04-04T06:28:23Z", 52 | "locale": "en-US", 53 | "intent": { 54 | "name": "TestCustomSlotTypeIntents", 55 | "confirmationStatus": "NONE", 56 | "slots": { 57 | "child_info": { 58 | "name": "child_info", 59 | "value": "friends info", 60 | "resolutions": { 61 | "resolutionsPerAuthority": [ 62 | { 63 | "authority": "amzn1.er-authority.echo-sdk.amzn1.ask.skill.26338c44-65da-4d58-aa75-c86b21271eb7.child_info_type", 64 | "status": { 65 | "code": "ER_SUCCESS_MATCH" 66 | }, 67 | "values": [ 68 | { 69 | "value": { 70 | "name": "friend_info", 71 | "id": "FRIEND_INFO" 72 | } 73 | } 74 | ] 75 | } 76 | ] 77 | }, 78 | "confirmationStatus": "NONE" 79 | } 80 | } 81 | }, 82 | "dialogState": "STARTED" 83 | } 84 | } 85 | 86 | 87 | class CustomSlotTypeIntegrationTests(unittest.TestCase): 88 | """ Integration tests of the custom slot type """ 89 | 90 | def setUp(self): 91 | self.app = Flask(__name__) 92 | self.app.config['ASK_VERIFY_REQUESTS'] = False 93 | self.ask = Ask(app=self.app, route='/ask') 94 | self.client = self.app.test_client() 95 | 96 | @self.ask.intent('TestCustomSlotTypeIntents') 97 | def custom_slot_type_intents(child_info): 98 | return statement(child_info) 99 | 100 | def tearDown(self): 101 | pass 102 | 103 | def test_custom_slot_type_intent(self): 104 | """ Test to see if custom slot type value is correct """ 105 | response = self.client.post('/ask', data=json.dumps(play_request)) 106 | self.assertEqual(200, response.status_code) 107 | 108 | data = json.loads(response.data.decode('utf-8')) 109 | self.assertEqual('friend_info', 110 | data['response']['outputSpeech']['text']) 111 | 112 | 113 | if __name__ == '__main__': 114 | unittest.main() 115 | -------------------------------------------------------------------------------- /tests/test_samples.py: -------------------------------------------------------------------------------- 1 | """ 2 | Smoke test using the samples. 3 | """ 4 | 5 | import unittest 6 | import os 7 | import six 8 | import sys 9 | import time 10 | import subprocess 11 | 12 | from requests import post 13 | 14 | import flask_ask 15 | 16 | 17 | launch = { 18 | "version": "1.0", 19 | "session": { 20 | "new": True, 21 | "sessionId": "amzn1.echo-api.session.0000000-0000-0000-0000-00000000000", 22 | "application": { 23 | "applicationId": "fake-application-id" 24 | }, 25 | "attributes": {}, 26 | "user": { 27 | "userId": "amzn1.account.AM3B00000000000000000000000" 28 | } 29 | }, 30 | "context": { 31 | "System": { 32 | "application": { 33 | "applicationId": "fake-application-id" 34 | }, 35 | "user": { 36 | "userId": "amzn1.account.AM3B00000000000000000000000" 37 | }, 38 | "device": { 39 | "supportedInterfaces": { 40 | "AudioPlayer": {} 41 | } 42 | } 43 | }, 44 | "AudioPlayer": { 45 | "offsetInMilliseconds": 0, 46 | "playerActivity": "IDLE" 47 | } 48 | }, 49 | "request": { 50 | "type": "LaunchRequest", 51 | "requestId": "string", 52 | "timestamp": "string", 53 | "locale": "string", 54 | "intent": { 55 | "name": "TestPlay", 56 | "slots": { 57 | } 58 | } 59 | } 60 | } 61 | 62 | 63 | project_root = os.path.abspath(os.path.join(flask_ask.__file__, '../..')) 64 | 65 | 66 | @unittest.skipIf(six.PY2, "Not yet supported on Python 2.x") 67 | class SmokeTestUsingSamples(unittest.TestCase): 68 | """ Try launching each sample and sending some requests to them. """ 69 | 70 | def setUp(self): 71 | self.python = sys.executable 72 | self.env = {'PYTHONPATH': project_root, 73 | 'ASK_VERIFY_REQUESTS': 'false'} 74 | if os.name == 'nt': 75 | self.env['SYSTEMROOT'] = os.getenv('SYSTEMROOT') 76 | self.env['PATH'] = os.getenv('PATH') 77 | 78 | def _launch(self, sample): 79 | prefix = os.path.join(project_root, 'samples/') 80 | path = prefix + sample 81 | process = subprocess.Popen([self.python, path], env=self.env) 82 | time.sleep(1) 83 | self.assertIsNone(process.poll(), 84 | msg='Poll should work,' 85 | 'otherwise we failed to launch') 86 | self.process = process 87 | 88 | def _post(self, route='/', data={}): 89 | url = 'http://127.0.0.1:5000' + str(route) 90 | print('POSTing to %s' % url) 91 | response = post(url, json=data) 92 | self.assertEqual(200, response.status_code) 93 | return response 94 | 95 | @staticmethod 96 | def _get_text(http_response): 97 | data = http_response.json() 98 | return data.get('response', {})\ 99 | .get('outputSpeech', {})\ 100 | .get('text', None) 101 | 102 | @staticmethod 103 | def _get_reprompt(http_response): 104 | data = http_response.json() 105 | return data.get('response', {})\ 106 | .get('reprompt', {})\ 107 | .get('outputSpeech', {})\ 108 | .get('text', None) 109 | 110 | def tearDown(self): 111 | try: 112 | self.process.terminate() 113 | self.process.communicate(timeout=1) 114 | except Exception as e: 115 | try: 116 | print('[%s]...trying to kill.' % str(e)) 117 | self.process.kill() 118 | self.process.communicate(timeout=1) 119 | except Exception as e: 120 | print('Error killing test python process: %s' % str(e)) 121 | print('*** it is recommended you manually kill with PID %s', 122 | self.process.pid) 123 | 124 | def test_helloworld(self): 125 | """ Test the HelloWorld sample project """ 126 | self._launch('helloworld/helloworld.py') 127 | response = self._post(data=launch) 128 | self.assertTrue('hello' in self._get_text(response)) 129 | 130 | def test_session_sample(self): 131 | """ Test the Session sample project """ 132 | self._launch('session/session.py') 133 | response = self._post(data=launch) 134 | self.assertTrue('favorite color' in self._get_text(response)) 135 | 136 | def test_audio_simple_demo(self): 137 | """ Test the SimpleDemo Audio sample project """ 138 | self._launch('audio/simple_demo/ask_audio.py') 139 | response = self._post(data=launch) 140 | self.assertTrue('audio example' in self._get_text(response)) 141 | 142 | def test_audio_playlist_demo(self): 143 | """ Test the Playlist Audio sample project """ 144 | self._launch('audio/playlist_demo/playlist.py') 145 | response = self._post(data=launch) 146 | self.assertTrue('playlist' in self._get_text(response)) 147 | 148 | def test_blueprints_demo(self): 149 | """ Test the sample project using Flask Blueprints """ 150 | self._launch('blueprint_demo/demo.py') 151 | response = self._post(route='/ask', data=launch) 152 | self.assertTrue('hello' in self._get_text(response)) 153 | 154 | def test_history_buff(self): 155 | """ Test the History Buff sample """ 156 | self._launch('historybuff/historybuff.py') 157 | response = self._post(data=launch) 158 | self.assertTrue('History buff' in self._get_text(response)) 159 | 160 | def test_spacegeek(self): 161 | """ Test the Spacegeek sample """ 162 | self._launch('spacegeek/spacegeek.py') 163 | response = self._post(data=launch) 164 | # response is random 165 | self.assertTrue(len(self._get_text(response)) > 1) 166 | 167 | def test_tidepooler(self): 168 | """ Test the Tide Pooler sample """ 169 | self._launch('tidepooler/tidepooler.py') 170 | response = self._post(data=launch) 171 | self.assertTrue('Which city' in self._get_reprompt(response)) 172 | -------------------------------------------------------------------------------- /tests/test_unicode.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import unittest 3 | from flask_ask import statement, question 4 | 5 | 6 | class UnicodeTests(unittest.TestCase): 7 | """ Test using Unicode in responses. (Issue #147) """ 8 | 9 | unicode_string = u"Was kann ich für dich tun?" 10 | 11 | def test_unicode_statements(self): 12 | """ Test unicode statement responses """ 13 | stmt = statement(self.unicode_string) 14 | speech = stmt._response['outputSpeech']['text'] 15 | print(speech) 16 | self.assertTrue(self.unicode_string in speech) 17 | 18 | def test_unicode_questions(self): 19 | """ Test unicode in question responses """ 20 | q = question(self.unicode_string) 21 | speech = q._response['outputSpeech']['text'] 22 | self.assertTrue(self.unicode_string in speech) 23 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27,py3 3 | skipsdist = True 4 | 5 | [testenv] 6 | deps = -rrequirements-dev.txt 7 | commands = python setup.py test 8 | 9 | --------------------------------------------------------------------------------