├── .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 |
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 |
31 |
32 | {% endif %} {% endblock %} {%- block footer %}
33 |
36 | {%- endblock %}
37 |
--------------------------------------------------------------------------------
/docs/_themes/flask/relations.html:
--------------------------------------------------------------------------------
1 |
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 |
148 |
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 |
5 | Which city would you like tide information for?
6 |
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 |
--------------------------------------------------------------------------------