clean up
This commit is contained in:
parent
83fa1b7156
commit
16e1b97810
|
|
@ -0,0 +1,2 @@
|
||||||
|
|
||||||
|
reader/ngrok\.exe
|
||||||
|
|
@ -1,102 +0,0 @@
|
||||||
# Byte-compiled / optimized / DLL files
|
|
||||||
__pycache__/
|
|
||||||
*.py[cod]
|
|
||||||
*$py.class
|
|
||||||
|
|
||||||
# C extensions
|
|
||||||
*.so
|
|
||||||
|
|
||||||
# Distribution / packaging
|
|
||||||
.Python
|
|
||||||
env/
|
|
||||||
build/
|
|
||||||
develop-eggs/
|
|
||||||
dist/
|
|
||||||
downloads/
|
|
||||||
eggs/
|
|
||||||
.eggs/
|
|
||||||
lib/
|
|
||||||
lib64/
|
|
||||||
parts/
|
|
||||||
sdist/
|
|
||||||
var/
|
|
||||||
*.egg-info/
|
|
||||||
.installed.cfg
|
|
||||||
*.egg
|
|
||||||
|
|
||||||
# PyInstaller
|
|
||||||
# Usually these files are written by a python script from a template
|
|
||||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
||||||
*.manifest
|
|
||||||
*.spec
|
|
||||||
|
|
||||||
# Installer logs
|
|
||||||
pip-log.txt
|
|
||||||
pip-delete-this-directory.txt
|
|
||||||
|
|
||||||
# Unit test / coverage reports
|
|
||||||
htmlcov/
|
|
||||||
.tox/
|
|
||||||
.coverage
|
|
||||||
.coverage.*
|
|
||||||
.cache
|
|
||||||
nosetests.xml
|
|
||||||
coverage.xml
|
|
||||||
*,cover
|
|
||||||
.hypothesis/
|
|
||||||
|
|
||||||
# Translations
|
|
||||||
*.mo
|
|
||||||
*.pot
|
|
||||||
|
|
||||||
# Django stuff:
|
|
||||||
*.log
|
|
||||||
local_settings.py
|
|
||||||
|
|
||||||
# Flask stuff:
|
|
||||||
instance/
|
|
||||||
.webassets-cache
|
|
||||||
|
|
||||||
# Scrapy stuff:
|
|
||||||
.scrapy
|
|
||||||
|
|
||||||
# Sphinx documentation
|
|
||||||
docs/_build/
|
|
||||||
|
|
||||||
# PyBuilder
|
|
||||||
target/
|
|
||||||
|
|
||||||
# IPython Notebook
|
|
||||||
.ipynb_checkpoints
|
|
||||||
|
|
||||||
# pyenv
|
|
||||||
.python-version
|
|
||||||
|
|
||||||
# celery beat schedule file
|
|
||||||
celerybeat-schedule
|
|
||||||
|
|
||||||
# dotenv
|
|
||||||
.env
|
|
||||||
|
|
||||||
# virtualenv
|
|
||||||
venv/
|
|
||||||
ENV/
|
|
||||||
|
|
||||||
# Spyder project settings
|
|
||||||
.spyderproject
|
|
||||||
|
|
||||||
# Rope project settings
|
|
||||||
.ropeproject
|
|
||||||
|
|
||||||
# Vagrant
|
|
||||||
.vagrant
|
|
||||||
|
|
||||||
# Misc
|
|
||||||
.DS_Store
|
|
||||||
temp
|
|
||||||
.idea/
|
|
||||||
|
|
||||||
# Project
|
|
||||||
backups
|
|
||||||
settings.cfg
|
|
||||||
fabfile/settings.py
|
|
||||||
|
|
@ -1,201 +0,0 @@
|
||||||
Apache License
|
|
||||||
Version 2.0, January 2004
|
|
||||||
http://www.apache.org/licenses/
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
||||||
|
|
||||||
1. Definitions.
|
|
||||||
|
|
||||||
"License" shall mean the terms and conditions for use, reproduction,
|
|
||||||
and distribution as defined by Sections 1 through 9 of this document.
|
|
||||||
|
|
||||||
"Licensor" shall mean the copyright owner or entity authorized by
|
|
||||||
the copyright owner that is granting the License.
|
|
||||||
|
|
||||||
"Legal Entity" shall mean the union of the acting entity and all
|
|
||||||
other entities that control, are controlled by, or are under common
|
|
||||||
control with that entity. For the purposes of this definition,
|
|
||||||
"control" means (i) the power, direct or indirect, to cause the
|
|
||||||
direction or management of such entity, whether by contract or
|
|
||||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
||||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
||||||
|
|
||||||
"You" (or "Your") shall mean an individual or Legal Entity
|
|
||||||
exercising permissions granted by this License.
|
|
||||||
|
|
||||||
"Source" form shall mean the preferred form for making modifications,
|
|
||||||
including but not limited to software source code, documentation
|
|
||||||
source, and configuration files.
|
|
||||||
|
|
||||||
"Object" form shall mean any form resulting from mechanical
|
|
||||||
transformation or translation of a Source form, including but
|
|
||||||
not limited to compiled object code, generated documentation,
|
|
||||||
and conversions to other media types.
|
|
||||||
|
|
||||||
"Work" shall mean the work of authorship, whether in Source or
|
|
||||||
Object form, made available under the License, as indicated by a
|
|
||||||
copyright notice that is included in or attached to the work
|
|
||||||
(an example is provided in the Appendix below).
|
|
||||||
|
|
||||||
"Derivative Works" shall mean any work, whether in Source or Object
|
|
||||||
form, that is based on (or derived from) the Work and for which the
|
|
||||||
editorial revisions, annotations, elaborations, or other modifications
|
|
||||||
represent, as a whole, an original work of authorship. For the purposes
|
|
||||||
of this License, Derivative Works shall not include works that remain
|
|
||||||
separable from, or merely link (or bind by name) to the interfaces of,
|
|
||||||
the Work and Derivative Works thereof.
|
|
||||||
|
|
||||||
"Contribution" shall mean any work of authorship, including
|
|
||||||
the original version of the Work and any modifications or additions
|
|
||||||
to that Work or Derivative Works thereof, that is intentionally
|
|
||||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
||||||
or by an individual or Legal Entity authorized to submit on behalf of
|
|
||||||
the copyright owner. For the purposes of this definition, "submitted"
|
|
||||||
means any form of electronic, verbal, or written communication sent
|
|
||||||
to the Licensor or its representatives, including but not limited to
|
|
||||||
communication on electronic mailing lists, source code control systems,
|
|
||||||
and issue tracking systems that are managed by, or on behalf of, the
|
|
||||||
Licensor for the purpose of discussing and improving the Work, but
|
|
||||||
excluding communication that is conspicuously marked or otherwise
|
|
||||||
designated in writing by the copyright owner as "Not a Contribution."
|
|
||||||
|
|
||||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
||||||
on behalf of whom a Contribution has been received by Licensor and
|
|
||||||
subsequently incorporated within the Work.
|
|
||||||
|
|
||||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
copyright license to reproduce, prepare Derivative Works of,
|
|
||||||
publicly display, publicly perform, sublicense, and distribute the
|
|
||||||
Work and such Derivative Works in Source or Object form.
|
|
||||||
|
|
||||||
3. Grant of Patent License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
(except as stated in this section) patent license to make, have made,
|
|
||||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
||||||
where such license applies only to those patent claims licensable
|
|
||||||
by such Contributor that are necessarily infringed by their
|
|
||||||
Contribution(s) alone or by combination of their Contribution(s)
|
|
||||||
with the Work to which such Contribution(s) was submitted. If You
|
|
||||||
institute patent litigation against any entity (including a
|
|
||||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
||||||
or a Contribution incorporated within the Work constitutes direct
|
|
||||||
or contributory patent infringement, then any patent licenses
|
|
||||||
granted to You under this License for that Work shall terminate
|
|
||||||
as of the date such litigation is filed.
|
|
||||||
|
|
||||||
4. Redistribution. You may reproduce and distribute copies of the
|
|
||||||
Work or Derivative Works thereof in any medium, with or without
|
|
||||||
modifications, and in Source or Object form, provided that You
|
|
||||||
meet the following conditions:
|
|
||||||
|
|
||||||
(a) You must give any other recipients of the Work or
|
|
||||||
Derivative Works a copy of this License; and
|
|
||||||
|
|
||||||
(b) You must cause any modified files to carry prominent notices
|
|
||||||
stating that You changed the files; and
|
|
||||||
|
|
||||||
(c) You must retain, in the Source form of any Derivative Works
|
|
||||||
that You distribute, all copyright, patent, trademark, and
|
|
||||||
attribution notices from the Source form of the Work,
|
|
||||||
excluding those notices that do not pertain to any part of
|
|
||||||
the Derivative Works; and
|
|
||||||
|
|
||||||
(d) If the Work includes a "NOTICE" text file as part of its
|
|
||||||
distribution, then any Derivative Works that You distribute must
|
|
||||||
include a readable copy of the attribution notices contained
|
|
||||||
within such NOTICE file, excluding those notices that do not
|
|
||||||
pertain to any part of the Derivative Works, in at least one
|
|
||||||
of the following places: within a NOTICE text file distributed
|
|
||||||
as part of the Derivative Works; within the Source form or
|
|
||||||
documentation, if provided along with the Derivative Works; or,
|
|
||||||
within a display generated by the Derivative Works, if and
|
|
||||||
wherever such third-party notices normally appear. The contents
|
|
||||||
of the NOTICE file are for informational purposes only and
|
|
||||||
do not modify the License. You may add Your own attribution
|
|
||||||
notices within Derivative Works that You distribute, alongside
|
|
||||||
or as an addendum to the NOTICE text from the Work, provided
|
|
||||||
that such additional attribution notices cannot be construed
|
|
||||||
as modifying the License.
|
|
||||||
|
|
||||||
You may add Your own copyright statement to Your modifications and
|
|
||||||
may provide additional or different license terms and conditions
|
|
||||||
for use, reproduction, or distribution of Your modifications, or
|
|
||||||
for any such Derivative Works as a whole, provided Your use,
|
|
||||||
reproduction, and distribution of the Work otherwise complies with
|
|
||||||
the conditions stated in this License.
|
|
||||||
|
|
||||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
||||||
any Contribution intentionally submitted for inclusion in the Work
|
|
||||||
by You to the Licensor shall be under the terms and conditions of
|
|
||||||
this License, without any additional terms or conditions.
|
|
||||||
Notwithstanding the above, nothing herein shall supersede or modify
|
|
||||||
the terms of any separate license agreement you may have executed
|
|
||||||
with Licensor regarding such Contributions.
|
|
||||||
|
|
||||||
6. Trademarks. This License does not grant permission to use the trade
|
|
||||||
names, trademarks, service marks, or product names of the Licensor,
|
|
||||||
except as required for reasonable and customary use in describing the
|
|
||||||
origin of the Work and reproducing the content of the NOTICE file.
|
|
||||||
|
|
||||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
||||||
agreed to in writing, Licensor provides the Work (and each
|
|
||||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
implied, including, without limitation, any warranties or conditions
|
|
||||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
||||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
||||||
appropriateness of using or redistributing the Work and assume any
|
|
||||||
risks associated with Your exercise of permissions under this License.
|
|
||||||
|
|
||||||
8. Limitation of Liability. In no event and under no legal theory,
|
|
||||||
whether in tort (including negligence), contract, or otherwise,
|
|
||||||
unless required by applicable law (such as deliberate and grossly
|
|
||||||
negligent acts) or agreed to in writing, shall any Contributor be
|
|
||||||
liable to You for damages, including any direct, indirect, special,
|
|
||||||
incidental, or consequential damages of any character arising as a
|
|
||||||
result of this License or out of the use or inability to use the
|
|
||||||
Work (including but not limited to damages for loss of goodwill,
|
|
||||||
work stoppage, computer failure or malfunction, or any and all
|
|
||||||
other commercial damages or losses), even if such Contributor
|
|
||||||
has been advised of the possibility of such damages.
|
|
||||||
|
|
||||||
9. Accepting Warranty or Additional Liability. While redistributing
|
|
||||||
the Work or Derivative Works thereof, You may choose to offer,
|
|
||||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
||||||
or other liability obligations and/or rights consistent with this
|
|
||||||
License. However, in accepting such obligations, You may act only
|
|
||||||
on Your own behalf and on Your sole responsibility, not on behalf
|
|
||||||
of any other Contributor, and only if You agree to indemnify,
|
|
||||||
defend, and hold each Contributor harmless for any liability
|
|
||||||
incurred by, or claims asserted against, such Contributor by reason
|
|
||||||
of your accepting any such warranty or additional liability.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
APPENDIX: How to apply the Apache License to your work.
|
|
||||||
|
|
||||||
To apply the Apache License to your work, attach the following
|
|
||||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
|
||||||
replaced with your own identifying information. (Don't include
|
|
||||||
the brackets!) The text should be enclosed in the appropriate
|
|
||||||
comment syntax for the file format. We also recommend that a
|
|
||||||
file or class name and description of purpose be included on the
|
|
||||||
same "printed page" as the copyright notice for easier
|
|
||||||
identification within third-party archives.
|
|
||||||
|
|
||||||
Copyright 2016 John Wheeler
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
include *.rst *.txt LICENSE tox.ini .travis.yml docs/Makefile .coveragerc conftest.py
|
|
||||||
recursive-include tests *.py
|
|
||||||
recursive-include docs *.rst
|
|
||||||
recursive-include docs *.py
|
|
||||||
prune docs/_build
|
|
||||||
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
GST
|
|
||||||
142
f-ask/README.rst
142
f-ask/README.rst
|
|
@ -1,142 +0,0 @@
|
||||||
|
|
||||||
.. image:: http://flask-ask.readthedocs.io/en/latest/_images/logo-full.png
|
|
||||||
|
|
||||||
===================================
|
|
||||||
Program the Amazon Echo with Python
|
|
||||||
===================================
|
|
||||||
|
|
||||||
Flask-Ask is a `Flask extension <http://flask.pocoo.org/extensions/>`_ that makes building Alexa skills for the Amazon Echo easier and much more fun.
|
|
||||||
|
|
||||||
* `Flask-Ask quickstart on Amazon's Developer Blog <https://developer.amazon.com/public/community/post/Tx14R0IYYGH3SKT/Flask-Ask-A-New-Python-Framework-for-Rapid-Alexa-Skills-Kit-Development>`_.
|
|
||||||
* `Level Up with our Alexa Skills Kit Video Tutorial <https://alexatutorial.com/>`_
|
|
||||||
* `Chat on Gitter.im <https://gitter.im/johnwheeler/flask-ask/>`_
|
|
||||||
|
|
||||||
The Basics
|
|
||||||
===============
|
|
||||||
|
|
||||||
A Flask-Ask application looks like this:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from flask import Flask
|
|
||||||
from flask_ask import Ask, statement
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
ask = Ask(app, '/')
|
|
||||||
|
|
||||||
@ask.intent('HelloIntent')
|
|
||||||
def hello(firstname):
|
|
||||||
speech_text = "Hello %s" % firstname
|
|
||||||
return statement(speech_text).simple_card('Hello', speech_text)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
app.run()
|
|
||||||
|
|
||||||
In the code above:
|
|
||||||
|
|
||||||
#. The ``Ask`` object is created by passing in the Flask application and a route to forward Alexa requests to.
|
|
||||||
#. The ``intent`` decorator maps ``HelloIntent`` to a view function ``hello``.
|
|
||||||
#. The intent's ``firstname`` slot is implicitly mapped to ``hello``'s ``firstname`` parameter.
|
|
||||||
#. Lastly, a builder constructs a spoken response and displays a contextual card in the Alexa smartphone/tablet app.
|
|
||||||
|
|
||||||
More code examples are in the `samples <https://github.com/johnwheeler/flask-ask/tree/master/samples>`_ directory.
|
|
||||||
|
|
||||||
Jinja Templates
|
|
||||||
---------------
|
|
||||||
|
|
||||||
Since Alexa responses are usually short phrases, you might find it convenient to put them in the same file.
|
|
||||||
Flask-Ask has a `Jinja template loader <http://jinja.pocoo.org/docs/dev/api/#loaders>`_ that loads
|
|
||||||
multiple templates from a single YAML file. For example, here's a template that supports the minimal voice interface
|
|
||||||
above:
|
|
||||||
|
|
||||||
.. code-block:: yaml
|
|
||||||
|
|
||||||
hello: Hello, {{ firstname }}
|
|
||||||
|
|
||||||
Templates are stored in a file called `templates.yaml` located in the application root. Checkout the `Tidepooler example <https://github.com/johnwheeler/flask-ask/tree/master/samples/tidepooler>`_ to see why it makes sense to extract speech out of the code and into templates as the number of spoken phrases grow.
|
|
||||||
|
|
||||||
Features
|
|
||||||
===============
|
|
||||||
|
|
||||||
Flask-Ask handles the boilerplate, so you can focus on writing clean code. Flask-Ask:
|
|
||||||
|
|
||||||
* Has decorators to map Alexa requests and intent slots to view functions
|
|
||||||
* Helps construct ask and tell responses, reprompts and cards
|
|
||||||
* Makes session management easy
|
|
||||||
* Allows for the separation of code and speech through Jinja templates
|
|
||||||
* Verifies Alexa request signatures
|
|
||||||
|
|
||||||
Installation
|
|
||||||
===============
|
|
||||||
|
|
||||||
To install Flask-Ask::
|
|
||||||
|
|
||||||
pip install flask-ask
|
|
||||||
|
|
||||||
Documentation
|
|
||||||
===============
|
|
||||||
|
|
||||||
These resources will get you up and running quickly:
|
|
||||||
|
|
||||||
* `5-minute quickstart <https://www.youtube.com/watch?v=cXL8FDUag-s>`_
|
|
||||||
* `Full online documentation <https://alexatutorial.com/flask-ask/>`_
|
|
||||||
|
|
||||||
Fantastic 3-part tutorial series by Harrison Kinsley
|
|
||||||
|
|
||||||
* `Intro and Skill Logic - Alexa Skills w/ Python and Flask-Ask Part 1 <https://pythonprogramming.net/intro-alexa-skill-flask-ask-python-tutorial/>`_
|
|
||||||
* `Headlines Function - Alexa Skills w/ Python and Flask-Ask Part 2 <https://pythonprogramming.net/headlines-function-alexa-skill-flask-ask-python-tutorial/>`_
|
|
||||||
* `Testing our Skill - Alexa Skills w/ Python and Flask-Ask Part 3 <https://pythonprogramming.net/testing-deploying-alexa-skill-flask-ask-python-tutorial/>`_
|
|
||||||
|
|
||||||
Deployment
|
|
||||||
===============
|
|
||||||
|
|
||||||
You can deploy using any WSGI compliant framework (uWSGI, Gunicorn). If you haven't deployed a Flask app to production, `checkout flask-live-starter <https://github.com/johnwheeler/flask-live-starter>`_.
|
|
||||||
|
|
||||||
To deploy on AWS Lambda, you have two options. Use `Zappa <https://github.com/Miserlou/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 <https://developer.amazon.com/blogs/post/8e8ad73a-99e9-4c0f-a7b3-60f92287b0bf/new-alexa-tutorial-deploy-flask-ask-skills-to-aws-lambda-with-zappa>`_ 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.
|
|
||||||
|
|
||||||
Alternatively, you can use AWS Lambda directly without the need for an AWS API Gateway endpoint. In this case you will need to `deploy <https://developer.amazon.com/docs/custom-skills/host-a-custom-skill-as-an-aws-lambda-function.html>`_ your Lambda function yourself and use `virtualenv <http://docs.aws.amazon.com/lambda/latest/dg/lambda-python-how-to-create-deployment-package.html>`_ 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:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
def lambda_handler(event, _context):
|
|
||||||
return ask.run_aws_lambda(event)
|
|
||||||
|
|
||||||
|
|
||||||
Development
|
|
||||||
===============
|
|
||||||
|
|
||||||
If you'd like to work from the Flask-Ask source, clone the project and run::
|
|
||||||
|
|
||||||
pip install -r requirements-dev.txt
|
|
||||||
|
|
||||||
This will install all base requirements from `requirements.txt` as well as requirements needed for running tests from the `tests` directory.
|
|
||||||
|
|
||||||
Tests can be run with::
|
|
||||||
|
|
||||||
python setup.py test
|
|
||||||
|
|
||||||
Or::
|
|
||||||
|
|
||||||
python -m unittest
|
|
||||||
|
|
||||||
To install from your local clone or fork of the project, run::
|
|
||||||
|
|
||||||
python setup.py install
|
|
||||||
|
|
||||||
Related projects
|
|
||||||
===============
|
|
||||||
|
|
||||||
`cookiecutter-flask-ask <https://github.com/chrisvoncsefalvay/cookiecutter-flask-ask>`_ is a Cookiecutter to easily bootstrap a Flask-Ask project, including documentation, speech assets and basic built-in intents.
|
|
||||||
|
|
||||||
Have a Google Home? Checkout `Flask-Assistant <https://github.com/treethought/flask-assistant>`_ (early alpha)
|
|
||||||
|
|
||||||
|
|
||||||
Thank You
|
|
||||||
===============
|
|
||||||
|
|
||||||
Thanks for checking this library out! I hope you find it useful.
|
|
||||||
|
|
||||||
Of course, there's always room for improvement.
|
|
||||||
Feel free to `open an issue <https://github.com/johnwheeler/flask-ask/issues>`_ so we can make Flask-Ask better.
|
|
||||||
|
|
||||||
Special thanks to `@kennethreitz <https://github.com/kennethreitz>`_ for his `sense <http://docs.python-requests.org/en/master/>`_ of `style <https://github.com/kennethreitz/records/blob/master/README.rst>`_, and of course, `@mitsuhiko <https://github.com/mitsuhiko>`_ for `Flask <https://www.palletsprojects.com/p/flask/>`_
|
|
||||||
|
|
@ -1,230 +0,0 @@
|
||||||
# Makefile for Sphinx documentation
|
|
||||||
#
|
|
||||||
|
|
||||||
# You can set these variables from the command line.
|
|
||||||
SPHINXOPTS =
|
|
||||||
SPHINXBUILD = sphinx-build
|
|
||||||
PAPER =
|
|
||||||
BUILDDIR = _build
|
|
||||||
|
|
||||||
# User-friendly check for sphinx-build
|
|
||||||
ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
|
|
||||||
$(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/)
|
|
||||||
endif
|
|
||||||
|
|
||||||
# Internal variables.
|
|
||||||
PAPEROPT_a4 = -D latex_paper_size=a4
|
|
||||||
PAPEROPT_letter = -D latex_paper_size=letter
|
|
||||||
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
|
||||||
# the i18n builder cannot share the environment and doctrees with the others
|
|
||||||
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
|
||||||
|
|
||||||
.PHONY: help
|
|
||||||
help:
|
|
||||||
@echo "Please use \`make <target>' where <target> is one of"
|
|
||||||
@echo " html to make standalone HTML files"
|
|
||||||
@echo " dirhtml to make HTML files named index.html in directories"
|
|
||||||
@echo " singlehtml to make a single large HTML file"
|
|
||||||
@echo " pickle to make pickle files"
|
|
||||||
@echo " json to make JSON files"
|
|
||||||
@echo " htmlhelp to make HTML files and a HTML help project"
|
|
||||||
@echo " qthelp to make HTML files and a qthelp project"
|
|
||||||
@echo " applehelp to make an Apple Help Book"
|
|
||||||
@echo " devhelp to make HTML files and a Devhelp project"
|
|
||||||
@echo " epub to make an epub"
|
|
||||||
@echo " epub3 to make an epub3"
|
|
||||||
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
|
|
||||||
@echo " latexpdf to make LaTeX files and run them through pdflatex"
|
|
||||||
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
|
|
||||||
@echo " text to make text files"
|
|
||||||
@echo " man to make manual pages"
|
|
||||||
@echo " texinfo to make Texinfo files"
|
|
||||||
@echo " info to make Texinfo files and run them through makeinfo"
|
|
||||||
@echo " gettext to make PO message catalogs"
|
|
||||||
@echo " changes to make an overview of all changed/added/deprecated items"
|
|
||||||
@echo " xml to make Docutils-native XML files"
|
|
||||||
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
|
|
||||||
@echo " linkcheck to check all external links for integrity"
|
|
||||||
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
|
|
||||||
@echo " coverage to run coverage check of the documentation (if enabled)"
|
|
||||||
@echo " dummy to check syntax errors of document sources"
|
|
||||||
|
|
||||||
.PHONY: clean
|
|
||||||
clean:
|
|
||||||
rm -rf $(BUILDDIR)/*
|
|
||||||
|
|
||||||
.PHONY: html
|
|
||||||
html:
|
|
||||||
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
|
||||||
@echo
|
|
||||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
|
||||||
|
|
||||||
.PHONY: dirhtml
|
|
||||||
dirhtml:
|
|
||||||
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
|
||||||
@echo
|
|
||||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
|
|
||||||
|
|
||||||
.PHONY: singlehtml
|
|
||||||
singlehtml:
|
|
||||||
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
|
|
||||||
@echo
|
|
||||||
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
|
|
||||||
|
|
||||||
.PHONY: pickle
|
|
||||||
pickle:
|
|
||||||
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
|
|
||||||
@echo
|
|
||||||
@echo "Build finished; now you can process the pickle files."
|
|
||||||
|
|
||||||
.PHONY: json
|
|
||||||
json:
|
|
||||||
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
|
|
||||||
@echo
|
|
||||||
@echo "Build finished; now you can process the JSON files."
|
|
||||||
|
|
||||||
.PHONY: htmlhelp
|
|
||||||
htmlhelp:
|
|
||||||
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
|
|
||||||
@echo
|
|
||||||
@echo "Build finished; now you can run HTML Help Workshop with the" \
|
|
||||||
".hhp project file in $(BUILDDIR)/htmlhelp."
|
|
||||||
|
|
||||||
.PHONY: qthelp
|
|
||||||
qthelp:
|
|
||||||
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
|
|
||||||
@echo
|
|
||||||
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
|
||||||
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
|
|
||||||
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Flask-Ask.qhcp"
|
|
||||||
@echo "To view the help file:"
|
|
||||||
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Flask-Ask.qhc"
|
|
||||||
|
|
||||||
.PHONY: applehelp
|
|
||||||
applehelp:
|
|
||||||
$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
|
|
||||||
@echo
|
|
||||||
@echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
|
|
||||||
@echo "N.B. You won't be able to view it unless you put it in" \
|
|
||||||
"~/Library/Documentation/Help or install it in your application" \
|
|
||||||
"bundle."
|
|
||||||
|
|
||||||
.PHONY: devhelp
|
|
||||||
devhelp:
|
|
||||||
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
|
|
||||||
@echo
|
|
||||||
@echo "Build finished."
|
|
||||||
@echo "To view the help file:"
|
|
||||||
@echo "# mkdir -p $$HOME/.local/share/devhelp/Flask-Ask"
|
|
||||||
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Flask-Ask"
|
|
||||||
@echo "# devhelp"
|
|
||||||
|
|
||||||
.PHONY: epub
|
|
||||||
epub:
|
|
||||||
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
|
|
||||||
@echo
|
|
||||||
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
|
|
||||||
|
|
||||||
.PHONY: epub3
|
|
||||||
epub3:
|
|
||||||
$(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3
|
|
||||||
@echo
|
|
||||||
@echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3."
|
|
||||||
|
|
||||||
.PHONY: latex
|
|
||||||
latex:
|
|
||||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
|
||||||
@echo
|
|
||||||
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
|
|
||||||
@echo "Run \`make' in that directory to run these through (pdf)latex" \
|
|
||||||
"(use \`make latexpdf' here to do that automatically)."
|
|
||||||
|
|
||||||
.PHONY: latexpdf
|
|
||||||
latexpdf:
|
|
||||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
|
||||||
@echo "Running LaTeX files through pdflatex..."
|
|
||||||
$(MAKE) -C $(BUILDDIR)/latex all-pdf
|
|
||||||
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
|
||||||
|
|
||||||
.PHONY: latexpdfja
|
|
||||||
latexpdfja:
|
|
||||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
|
||||||
@echo "Running LaTeX files through platex and dvipdfmx..."
|
|
||||||
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
|
|
||||||
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
|
||||||
|
|
||||||
.PHONY: text
|
|
||||||
text:
|
|
||||||
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
|
|
||||||
@echo
|
|
||||||
@echo "Build finished. The text files are in $(BUILDDIR)/text."
|
|
||||||
|
|
||||||
.PHONY: man
|
|
||||||
man:
|
|
||||||
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
|
|
||||||
@echo
|
|
||||||
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
|
|
||||||
|
|
||||||
.PHONY: texinfo
|
|
||||||
texinfo:
|
|
||||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
|
||||||
@echo
|
|
||||||
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
|
|
||||||
@echo "Run \`make' in that directory to run these through makeinfo" \
|
|
||||||
"(use \`make info' here to do that automatically)."
|
|
||||||
|
|
||||||
.PHONY: info
|
|
||||||
info:
|
|
||||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
|
||||||
@echo "Running Texinfo files through makeinfo..."
|
|
||||||
make -C $(BUILDDIR)/texinfo info
|
|
||||||
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
|
|
||||||
|
|
||||||
.PHONY: gettext
|
|
||||||
gettext:
|
|
||||||
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
|
|
||||||
@echo
|
|
||||||
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
|
|
||||||
|
|
||||||
.PHONY: changes
|
|
||||||
changes:
|
|
||||||
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
|
|
||||||
@echo
|
|
||||||
@echo "The overview file is in $(BUILDDIR)/changes."
|
|
||||||
|
|
||||||
.PHONY: linkcheck
|
|
||||||
linkcheck:
|
|
||||||
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
|
|
||||||
@echo
|
|
||||||
@echo "Link check complete; look for any errors in the above output " \
|
|
||||||
"or in $(BUILDDIR)/linkcheck/output.txt."
|
|
||||||
|
|
||||||
.PHONY: doctest
|
|
||||||
doctest:
|
|
||||||
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
|
|
||||||
@echo "Testing of doctests in the sources finished, look at the " \
|
|
||||||
"results in $(BUILDDIR)/doctest/output.txt."
|
|
||||||
|
|
||||||
.PHONY: coverage
|
|
||||||
coverage:
|
|
||||||
$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
|
|
||||||
@echo "Testing of coverage in the sources finished, look at the " \
|
|
||||||
"results in $(BUILDDIR)/coverage/python.txt."
|
|
||||||
|
|
||||||
.PHONY: xml
|
|
||||||
xml:
|
|
||||||
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
|
|
||||||
@echo
|
|
||||||
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
|
|
||||||
|
|
||||||
.PHONY: pseudoxml
|
|
||||||
pseudoxml:
|
|
||||||
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
|
|
||||||
@echo
|
|
||||||
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
|
|
||||||
|
|
||||||
.PHONY: dummy
|
|
||||||
dummy:
|
|
||||||
$(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy
|
|
||||||
@echo
|
|
||||||
@echo "Build finished. Dummy builder generates no files."
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 39 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.4 KiB |
|
|
@ -1,31 +0,0 @@
|
||||||
<br>
|
|
||||||
|
|
||||||
<h3>Resources</h3>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<a href="https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/alexa-skills-kit-interface-reference">ASK JSON Reference</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/speech-synthesis-markup-language-ssml-reference">ASK SSML Reference</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/alexa-skills-kit-interaction-model-reference">ASK Interaction Model Reference</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li><a href="http://flask.readthedocs.io/en/latest/">Flask Documentation</a></li>
|
|
||||||
<li><a href="http://jinja2.readthedocs.io/en/latest/">Jinja2 Documentation</a></li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<br>
|
|
||||||
|
|
||||||
<h3>Project Links</h3>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li><a href="http://pypi.python.org/pypi/flask-ask">Flask-Ask @ PyPI</a></li>
|
|
||||||
<li><a href="http://github.com/johnwheeler/flask-ask">Flask-Ask @ GitHub</a></li>
|
|
||||||
<li><a href="http://github.com/johnwheeler/flask-ask/issues">Issue Tracker</a></li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<br>
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
<p class="logo">
|
|
||||||
<a href="{{ pathto(master_doc) }}">
|
|
||||||
<img src="{{ pathto('_static/logo-sm.png', 1) }}" alt="Logo">
|
|
||||||
</a>
|
|
||||||
<p>
|
|
||||||
|
|
||||||
<small>
|
|
||||||
Alexa Skills Kit Development for Amazon Echo Devices with Python
|
|
||||||
</small>
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
|
|
||||||
<h3>Stay Informed</h3>
|
|
||||||
|
|
||||||
<a class="github-button" href="https://github.com/johnwheeler/flask-ask" data-icon="octicon-star" data-style="mega" data-count-href="/johnwheeler/flask-ask/stargazers" data-count-api="/repos/johnwheeler/flask-ask#stargazers_count" data-count-aria-label="# stargazers on GitHub" aria-label="Star johnwheeler/flask-ask on GitHub">Star</a>
|
|
||||||
|
|
||||||
<br>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<small>Receive updates on new releases and upcoming projects.</small>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<a class="github-button" href="https://github.com/johnwheeler" aria-label="Follow @johnwheeler on GitHub">Follow @johnwheeler</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<a class="twitter-follow-button" data-show-count="false" href="https://twitter.com/_johnwheeler">Follow @_johnwheeler</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<br>
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
Copyright (c) 2010 by Armin Ronacher.
|
|
||||||
|
|
||||||
Some rights reserved.
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms of the theme, with or
|
|
||||||
without modification, are permitted provided that the following conditions
|
|
||||||
are met:
|
|
||||||
|
|
||||||
* Redistributions of source code must retain the above copyright
|
|
||||||
notice, this list of conditions and the following disclaimer.
|
|
||||||
|
|
||||||
* Redistributions in binary form must reproduce the above
|
|
||||||
copyright notice, this list of conditions and the following
|
|
||||||
disclaimer in the documentation and/or other materials provided
|
|
||||||
with the distribution.
|
|
||||||
|
|
||||||
* The names of the contributors may not be used to endorse or
|
|
||||||
promote products derived from this software without specific
|
|
||||||
prior written permission.
|
|
||||||
|
|
||||||
We kindly ask you to only use these themes in an unmodified manner just
|
|
||||||
for Flask and Flask-related products, not for unrelated projects. If you
|
|
||||||
like the visual style and want to use it for your own projects, please
|
|
||||||
consider making some larger changes to the themes (such as changing
|
|
||||||
font faces, sizes, colors or margins).
|
|
||||||
|
|
||||||
THIS THEME IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
||||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
||||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
||||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
|
||||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
||||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
||||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
||||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
||||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
||||||
ARISING IN ANY WAY OUT OF THE USE OF THIS THEME, EVEN IF ADVISED OF THE
|
|
||||||
POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
Flask Sphinx Styles
|
|
||||||
===================
|
|
||||||
|
|
||||||
This repository contains sphinx styles for Flask and Flask related
|
|
||||||
projects. To use this style in your Sphinx documentation, follow
|
|
||||||
this guide:
|
|
||||||
|
|
||||||
1. put this folder as _themes into your docs folder. Alternatively
|
|
||||||
you can also use git submodules to check out the contents there.
|
|
||||||
2. add this to your conf.py:
|
|
||||||
|
|
||||||
sys.path.append(os.path.abspath('_themes'))
|
|
||||||
html_theme_path = ['_themes']
|
|
||||||
html_theme = 'flask'
|
|
||||||
|
|
||||||
The following themes exist:
|
|
||||||
|
|
||||||
- 'flask' - the standard flask documentation theme for large
|
|
||||||
projects
|
|
||||||
- 'flask_small' - small one-page theme. Intended to be used by
|
|
||||||
very small addon libraries for flask.
|
|
||||||
|
|
||||||
The following options exist for the flask_small theme:
|
|
||||||
|
|
||||||
[options]
|
|
||||||
index_logo = '' filename of a picture in _static
|
|
||||||
to be used as replacement for the
|
|
||||||
h1 in the index.rst file.
|
|
||||||
index_logo_height = 120px height of the index logo
|
|
||||||
github_fork = '' repository name on github for the
|
|
||||||
"fork me" badge
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
{%- extends "basic/layout.html" %} {%- block extrahead %} {{ super() }}
|
|
||||||
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css" rel="stylesheet" integrity="sha384-T8Gy5hrqNKT+hzMclPo118YTQO6cYprQmhrYwIiQ/3axmI1hQomh7Ud2hPOy8SP1" crossorigin="anonymous">
|
|
||||||
|
|
||||||
<!-- Twitter Buttons -->
|
|
||||||
<script>
|
|
||||||
window.twttr = (function(d, s, id) {
|
|
||||||
var js, fjs = d.getElementsByTagName(s)[0],
|
|
||||||
t = window.twttr || {};
|
|
||||||
if (d.getElementById(id)) return t;
|
|
||||||
js = d.createElement(s);
|
|
||||||
js.id = id;
|
|
||||||
js.src = "https://platform.twitter.com/widgets.js";
|
|
||||||
fjs.parentNode.insertBefore(js, fjs);
|
|
||||||
|
|
||||||
t._e = [];
|
|
||||||
t.ready = function(f) {
|
|
||||||
t._e.push(f);
|
|
||||||
};
|
|
||||||
|
|
||||||
return t;
|
|
||||||
}(document, "script", "twitter-wjs"));
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Place this tag in your head or just before your close body tag. -->
|
|
||||||
<script async defer src="https://buttons.github.io/buttons.js"></script>
|
|
||||||
|
|
||||||
{% if theme_touch_icon %}
|
|
||||||
<link rel="apple-touch-icon" href="{{ pathto('_static/' ~ theme_touch_icon, 1) }}" /> {% endif %} {% endblock %} {%- block relbar2 %} {% if theme_github_fork %}
|
|
||||||
<a href="http://github.com/{{ theme_github_fork }}">
|
|
||||||
<img style="position: fixed; top: 0; right: 0; border: 0;" src="https://s3.amazonaws.com/github/ribbons/forkme_right_darkblue_121621.png" alt="Fork me on GitHub" />
|
|
||||||
</a>
|
|
||||||
{% endif %} {% endblock %} {%- block footer %}
|
|
||||||
<div class="footer">
|
|
||||||
© Copyright {{ copyright }}. Created using <a href="http://sphinx.pocoo.org/">Sphinx</a>.
|
|
||||||
</div>
|
|
||||||
{%- endblock %}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
<h3>Related Topics</h3>
|
|
||||||
<ul>
|
|
||||||
<li><a href="{{ pathto(master_doc) }}">Documentation overview</a><ul>
|
|
||||||
{%- for parent in parents %}
|
|
||||||
<li><a href="{{ parent.link|e }}">{{ parent.title }}</a><ul>
|
|
||||||
{%- endfor %}
|
|
||||||
{%- if prev %}
|
|
||||||
<li>Previous: <a href="{{ prev.link|e }}" title="{{ _('previous chapter')
|
|
||||||
}}">{{ prev.title }}</a></li>
|
|
||||||
{%- endif %}
|
|
||||||
{%- if next %}
|
|
||||||
<li>Next: <a href="{{ next.link|e }}" title="{{ _('next chapter')
|
|
||||||
}}">{{ next.title }}</a></li>
|
|
||||||
{%- endif %}
|
|
||||||
{%- for parent in parents %}
|
|
||||||
</ul></li>
|
|
||||||
{%- endfor %}
|
|
||||||
</ul></li>
|
|
||||||
</ul>
|
|
||||||
|
|
@ -1,581 +0,0 @@
|
||||||
/*
|
|
||||||
* flasky.css_t
|
|
||||||
* ~~~~~~~~~~~~
|
|
||||||
*
|
|
||||||
* :copyright: Copyright 2010 by Armin Ronacher.
|
|
||||||
* :license: Flask Design License, see LICENSE for details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
{% set page_width = '940px' %}
|
|
||||||
{% set sidebar_width = '220px' %}
|
|
||||||
|
|
||||||
@import url("basic.css");
|
|
||||||
|
|
||||||
/* -- page layout ----------------------------------------------------------- */
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: 'Georgia', serif;
|
|
||||||
font-size: 17px;
|
|
||||||
background-color: white;
|
|
||||||
color: #000;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.document {
|
|
||||||
width: {{ page_width }};
|
|
||||||
margin: 30px auto 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.documentwrapper {
|
|
||||||
float: left;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.bodywrapper {
|
|
||||||
margin: 0 0 0 {{ sidebar_width }};
|
|
||||||
}
|
|
||||||
|
|
||||||
div.sphinxsidebar {
|
|
||||||
width: {{ sidebar_width }};
|
|
||||||
}
|
|
||||||
|
|
||||||
hr {
|
|
||||||
border: 1px solid #B1B4B6;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.body {
|
|
||||||
background-color: #ffffff;
|
|
||||||
color: #3E4349;
|
|
||||||
padding: 0 30px 0 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
img.floatingflask {
|
|
||||||
padding: 0 0 10px 10px;
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.footer {
|
|
||||||
width: {{ page_width }};
|
|
||||||
margin: 20px auto 30px auto;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #888;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.footer a {
|
|
||||||
color: #888;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.related {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.sphinxsidebar a {
|
|
||||||
color: #444;
|
|
||||||
text-decoration: none;
|
|
||||||
border-bottom: 1px dotted #999;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.sphinxsidebar a:hover {
|
|
||||||
border-bottom: 1px solid #999;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.sphinxsidebar {
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.sphinxsidebarwrapper {
|
|
||||||
padding: 18px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.sphinxsidebarwrapper p.logo {
|
|
||||||
padding: 0 0 6px 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.sphinxsidebar h3,
|
|
||||||
div.sphinxsidebar h4 {
|
|
||||||
font-family: 'Garamond', 'Georgia', serif;
|
|
||||||
color: #444;
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: normal;
|
|
||||||
margin: 0 0 5px 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.sphinxsidebar h4 {
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.sphinxsidebar h3 a {
|
|
||||||
color: #444;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.sphinxsidebar p.logo a,
|
|
||||||
div.sphinxsidebar h3 a,
|
|
||||||
div.sphinxsidebar p.logo a:hover,
|
|
||||||
div.sphinxsidebar h3 a:hover {
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.sphinxsidebar p {
|
|
||||||
color: #555;
|
|
||||||
margin: 10px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.sphinxsidebar ul {
|
|
||||||
margin: 10px 0;
|
|
||||||
padding: 0;
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.sphinxsidebar input {
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
font-family: 'Georgia', serif;
|
|
||||||
font-size: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -- body styles ----------------------------------------------------------- */
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: #004B6B;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover {
|
|
||||||
color: #6D4100;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.body h1,
|
|
||||||
div.body h2,
|
|
||||||
div.body h3,
|
|
||||||
div.body h4,
|
|
||||||
div.body h5,
|
|
||||||
div.body h6 {
|
|
||||||
font-family: 'Garamond', 'Georgia', serif;
|
|
||||||
font-weight: normal;
|
|
||||||
margin: 30px 0px 10px 0px;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
{% if theme_index_logo %}
|
|
||||||
div.indexwrapper h1 {
|
|
||||||
text-indent: -999999px;
|
|
||||||
background: url({{ theme_index_logo }}) no-repeat center center;
|
|
||||||
height: {{ theme_index_logo_height }};
|
|
||||||
}
|
|
||||||
{% endif %}
|
|
||||||
div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; }
|
|
||||||
div.body h2 { font-size: 180%; }
|
|
||||||
div.body h3 { font-size: 150%; }
|
|
||||||
div.body h4 { font-size: 130%; }
|
|
||||||
div.body h5 { font-size: 100%; }
|
|
||||||
div.body h6 { font-size: 100%; }
|
|
||||||
|
|
||||||
a.headerlink {
|
|
||||||
color: #ddd;
|
|
||||||
padding: 0 4px;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
a.headerlink:hover {
|
|
||||||
color: #444;
|
|
||||||
background: #eaeaea;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.body p, div.body dd, div.body li {
|
|
||||||
line-height: 1.4em;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.admonition {
|
|
||||||
background: #fafafa;
|
|
||||||
margin: 20px -30px;
|
|
||||||
padding: 10px 30px;
|
|
||||||
border-top: 1px solid #ccc;
|
|
||||||
border-bottom: 1px solid #ccc;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.admonition tt.xref, div.admonition a tt {
|
|
||||||
border-bottom: 1px solid #fafafa;
|
|
||||||
}
|
|
||||||
|
|
||||||
dd div.admonition {
|
|
||||||
margin-left: -60px;
|
|
||||||
padding-left: 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.admonition p.admonition-title {
|
|
||||||
font-family: 'Garamond', 'Georgia', serif;
|
|
||||||
font-weight: normal;
|
|
||||||
font-size: 24px;
|
|
||||||
margin: 0 0 10px 0;
|
|
||||||
padding: 0;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.admonition p.last {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.highlight {
|
|
||||||
background-color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
dt:target, .highlight {
|
|
||||||
background: #FAF3E8;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.note {
|
|
||||||
background-color: #eee;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.seealso {
|
|
||||||
background-color: #ffc;
|
|
||||||
border: 1px solid #ff6;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.topic {
|
|
||||||
background-color: #eee;
|
|
||||||
padding: 0 7px 7px 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
p.admonition-title {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
p.admonition-title:after {
|
|
||||||
content: ":";
|
|
||||||
}
|
|
||||||
|
|
||||||
pre, tt {
|
|
||||||
font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
|
|
||||||
img.screenshot {
|
|
||||||
}
|
|
||||||
|
|
||||||
tt.descname, tt.descclassname {
|
|
||||||
font-size: 0.95em;
|
|
||||||
}
|
|
||||||
|
|
||||||
tt.descname {
|
|
||||||
padding-right: 0.08em;
|
|
||||||
}
|
|
||||||
|
|
||||||
img.screenshot {
|
|
||||||
-moz-box-shadow: 2px 2px 4px #eee;
|
|
||||||
-webkit-box-shadow: 2px 2px 4px #eee;
|
|
||||||
box-shadow: 2px 2px 4px #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.docutils {
|
|
||||||
border: 1px solid #888;
|
|
||||||
-moz-box-shadow: 2px 2px 4px #eee;
|
|
||||||
-webkit-box-shadow: 2px 2px 4px #eee;
|
|
||||||
box-shadow: 2px 2px 4px #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.docutils td, table.docutils th {
|
|
||||||
border: 1px solid #888;
|
|
||||||
padding: 0.25em 0.7em;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.field-list, table.footnote {
|
|
||||||
border: none;
|
|
||||||
-moz-box-shadow: none;
|
|
||||||
-webkit-box-shadow: none;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.footnote {
|
|
||||||
margin: 15px 0;
|
|
||||||
width: 100%;
|
|
||||||
border: 1px solid #eee;
|
|
||||||
background: #fdfdfd;
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.footnote + table.footnote {
|
|
||||||
margin-top: -15px;
|
|
||||||
border-top: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.field-list th {
|
|
||||||
padding: 0 0.8em 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.field-list td {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.footnote td.label {
|
|
||||||
width: 0px;
|
|
||||||
padding: 0.3em 0 0.3em 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.footnote td {
|
|
||||||
padding: 0.3em 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
dl {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
dl dd {
|
|
||||||
margin-left: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
blockquote {
|
|
||||||
margin: 0 0 0 30px;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul, ol {
|
|
||||||
margin: 10px 0 10px 30px;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre {
|
|
||||||
background: #eee;
|
|
||||||
margin: 12px 0px;
|
|
||||||
padding: 11px 14px;
|
|
||||||
line-height: 1.3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
dl pre, blockquote pre, li pre {
|
|
||||||
margin-left: -60px;
|
|
||||||
padding-left: 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
dl dl pre {
|
|
||||||
margin-left: -90px;
|
|
||||||
padding-left: 90px;
|
|
||||||
}
|
|
||||||
|
|
||||||
tt {
|
|
||||||
background-color: #ecf0f3;
|
|
||||||
color: #222;
|
|
||||||
/* padding: 1px 2px; */
|
|
||||||
}
|
|
||||||
|
|
||||||
tt.xref, a tt {
|
|
||||||
background-color: #FBFBFB;
|
|
||||||
border-bottom: 1px solid white;
|
|
||||||
}
|
|
||||||
|
|
||||||
a.reference {
|
|
||||||
text-decoration: none;
|
|
||||||
border-bottom: 1px dotted #004B6B;
|
|
||||||
}
|
|
||||||
|
|
||||||
a.reference:hover {
|
|
||||||
border-bottom: 1px solid #6D4100;
|
|
||||||
}
|
|
||||||
|
|
||||||
a.footnote-reference {
|
|
||||||
text-decoration: none;
|
|
||||||
font-size: 0.7em;
|
|
||||||
vertical-align: top;
|
|
||||||
border-bottom: 1px dotted #004B6B;
|
|
||||||
}
|
|
||||||
|
|
||||||
a.footnote-reference:hover {
|
|
||||||
border-bottom: 1px solid #6D4100;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover tt {
|
|
||||||
background: #EEE;
|
|
||||||
}
|
|
||||||
|
|
||||||
small {
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@media screen and (max-width: 870px) {
|
|
||||||
|
|
||||||
div.sphinxsidebar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.document {
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
div.documentwrapper {
|
|
||||||
margin-left: 0;
|
|
||||||
margin-top: 0;
|
|
||||||
margin-right: 0;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.bodywrapper {
|
|
||||||
margin-top: 0;
|
|
||||||
margin-right: 0;
|
|
||||||
margin-bottom: 0;
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.document {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bodywrapper {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.github {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@media screen and (max-width: 875px) {
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 20px 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.documentwrapper {
|
|
||||||
float: none;
|
|
||||||
background: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.sphinxsidebar {
|
|
||||||
display: block;
|
|
||||||
float: none;
|
|
||||||
width: 102.5%;
|
|
||||||
margin: 50px -30px -20px -30px;
|
|
||||||
padding: 10px 20px;
|
|
||||||
background: #333;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p,
|
|
||||||
div.sphinxsidebar h3 a {
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.sphinxsidebar a {
|
|
||||||
color: #aaa;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.sphinxsidebar p.logo {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.document {
|
|
||||||
width: 100%;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.related {
|
|
||||||
display: block;
|
|
||||||
margin: 0;
|
|
||||||
padding: 10px 0 20px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.related ul,
|
|
||||||
div.related ul li {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.footer {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.bodywrapper {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.body {
|
|
||||||
min-height: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rtd_doc_footer {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.document {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.github {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* scrollbars */
|
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-button:start:decrement,
|
|
||||||
::-webkit-scrollbar-button:end:increment {
|
|
||||||
display: block;
|
|
||||||
height: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-button:vertical:increment {
|
|
||||||
background-color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-track-piece {
|
|
||||||
background-color: #eee;
|
|
||||||
-webkit-border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:vertical {
|
|
||||||
height: 50px;
|
|
||||||
background-color: #ccc;
|
|
||||||
-webkit-border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:horizontal {
|
|
||||||
width: 50px;
|
|
||||||
background-color: #ccc;
|
|
||||||
-webkit-border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* misc. */
|
|
||||||
|
|
||||||
.revsys-inline {
|
|
||||||
display: none!important;
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
[theme]
|
|
||||||
inherit = basic
|
|
||||||
stylesheet = flasky.css
|
|
||||||
pygments_style = flask_theme_support.FlaskyStyle
|
|
||||||
|
|
||||||
[options]
|
|
||||||
index_logo = ''
|
|
||||||
index_logo_height = 120px
|
|
||||||
touch_icon =
|
|
||||||
github_fork = ''
|
|
||||||
|
|
@ -1,86 +0,0 @@
|
||||||
# flasky extensions. flasky pygments style based on tango style
|
|
||||||
from pygments.style import Style
|
|
||||||
from pygments.token import Keyword, Name, Comment, String, Error, \
|
|
||||||
Number, Operator, Generic, Whitespace, Punctuation, Other, Literal
|
|
||||||
|
|
||||||
|
|
||||||
class FlaskyStyle(Style):
|
|
||||||
background_color = "#f8f8f8"
|
|
||||||
default_style = ""
|
|
||||||
|
|
||||||
styles = {
|
|
||||||
# No corresponding class for the following:
|
|
||||||
#Text: "", # class: ''
|
|
||||||
Whitespace: "underline #f8f8f8", # class: 'w'
|
|
||||||
Error: "#a40000 border:#ef2929", # class: 'err'
|
|
||||||
Other: "#000000", # class 'x'
|
|
||||||
|
|
||||||
Comment: "italic #8f5902", # class: 'c'
|
|
||||||
Comment.Preproc: "noitalic", # class: 'cp'
|
|
||||||
|
|
||||||
Keyword: "bold #004461", # class: 'k'
|
|
||||||
Keyword.Constant: "bold #004461", # class: 'kc'
|
|
||||||
Keyword.Declaration: "bold #004461", # class: 'kd'
|
|
||||||
Keyword.Namespace: "bold #004461", # class: 'kn'
|
|
||||||
Keyword.Pseudo: "bold #004461", # class: 'kp'
|
|
||||||
Keyword.Reserved: "bold #004461", # class: 'kr'
|
|
||||||
Keyword.Type: "bold #004461", # class: 'kt'
|
|
||||||
|
|
||||||
Operator: "#582800", # class: 'o'
|
|
||||||
Operator.Word: "bold #004461", # class: 'ow' - like keywords
|
|
||||||
|
|
||||||
Punctuation: "bold #000000", # class: 'p'
|
|
||||||
|
|
||||||
# because special names such as Name.Class, Name.Function, etc.
|
|
||||||
# are not recognized as such later in the parsing, we choose them
|
|
||||||
# to look the same as ordinary variables.
|
|
||||||
Name: "#000000", # class: 'n'
|
|
||||||
Name.Attribute: "#c4a000", # class: 'na' - to be revised
|
|
||||||
Name.Builtin: "#004461", # class: 'nb'
|
|
||||||
Name.Builtin.Pseudo: "#3465a4", # class: 'bp'
|
|
||||||
Name.Class: "#000000", # class: 'nc' - to be revised
|
|
||||||
Name.Constant: "#000000", # class: 'no' - to be revised
|
|
||||||
Name.Decorator: "#888", # class: 'nd' - to be revised
|
|
||||||
Name.Entity: "#ce5c00", # class: 'ni'
|
|
||||||
Name.Exception: "bold #cc0000", # class: 'ne'
|
|
||||||
Name.Function: "#000000", # class: 'nf'
|
|
||||||
Name.Property: "#000000", # class: 'py'
|
|
||||||
Name.Label: "#f57900", # class: 'nl'
|
|
||||||
Name.Namespace: "#000000", # class: 'nn' - to be revised
|
|
||||||
Name.Other: "#000000", # class: 'nx'
|
|
||||||
Name.Tag: "bold #004461", # class: 'nt' - like a keyword
|
|
||||||
Name.Variable: "#000000", # class: 'nv' - to be revised
|
|
||||||
Name.Variable.Class: "#000000", # class: 'vc' - to be revised
|
|
||||||
Name.Variable.Global: "#000000", # class: 'vg' - to be revised
|
|
||||||
Name.Variable.Instance: "#000000", # class: 'vi' - to be revised
|
|
||||||
|
|
||||||
Number: "#990000", # class: 'm'
|
|
||||||
|
|
||||||
Literal: "#000000", # class: 'l'
|
|
||||||
Literal.Date: "#000000", # class: 'ld'
|
|
||||||
|
|
||||||
String: "#4e9a06", # class: 's'
|
|
||||||
String.Backtick: "#4e9a06", # class: 'sb'
|
|
||||||
String.Char: "#4e9a06", # class: 'sc'
|
|
||||||
String.Doc: "italic #8f5902", # class: 'sd' - like a comment
|
|
||||||
String.Double: "#4e9a06", # class: 's2'
|
|
||||||
String.Escape: "#4e9a06", # class: 'se'
|
|
||||||
String.Heredoc: "#4e9a06", # class: 'sh'
|
|
||||||
String.Interpol: "#4e9a06", # class: 'si'
|
|
||||||
String.Other: "#4e9a06", # class: 'sx'
|
|
||||||
String.Regex: "#4e9a06", # class: 'sr'
|
|
||||||
String.Single: "#4e9a06", # class: 's1'
|
|
||||||
String.Symbol: "#4e9a06", # class: 'ss'
|
|
||||||
|
|
||||||
Generic: "#000000", # class: 'g'
|
|
||||||
Generic.Deleted: "#a40000", # class: 'gd'
|
|
||||||
Generic.Emph: "italic #000000", # class: 'ge'
|
|
||||||
Generic.Error: "#ef2929", # class: 'gr'
|
|
||||||
Generic.Heading: "bold #000080", # class: 'gh'
|
|
||||||
Generic.Inserted: "#00A000", # class: 'gi'
|
|
||||||
Generic.Output: "#888", # class: 'go'
|
|
||||||
Generic.Prompt: "#745334", # class: 'gp'
|
|
||||||
Generic.Strong: "bold #000000", # class: 'gs'
|
|
||||||
Generic.Subheading: "bold #800080", # class: 'gu'
|
|
||||||
Generic.Traceback: "bold #a40000", # class: 'gt'
|
|
||||||
}
|
|
||||||
|
|
@ -1,204 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
# Flask documentation build configuration file, created by
|
|
||||||
# sphinx-quickstart on Tue Apr 6 15:24:58 2010.
|
|
||||||
#
|
|
||||||
# This file is execfile()d with the current directory set to its containing dir.
|
|
||||||
#
|
|
||||||
# Note that not all possible configuration values are present in this
|
|
||||||
# autogenerated file.
|
|
||||||
#
|
|
||||||
# All configuration values have a default; values that are commented out
|
|
||||||
# serve to show the default.
|
|
||||||
from __future__ import print_function
|
|
||||||
import sys, os
|
|
||||||
|
|
||||||
# If extensions (or modules to document with autodoc) are in another directory,
|
|
||||||
# add these directories to sys.path here. If the directory is relative to the
|
|
||||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
|
||||||
sys.path.append(os.path.abspath('_themes'))
|
|
||||||
sys.path.append(os.path.abspath('.'))
|
|
||||||
|
|
||||||
# -- General configuration -----------------------------------------------------
|
|
||||||
|
|
||||||
# If your documentation needs a minimal Sphinx version, state it here.
|
|
||||||
#needs_sphinx = '1.0'
|
|
||||||
|
|
||||||
# Add any Sphinx extension module names here, as strings. They can be extensions
|
|
||||||
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
|
||||||
extensions = [
|
|
||||||
'sphinx.ext.autodoc',
|
|
||||||
'sphinx.ext.intersphinx',
|
|
||||||
'flaskdocext'
|
|
||||||
]
|
|
||||||
|
|
||||||
# Add any paths that contain templates here, relative to this directory.
|
|
||||||
templates_path = ['_templates']
|
|
||||||
|
|
||||||
# The suffix of source filenames.
|
|
||||||
source_suffix = '.rst'
|
|
||||||
|
|
||||||
# The encoding of source files.
|
|
||||||
#source_encoding = 'utf-8-sig'
|
|
||||||
|
|
||||||
# The master toctree document.
|
|
||||||
master_doc = 'index'
|
|
||||||
|
|
||||||
# General information about the project.
|
|
||||||
project = u'Flask-Ask'
|
|
||||||
copyright = u'2016, John Wheeler'
|
|
||||||
|
|
||||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
|
||||||
# for a list of supported languages.
|
|
||||||
#language = None
|
|
||||||
|
|
||||||
# There are two options for replacing |today|: either, you set today to some
|
|
||||||
# non-false value, then it is used:
|
|
||||||
#today = ''
|
|
||||||
# Else, today_fmt is used as the format for a strftime call.
|
|
||||||
#today_fmt = '%B %d, %Y'
|
|
||||||
|
|
||||||
# List of patterns, relative to source directory, that match files and
|
|
||||||
# directories to ignore when looking for source files.
|
|
||||||
exclude_patterns = ['_build']
|
|
||||||
|
|
||||||
# The reST default role (used for this markup: `text`) to use for all documents.
|
|
||||||
#default_role = None
|
|
||||||
|
|
||||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
|
||||||
#add_function_parentheses = True
|
|
||||||
|
|
||||||
# If true, the current module name will be prepended to all description
|
|
||||||
# unit titles (such as .. function::).
|
|
||||||
#add_module_names = True
|
|
||||||
|
|
||||||
# If true, sectionauthor and moduleauthor directives will be shown in the
|
|
||||||
# output. They are ignored by default.
|
|
||||||
#show_authors = False
|
|
||||||
|
|
||||||
# A list of ignored prefixes for module index sorting.
|
|
||||||
#modindex_common_prefix = []
|
|
||||||
|
|
||||||
|
|
||||||
# -- Options for HTML output ---------------------------------------------------
|
|
||||||
|
|
||||||
# The theme to use for HTML and HTML Help pages. Major themes that come with
|
|
||||||
# Sphinx are currently 'default' and 'sphinxdoc'.
|
|
||||||
html_theme = 'flask'
|
|
||||||
|
|
||||||
# Theme options are theme-specific and customize the look and feel of a theme
|
|
||||||
# further. For a list of options available for each theme, see the
|
|
||||||
# documentation.
|
|
||||||
html_theme_options = {
|
|
||||||
'github_fork': 'johnwheeler/flask-ask'
|
|
||||||
}
|
|
||||||
|
|
||||||
html_sidebars = {
|
|
||||||
'index': ['globaltoc.html', 'links.html', 'stayinformed.html'],
|
|
||||||
'**': ['sidebarlogo.html', 'globaltoc.html', 'links.html', 'stayinformed.html']
|
|
||||||
}
|
|
||||||
|
|
||||||
# Add any paths that contain custom themes here, relative to this directory.
|
|
||||||
html_theme_path = ['_themes']
|
|
||||||
|
|
||||||
# The name for this set of Sphinx documents. If None, it defaults to
|
|
||||||
# "<project> v<release> documentation".
|
|
||||||
#html_title = None
|
|
||||||
|
|
||||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
|
||||||
#html_short_title = None
|
|
||||||
|
|
||||||
# The name of an image file (relative to this directory) to place at the top
|
|
||||||
# of the sidebar. Do not set, template magic!
|
|
||||||
#html_logo = None
|
|
||||||
|
|
||||||
# The name of an image file (within the static path) to use as favicon of the
|
|
||||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
|
||||||
# pixels large.
|
|
||||||
#html_favicon = "flask-favicon.ico"
|
|
||||||
|
|
||||||
# Add any paths that contain custom static files (such as style sheets) here,
|
|
||||||
# relative to this directory. They are copied after the builtin static files,
|
|
||||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
|
||||||
html_static_path = ['_static']
|
|
||||||
|
|
||||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
|
||||||
# using the given strftime format.
|
|
||||||
#html_last_updated_fmt = '%b %d, %Y'
|
|
||||||
|
|
||||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
|
||||||
# typographically correct entities.
|
|
||||||
#html_use_smartypants = True
|
|
||||||
|
|
||||||
# Custom sidebar templates, maps document names to template names.
|
|
||||||
# html_sidebars = {
|
|
||||||
# 'index': ['sidebarintro.html', 'sourcelink.html', 'searchbox.html'],
|
|
||||||
# '**': ['sidebarlogo.html', 'localtoc.html', 'relations.html',
|
|
||||||
# 'sourcelink.html', 'searchbox.html']
|
|
||||||
# }
|
|
||||||
|
|
||||||
# Additional templates that should be rendered to pages, maps page names to
|
|
||||||
# template names.
|
|
||||||
#html_additional_pages = {}
|
|
||||||
|
|
||||||
# If false, no module index is generated.
|
|
||||||
html_use_modindex = False
|
|
||||||
|
|
||||||
# If false, no index is generated.
|
|
||||||
#html_use_index = True
|
|
||||||
|
|
||||||
# If true, the index is split into individual pages for each letter.
|
|
||||||
#html_split_index = False
|
|
||||||
|
|
||||||
# If true, links to the reST sources are added to the pages.
|
|
||||||
#html_show_sourcelink = True
|
|
||||||
|
|
||||||
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
|
||||||
html_show_sphinx = False
|
|
||||||
|
|
||||||
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
|
||||||
#html_show_copyright = True
|
|
||||||
|
|
||||||
# If true, an OpenSearch description file will be output, and all pages will
|
|
||||||
# contain a <link> tag referring to it. The value of this option must be the
|
|
||||||
# base URL from which the finished HTML is served.
|
|
||||||
#html_use_opensearch = ''
|
|
||||||
|
|
||||||
# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
|
|
||||||
#html_file_suffix = ''
|
|
||||||
|
|
||||||
# -- Options for Epub output ---------------------------------------------------
|
|
||||||
|
|
||||||
# Bibliographic Dublin Core info.
|
|
||||||
#epub_title = ''
|
|
||||||
#epub_author = ''
|
|
||||||
#epub_publisher = ''
|
|
||||||
#epub_copyright = ''
|
|
||||||
|
|
||||||
# The language of the text. It defaults to the language option
|
|
||||||
# or en if the language is not set.
|
|
||||||
#epub_language = ''
|
|
||||||
|
|
||||||
# The scheme of the identifier. Typical schemes are ISBN or URL.
|
|
||||||
#epub_scheme = ''
|
|
||||||
|
|
||||||
# The unique identifier of the text. This can be a ISBN number
|
|
||||||
# or the project homepage.
|
|
||||||
#epub_identifier = ''
|
|
||||||
|
|
||||||
# A unique identification for the text.
|
|
||||||
#epub_uid = ''
|
|
||||||
|
|
||||||
# HTML files that should be inserted before the pages created by sphinx.
|
|
||||||
# The format is a list of tuples containing the path and title.
|
|
||||||
#epub_pre_files = []
|
|
||||||
|
|
||||||
# HTML files shat should be inserted after the pages created by sphinx.
|
|
||||||
# The format is a list of tuples containing the path and title.
|
|
||||||
#epub_post_files = []
|
|
||||||
|
|
||||||
# A list of files that should not be packed into the epub file.
|
|
||||||
#epub_exclude_files = []
|
|
||||||
|
|
||||||
# The depth of the table of contents in toc.ncx.
|
|
||||||
#epub_tocdepth = 3
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
Configuration
|
|
||||||
=============
|
|
||||||
|
|
||||||
Configuration
|
|
||||||
-------------
|
|
||||||
|
|
||||||
Flask-Ask exposes the following configuration variables:
|
|
||||||
|
|
||||||
============================ ============================================================================================
|
|
||||||
`ASK_APPLICATION_ID` Turn on application ID verification by setting this variable to an application ID or a
|
|
||||||
list of allowed application IDs. By default, application ID verification is disabled and a
|
|
||||||
warning is logged. This variable should be set in production to ensure
|
|
||||||
requests are being sent by the applications you specify. **Default:** ``None``
|
|
||||||
`ASK_VERIFY_REQUESTS` Enables or disables
|
|
||||||
`Alexa request verification <https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/developing-an-alexa-skill-as-a-web-service#checking-the-signature-of-the-request>`_,
|
|
||||||
which ensures requests sent to your skill are
|
|
||||||
from Amazon's Alexa service. This setting should not be disabled in production. It is
|
|
||||||
useful for mocking JSON requests in automated tests. **Default:** ``True``
|
|
||||||
`ASK_VERIFY_TIMESTAMP_DEBUG` Turn on request timestamp verification while debugging by setting this to ``True``.
|
|
||||||
Timestamp verification helps mitigate against
|
|
||||||
`replay attacks <https://en.wikipedia.org/wiki/Replay_attack>`_. It
|
|
||||||
relies on the system clock being synchronized with an NTP server. This setting should not
|
|
||||||
be enabled in production. **Default:** ``False``
|
|
||||||
============================ ============================================================================================
|
|
||||||
|
|
||||||
Logging
|
|
||||||
-------
|
|
||||||
|
|
||||||
To see the JSON request / response structures pretty printed in the logs, turn on ``DEBUG``-level logging::
|
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
logging.getLogger('flask_ask').setLevel(logging.DEBUG)
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
Table Of Contents
|
|
||||||
-----------------
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
:maxdepth: 2
|
|
||||||
|
|
||||||
getting_started
|
|
||||||
requests
|
|
||||||
responses
|
|
||||||
configuration
|
|
||||||
user_contributions
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
import re
|
|
||||||
import inspect
|
|
||||||
|
|
||||||
|
|
||||||
_internal_mark_re = re.compile(r'^\s*:internal:\s*$(?m)')
|
|
||||||
|
|
||||||
|
|
||||||
def skip_member(app, what, name, obj, skip, options):
|
|
||||||
docstring = inspect.getdoc(obj)
|
|
||||||
if skip:
|
|
||||||
return True
|
|
||||||
return _internal_mark_re.search(docstring or '') is not None
|
|
||||||
|
|
||||||
|
|
||||||
def setup(app):
|
|
||||||
app.connect('autodoc-skip-member', skip_member)
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
Getting Started
|
|
||||||
===============
|
|
||||||
|
|
||||||
Installation
|
|
||||||
------------
|
|
||||||
To install Flask-Ask::
|
|
||||||
|
|
||||||
pip install flask-ask
|
|
||||||
|
|
||||||
|
|
||||||
A Minimal Voice User Interface
|
|
||||||
------------------------------
|
|
||||||
A Flask-Ask application looks like this:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from flask import Flask, render_template
|
|
||||||
from flask_ask import Ask, statement
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
ask = Ask(app, '/')
|
|
||||||
|
|
||||||
@ask.intent('HelloIntent')
|
|
||||||
def hello(firstname):
|
|
||||||
text = render_template('hello', firstname=firstname)
|
|
||||||
return statement(text).simple_card('Hello', text)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
app.run(debug=True)
|
|
||||||
|
|
||||||
In the code above:
|
|
||||||
|
|
||||||
#. The ``Ask`` object is created by passing in the Flask application and a route to forward Alexa requests to.
|
|
||||||
#. The ``intent`` decorator maps ``HelloIntent`` to a view function ``hello``.
|
|
||||||
#. The intent's ``firstname`` slot is implicitly mapped to ``hello``'s ``firstname`` parameter.
|
|
||||||
#. Jinja templates are supported. Internally, templates are loaded from a YAML file (discussed further below).
|
|
||||||
#. Lastly, a builder constructs a spoken response and displays a contextual card in the Alexa smartphone/tablet app.
|
|
||||||
|
|
||||||
Since Alexa responses are usually short phrases, it's convenient to put them in the same file.
|
|
||||||
Flask-Ask has a `Jinja template loader <http://jinja.pocoo.org/docs/dev/api/#loaders>`_ that loads
|
|
||||||
multiple templates from a single YAML file. For example, here's a template that supports the minimal voice interface
|
|
||||||
above.Templates are stored in a file called `templates.yaml` located in the application root:
|
|
||||||
|
|
||||||
.. code-block:: yaml
|
|
||||||
|
|
||||||
hello: Hello, {{ firstname }}
|
|
||||||
|
|
||||||
For more information about how the Alexa Skills Kit works, see `Understanding Custom Skills <https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/overviews/understanding-custom-skills>`_ in the Alexa Skills Kit documentation.
|
|
||||||
|
|
||||||
Additionally, more code and template examples are in the `samples <https://github.com/johnwheeler/flask-ask/tree/master/samples>`_ directory.
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
:orphan:
|
|
||||||
|
|
||||||
.. image:: _static/logo-full.png
|
|
||||||
:alt: Flask-Ask: Alexa Skills Kit Development for Amazon Echo Devices with Python
|
|
||||||
|
|
||||||
|
|
||||||
😎 `Lighten your cognitive load. Level up with the Alexa Skills Kit Video Tutorial <https://alexatutorial.com/>`_.
|
|
||||||
|
|
||||||
|
|
||||||
.. raw:: html
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<a class="github-button" href="https://github.com/johnwheeler/flask-ask" data-icon="octicon-star" data-style="mega" data-count-href="/johnwheeler/flask-ask/stargazers" data-count-api="/repos/johnwheeler/flask-ask#stargazers_count" data-count-aria-label="# stargazers on GitHub" aria-label="Star johnwheeler/flask-ask on GitHub">Star</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
|
|
||||||
Welcome to Flask-Ask
|
|
||||||
====================
|
|
||||||
|
|
||||||
Building high-quality Alexa skills for Amazon Echo Devices takes time. Flask-Ask makes it easier and much more fun.
|
|
||||||
Use Flask-Ask with `ngrok <https://ngrok.com/>`_ to eliminate the deploy-to-test step and get work done faster.
|
|
||||||
|
|
||||||
Flask-Ask:
|
|
||||||
|
|
||||||
* Has decorators to map Alexa requests and intent slots to view functions
|
|
||||||
* Helps construct ask and tell responses, reprompts and cards
|
|
||||||
* Makes session management easy
|
|
||||||
* Allows for the separation of code and speech through Jinja templates
|
|
||||||
* Verifies Alexa request signatures
|
|
||||||
|
|
||||||
.. raw:: html
|
|
||||||
|
|
||||||
<br>
|
|
||||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/cXL8FDUag-s" frameborder="0" allowfullscreen></iframe>
|
|
||||||
|
|
||||||
Follow along with this quickstart on `Amazon
|
|
||||||
<https://developer.amazon.com/public/community/post/Tx14R0IYYGH3SKT/Flask-Ask-A-New-Python-Framework-for-Rapid-Alexa-Skills-Kit-Development>`_.
|
|
||||||
|
|
||||||
.. include:: contents.rst.inc
|
|
||||||
|
|
||||||
.. raw:: html
|
|
||||||
|
|
||||||
<br>
|
|
||||||
<blockquote class="twitter-tweet" data-lang="en"><p lang="en" dir="ltr">If you have an Alexa, Flask-Ask looks fantastic! <a href="https://t.co/vpoFsVNqI5">https://t.co/vpoFsVNqI5</a></p>— Kenneth Reitz (@kennethreitz) <a href="https://twitter.com/kennethreitz/status/736234634292297729">May 27, 2016</a></blockquote>
|
|
||||||
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>
|
|
||||||
|
|
@ -1,281 +0,0 @@
|
||||||
@ECHO OFF
|
|
||||||
|
|
||||||
REM Command file for Sphinx documentation
|
|
||||||
|
|
||||||
if "%SPHINXBUILD%" == "" (
|
|
||||||
set SPHINXBUILD=sphinx-build
|
|
||||||
)
|
|
||||||
set BUILDDIR=_build
|
|
||||||
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
|
|
||||||
set I18NSPHINXOPTS=%SPHINXOPTS% .
|
|
||||||
if NOT "%PAPER%" == "" (
|
|
||||||
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
|
|
||||||
set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "" goto help
|
|
||||||
|
|
||||||
if "%1" == "help" (
|
|
||||||
:help
|
|
||||||
echo.Please use `make ^<target^>` where ^<target^> is one of
|
|
||||||
echo. html to make standalone HTML files
|
|
||||||
echo. dirhtml to make HTML files named index.html in directories
|
|
||||||
echo. singlehtml to make a single large HTML file
|
|
||||||
echo. pickle to make pickle files
|
|
||||||
echo. json to make JSON files
|
|
||||||
echo. htmlhelp to make HTML files and a HTML help project
|
|
||||||
echo. qthelp to make HTML files and a qthelp project
|
|
||||||
echo. devhelp to make HTML files and a Devhelp project
|
|
||||||
echo. epub to make an epub
|
|
||||||
echo. epub3 to make an epub3
|
|
||||||
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
|
|
||||||
echo. text to make text files
|
|
||||||
echo. man to make manual pages
|
|
||||||
echo. texinfo to make Texinfo files
|
|
||||||
echo. gettext to make PO message catalogs
|
|
||||||
echo. changes to make an overview over all changed/added/deprecated items
|
|
||||||
echo. xml to make Docutils-native XML files
|
|
||||||
echo. pseudoxml to make pseudoxml-XML files for display purposes
|
|
||||||
echo. linkcheck to check all external links for integrity
|
|
||||||
echo. doctest to run all doctests embedded in the documentation if enabled
|
|
||||||
echo. coverage to run coverage check of the documentation if enabled
|
|
||||||
echo. dummy to check syntax errors of document sources
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "clean" (
|
|
||||||
for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
|
|
||||||
del /q /s %BUILDDIR%\*
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
REM Check if sphinx-build is available and fallback to Python version if any
|
|
||||||
%SPHINXBUILD% 1>NUL 2>NUL
|
|
||||||
if errorlevel 9009 goto sphinx_python
|
|
||||||
goto sphinx_ok
|
|
||||||
|
|
||||||
:sphinx_python
|
|
||||||
|
|
||||||
set SPHINXBUILD=python -m sphinx.__init__
|
|
||||||
%SPHINXBUILD% 2> nul
|
|
||||||
if errorlevel 9009 (
|
|
||||||
echo.
|
|
||||||
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
|
||||||
echo.installed, then set the SPHINXBUILD environment variable to point
|
|
||||||
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
|
||||||
echo.may add the Sphinx directory to PATH.
|
|
||||||
echo.
|
|
||||||
echo.If you don't have Sphinx installed, grab it from
|
|
||||||
echo.http://sphinx-doc.org/
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
|
|
||||||
:sphinx_ok
|
|
||||||
|
|
||||||
|
|
||||||
if "%1" == "html" (
|
|
||||||
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
|
|
||||||
if errorlevel 1 exit /b 1
|
|
||||||
echo.
|
|
||||||
echo.Build finished. The HTML pages are in %BUILDDIR%/html.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "dirhtml" (
|
|
||||||
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
|
|
||||||
if errorlevel 1 exit /b 1
|
|
||||||
echo.
|
|
||||||
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "singlehtml" (
|
|
||||||
%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
|
|
||||||
if errorlevel 1 exit /b 1
|
|
||||||
echo.
|
|
||||||
echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "pickle" (
|
|
||||||
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
|
|
||||||
if errorlevel 1 exit /b 1
|
|
||||||
echo.
|
|
||||||
echo.Build finished; now you can process the pickle files.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "json" (
|
|
||||||
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
|
|
||||||
if errorlevel 1 exit /b 1
|
|
||||||
echo.
|
|
||||||
echo.Build finished; now you can process the JSON files.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "htmlhelp" (
|
|
||||||
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
|
|
||||||
if errorlevel 1 exit /b 1
|
|
||||||
echo.
|
|
||||||
echo.Build finished; now you can run HTML Help Workshop with the ^
|
|
||||||
.hhp project file in %BUILDDIR%/htmlhelp.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "qthelp" (
|
|
||||||
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
|
|
||||||
if errorlevel 1 exit /b 1
|
|
||||||
echo.
|
|
||||||
echo.Build finished; now you can run "qcollectiongenerator" with the ^
|
|
||||||
.qhcp project file in %BUILDDIR%/qthelp, like this:
|
|
||||||
echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Flask-Ask.qhcp
|
|
||||||
echo.To view the help file:
|
|
||||||
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Flask-Ask.ghc
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "devhelp" (
|
|
||||||
%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
|
|
||||||
if errorlevel 1 exit /b 1
|
|
||||||
echo.
|
|
||||||
echo.Build finished.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "epub" (
|
|
||||||
%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
|
|
||||||
if errorlevel 1 exit /b 1
|
|
||||||
echo.
|
|
||||||
echo.Build finished. The epub file is in %BUILDDIR%/epub.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "epub3" (
|
|
||||||
%SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3
|
|
||||||
if errorlevel 1 exit /b 1
|
|
||||||
echo.
|
|
||||||
echo.Build finished. The epub3 file is in %BUILDDIR%/epub3.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "latex" (
|
|
||||||
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
|
|
||||||
if errorlevel 1 exit /b 1
|
|
||||||
echo.
|
|
||||||
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "latexpdf" (
|
|
||||||
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
|
|
||||||
cd %BUILDDIR%/latex
|
|
||||||
make all-pdf
|
|
||||||
cd %~dp0
|
|
||||||
echo.
|
|
||||||
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "latexpdfja" (
|
|
||||||
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
|
|
||||||
cd %BUILDDIR%/latex
|
|
||||||
make all-pdf-ja
|
|
||||||
cd %~dp0
|
|
||||||
echo.
|
|
||||||
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "text" (
|
|
||||||
%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
|
|
||||||
if errorlevel 1 exit /b 1
|
|
||||||
echo.
|
|
||||||
echo.Build finished. The text files are in %BUILDDIR%/text.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "man" (
|
|
||||||
%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
|
|
||||||
if errorlevel 1 exit /b 1
|
|
||||||
echo.
|
|
||||||
echo.Build finished. The manual pages are in %BUILDDIR%/man.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "texinfo" (
|
|
||||||
%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
|
|
||||||
if errorlevel 1 exit /b 1
|
|
||||||
echo.
|
|
||||||
echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "gettext" (
|
|
||||||
%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
|
|
||||||
if errorlevel 1 exit /b 1
|
|
||||||
echo.
|
|
||||||
echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "changes" (
|
|
||||||
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
|
|
||||||
if errorlevel 1 exit /b 1
|
|
||||||
echo.
|
|
||||||
echo.The overview file is in %BUILDDIR%/changes.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "linkcheck" (
|
|
||||||
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
|
|
||||||
if errorlevel 1 exit /b 1
|
|
||||||
echo.
|
|
||||||
echo.Link check complete; look for any errors in the above output ^
|
|
||||||
or in %BUILDDIR%/linkcheck/output.txt.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "doctest" (
|
|
||||||
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
|
|
||||||
if errorlevel 1 exit /b 1
|
|
||||||
echo.
|
|
||||||
echo.Testing of doctests in the sources finished, look at the ^
|
|
||||||
results in %BUILDDIR%/doctest/output.txt.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "coverage" (
|
|
||||||
%SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage
|
|
||||||
if errorlevel 1 exit /b 1
|
|
||||||
echo.
|
|
||||||
echo.Testing of coverage in the sources finished, look at the ^
|
|
||||||
results in %BUILDDIR%/coverage/python.txt.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "xml" (
|
|
||||||
%SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
|
|
||||||
if errorlevel 1 exit /b 1
|
|
||||||
echo.
|
|
||||||
echo.Build finished. The XML files are in %BUILDDIR%/xml.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "pseudoxml" (
|
|
||||||
%SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
|
|
||||||
if errorlevel 1 exit /b 1
|
|
||||||
echo.
|
|
||||||
echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "dummy" (
|
|
||||||
%SPHINXBUILD% -b dummy %ALLSPHINXOPTS% %BUILDDIR%/dummy
|
|
||||||
if errorlevel 1 exit /b 1
|
|
||||||
echo.
|
|
||||||
echo.Build finished. Dummy builder generates no files.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
:end
|
|
||||||
|
|
@ -1,265 +0,0 @@
|
||||||
Handling Requests
|
|
||||||
=================
|
|
||||||
|
|
||||||
With the Alexa Skills Kit, spoken phrases are mapped to actions executed on a server. Alexa converts
|
|
||||||
speech into JSON and delivers the JSON to your application.
|
|
||||||
For example, the phrase:
|
|
||||||
|
|
||||||
"Alexa, Tell HelloApp to say hi to John"
|
|
||||||
|
|
||||||
produces JSON like the following:
|
|
||||||
|
|
||||||
.. code-block:: javascript
|
|
||||||
|
|
||||||
"request": {
|
|
||||||
"intent": {
|
|
||||||
"name": "HelloIntent",
|
|
||||||
"slots": {
|
|
||||||
"firstname": {
|
|
||||||
"name": "firstname",
|
|
||||||
"value": "John"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
...
|
|
||||||
}
|
|
||||||
|
|
||||||
Parameters called 'slots' are defined and parsed out of speech at runtime.
|
|
||||||
For example, the spoken word 'John' above is parsed into the slot named ``firstname`` with the ``AMAZON.US_FIRST_NAME``
|
|
||||||
data type.
|
|
||||||
|
|
||||||
For detailed information, see
|
|
||||||
`Handling Requests Sent by Alexa <https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/handling-requests-sent-by-alexa>`_
|
|
||||||
on the Amazon developer website.
|
|
||||||
|
|
||||||
This section shows how to process Alexa requests with Flask-Ask. It contains the following subsections:
|
|
||||||
|
|
||||||
.. contents::
|
|
||||||
:local:
|
|
||||||
:backlinks: none
|
|
||||||
|
|
||||||
Mapping Alexa Requests to View Functions
|
|
||||||
----------------------------------------
|
|
||||||
|
|
||||||
📼 Here is a video demo on `Handling Requests with Flask-Ask video <https://youtu.be/6RoSi3G1chk>`_.
|
|
||||||
|
|
||||||
Flask-Ask has decorators to map Alexa requests to view functions.
|
|
||||||
|
|
||||||
The ``launch`` decorator handles launch requests::
|
|
||||||
|
|
||||||
@ask.launch
|
|
||||||
def launched():
|
|
||||||
return question('Welcome to Foo')
|
|
||||||
|
|
||||||
The ``intent`` decorator handles intent requests::
|
|
||||||
|
|
||||||
@ask.intent('HelloWorldIntent')
|
|
||||||
def hello():
|
|
||||||
return statement('Hello, world')
|
|
||||||
|
|
||||||
The ``session_ended`` decorator is for the session ended request::
|
|
||||||
|
|
||||||
@ask.session_ended
|
|
||||||
def session_ended():
|
|
||||||
return "{}", 200
|
|
||||||
|
|
||||||
Launch and intent requests can both start sessions. Avoid duplicate code with the ``on_session_started`` callback::
|
|
||||||
|
|
||||||
@ask.on_session_started
|
|
||||||
def new_session():
|
|
||||||
log.info('new session started')
|
|
||||||
|
|
||||||
|
|
||||||
Mapping Intent Slots to View Function Parameters
|
|
||||||
------------------------------------------------
|
|
||||||
|
|
||||||
📼 Here is a video demo on `Intent Slots with Flask-Ask video <https://youtu.be/AnyZG2AJE4o>`_.
|
|
||||||
|
|
||||||
|
|
||||||
When Parameter and Slot Names Differ
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
Tell Flask-Ask when slot and view function parameter names differ with ``mapping``::
|
|
||||||
|
|
||||||
@ask.intent('WeatherIntent', mapping={'city': 'City'})
|
|
||||||
def weather(city):
|
|
||||||
return statement('I predict great weather for {}'.format(city))
|
|
||||||
|
|
||||||
Above, the parameter ``city`` is mapped to the slot ``City``.
|
|
||||||
|
|
||||||
|
|
||||||
Assigning Default Values when Slots are Empty
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
Parameters are assigned a value of ``None`` if the Alexa service:
|
|
||||||
|
|
||||||
* Does not return a corresponding slot in the request
|
|
||||||
* Includes a corresponding slot without its ``value`` attribute
|
|
||||||
* Includes a corresponding slot with an empty ``value`` attribute (e.g. ``""``)
|
|
||||||
|
|
||||||
Use the ``default`` parameter for default values instead of ``None``. The default itself should be a
|
|
||||||
literal or a callable that resolves to a value. The next example shows the literal ``'World'``::
|
|
||||||
|
|
||||||
@ask.intent('HelloIntent', default={'name': 'World'})
|
|
||||||
def hello(name):
|
|
||||||
return statement('Hello, {}'.format(name))
|
|
||||||
|
|
||||||
|
|
||||||
Converting Slots Values to Python Data Types
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
📼 Here is a video demo on `Slot Conversions with Flask-Ask video <https://youtu.be/gP86uugBico>`_.
|
|
||||||
|
|
||||||
When slot values are available, they're always assigned to parameters as strings. Convert to other Python
|
|
||||||
data types with ``convert``. ``convert`` is a ``dict`` that maps parameter names to callables::
|
|
||||||
|
|
||||||
@ask.intent('AddIntent', convert={'x': int, 'y': int})
|
|
||||||
def add(x, y):
|
|
||||||
z = x + y
|
|
||||||
return statement('{} plus {} equals {}'.format(x, y, z))
|
|
||||||
|
|
||||||
|
|
||||||
Above, ``x`` and ``y`` will both be passed to ``int()`` and thus converted to ``int`` instances.
|
|
||||||
|
|
||||||
Flask-Ask provides convenient API constants for Amazon ``AMAZON.DATE``, ``AMAZON.TIME``, and ``AMAZON.DURATION``
|
|
||||||
types exist since those are harder to build callables against. Instead of trying to define functions that work with
|
|
||||||
inputs like those in Amazon's
|
|
||||||
`documentation <https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/alexa-skills-kit-interaction-model-reference#Slot%20Types>`_,
|
|
||||||
just pass the strings in the second column below:
|
|
||||||
|
|
||||||
📼 Here is a video demo on `Slot Conversion Helpers with Flask-Ask video <https://youtu.be/2RoRoABK_VE>`_.
|
|
||||||
|
|
||||||
=================== =============== ======================
|
|
||||||
Amazon Data Type String Python Data Type
|
|
||||||
=================== =============== ======================
|
|
||||||
``AMAZON.DATE`` ``'date'`` ``datetime.date``
|
|
||||||
``AMAZON.TIME`` ``'time'`` ``datetime.time``
|
|
||||||
``AMAZON.DURATION`` ``'timedelta'`` ``datetime.timedelta``
|
|
||||||
=================== =============== ======================
|
|
||||||
|
|
||||||
**Examples**
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
convert={'the_date': 'date'}
|
|
||||||
|
|
||||||
converts ``'2015-11-24'``, ``'2015-W48-WE'``, or ``'201X'`` into a ``datetime.date``
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
convert={'appointment_time': 'time'}
|
|
||||||
|
|
||||||
converts ``'06:00'``, ``'14:15'``, or ``'23:59'`` into a ``datetime.time``.
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
convert={'ago': 'timedelta'}
|
|
||||||
|
|
||||||
converts ``'PT10M'``, ``'PT45S'``, or ``'P2YT3H10M'`` into a ``datetime.timedelta``.
|
|
||||||
|
|
||||||
|
|
||||||
Handling Conversion Errors
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
Sometimes Alexa doesn't understand what's said, and slots come in with question marks:
|
|
||||||
|
|
||||||
.. code-block:: javascript
|
|
||||||
|
|
||||||
"slots": {
|
|
||||||
"age": {
|
|
||||||
"name": "age",
|
|
||||||
"value": "?"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Recover gracefully with the ``convert_errors`` context local. Import it to use it:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
...
|
|
||||||
from flask_ask import statement, question, convert_errors
|
|
||||||
|
|
||||||
|
|
||||||
@ask.intent('AgeIntent', convert={'age': int})
|
|
||||||
def say_age(age):
|
|
||||||
if 'age' in convert_errors:
|
|
||||||
# since age failed to convert, it keeps its string
|
|
||||||
# value (e.g. "?") for later interrogation.
|
|
||||||
return question("Can you please repeat your age?")
|
|
||||||
|
|
||||||
# conversion guaranteed to have succeeded
|
|
||||||
# age is an int
|
|
||||||
return statement("Your age is {}".format(age))
|
|
||||||
|
|
||||||
|
|
||||||
``convert_errors`` is a ``dict`` that maps parameter names to the ``Exceptions`` raised during
|
|
||||||
conversion. When writing your own converters, raise ``Exceptions`` on failure, so
|
|
||||||
they work with ``convert_errors``::
|
|
||||||
|
|
||||||
def to_direction_const(s):
|
|
||||||
if s.lower() not in ['left', 'right']
|
|
||||||
raise Exception("must be left or right")
|
|
||||||
return LEFT if s == 'left' else RIGHT
|
|
||||||
|
|
||||||
@ask.intent('TurnIntent', convert={'direction': to_direction_const})
|
|
||||||
def turn(direction):
|
|
||||||
# do something with direction
|
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
That ``convert_errors`` is a ``dict`` allows for granular error recovery::
|
|
||||||
|
|
||||||
if 'something' in convert_errors:
|
|
||||||
# Did something fail?
|
|
||||||
|
|
||||||
or::
|
|
||||||
|
|
||||||
if convert_errors:
|
|
||||||
# Did anything fail?
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
``session``, ``context``, ``request`` and ``version`` Context Locals
|
|
||||||
---------------------------------------------------------------------
|
|
||||||
An Alexa
|
|
||||||
`request payload <https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/alexa-skills-kit-interface-reference#Request%20Format>`_
|
|
||||||
has four top-level elements: ``session``, ``context``, ``request`` and ``version``. Like Flask, Flask-Ask provides `context
|
|
||||||
locals <http://werkzeug.pocoo.org/docs/0.11/local/>`_ that spare you from having to add these as extra parameters to
|
|
||||||
your functions. However, the ``request`` and ``session`` objects are distinct from Flask's ``request`` and ``session``.
|
|
||||||
Flask-Ask's ``request``, ``context`` and ``session`` correspond to the Alexa request payload components while Flask's correspond
|
|
||||||
to lower-level HTTP constructs.
|
|
||||||
|
|
||||||
To use Flask-Ask's context locals, just import them::
|
|
||||||
|
|
||||||
from flask import App
|
|
||||||
from flask_ask import Ask, request, context, session, version
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
ask = Ask(app)
|
|
||||||
log = logging.getLogger()
|
|
||||||
|
|
||||||
@ask.intent('ExampleIntent')
|
|
||||||
def example():
|
|
||||||
log.info("Request ID: {}".format(request.requestId))
|
|
||||||
log.info("Request Type: {}".format(request.type))
|
|
||||||
log.info("Request Timestamp: {}".format(request.timestamp))
|
|
||||||
log.info("Session New?: {}".format(session.new))
|
|
||||||
log.info("User ID: {}".format(session.user.userId))
|
|
||||||
log.info("Alexa Version: {}".format(version))
|
|
||||||
log.info("Device ID: {}".format(context.System.device.deviceId))
|
|
||||||
log.info("Consent Token: {}".format(context.System.user.permissions.consentToken))
|
|
||||||
...
|
|
||||||
|
|
||||||
If you want to use both Flask and Flask-Ask context locals in the same module, use ``import as``::
|
|
||||||
|
|
||||||
from flask import App, request, session
|
|
||||||
from flask_ask import (
|
|
||||||
Ask,
|
|
||||||
request as ask_request,
|
|
||||||
session as ask_session,
|
|
||||||
version
|
|
||||||
)
|
|
||||||
|
|
||||||
For a complete reference on ``request``, ``context`` and ``session`` fields, see the
|
|
||||||
`JSON Interface Reference for Custom Skills <https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/alexa-skills-kit-interface-reference>`_
|
|
||||||
in the Alexa Skills Kit documentation.
|
|
||||||
|
|
@ -1,152 +0,0 @@
|
||||||
Building Responses
|
|
||||||
==================
|
|
||||||
|
|
||||||
📼 Here is a video demo on `Building Responses with Flask-Ask video <https://youtu.be/mObuAlfxnl8>`_ .
|
|
||||||
|
|
||||||
The two primary constructs in Flask-Ask for creating responses are ``statement`` and ``question``.
|
|
||||||
|
|
||||||
Statements terminate Echo sessions. The user is free to start another session, but Alexa will have no memory of it
|
|
||||||
(unless persistence is programmed separately on the server with a database or the like).
|
|
||||||
|
|
||||||
A ``question``, on the other hand, prompts the user for additional speech and keeps a session open.
|
|
||||||
This session is similar to an HTTP session but the implementation is different. Since your application is
|
|
||||||
communicating with the Alexa service instead of a browser, there are no cookies or local storage. Instead, the
|
|
||||||
session is maintained in both the request and response JSON structures. In addition to the session component of
|
|
||||||
questions, questions also allow a ``reprompt``, which is typically a rephrasing of the question if user did not answer
|
|
||||||
the first time.
|
|
||||||
|
|
||||||
This sections shows how to build responses with Flask-Ask. It contains the following subsections:
|
|
||||||
|
|
||||||
.. contents::
|
|
||||||
:local:
|
|
||||||
:backlinks: none
|
|
||||||
|
|
||||||
Telling with ``statement``
|
|
||||||
--------------------------
|
|
||||||
``statement`` closes the session::
|
|
||||||
|
|
||||||
@ask.intent('AllYourBaseIntent')
|
|
||||||
def all_your_base():
|
|
||||||
return statement('All your base are belong to us')
|
|
||||||
|
|
||||||
|
|
||||||
Asking with ``question``
|
|
||||||
------------------------
|
|
||||||
Asking with ``question`` prompts the user for a response while keeping the session open::
|
|
||||||
|
|
||||||
@ask.intent('AppointmentIntent')
|
|
||||||
def make_appointment():
|
|
||||||
return question("What day would you like to make an appointment for?")
|
|
||||||
|
|
||||||
If the user doesn't respond, encourage them by rephrasing the question with ``reprompt``::
|
|
||||||
|
|
||||||
@ask.intent('AppointmentIntent')
|
|
||||||
def make_appointment():
|
|
||||||
return question("What day would you like to make an appointment for?") \
|
|
||||||
.reprompt("I didn't get that. When would you like to be seen?")
|
|
||||||
|
|
||||||
|
|
||||||
Session Management
|
|
||||||
------------------
|
|
||||||
|
|
||||||
The ``session`` context local has an ``attributes`` dictionary for persisting information across requests::
|
|
||||||
|
|
||||||
session.attributes['city'] = "San Francisco"
|
|
||||||
|
|
||||||
When the response is rendered, the session attributes are automatically copied over into
|
|
||||||
the response's ``sessionAttributes`` structure.
|
|
||||||
|
|
||||||
The renderer looks for an ``attribute_encoder`` on the session. If the renderer finds one, it will pass it to
|
|
||||||
``json.dumps`` as either that function's ``cls`` or ``default`` keyword parameters depending on whether
|
|
||||||
a ``json.JSONEncoder`` or a function is used, respectively.
|
|
||||||
|
|
||||||
Here's an example that uses a function::
|
|
||||||
|
|
||||||
def _json_date_handler(obj):
|
|
||||||
if isinstance(obj, datetime.date):
|
|
||||||
return obj.isoformat()
|
|
||||||
|
|
||||||
session.attributes['date'] = date
|
|
||||||
session.attributes_encoder = _json_date_handler
|
|
||||||
|
|
||||||
See the `json.dump documentation <https://docs.python.org/2/library/json.html#json.dump>`_ for for details about
|
|
||||||
that method's ``cls`` and ``default`` parameters.
|
|
||||||
|
|
||||||
|
|
||||||
Automatic Handling of Plaintext and SSML
|
|
||||||
----------------------------------------
|
|
||||||
The Alexa Skills Kit supports plain text or
|
|
||||||
`SSML <https://en.wikipedia.org/wiki/Speech_Synthesis_Markup_Language>`_ outputs. Flask-Ask automatically
|
|
||||||
detects if your speech text contains SSML by attempting to parse it into XML, and checking
|
|
||||||
if the root element is ``speak``::
|
|
||||||
|
|
||||||
try:
|
|
||||||
xmldoc = ElementTree.fromstring(text)
|
|
||||||
if xmldoc.tag == 'speak':
|
|
||||||
# output type is 'SSML'
|
|
||||||
except ElementTree.ParseError:
|
|
||||||
pass
|
|
||||||
# output type is 'PlainText'
|
|
||||||
|
|
||||||
|
|
||||||
Displaying Cards in the Alexa Smartphone/Tablet App
|
|
||||||
---------------------------------------------------
|
|
||||||
In addition to speaking back, Flask-Ask can display contextual cards in the Alexa smartphone/tablet app. All four
|
|
||||||
of the Alexa Skills Kit card types are supported.
|
|
||||||
|
|
||||||
Simple cards display a title and message::
|
|
||||||
|
|
||||||
@ask.intent('AllYourBaseIntent')
|
|
||||||
def all_your_base():
|
|
||||||
return statement('All your base are belong to us') \
|
|
||||||
.simple_card(title='CATS says...', content='Make your time')
|
|
||||||
|
|
||||||
Standard cards are like simple cards but they also support small and large image URLs::
|
|
||||||
|
|
||||||
@ask.intent('AllYourBaseIntent')
|
|
||||||
def all_your_base():
|
|
||||||
return statement('All your base are belong to us') \
|
|
||||||
.standard_card(title='CATS says...',
|
|
||||||
text='Make your time',
|
|
||||||
small_image_url='https://example.com/small.png',
|
|
||||||
large_image_url='https://example.com/large.png')
|
|
||||||
|
|
||||||
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::
|
|
||||||
|
|
||||||
@ask.intent('AllYourBaseIntent')
|
|
||||||
def all_your_base():
|
|
||||||
return statement('Please link your account in the Alexa app') \
|
|
||||||
.link_account_card()
|
|
||||||
|
|
||||||
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::
|
|
||||||
|
|
||||||
@ask.intent('AllYourBaseIntent')
|
|
||||||
def all_your_base():
|
|
||||||
return statement('Please allow access to your location') \
|
|
||||||
.consent_card("read::alexa:device:all:address")
|
|
||||||
|
|
||||||
|
|
||||||
Jinja Templates
|
|
||||||
---------------
|
|
||||||
You can also use Jinja templates. Define them in a YAML file named `templates.yaml` inside your application root::
|
|
||||||
|
|
||||||
@ask.intent('RBelongToUsIntent')
|
|
||||||
def all_your_base():
|
|
||||||
notice = render_template('all_your_base_msg', who='us')
|
|
||||||
return statement(notice)
|
|
||||||
|
|
||||||
.. code-block:: yaml
|
|
||||||
|
|
||||||
all_your_base_msg: All your base are belong to {{ who }}
|
|
||||||
|
|
||||||
multiple_line_example: |
|
|
||||||
<speak>
|
|
||||||
I am a multi-line SSML template. My content spans more than one line,
|
|
||||||
so there's a pipe and a newline that separates my name and value.
|
|
||||||
Enjoy the sounds of the ocean.
|
|
||||||
<audio src='https://s3.amazonaws.com/ask-storage/tidePooler/OceanWaves.mp3'/>
|
|
||||||
</speak>
|
|
||||||
|
|
||||||
You can also use a custom templates file passed into the Ask object::
|
|
||||||
|
|
||||||
ask = Ask(app, '/', None, 'custom-templates.yml')
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
User Contributions
|
|
||||||
==================
|
|
||||||
|
|
||||||
Have an article or video to submit? Please send it to john@johnwheeler.org
|
|
||||||
|
|
||||||
`Flask-Ask: A New Python Framework for Rapid Alexa Skills Kit Development <https://developer.amazon.com/public/community/post/Tx14R0IYYGH3SKT/Flask-Ask-A-New-Python-Framework-for-Rapid-Alexa-Skills-Kit-Development>`_
|
|
||||||
|
|
||||||
by John Wheeler
|
|
||||||
|
|
||||||
`Running with Alexa Part I. <http://www.timkl.com/posts/running-with-alexa>`_
|
|
||||||
|
|
||||||
by Tim Kjær Lange
|
|
||||||
|
|
||||||
`Intro and Skill Logic - Alexa Skills w/ Python and Flask-Ask Part 1 <https://pythonprogramming.net/intro-alexa-skill-flask-ask-python-tutorial/>`_
|
|
||||||
|
|
||||||
by Harrison Kinsley
|
|
||||||
|
|
||||||
`Headlines Function - Alexa Skills w/ Python and Flask-Ask Part 2 <https://pythonprogramming.net/headlines-function-alexa-skill-flask-ask-python-tutorial/>`_
|
|
||||||
|
|
||||||
by Harrison Kinsley
|
|
||||||
|
|
||||||
`Testing our Skill - Alexa Skills w/ Python and Flask-Ask Part 3 <https://pythonprogramming.net/testing-deploying-alexa-skill-flask-ask-python-tutorial/>`_
|
|
||||||
|
|
||||||
by Harrison Kinsley
|
|
||||||
|
|
||||||
`Flask-Ask — A tutorial on a simple and easy way to build complex Alexa Skills <https://blog.craftworkz.co/flask-ask-a-tutorial-on-a-simple-and-easy-way-to-build-complex-alexa-skills-426a6b3ff8bc#.70eay9n07>`_
|
|
||||||
|
|
||||||
by Bjorn Vuylsteker
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
import logging
|
|
||||||
|
|
||||||
logger = logging.getLogger('flask_ask')
|
|
||||||
logger.addHandler(logging.StreamHandler())
|
|
||||||
if logger.level == logging.NOTSET:
|
|
||||||
logger.setLevel(logging.WARN)
|
|
||||||
|
|
||||||
|
|
||||||
from .core import (
|
|
||||||
Ask,
|
|
||||||
request,
|
|
||||||
session,
|
|
||||||
version,
|
|
||||||
context,
|
|
||||||
current_stream,
|
|
||||||
convert_errors
|
|
||||||
)
|
|
||||||
|
|
||||||
from .models import (
|
|
||||||
question,
|
|
||||||
statement,
|
|
||||||
audio,
|
|
||||||
delegate,
|
|
||||||
elicit_slot,
|
|
||||||
confirm_slot,
|
|
||||||
confirm_intent,
|
|
||||||
buy,
|
|
||||||
upsell,
|
|
||||||
refund
|
|
||||||
)
|
|
||||||
|
|
@ -1,80 +0,0 @@
|
||||||
"""
|
|
||||||
Stream cache functions
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
def push_stream(cache, user_id, stream):
|
|
||||||
"""
|
|
||||||
Push a stream onto the stream stack in cache.
|
|
||||||
|
|
||||||
:param cache: werkzeug BasicCache-like object
|
|
||||||
:param user_id: id of user, used as key in cache
|
|
||||||
:param stream: stream object to push onto stack
|
|
||||||
|
|
||||||
:return: True on successful update,
|
|
||||||
False if failed to update,
|
|
||||||
None if invalid input was given
|
|
||||||
"""
|
|
||||||
stack = cache.get(user_id)
|
|
||||||
if stack is None:
|
|
||||||
stack = []
|
|
||||||
if stream:
|
|
||||||
stack.append(stream)
|
|
||||||
return cache.set(user_id, stack)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def pop_stream(cache, user_id):
|
|
||||||
"""
|
|
||||||
Pop an item off the stack in the cache. If stack
|
|
||||||
is empty after pop, it deletes the stack.
|
|
||||||
|
|
||||||
:param cache: werkzeug BasicCache-like object
|
|
||||||
:param user_id: id of user, used as key in cache
|
|
||||||
|
|
||||||
:return: top item from stack, otherwise None
|
|
||||||
"""
|
|
||||||
stack = cache.get(user_id)
|
|
||||||
if stack is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
result = stack.pop()
|
|
||||||
|
|
||||||
if len(stack) == 0:
|
|
||||||
cache.delete(user_id)
|
|
||||||
else:
|
|
||||||
cache.set(user_id, stack)
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def set_stream(cache, user_id, stream):
|
|
||||||
"""
|
|
||||||
Overwrite stack in the cache.
|
|
||||||
|
|
||||||
:param cache: werkzeug BasicCache-liek object
|
|
||||||
:param user_id: id of user, used as key in cache
|
|
||||||
:param stream: value to initialize new stack with
|
|
||||||
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
if stream:
|
|
||||||
return cache.set(user_id, [stream])
|
|
||||||
|
|
||||||
|
|
||||||
def top_stream(cache, user_id):
|
|
||||||
"""
|
|
||||||
Peek at the top of the stack in the cache.
|
|
||||||
|
|
||||||
:param cache: werkzeug BasicCache-like object
|
|
||||||
:param user_id: id of user, used as key in cache
|
|
||||||
|
|
||||||
:return: top item in user's cached stack, otherwise None
|
|
||||||
"""
|
|
||||||
if not user_id:
|
|
||||||
return None
|
|
||||||
|
|
||||||
stack = cache.get(user_id)
|
|
||||||
if stack is None:
|
|
||||||
return None
|
|
||||||
return stack.pop()
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
import re
|
|
||||||
from datetime import datetime, time
|
|
||||||
|
|
||||||
import aniso8601
|
|
||||||
|
|
||||||
from . import logger
|
|
||||||
|
|
||||||
|
|
||||||
_DATE_PATTERNS = {
|
|
||||||
# "today", "tomorrow", "november twenty-fifth": 2015-11-25
|
|
||||||
'^\d{4}-\d{2}-\d{2}$': '%Y-%m-%d',
|
|
||||||
# "this week", "next week": 2015-W48
|
|
||||||
'^\d{4}-W\d{2}$': '%Y-W%U-%w',
|
|
||||||
# "this weekend": 2015-W48-WE
|
|
||||||
'^\d{4}-W\d{2}-WE$': '%Y-W%U-WE-%w',
|
|
||||||
# "this month": 2015-11
|
|
||||||
'^\d{4}-\d{2}$': '%Y-%m',
|
|
||||||
# "next year": 2016
|
|
||||||
'^\d{4}$': '%Y',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def to_date(amazon_date):
|
|
||||||
# make so 'next decade' matches work against 'next year' regex
|
|
||||||
amazon_date = re.sub('X$', '0', amazon_date)
|
|
||||||
for re_pattern, format_pattern in list(_DATE_PATTERNS.items()):
|
|
||||||
if re.match(re_pattern, amazon_date):
|
|
||||||
if '%U' in format_pattern:
|
|
||||||
# http://stackoverflow.com/a/17087427/1163855
|
|
||||||
amazon_date += '-0'
|
|
||||||
return datetime.strptime(amazon_date, format_pattern).date()
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def to_time(amazon_time):
|
|
||||||
if amazon_time == "AM":
|
|
||||||
return time(hour=0)
|
|
||||||
if amazon_time == "PM":
|
|
||||||
return time(hour=12)
|
|
||||||
if amazon_time == "MO":
|
|
||||||
return time(hour=5)
|
|
||||||
if amazon_time == "AF":
|
|
||||||
return time(hour=12)
|
|
||||||
if amazon_time == "EV":
|
|
||||||
return time(hour=17)
|
|
||||||
if amazon_time == "NI":
|
|
||||||
return time(hour=21)
|
|
||||||
try:
|
|
||||||
return aniso8601.parse_time(amazon_time)
|
|
||||||
except ValueError as e:
|
|
||||||
logger.warn("ValueError for amazon_time '{}'.".format(amazon_time))
|
|
||||||
logger.warn("ValueError message: {}".format(e.message))
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def to_timedelta(amazon_duration):
|
|
||||||
return aniso8601.parse_duration(amazon_duration)
|
|
||||||
|
|
@ -1,951 +0,0 @@
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import yaml
|
|
||||||
import inspect
|
|
||||||
import io
|
|
||||||
from datetime import datetime
|
|
||||||
from functools import wraps, partial
|
|
||||||
|
|
||||||
import aniso8601
|
|
||||||
from werkzeug.contrib.cache import SimpleCache
|
|
||||||
from werkzeug.local import LocalProxy, LocalStack
|
|
||||||
from jinja2 import BaseLoader, ChoiceLoader, TemplateNotFound
|
|
||||||
from flask import current_app, json, request as flask_request, _app_ctx_stack
|
|
||||||
|
|
||||||
from . import verifier, logger
|
|
||||||
from .convert import to_date, to_time, to_timedelta
|
|
||||||
from .cache import top_stream, set_stream
|
|
||||||
import collections
|
|
||||||
|
|
||||||
|
|
||||||
def find_ask():
|
|
||||||
"""
|
|
||||||
Find our instance of Ask, navigating Local's and possible blueprints.
|
|
||||||
|
|
||||||
Note: This only supports returning a reference to the first instance
|
|
||||||
of Ask found.
|
|
||||||
"""
|
|
||||||
if hasattr(current_app, 'ask'):
|
|
||||||
return getattr(current_app, 'ask')
|
|
||||||
else:
|
|
||||||
if hasattr(current_app, 'blueprints'):
|
|
||||||
blueprints = getattr(current_app, 'blueprints')
|
|
||||||
for blueprint_name in blueprints:
|
|
||||||
if hasattr(blueprints[blueprint_name], 'ask'):
|
|
||||||
return getattr(blueprints[blueprint_name], 'ask')
|
|
||||||
|
|
||||||
|
|
||||||
def dbgdump(obj, default=None, cls=None):
|
|
||||||
if current_app.config.get('ASK_PRETTY_DEBUG_LOGS', False):
|
|
||||||
indent = 2
|
|
||||||
else:
|
|
||||||
indent = None
|
|
||||||
msg = json.dumps(obj, indent=indent, default=default, cls=cls)
|
|
||||||
logger.debug(msg)
|
|
||||||
|
|
||||||
|
|
||||||
request = LocalProxy(lambda: find_ask().request)
|
|
||||||
session = LocalProxy(lambda: find_ask().session)
|
|
||||||
version = LocalProxy(lambda: find_ask().version)
|
|
||||||
context = LocalProxy(lambda: find_ask().context)
|
|
||||||
convert_errors = LocalProxy(lambda: find_ask().convert_errors)
|
|
||||||
current_stream = LocalProxy(lambda: find_ask().current_stream)
|
|
||||||
stream_cache = LocalProxy(lambda: find_ask().stream_cache)
|
|
||||||
|
|
||||||
from . import models
|
|
||||||
|
|
||||||
|
|
||||||
_converters = {'date': to_date, 'time': to_time, 'timedelta': to_timedelta}
|
|
||||||
|
|
||||||
|
|
||||||
class Ask(object):
|
|
||||||
"""The Ask object provides the central interface for interacting with the Alexa service.
|
|
||||||
|
|
||||||
Ask object maps Alexa Requests to flask view functions and handles Alexa sessions.
|
|
||||||
The constructor is passed a Flask App instance, and URL endpoint.
|
|
||||||
The Flask instance allows the convienient API of endpoints and their view functions,
|
|
||||||
so that Alexa requests may be mapped with syntax similar to a typical Flask server.
|
|
||||||
Route provides the entry point for the skill, and must be provided if an app is given.
|
|
||||||
|
|
||||||
Keyword Arguments:
|
|
||||||
app {Flask object} -- App instance - created with Flask(__name__) (default: {None})
|
|
||||||
route {str} -- entry point to which initial Alexa Requests are forwarded (default: {None})
|
|
||||||
blueprint {Flask blueprint} -- Flask Blueprint instance to use instead of Flask App (default: {None})
|
|
||||||
stream_cache {Werkzeug BasicCache} -- BasicCache-like object for storing Audio stream data (default: {SimpleCache})
|
|
||||||
path {str} -- path to templates yaml file for VUI dialog (default: {'templates.yaml'})
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, app=None, route=None, blueprint=None, stream_cache=None, path='templates.yaml'):
|
|
||||||
self.app = app
|
|
||||||
self._route = route
|
|
||||||
self._intent_view_funcs = {}
|
|
||||||
self._intent_converts = {}
|
|
||||||
self._intent_defaults = {}
|
|
||||||
self._intent_mappings = {}
|
|
||||||
self._launch_view_func = None
|
|
||||||
self._session_ended_view_func = None
|
|
||||||
self._on_session_started_callback = None
|
|
||||||
self._default_intent_view_func = None
|
|
||||||
self._player_request_view_funcs = {}
|
|
||||||
self._player_mappings = {}
|
|
||||||
self._player_converts = {}
|
|
||||||
if app is not None:
|
|
||||||
self.init_app(app, path)
|
|
||||||
elif blueprint is not None:
|
|
||||||
self.init_blueprint(blueprint, path)
|
|
||||||
if stream_cache is None:
|
|
||||||
self.stream_cache = SimpleCache()
|
|
||||||
else:
|
|
||||||
self.stream_cache = stream_cache
|
|
||||||
|
|
||||||
def init_app(self, app, path='templates.yaml'):
|
|
||||||
"""Initializes Ask app by setting configuration variables, loading templates, and maps Ask route to a flask view.
|
|
||||||
|
|
||||||
The Ask instance is given the following configuration variables by calling on Flask's configuration:
|
|
||||||
|
|
||||||
`ASK_APPLICATION_ID`:
|
|
||||||
|
|
||||||
Turn on application ID verification by setting this variable to an application ID or a
|
|
||||||
list of allowed application IDs. By default, application ID verification is disabled and a
|
|
||||||
warning is logged. This variable should be set in production to ensure
|
|
||||||
requests are being sent by the applications you specify.
|
|
||||||
Default: None
|
|
||||||
|
|
||||||
`ASK_VERIFY_REQUESTS`:
|
|
||||||
|
|
||||||
Enables or disables Alexa request verification, which ensures requests sent to your skill
|
|
||||||
are from Amazon's Alexa service. This setting should not be disabled in production.
|
|
||||||
It is useful for mocking JSON requests in automated tests.
|
|
||||||
Default: True
|
|
||||||
|
|
||||||
`ASK_VERIFY_TIMESTAMP_DEBUG`:
|
|
||||||
|
|
||||||
Turn on request timestamp verification while debugging by setting this to True.
|
|
||||||
Timestamp verification helps mitigate against replay attacks. It relies on the system clock
|
|
||||||
being synchronized with an NTP server. This setting should not be enabled in production.
|
|
||||||
Default: False
|
|
||||||
|
|
||||||
`ASK_PRETTY_DEBUG_LOGS`:
|
|
||||||
|
|
||||||
Add tabs and linebreaks to the Alexa request and response printed to the debug log.
|
|
||||||
This improves readability when printing to the console, but breaks formatting when logging to CloudWatch.
|
|
||||||
Default: False
|
|
||||||
"""
|
|
||||||
if self._route is None:
|
|
||||||
raise TypeError("route is a required argument when app is not None")
|
|
||||||
|
|
||||||
self.app = app
|
|
||||||
|
|
||||||
app.ask = self
|
|
||||||
|
|
||||||
app.add_url_rule(self._route, view_func=self._flask_view_func, methods=['POST'])
|
|
||||||
app.jinja_loader = ChoiceLoader([app.jinja_loader, YamlLoader(app, path)])
|
|
||||||
|
|
||||||
def init_blueprint(self, blueprint, path='templates.yaml'):
|
|
||||||
"""Initialize a Flask Blueprint, similar to init_app, but without the access
|
|
||||||
to the application config.
|
|
||||||
|
|
||||||
Keyword Arguments:
|
|
||||||
blueprint {Flask Blueprint} -- Flask Blueprint instance to initialize (Default: {None})
|
|
||||||
path {str} -- path to templates yaml file, relative to Blueprint (Default: {'templates.yaml'})
|
|
||||||
"""
|
|
||||||
if self._route is not None:
|
|
||||||
raise TypeError("route cannot be set when using blueprints!")
|
|
||||||
|
|
||||||
# we need to tuck our reference to this Ask instance into the blueprint object and find it later!
|
|
||||||
blueprint.ask = self
|
|
||||||
|
|
||||||
# BlueprintSetupState.add_url_rule gets called underneath the covers and
|
|
||||||
# concats the rule string, so we should set to an empty string to allow
|
|
||||||
# Blueprint('blueprint_api', __name__, url_prefix="/ask") to result in
|
|
||||||
# exposing the rule at "/ask" and not "/ask/".
|
|
||||||
blueprint.add_url_rule("", view_func=self._flask_view_func, methods=['POST'])
|
|
||||||
blueprint.jinja_loader = ChoiceLoader([YamlLoader(blueprint, path)])
|
|
||||||
|
|
||||||
@property
|
|
||||||
def ask_verify_requests(self):
|
|
||||||
return current_app.config.get('ASK_VERIFY_REQUESTS', True)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def ask_verify_timestamp_debug(self):
|
|
||||||
return current_app.config.get('ASK_VERIFY_TIMESTAMP_DEBUG', False)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def ask_application_id(self):
|
|
||||||
return current_app.config.get('ASK_APPLICATION_ID', None)
|
|
||||||
|
|
||||||
def on_session_started(self, f):
|
|
||||||
"""Decorator to call wrapped function upon starting a session.
|
|
||||||
|
|
||||||
@ask.on_session_started
|
|
||||||
def new_session():
|
|
||||||
log.info('new session started')
|
|
||||||
|
|
||||||
Because both launch and intent requests may begin a session, this decorator is used call
|
|
||||||
a function regardless of how the session began.
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
f {function} -- function to be called when session is started.
|
|
||||||
"""
|
|
||||||
self._on_session_started_callback = f
|
|
||||||
|
|
||||||
def launch(self, f):
|
|
||||||
"""Decorator maps a view function as the endpoint for an Alexa LaunchRequest and starts the skill.
|
|
||||||
|
|
||||||
@ask.launch
|
|
||||||
def launched():
|
|
||||||
return question('Welcome to Foo')
|
|
||||||
|
|
||||||
The wrapped function is registered as the launch view function and renders the response
|
|
||||||
for requests to the Launch URL.
|
|
||||||
A request to the launch URL is verified with the Alexa server before the payload is
|
|
||||||
passed to the view function.
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
f {function} -- Launch view function
|
|
||||||
"""
|
|
||||||
self._launch_view_func = f
|
|
||||||
|
|
||||||
@wraps(f)
|
|
||||||
def wrapper(*args, **kw):
|
|
||||||
self._flask_view_func(*args, **kw)
|
|
||||||
return f
|
|
||||||
|
|
||||||
def session_ended(self, f):
|
|
||||||
"""Decorator routes Alexa SessionEndedRequest to the wrapped view function to end the skill.
|
|
||||||
|
|
||||||
@ask.session_ended
|
|
||||||
def session_ended():
|
|
||||||
return "{}", 200
|
|
||||||
|
|
||||||
The wrapped function is registered as the session_ended view function
|
|
||||||
and renders the response for requests to the end of the session.
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
f {function} -- session_ended view function
|
|
||||||
"""
|
|
||||||
self._session_ended_view_func = f
|
|
||||||
|
|
||||||
@wraps(f)
|
|
||||||
def wrapper(*args, **kw):
|
|
||||||
self._flask_view_func(*args, **kw)
|
|
||||||
return f
|
|
||||||
|
|
||||||
def intent(self, intent_name, mapping={}, convert={}, default={}):
|
|
||||||
"""Decorator routes an Alexa IntentRequest and provides the slot parameters to the wrapped function.
|
|
||||||
|
|
||||||
Functions decorated as an intent are registered as the view function for the Intent's URL,
|
|
||||||
and provide the backend responses to give your Skill its functionality.
|
|
||||||
|
|
||||||
@ask.intent('WeatherIntent', mapping={'city': 'City'})
|
|
||||||
def weather(city):
|
|
||||||
return statement('I predict great weather for {}'.format(city))
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
intent_name {str} -- Name of the intent request to be mapped to the decorated function
|
|
||||||
|
|
||||||
Keyword Arguments:
|
|
||||||
mapping {dict} -- Maps parameters to intent slots of a different name
|
|
||||||
default: {}
|
|
||||||
|
|
||||||
convert {dict} -- Converts slot values to data types before assignment to parameters
|
|
||||||
default: {}
|
|
||||||
|
|
||||||
default {dict} -- Provides default values for Intent slots if Alexa reuqest
|
|
||||||
returns no corresponding slot, or a slot with an empty value
|
|
||||||
default: {}
|
|
||||||
"""
|
|
||||||
def decorator(f):
|
|
||||||
self._intent_view_funcs[intent_name] = f
|
|
||||||
self._intent_mappings[intent_name] = mapping
|
|
||||||
self._intent_converts[intent_name] = convert
|
|
||||||
self._intent_defaults[intent_name] = default
|
|
||||||
|
|
||||||
@wraps(f)
|
|
||||||
def wrapper(*args, **kw):
|
|
||||||
self._flask_view_func(*args, **kw)
|
|
||||||
return f
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
def default_intent(self, f):
|
|
||||||
"""Decorator routes any Alexa IntentRequest that is not matched by any existing @ask.intent routing."""
|
|
||||||
self._default_intent_view_func = f
|
|
||||||
|
|
||||||
@wraps(f)
|
|
||||||
def wrapper(*args, **kw):
|
|
||||||
self._flask_view_func(*args, **kw)
|
|
||||||
return f
|
|
||||||
|
|
||||||
def display_element_selected(self, f):
|
|
||||||
"""Decorator routes Alexa Display.ElementSelected request to the wrapped view function.
|
|
||||||
|
|
||||||
@ask.display_element_selected
|
|
||||||
def eval_element():
|
|
||||||
return "", 200
|
|
||||||
|
|
||||||
The wrapped function is registered as the display_element_selected view function
|
|
||||||
and renders the response for requests.
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
f {function} -- display_element_selected view function
|
|
||||||
"""
|
|
||||||
self._display_element_selected_func = f
|
|
||||||
|
|
||||||
@wraps(f)
|
|
||||||
def wrapper(*args, **kw):
|
|
||||||
self._flask_view_func(*args, **kw)
|
|
||||||
return f
|
|
||||||
|
|
||||||
|
|
||||||
def on_purchase_completed(self, mapping={'payload': 'payload','name':'name','status':'status','token':'token'}, convert={}, default={}):
|
|
||||||
"""Decorator routes an Connections.Response to the wrapped function.
|
|
||||||
|
|
||||||
Request is sent when Alexa completes the purchase flow.
|
|
||||||
See https://developer.amazon.com/docs/in-skill-purchase/add-isps-to-a-skill.html#handle-results
|
|
||||||
|
|
||||||
|
|
||||||
The wrapped view function may accept parameters from the Request.
|
|
||||||
In addition to locale, requestId, timestamp, and type
|
|
||||||
|
|
||||||
|
|
||||||
@ask.on_purchase_completed( mapping={'payload': 'payload','name':'name','status':'status','token':'token'})
|
|
||||||
def completed(payload, name, status, token):
|
|
||||||
logger.info(payload)
|
|
||||||
logger.info(name)
|
|
||||||
logger.info(status)
|
|
||||||
logger.info(token)
|
|
||||||
|
|
||||||
"""
|
|
||||||
def decorator(f):
|
|
||||||
self._intent_view_funcs['Connections.Response'] = f
|
|
||||||
self._intent_mappings['Connections.Response'] = mapping
|
|
||||||
self._intent_converts['Connections.Response'] = convert
|
|
||||||
self._intent_defaults['Connections.Response'] = default
|
|
||||||
@wraps(f)
|
|
||||||
def wrapper(*args, **kwargs):
|
|
||||||
self._flask_view_func(*args, **kwargs)
|
|
||||||
return f
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
|
|
||||||
def on_playback_started(self, mapping={'offset': 'offsetInMilliseconds'}, convert={}, default={}):
|
|
||||||
"""Decorator routes an AudioPlayer.PlaybackStarted Request to the wrapped function.
|
|
||||||
|
|
||||||
Request sent when Alexa begins playing the audio stream previously sent in a Play directive.
|
|
||||||
This lets your skill verify that playback began successfully.
|
|
||||||
This request is also sent when Alexa resumes playback after pausing it for a voice request.
|
|
||||||
|
|
||||||
The wrapped view function may accept parameters from the AudioPlayer Request.
|
|
||||||
In addition to locale, requestId, timestamp, and type
|
|
||||||
AudioPlayer Requests include:
|
|
||||||
offsetInMilliseconds - Position in stream when request was sent.
|
|
||||||
Not end of stream, often few ms after Play Directive offset.
|
|
||||||
This parameter is automatically mapped to 'offset' by default
|
|
||||||
|
|
||||||
token - token of the stream that is nearly finished.
|
|
||||||
|
|
||||||
@ask.on_playback_started()
|
|
||||||
def on_playback_start(token, offset):
|
|
||||||
logger.info('stream has token {}'.format(token))
|
|
||||||
logger.info('Current position within the stream is {} ms'.format(offset))
|
|
||||||
"""
|
|
||||||
def decorator(f):
|
|
||||||
self._intent_view_funcs['AudioPlayer.PlaybackStarted'] = f
|
|
||||||
self._intent_mappings['AudioPlayer.PlaybackStarted'] = mapping
|
|
||||||
self._intent_converts['AudioPlayer.PlaybackStarted'] = convert
|
|
||||||
self._intent_defaults['AudioPlayer.PlaybackStarted'] = default
|
|
||||||
|
|
||||||
@wraps(f)
|
|
||||||
def wrapper(*args, **kwargs):
|
|
||||||
self._flask_view_func(*args, **kwargs)
|
|
||||||
return f
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
def on_playback_finished(self, mapping={'offset': 'offsetInMilliseconds'}, convert={}, default={}):
|
|
||||||
"""Decorator routes an AudioPlayer.PlaybackFinished Request to the wrapped function.
|
|
||||||
|
|
||||||
This type of request is sent when the stream Alexa is playing comes to an end on its own.
|
|
||||||
|
|
||||||
Note: If your skill explicitly stops the playback with the Stop directive,
|
|
||||||
Alexa sends PlaybackStopped instead of PlaybackFinished.
|
|
||||||
|
|
||||||
The wrapped view function may accept parameters from the AudioPlayer Request.
|
|
||||||
In addition to locale, requestId, timestamp, and type
|
|
||||||
AudioPlayer Requests include:
|
|
||||||
offsetInMilliseconds - Position in stream when request was sent.
|
|
||||||
Not end of stream, often few ms after Play Directive offset.
|
|
||||||
This parameter is automatically mapped to 'offset' by default.
|
|
||||||
|
|
||||||
token - token of the stream that is nearly finished.
|
|
||||||
|
|
||||||
Audioplayer Requests do not include the stream URL, it must be accessed from current_stream.url
|
|
||||||
"""
|
|
||||||
def decorator(f):
|
|
||||||
self._intent_view_funcs['AudioPlayer.PlaybackFinished'] = f
|
|
||||||
self._intent_mappings['AudioPlayer.PlaybackFinished'] = mapping
|
|
||||||
self._intent_converts['AudioPlayer.PlaybackFinished'] = convert
|
|
||||||
self._intent_defaults['AudioPlayer.PlaybackFinished'] = default
|
|
||||||
|
|
||||||
@wraps(f)
|
|
||||||
def wrapper(*args, **kwargs):
|
|
||||||
self._flask_view_func(*args, **kwargs)
|
|
||||||
return f
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
def on_playback_stopped(self, mapping={'offset': 'offsetInMilliseconds'}, convert={}, default={}):
|
|
||||||
"""Decorator routes an AudioPlayer.PlaybackStopped Request to the wrapped function.
|
|
||||||
|
|
||||||
Sent when Alexa stops playing an audio stream in response to one of the following:
|
|
||||||
-AudioPlayer.Stop
|
|
||||||
-AudioPlayer.Play with a playBehavior of REPLACE_ALL.
|
|
||||||
-AudioPlayer.ClearQueue with a clearBehavior of CLEAR_ALL.
|
|
||||||
|
|
||||||
This request is also sent if the user makes a voice request to Alexa,
|
|
||||||
since this temporarily pauses the playback.
|
|
||||||
In this case, the playback begins automatically once the voice interaction is complete.
|
|
||||||
|
|
||||||
Note: If playback stops because the audio stream comes to an end on its own,
|
|
||||||
Alexa sends PlaybackFinished instead of PlaybackStopped.
|
|
||||||
|
|
||||||
The wrapped view function may accept parameters from the AudioPlayer Request.
|
|
||||||
In addition to locale, requestId, timestamp, and type
|
|
||||||
AudioPlayer Requests include:
|
|
||||||
offsetInMilliseconds - Position in stream when request was sent.
|
|
||||||
Not end of stream, often few ms after Play Directive offset.
|
|
||||||
This parameter is automatically mapped to 'offset' by default.
|
|
||||||
|
|
||||||
token - token of the stream that is nearly finished.
|
|
||||||
|
|
||||||
Audioplayer Requests do not include the stream URL, it must be accessed from current_stream.url
|
|
||||||
"""
|
|
||||||
def decorator(f):
|
|
||||||
self._intent_view_funcs['AudioPlayer.PlaybackStopped'] = f
|
|
||||||
self._intent_mappings['AudioPlayer.PlaybackStopped'] = mapping
|
|
||||||
self._intent_converts['AudioPlayer.PlaybackStopped'] = convert
|
|
||||||
self._intent_defaults['AudioPlayer.PlaybackStopped'] = default
|
|
||||||
|
|
||||||
@wraps(f)
|
|
||||||
def wrapper(*args, **kwargs):
|
|
||||||
self._flask_view_func(*args, **kwargs)
|
|
||||||
return f
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
def on_playback_nearly_finished(self, mapping={'offset': 'offsetInMilliseconds'}, convert={}, default={}):
|
|
||||||
"""Decorator routes an AudioPlayer.PlaybackNearlyFinished Request to the wrapped function.
|
|
||||||
|
|
||||||
This AudioPlayer Request sent when the device is ready to receive a new stream.
|
|
||||||
To progress through a playlist, respond to this request with an enqueue or play_next audio response.
|
|
||||||
|
|
||||||
**Note** that this request is sent when Alexa is ready to receive a new stream to enqueue, and not
|
|
||||||
necessarily when the stream's offset is near the end.
|
|
||||||
The request may be sent by Alexa immediately after your skill sends a Play Directive.
|
|
||||||
|
|
||||||
The wrapped view function may accept parameters from the AudioPlayer Request.
|
|
||||||
In addition to locale, requestId, timestamp, and type
|
|
||||||
This AudioPlayer Request includes:
|
|
||||||
AudioPlayer Requests include:
|
|
||||||
offsetInMilliseconds - Position in stream when request was sent.
|
|
||||||
Not end of stream, often few ms after Play Directive offset.
|
|
||||||
This parameter is automatically mapped to 'offset' by default.
|
|
||||||
|
|
||||||
token - token of the stream that is nearly finished.
|
|
||||||
|
|
||||||
Audioplayer Requests do not include the stream URL, and must be accessed from current_stream
|
|
||||||
|
|
||||||
Example usage:
|
|
||||||
|
|
||||||
@ask.on_playback_nearly_finished()
|
|
||||||
def play_next_stream():
|
|
||||||
audio().enqueue(my_next_song)
|
|
||||||
|
|
||||||
# offsetInMilliseconds is mapped to offset by default for convenience
|
|
||||||
@ask.on_playback_nearly_finished()
|
|
||||||
def show_request_feedback(offset, token):
|
|
||||||
logging.info('Nearly Finished')
|
|
||||||
logging.info('Stream at {} ms when Playback Request sent'.format(offset))
|
|
||||||
logging.info('Stream holds the token {}'.format(token))
|
|
||||||
logging.info('Streaming from {}'.format(current_stream.url))
|
|
||||||
|
|
||||||
# example of changing the default parameter mapping
|
|
||||||
@ask.on_playback_nearly_finished(mapping={'pos': 'offsetInMilliseconds', 'stream_token': 'token'})
|
|
||||||
def show_request_feedback(pos, stream_token):
|
|
||||||
_infodump('Nearly Finished')
|
|
||||||
_infodump('Stream at {} ms when Playback Request sent'.format(pos))
|
|
||||||
_infodump('Stream holds the token {}'.format(stream_token))
|
|
||||||
"""
|
|
||||||
def decorator(f):
|
|
||||||
self._intent_view_funcs['AudioPlayer.PlaybackNearlyFinished'] = f
|
|
||||||
self._intent_mappings['AudioPlayer.PlaybackNearlyFinished'] = mapping
|
|
||||||
self._intent_converts['AudioPlayer.PlaybackNearlyFinished'] = convert
|
|
||||||
self._intent_defaults['AudioPlayer.PlaybackNearlyFinished'] = default
|
|
||||||
|
|
||||||
@wraps(f)
|
|
||||||
def wrapper(*args, **kwargs):
|
|
||||||
self._flask_view_func(*args, **kwargs)
|
|
||||||
return f
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
def on_playback_failed(self, mapping={}, convert={}, default={}):
|
|
||||||
"""Decorator routes an AudioPlayer.PlaybackFailed Request to the wrapped function.
|
|
||||||
|
|
||||||
This AudioPlayer Request sent when Alexa encounters an error when attempting to play a stream.
|
|
||||||
|
|
||||||
The wrapped view function may accept parameters from the AudioPlayer Request.
|
|
||||||
In addition to locale, requestId, timestamp, and type
|
|
||||||
|
|
||||||
PlayBackFailed Requests include:
|
|
||||||
error - Contains error info under parameters type and message
|
|
||||||
|
|
||||||
token - represents the stream that failed to play.
|
|
||||||
|
|
||||||
currentPlaybackState - Details about the playback activity occurring at the time of the error
|
|
||||||
Contains the following parameters:
|
|
||||||
|
|
||||||
token - represents the audio stream currently playing when the error occurred.
|
|
||||||
Note that this may be different from the value of the request.token property.
|
|
||||||
|
|
||||||
offsetInMilliseconds - Position in stream when request was sent.
|
|
||||||
Not end of stream, often few ms after Play Directive offset.
|
|
||||||
This parameter is automatically mapped to 'offset' by default.
|
|
||||||
|
|
||||||
playerActivity - player state when the error occurred
|
|
||||||
"""
|
|
||||||
def decorator(f):
|
|
||||||
self._intent_view_funcs['AudioPlayer.PlaybackFailed'] = f
|
|
||||||
self._intent_mappings['AudioPlayer.PlaybackFailed'] = mapping
|
|
||||||
self._intent_converts['AudioPlayer.PlaybackFailed'] = convert
|
|
||||||
self._intent_defaults['AudioPlayer.PlaybackFailed'] = default
|
|
||||||
|
|
||||||
@wraps(f)
|
|
||||||
def wrapper(*args, **kwargs):
|
|
||||||
self._flask_view_func(*args, **kwargs)
|
|
||||||
return f
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
@property
|
|
||||||
def request(self):
|
|
||||||
return getattr(_app_ctx_stack.top, '_ask_request', None)
|
|
||||||
|
|
||||||
@request.setter
|
|
||||||
def request(self, value):
|
|
||||||
_app_ctx_stack.top._ask_request = value
|
|
||||||
|
|
||||||
@property
|
|
||||||
def session(self):
|
|
||||||
return getattr(_app_ctx_stack.top, '_ask_session', models._Field())
|
|
||||||
|
|
||||||
@session.setter
|
|
||||||
def session(self, value):
|
|
||||||
_app_ctx_stack.top._ask_session = value
|
|
||||||
|
|
||||||
@property
|
|
||||||
def version(self):
|
|
||||||
return getattr(_app_ctx_stack.top, '_ask_version', None)
|
|
||||||
|
|
||||||
@version.setter
|
|
||||||
def version(self, value):
|
|
||||||
_app_ctx_stack.top._ask_version = value
|
|
||||||
|
|
||||||
@property
|
|
||||||
def context(self):
|
|
||||||
return getattr(_app_ctx_stack.top, '_ask_context', None)
|
|
||||||
|
|
||||||
@context.setter
|
|
||||||
def context(self, value):
|
|
||||||
_app_ctx_stack.top._ask_context = value
|
|
||||||
|
|
||||||
@property
|
|
||||||
def convert_errors(self):
|
|
||||||
return getattr(_app_ctx_stack.top, '_ask_convert_errors', None)
|
|
||||||
|
|
||||||
@convert_errors.setter
|
|
||||||
def convert_errors(self, value):
|
|
||||||
_app_ctx_stack.top._ask_convert_errors = value
|
|
||||||
|
|
||||||
@property
|
|
||||||
def current_stream(self):
|
|
||||||
#return getattr(_app_ctx_stack.top, '_ask_current_stream', models._Field())
|
|
||||||
user = self._get_user()
|
|
||||||
if user:
|
|
||||||
stream = top_stream(self.stream_cache, user)
|
|
||||||
if stream:
|
|
||||||
current = models._Field()
|
|
||||||
current.__dict__.update(stream)
|
|
||||||
return current
|
|
||||||
return models._Field()
|
|
||||||
|
|
||||||
@current_stream.setter
|
|
||||||
def current_stream(self, value):
|
|
||||||
# assumption 1 is we get a models._Field as value
|
|
||||||
# assumption 2 is if someone sets a value, it's resetting the stack
|
|
||||||
user = self._get_user()
|
|
||||||
if user:
|
|
||||||
set_stream(self.stream_cache, user, value.__dict__)
|
|
||||||
|
|
||||||
def run_aws_lambda(self, event):
|
|
||||||
"""Invoke the Flask Ask application from an AWS Lambda function handler.
|
|
||||||
|
|
||||||
Use this method to service AWS Lambda requests from a custom Alexa
|
|
||||||
skill. This method will invoke your Flask application providing a
|
|
||||||
WSGI-compatible environment that wraps the original Alexa event
|
|
||||||
provided to the AWS Lambda handler. Returns the output generated by
|
|
||||||
a Flask Ask application, which should be used as the return value
|
|
||||||
to the AWS Lambda handler function.
|
|
||||||
|
|
||||||
Example usage:
|
|
||||||
|
|
||||||
from flask import Flask
|
|
||||||
from flask_ask import Ask, statement
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
ask = Ask(app, '/')
|
|
||||||
|
|
||||||
# This function name is what you defined when you create an
|
|
||||||
# AWS Lambda function. By default, AWS calls this function
|
|
||||||
# lambda_handler.
|
|
||||||
def lambda_handler(event, _context):
|
|
||||||
return ask.run_aws_lambda(event)
|
|
||||||
|
|
||||||
@ask.intent('HelloIntent')
|
|
||||||
def hello(firstname):
|
|
||||||
speech_text = "Hello %s" % firstname
|
|
||||||
return statement(speech_text).simple_card('Hello', speech_text)
|
|
||||||
"""
|
|
||||||
|
|
||||||
# We are guaranteed to be called by AWS as a Lambda function does not
|
|
||||||
# expose a public facing interface.
|
|
||||||
self.app.config['ASK_VERIFY_REQUESTS'] = False
|
|
||||||
|
|
||||||
# Convert an environment variable to a WSGI "bytes-as-unicode" string
|
|
||||||
enc, esc = sys.getfilesystemencoding(), 'surrogateescape'
|
|
||||||
def unicode_to_wsgi(u):
|
|
||||||
return u.encode(enc, esc).decode('iso-8859-1')
|
|
||||||
|
|
||||||
# Create a WSGI-compatible environ that can be passed to the
|
|
||||||
# application. It is loaded with the OS environment variables,
|
|
||||||
# mandatory CGI-like variables, as well as the mandatory WSGI
|
|
||||||
# variables.
|
|
||||||
environ = {k: unicode_to_wsgi(v) for k, v in os.environ.items()}
|
|
||||||
environ['REQUEST_METHOD'] = 'POST'
|
|
||||||
environ['PATH_INFO'] = '/'
|
|
||||||
environ['SERVER_NAME'] = 'AWS-Lambda'
|
|
||||||
environ['SERVER_PORT'] = '80'
|
|
||||||
environ['SERVER_PROTOCOL'] = 'HTTP/1.0'
|
|
||||||
environ['wsgi.version'] = (1, 0)
|
|
||||||
environ['wsgi.url_scheme'] = 'http'
|
|
||||||
environ['wsgi.errors'] = sys.stderr
|
|
||||||
environ['wsgi.multithread'] = False
|
|
||||||
environ['wsgi.multiprocess'] = False
|
|
||||||
environ['wsgi.run_once'] = True
|
|
||||||
|
|
||||||
# Convert the event provided by the AWS Lambda handler to a JSON
|
|
||||||
# string that can be read as the body of a HTTP POST request.
|
|
||||||
body = json.dumps(event)
|
|
||||||
environ['CONTENT_TYPE'] = 'application/json'
|
|
||||||
environ['CONTENT_LENGTH'] = len(body)
|
|
||||||
|
|
||||||
PY3 = sys.version_info[0] == 3
|
|
||||||
|
|
||||||
if PY3:
|
|
||||||
environ['wsgi.input'] = io.StringIO(body)
|
|
||||||
else:
|
|
||||||
environ['wsgi.input'] = io.BytesIO(body)
|
|
||||||
|
|
||||||
# Start response is a required callback that must be passed when
|
|
||||||
# the application is invoked. It is used to set HTTP status and
|
|
||||||
# headers. Read the WSGI spec for details (PEP3333).
|
|
||||||
headers = []
|
|
||||||
def start_response(status, response_headers, _exc_info=None):
|
|
||||||
headers[:] = [status, response_headers]
|
|
||||||
|
|
||||||
# Invoke the actual Flask application providing our environment,
|
|
||||||
# with our Alexa event as the body of the HTTP request, as well
|
|
||||||
# as the callback function above. The result will be an iterator
|
|
||||||
# that provides a serialized JSON string for our Alexa response.
|
|
||||||
result = self.app(environ, start_response)
|
|
||||||
try:
|
|
||||||
if not headers:
|
|
||||||
raise AssertionError("start_response() not called by WSGI app")
|
|
||||||
|
|
||||||
output = b"".join(result)
|
|
||||||
if not headers[0].startswith("2"):
|
|
||||||
raise AssertionError("Non-2xx from app: hdrs={}, body={}".format(headers, output))
|
|
||||||
|
|
||||||
# The Lambda handler expects a Python object that can be
|
|
||||||
# serialized as JSON, so we need to take the already serialized
|
|
||||||
# JSON and deserialize it.
|
|
||||||
return json.loads(output)
|
|
||||||
|
|
||||||
finally:
|
|
||||||
# Per the WSGI spec, we need to invoke the close method if it
|
|
||||||
# is implemented on the result object.
|
|
||||||
if hasattr(result, 'close'):
|
|
||||||
result.close()
|
|
||||||
|
|
||||||
|
|
||||||
def _get_user(self):
|
|
||||||
if self.context:
|
|
||||||
return self.context.get('System', {}).get('user', {}).get('userId')
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _alexa_request(self, verify=True):
|
|
||||||
raw_body = flask_request.data
|
|
||||||
alexa_request_payload = json.loads(raw_body)
|
|
||||||
|
|
||||||
if verify:
|
|
||||||
cert_url = flask_request.headers['Signaturecertchainurl']
|
|
||||||
signature = flask_request.headers['Signature']
|
|
||||||
|
|
||||||
# load certificate - this verifies a the certificate url and format under the hood
|
|
||||||
cert = verifier.load_certificate(cert_url)
|
|
||||||
# verify signature
|
|
||||||
verifier.verify_signature(cert, signature, raw_body)
|
|
||||||
|
|
||||||
# verify timestamp
|
|
||||||
raw_timestamp = alexa_request_payload.get('request', {}).get('timestamp')
|
|
||||||
timestamp = self._parse_timestamp(raw_timestamp)
|
|
||||||
|
|
||||||
if not current_app.debug or self.ask_verify_timestamp_debug:
|
|
||||||
verifier.verify_timestamp(timestamp)
|
|
||||||
|
|
||||||
# verify application id
|
|
||||||
try:
|
|
||||||
application_id = alexa_request_payload['session']['application']['applicationId']
|
|
||||||
except KeyError:
|
|
||||||
application_id = alexa_request_payload['context'][
|
|
||||||
'System']['application']['applicationId']
|
|
||||||
if self.ask_application_id is not None:
|
|
||||||
verifier.verify_application_id(application_id, self.ask_application_id)
|
|
||||||
|
|
||||||
return alexa_request_payload
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _parse_timestamp(timestamp):
|
|
||||||
"""
|
|
||||||
Parse a given timestamp value, raising ValueError if None or Flasey
|
|
||||||
"""
|
|
||||||
if timestamp:
|
|
||||||
try:
|
|
||||||
return aniso8601.parse_datetime(timestamp)
|
|
||||||
except AttributeError:
|
|
||||||
# raised by aniso8601 if raw_timestamp is not valid string
|
|
||||||
# in ISO8601 format
|
|
||||||
try:
|
|
||||||
return datetime.utcfromtimestamp(timestamp)
|
|
||||||
except:
|
|
||||||
# relax the timestamp a bit in case it was sent in millis
|
|
||||||
return datetime.utcfromtimestamp(timestamp/1000)
|
|
||||||
|
|
||||||
raise ValueError('Invalid timestamp value! Cannot parse from either ISO8601 string or UTC timestamp.')
|
|
||||||
|
|
||||||
|
|
||||||
def _update_stream(self):
|
|
||||||
fresh_stream = models._Field()
|
|
||||||
fresh_stream.__dict__.update(self.current_stream.__dict__) # keeps url attribute after stopping stream
|
|
||||||
fresh_stream.__dict__.update(self._from_directive())
|
|
||||||
|
|
||||||
context_info = self._from_context()
|
|
||||||
if context_info != None:
|
|
||||||
fresh_stream.__dict__.update(context_info)
|
|
||||||
|
|
||||||
self.current_stream = fresh_stream
|
|
||||||
dbgdump(current_stream.__dict__)
|
|
||||||
|
|
||||||
def _from_context(self):
|
|
||||||
return getattr(self.context, 'AudioPlayer', {})
|
|
||||||
|
|
||||||
def _from_directive(self):
|
|
||||||
from_buffer = top_stream(self.stream_cache, self._get_user())
|
|
||||||
if from_buffer:
|
|
||||||
if self.request.intent and 'PauseIntent' in self.request.intent.name:
|
|
||||||
return {}
|
|
||||||
return from_buffer
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def _flask_view_func(self, *args, **kwargs):
|
|
||||||
ask_payload = self._alexa_request(verify=self.ask_verify_requests)
|
|
||||||
dbgdump(ask_payload)
|
|
||||||
request_body = models._Field(ask_payload)
|
|
||||||
|
|
||||||
self.request = request_body.request
|
|
||||||
self.version = request_body.version
|
|
||||||
self.context = getattr(request_body, 'context', models._Field())
|
|
||||||
self.session = getattr(request_body, 'session', self.session) # to keep old session.attributes through AudioRequests
|
|
||||||
|
|
||||||
if not self.session:
|
|
||||||
self.session = models._Field()
|
|
||||||
if not self.session.attributes:
|
|
||||||
self.session.attributes = models._Field()
|
|
||||||
|
|
||||||
self._update_stream()
|
|
||||||
|
|
||||||
# add current dialog state in session
|
|
||||||
try:
|
|
||||||
self.session["dialogState"] = request.dialogState
|
|
||||||
except KeyError:
|
|
||||||
self.session["dialogState"] = "unknown"
|
|
||||||
|
|
||||||
try:
|
|
||||||
if self.session.new and self._on_session_started_callback is not None:
|
|
||||||
self._on_session_started_callback()
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
result = None
|
|
||||||
request_type = self.request.type
|
|
||||||
|
|
||||||
if request_type == 'LaunchRequest' and self._launch_view_func:
|
|
||||||
result = self._launch_view_func()
|
|
||||||
elif request_type == 'SessionEndedRequest':
|
|
||||||
if self._session_ended_view_func:
|
|
||||||
result = self._session_ended_view_func()
|
|
||||||
else:
|
|
||||||
result = "{}", 200
|
|
||||||
elif request_type == 'IntentRequest' and self._intent_view_funcs:
|
|
||||||
result = self._map_intent_to_view_func(self.request.intent)()
|
|
||||||
elif request_type == 'Display.ElementSelected' and self._display_element_selected_func:
|
|
||||||
result = self._display_element_selected_func()
|
|
||||||
elif 'AudioPlayer' in request_type:
|
|
||||||
result = self._map_player_request_to_func(self.request.type)()
|
|
||||||
# routes to on_playback funcs
|
|
||||||
# user can also access state of content.AudioPlayer with current_stream
|
|
||||||
elif 'Connections.Response' in request_type:
|
|
||||||
result = self._map_purchase_request_to_func(self.request.type)()
|
|
||||||
|
|
||||||
if result is not None:
|
|
||||||
if isinstance(result, models._Response):
|
|
||||||
return result.render_response()
|
|
||||||
return result
|
|
||||||
return "", 400
|
|
||||||
|
|
||||||
def _map_intent_to_view_func(self, intent):
|
|
||||||
"""Provides appropiate parameters to the intent functions."""
|
|
||||||
if intent.name in self._intent_view_funcs:
|
|
||||||
view_func = self._intent_view_funcs[intent.name]
|
|
||||||
elif self._default_intent_view_func is not None:
|
|
||||||
view_func = self._default_intent_view_func
|
|
||||||
else:
|
|
||||||
raise NotImplementedError('Intent "{}" not found and no default intent specified.'.format(intent.name))
|
|
||||||
|
|
||||||
PY3 = sys.version_info[0] == 3
|
|
||||||
if PY3:
|
|
||||||
argspec = inspect.getfullargspec(view_func)
|
|
||||||
else:
|
|
||||||
argspec = inspect.getargspec(view_func)
|
|
||||||
|
|
||||||
arg_names = argspec.args
|
|
||||||
arg_values = self._map_params_to_view_args(intent.name, arg_names)
|
|
||||||
|
|
||||||
return partial(view_func, *arg_values)
|
|
||||||
|
|
||||||
def _map_player_request_to_func(self, player_request_type):
|
|
||||||
"""Provides appropriate parameters to the on_playback functions."""
|
|
||||||
# calbacks for on_playback requests are optional
|
|
||||||
view_func = self._intent_view_funcs.get(player_request_type, lambda: None)
|
|
||||||
|
|
||||||
argspec = inspect.getargspec(view_func)
|
|
||||||
arg_names = argspec.args
|
|
||||||
arg_values = self._map_params_to_view_args(player_request_type, arg_names)
|
|
||||||
|
|
||||||
return partial(view_func, *arg_values)
|
|
||||||
|
|
||||||
def _map_purchase_request_to_func(self, purchase_request_type):
|
|
||||||
"""Provides appropriate parameters to the on_purchase functions."""
|
|
||||||
|
|
||||||
if purchase_request_type in self._intent_view_funcs:
|
|
||||||
view_func = self._intent_view_funcs[purchase_request_type]
|
|
||||||
else:
|
|
||||||
raise NotImplementedError('Request type "{}" not found and no default view specified.'.format(purchase_request_type))
|
|
||||||
|
|
||||||
argspec = inspect.getargspec(view_func)
|
|
||||||
arg_names = argspec.args
|
|
||||||
arg_values = self._map_params_to_view_args(purchase_request_type, arg_names)
|
|
||||||
|
|
||||||
print('_map_purchase_request_to_func', arg_names, arg_values, view_func, purchase_request_type)
|
|
||||||
return partial(view_func, *arg_values)
|
|
||||||
|
|
||||||
def _get_slot_value(self, slot_object):
|
|
||||||
slot_name = slot_object.name
|
|
||||||
slot_value = getattr(slot_object, 'value', None)
|
|
||||||
resolutions = getattr(slot_object, 'resolutions', None)
|
|
||||||
|
|
||||||
if resolutions is not None:
|
|
||||||
resolutions_per_authority = getattr(resolutions, 'resolutionsPerAuthority', None)
|
|
||||||
if resolutions_per_authority is not None and len(resolutions_per_authority) > 0:
|
|
||||||
values = resolutions_per_authority[0].get('values', None)
|
|
||||||
if values is not None and len(values) > 0:
|
|
||||||
value = values[0].get('value', None)
|
|
||||||
if value is not None:
|
|
||||||
slot_value = value.get('name', slot_value)
|
|
||||||
|
|
||||||
return slot_value
|
|
||||||
|
|
||||||
def _map_params_to_view_args(self, view_name, arg_names):
|
|
||||||
|
|
||||||
arg_values = []
|
|
||||||
convert = self._intent_converts.get(view_name)
|
|
||||||
default = self._intent_defaults.get(view_name)
|
|
||||||
mapping = self._intent_mappings.get(view_name)
|
|
||||||
|
|
||||||
convert_errors = {}
|
|
||||||
|
|
||||||
request_data = {}
|
|
||||||
intent = getattr(self.request, 'intent', None)
|
|
||||||
if intent is not None:
|
|
||||||
if intent.slots is not None:
|
|
||||||
for slot_key in intent.slots.keys():
|
|
||||||
slot_object = getattr(intent.slots, slot_key)
|
|
||||||
request_data[slot_object.name] = self._get_slot_value(slot_object=slot_object)
|
|
||||||
|
|
||||||
else:
|
|
||||||
for param_name in self.request:
|
|
||||||
request_data[param_name] = getattr(self.request, param_name, None)
|
|
||||||
|
|
||||||
for arg_name in arg_names:
|
|
||||||
param_or_slot = mapping.get(arg_name, arg_name)
|
|
||||||
arg_value = request_data.get(param_or_slot)
|
|
||||||
if arg_value is None or arg_value == "":
|
|
||||||
if arg_name in default:
|
|
||||||
default_value = default[arg_name]
|
|
||||||
if isinstance(default_value, collections.Callable):
|
|
||||||
default_value = default_value()
|
|
||||||
arg_value = default_value
|
|
||||||
elif arg_name in convert:
|
|
||||||
shorthand_or_function = convert[arg_name]
|
|
||||||
if shorthand_or_function in _converters:
|
|
||||||
shorthand = shorthand_or_function
|
|
||||||
convert_func = _converters[shorthand]
|
|
||||||
else:
|
|
||||||
convert_func = shorthand_or_function
|
|
||||||
try:
|
|
||||||
arg_value = convert_func(arg_value)
|
|
||||||
except Exception as e:
|
|
||||||
convert_errors[arg_name] = e
|
|
||||||
arg_values.append(arg_value)
|
|
||||||
self.convert_errors = convert_errors
|
|
||||||
return arg_values
|
|
||||||
|
|
||||||
|
|
||||||
class YamlLoader(BaseLoader):
|
|
||||||
|
|
||||||
def __init__(self, app, path):
|
|
||||||
self.path = app.root_path + os.path.sep + path
|
|
||||||
self.mapping = {}
|
|
||||||
self._reload_mapping()
|
|
||||||
|
|
||||||
def _reload_mapping(self):
|
|
||||||
if os.path.isfile(self.path):
|
|
||||||
self.last_mtime = os.path.getmtime(self.path)
|
|
||||||
with open(self.path) as f:
|
|
||||||
self.mapping = yaml.safe_load(f.read())
|
|
||||||
|
|
||||||
def get_source(self, environment, template):
|
|
||||||
if not os.path.isfile(self.path):
|
|
||||||
return None, None, None
|
|
||||||
if self.last_mtime != os.path.getmtime(self.path):
|
|
||||||
self._reload_mapping()
|
|
||||||
if template in self.mapping:
|
|
||||||
source = self.mapping[template]
|
|
||||||
return source, None, lambda: source == self.mapping.get(template)
|
|
||||||
raise TemplateNotFound(template)
|
|
||||||
|
|
@ -1,460 +0,0 @@
|
||||||
import inspect
|
|
||||||
from flask import json
|
|
||||||
from xml.etree import ElementTree
|
|
||||||
import aniso8601
|
|
||||||
from .core import session, context, current_stream, stream_cache, dbgdump
|
|
||||||
from .cache import push_stream
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
|
|
||||||
class _Field(dict):
|
|
||||||
"""Container to represent Alexa Request Data.
|
|
||||||
|
|
||||||
Initialized with request_json and creates a dict object with attributes
|
|
||||||
to be accessed via dot notation or as a dict key-value.
|
|
||||||
|
|
||||||
Parameters within the request_json that contain their data as a json object
|
|
||||||
are also represented as a _Field object.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
payload_object = _Field(alexa_json_payload)
|
|
||||||
|
|
||||||
request_type_from_keys = payload_object['request']['type']
|
|
||||||
request_type_from_attrs = payload_object.request.type
|
|
||||||
|
|
||||||
assert request_type_from_keys == request_type_from_attrs
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, request_json={}):
|
|
||||||
super(_Field, self).__init__(request_json)
|
|
||||||
for key, value in request_json.items():
|
|
||||||
if isinstance(value, dict):
|
|
||||||
value = _Field(value)
|
|
||||||
self[key] = value
|
|
||||||
|
|
||||||
def __getattr__(self, attr):
|
|
||||||
# converts timestamp str to datetime.datetime object
|
|
||||||
if 'timestamp' in attr:
|
|
||||||
return aniso8601.parse_datetime(self.get(attr))
|
|
||||||
return self.get(attr)
|
|
||||||
|
|
||||||
def __setattr__(self, key, value):
|
|
||||||
self.__setitem__(key, value)
|
|
||||||
|
|
||||||
|
|
||||||
class _Response(object):
|
|
||||||
|
|
||||||
def __init__(self, speech):
|
|
||||||
self._json_default = None
|
|
||||||
self._response = {
|
|
||||||
'outputSpeech': _output_speech(speech)
|
|
||||||
}
|
|
||||||
|
|
||||||
def simple_card(self, title=None, content=None):
|
|
||||||
card = {
|
|
||||||
'type': 'Simple',
|
|
||||||
'title': title,
|
|
||||||
'content': content
|
|
||||||
}
|
|
||||||
self._response['card'] = card
|
|
||||||
return self
|
|
||||||
|
|
||||||
def standard_card(self, title=None, text=None, small_image_url=None, large_image_url=None):
|
|
||||||
card = {
|
|
||||||
'type': 'Standard',
|
|
||||||
'title': title,
|
|
||||||
'text': text
|
|
||||||
}
|
|
||||||
|
|
||||||
if any((small_image_url, large_image_url)):
|
|
||||||
card['image'] = {}
|
|
||||||
if small_image_url is not None:
|
|
||||||
card['image']['smallImageUrl'] = small_image_url
|
|
||||||
if large_image_url is not None:
|
|
||||||
card['image']['largeImageUrl'] = large_image_url
|
|
||||||
|
|
||||||
self._response['card'] = card
|
|
||||||
return self
|
|
||||||
|
|
||||||
def list_display_render(self, template=None, title=None, backButton='HIDDEN', token=None, background_image_url=None, image=None, listItems=None, hintText=None):
|
|
||||||
directive = [
|
|
||||||
{
|
|
||||||
'type': 'Display.RenderTemplate',
|
|
||||||
'template': {
|
|
||||||
'type': template,
|
|
||||||
'backButton': backButton,
|
|
||||||
'title': title,
|
|
||||||
'listItems': listItems
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
if background_image_url is not None:
|
|
||||||
directive[0]['template']['backgroundImage'] = {
|
|
||||||
'sources': [
|
|
||||||
{'url': background_image_url}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
if hintText is not None:
|
|
||||||
hint = {
|
|
||||||
'type':'Hint',
|
|
||||||
'hint': {
|
|
||||||
'type':"PlainText",
|
|
||||||
'text': hintText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
directive.append(hint)
|
|
||||||
self._response['directives'] = directive
|
|
||||||
return self
|
|
||||||
|
|
||||||
def display_render(self, template=None, title=None, backButton='HIDDEN', token=None, background_image_url=None, image=None, text=None, hintText=None):
|
|
||||||
directive = [
|
|
||||||
{
|
|
||||||
'type': 'Display.RenderTemplate',
|
|
||||||
'template': {
|
|
||||||
'type': template,
|
|
||||||
'backButton': backButton,
|
|
||||||
'title': title,
|
|
||||||
'textContent': text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
if background_image_url is not None:
|
|
||||||
directive[0]['template']['backgroundImage'] = {
|
|
||||||
'sources': [
|
|
||||||
{'url': background_image_url}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
if image is not None:
|
|
||||||
directive[0]['template']['image'] = {
|
|
||||||
'sources': [
|
|
||||||
{'url': image}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
if token is not None:
|
|
||||||
directive[0]['template']['token'] = token
|
|
||||||
|
|
||||||
if hintText is not None:
|
|
||||||
hint = {
|
|
||||||
'type':'Hint',
|
|
||||||
'hint': {
|
|
||||||
'type':"PlainText",
|
|
||||||
'text': hintText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
directive.append(hint)
|
|
||||||
|
|
||||||
self._response['directives'] = directive
|
|
||||||
return self
|
|
||||||
|
|
||||||
def link_account_card(self):
|
|
||||||
card = {'type': 'LinkAccount'}
|
|
||||||
self._response['card'] = card
|
|
||||||
return self
|
|
||||||
|
|
||||||
def consent_card(self, permissions):
|
|
||||||
card = {
|
|
||||||
'type': 'AskForPermissionsConsent',
|
|
||||||
'permissions': [permissions]
|
|
||||||
}
|
|
||||||
self._response['card'] = card
|
|
||||||
return self
|
|
||||||
|
|
||||||
def render_response(self):
|
|
||||||
response_wrapper = {
|
|
||||||
'version': '1.0',
|
|
||||||
'response': self._response,
|
|
||||||
'sessionAttributes': session.attributes
|
|
||||||
}
|
|
||||||
|
|
||||||
kw = {}
|
|
||||||
if hasattr(session, 'attributes_encoder'):
|
|
||||||
json_encoder = session.attributes_encoder
|
|
||||||
kwargname = 'cls' if inspect.isclass(json_encoder) else 'default'
|
|
||||||
kw[kwargname] = json_encoder
|
|
||||||
dbgdump(response_wrapper, **kw)
|
|
||||||
|
|
||||||
return json.dumps(response_wrapper, **kw)
|
|
||||||
|
|
||||||
|
|
||||||
class statement(_Response):
|
|
||||||
|
|
||||||
def __init__(self, speech):
|
|
||||||
super(statement, self).__init__(speech)
|
|
||||||
self._response['shouldEndSession'] = True
|
|
||||||
|
|
||||||
|
|
||||||
class question(_Response):
|
|
||||||
|
|
||||||
def __init__(self, speech):
|
|
||||||
super(question, self).__init__(speech)
|
|
||||||
self._response['shouldEndSession'] = False
|
|
||||||
|
|
||||||
def reprompt(self, reprompt):
|
|
||||||
reprompt = {'outputSpeech': _output_speech(reprompt)}
|
|
||||||
self._response['reprompt'] = reprompt
|
|
||||||
return self
|
|
||||||
|
|
||||||
|
|
||||||
class buy(_Response):
|
|
||||||
|
|
||||||
def __init__(self, productId=None):
|
|
||||||
self._response = {
|
|
||||||
'shouldEndSession': True,
|
|
||||||
'directives': [{
|
|
||||||
'type': 'Connections.SendRequest',
|
|
||||||
'name': 'Buy',
|
|
||||||
'payload': {
|
|
||||||
'InSkillProduct': {
|
|
||||||
'productId': productId
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'token': 'correlationToken'
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class refund(_Response):
|
|
||||||
|
|
||||||
def __init__(self, productId=None):
|
|
||||||
self._response = {
|
|
||||||
'shouldEndSession': True,
|
|
||||||
'directives': [{
|
|
||||||
'type': 'Connections.SendRequest',
|
|
||||||
'name': 'Cancel',
|
|
||||||
'payload': {
|
|
||||||
'InSkillProduct': {
|
|
||||||
'productId': productId
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'token': 'correlationToken'
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
|
|
||||||
class upsell(_Response):
|
|
||||||
|
|
||||||
def __init__(self, productId=None, msg=None):
|
|
||||||
self._response = {
|
|
||||||
'shouldEndSession': True,
|
|
||||||
'directives': [{
|
|
||||||
'type': 'Connections.SendRequest',
|
|
||||||
'name': 'Upsell',
|
|
||||||
'payload': {
|
|
||||||
'InSkillProduct': {
|
|
||||||
'productId': productId
|
|
||||||
},
|
|
||||||
'upsellMessage': msg
|
|
||||||
},
|
|
||||||
'token': 'correlationToken'
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
|
|
||||||
class delegate(_Response):
|
|
||||||
|
|
||||||
def __init__(self, updated_intent=None):
|
|
||||||
self._response = {
|
|
||||||
'shouldEndSession': False,
|
|
||||||
'directives': [{'type': 'Dialog.Delegate'}]
|
|
||||||
}
|
|
||||||
|
|
||||||
if updated_intent:
|
|
||||||
self._response['directives'][0]['updatedIntent'] = updated_intent
|
|
||||||
|
|
||||||
|
|
||||||
class elicit_slot(_Response):
|
|
||||||
"""
|
|
||||||
Sends an ElicitSlot directive.
|
|
||||||
slot - The slot name to elicit
|
|
||||||
speech - The output speech
|
|
||||||
updated_intent - Optional updated intent
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, slot, speech, updated_intent=None):
|
|
||||||
self._response = {
|
|
||||||
'shouldEndSession': False,
|
|
||||||
'directives': [{
|
|
||||||
'type': 'Dialog.ElicitSlot',
|
|
||||||
'slotToElicit': slot,
|
|
||||||
}],
|
|
||||||
'outputSpeech': _output_speech(speech),
|
|
||||||
}
|
|
||||||
|
|
||||||
if updated_intent:
|
|
||||||
self._response['directives'][0]['updatedIntent'] = updated_intent
|
|
||||||
|
|
||||||
class confirm_slot(_Response):
|
|
||||||
"""
|
|
||||||
Sends a ConfirmSlot directive.
|
|
||||||
slot - The slot name to confirm
|
|
||||||
speech - The output speech
|
|
||||||
updated_intent - Optional updated intent
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, slot, speech, updated_intent=None):
|
|
||||||
self._response = {
|
|
||||||
'shouldEndSession': False,
|
|
||||||
'directives': [{
|
|
||||||
'type': 'Dialog.ConfirmSlot',
|
|
||||||
'slotToConfirm': slot,
|
|
||||||
}],
|
|
||||||
'outputSpeech': _output_speech(speech),
|
|
||||||
}
|
|
||||||
|
|
||||||
if updated_intent:
|
|
||||||
self._response['directives'][0]['updatedIntent'] = updated_intent
|
|
||||||
|
|
||||||
class confirm_intent(_Response):
|
|
||||||
"""
|
|
||||||
Sends a ConfirmIntent directive.
|
|
||||||
|
|
||||||
"""
|
|
||||||
def __init__(self, speech, updated_intent=None):
|
|
||||||
self._response = {
|
|
||||||
'shouldEndSession': False,
|
|
||||||
'directives': [{
|
|
||||||
'type': 'Dialog.ConfirmIntent',
|
|
||||||
}],
|
|
||||||
'outputSpeech': _output_speech(speech),
|
|
||||||
}
|
|
||||||
|
|
||||||
if updated_intent:
|
|
||||||
self._response['directives'][0]['updatedIntent'] = updated_intent
|
|
||||||
|
|
||||||
|
|
||||||
class audio(_Response):
|
|
||||||
"""Returns a response object with an Amazon AudioPlayer Directive.
|
|
||||||
|
|
||||||
Responses for LaunchRequests and IntentRequests may include outputSpeech in addition to an audio directive
|
|
||||||
|
|
||||||
Note that responses to AudioPlayer requests do not allow outputSpeech.
|
|
||||||
These must only include AudioPlayer Directives.
|
|
||||||
|
|
||||||
@ask.intent('PlayFooAudioIntent')
|
|
||||||
def play_foo_audio():
|
|
||||||
speech = 'playing from foo'
|
|
||||||
stream_url = www.foo.com
|
|
||||||
return audio(speech).play(stream_url)
|
|
||||||
|
|
||||||
|
|
||||||
@ask.intent('AMAZON.PauseIntent')
|
|
||||||
def stop_audio():
|
|
||||||
return audio('Ok, stopping the audio').stop()
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, speech=''):
|
|
||||||
super(audio, self).__init__(speech)
|
|
||||||
if not speech:
|
|
||||||
self._response = {}
|
|
||||||
self._response['directives'] = []
|
|
||||||
|
|
||||||
def play(self, stream_url, offset=0, opaque_token=None):
|
|
||||||
"""Sends a Play Directive to begin playback and replace current and enqueued streams."""
|
|
||||||
|
|
||||||
self._response['shouldEndSession'] = True
|
|
||||||
directive = self._play_directive('REPLACE_ALL')
|
|
||||||
directive['audioItem'] = self._audio_item(stream_url=stream_url, offset=offset, opaque_token=opaque_token)
|
|
||||||
self._response['directives'].append(directive)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def enqueue(self, stream_url, offset=0, opaque_token=None):
|
|
||||||
"""Adds stream to the queue. Does not impact the currently playing stream."""
|
|
||||||
directive = self._play_directive('ENQUEUE')
|
|
||||||
audio_item = self._audio_item(stream_url=stream_url,
|
|
||||||
offset=offset,
|
|
||||||
push_buffer=False,
|
|
||||||
opaque_token=opaque_token)
|
|
||||||
audio_item['stream']['expectedPreviousToken'] = current_stream.token
|
|
||||||
|
|
||||||
directive['audioItem'] = audio_item
|
|
||||||
self._response['directives'].append(directive)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def play_next(self, stream_url=None, offset=0, opaque_token=None):
|
|
||||||
"""Replace all streams in the queue but does not impact the currently playing stream."""
|
|
||||||
|
|
||||||
directive = self._play_directive('REPLACE_ENQUEUED')
|
|
||||||
directive['audioItem'] = self._audio_item(stream_url=stream_url, offset=offset, opaque_token=opaque_token)
|
|
||||||
self._response['directives'].append(directive)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def resume(self):
|
|
||||||
"""Sends Play Directive to resume playback at the paused offset"""
|
|
||||||
directive = self._play_directive('REPLACE_ALL')
|
|
||||||
directive['audioItem'] = self._audio_item()
|
|
||||||
self._response['directives'].append(directive)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def _play_directive(self, behavior):
|
|
||||||
directive = {}
|
|
||||||
directive['type'] = 'AudioPlayer.Play'
|
|
||||||
directive['playBehavior'] = behavior
|
|
||||||
return directive
|
|
||||||
|
|
||||||
def _audio_item(self, stream_url=None, offset=0, push_buffer=True, opaque_token=None):
|
|
||||||
"""Builds an AudioPlayer Directive's audioItem and updates current_stream"""
|
|
||||||
audio_item = {'stream': {}}
|
|
||||||
stream = audio_item['stream']
|
|
||||||
|
|
||||||
# existing stream
|
|
||||||
if not stream_url:
|
|
||||||
# stream.update(current_stream.__dict__)
|
|
||||||
stream['url'] = current_stream.url
|
|
||||||
stream['token'] = current_stream.token
|
|
||||||
stream['offsetInMilliseconds'] = current_stream.offsetInMilliseconds
|
|
||||||
|
|
||||||
# new stream
|
|
||||||
else:
|
|
||||||
stream['url'] = stream_url
|
|
||||||
stream['token'] = opaque_token or str(uuid.uuid4())
|
|
||||||
stream['offsetInMilliseconds'] = offset
|
|
||||||
|
|
||||||
if push_buffer: # prevents enqueued streams from becoming current_stream
|
|
||||||
push_stream(stream_cache, context['System']['user']['userId'], stream)
|
|
||||||
return audio_item
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
"""Sends AudioPlayer.Stop Directive to stop the current stream playback"""
|
|
||||||
self._response['directives'].append({'type': 'AudioPlayer.Stop'})
|
|
||||||
return self
|
|
||||||
|
|
||||||
def clear_queue(self, stop=False):
|
|
||||||
"""Clears queued streams and optionally stops current stream.
|
|
||||||
|
|
||||||
Keyword Arguments:
|
|
||||||
stop {bool} set True to stop current current stream and clear queued streams.
|
|
||||||
set False to clear queued streams and allow current stream to finish
|
|
||||||
default: {False}
|
|
||||||
"""
|
|
||||||
|
|
||||||
directive = {}
|
|
||||||
directive['type'] = 'AudioPlayer.ClearQueue'
|
|
||||||
if stop:
|
|
||||||
directive['clearBehavior'] = 'CLEAR_ALL'
|
|
||||||
else:
|
|
||||||
directive['clearBehavior'] = 'CLEAR_ENQUEUED'
|
|
||||||
|
|
||||||
self._response['directives'].append(directive)
|
|
||||||
return self
|
|
||||||
|
|
||||||
|
|
||||||
def _copyattr(src, dest, attr, convert=None):
|
|
||||||
if attr in src:
|
|
||||||
value = src[attr]
|
|
||||||
if convert is not None:
|
|
||||||
value = convert(value)
|
|
||||||
setattr(dest, attr, value)
|
|
||||||
|
|
||||||
|
|
||||||
def _output_speech(speech):
|
|
||||||
try:
|
|
||||||
xmldoc = ElementTree.fromstring(speech)
|
|
||||||
if xmldoc.tag == 'speak':
|
|
||||||
return {'type': 'SSML', 'ssml': speech}
|
|
||||||
except (UnicodeEncodeError, ElementTree.ParseError) as e:
|
|
||||||
pass
|
|
||||||
return {'type': 'PlainText', 'text': speech}
|
|
||||||
|
|
@ -1,69 +0,0 @@
|
||||||
import os
|
|
||||||
import base64
|
|
||||||
import posixpath
|
|
||||||
from datetime import datetime
|
|
||||||
from six.moves.urllib.parse import urlparse
|
|
||||||
from six.moves.urllib.request import urlopen
|
|
||||||
|
|
||||||
from OpenSSL import crypto
|
|
||||||
|
|
||||||
from . import logger
|
|
||||||
|
|
||||||
|
|
||||||
class VerificationError(Exception): pass
|
|
||||||
|
|
||||||
|
|
||||||
def load_certificate(cert_url):
|
|
||||||
if not _valid_certificate_url(cert_url):
|
|
||||||
raise VerificationError("Certificate URL verification failed")
|
|
||||||
cert_data = urlopen(cert_url).read()
|
|
||||||
cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert_data)
|
|
||||||
if not _valid_certificate(cert):
|
|
||||||
raise VerificationError("Certificate verification failed")
|
|
||||||
return cert
|
|
||||||
|
|
||||||
|
|
||||||
def verify_signature(cert, signature, signed_data):
|
|
||||||
try:
|
|
||||||
signature = base64.b64decode(signature)
|
|
||||||
crypto.verify(cert, signature, signed_data, 'sha1')
|
|
||||||
except crypto.Error as e:
|
|
||||||
raise VerificationError(e)
|
|
||||||
|
|
||||||
|
|
||||||
def verify_timestamp(timestamp):
|
|
||||||
dt = datetime.utcnow() - timestamp.replace(tzinfo=None)
|
|
||||||
if abs(dt.total_seconds()) > 150:
|
|
||||||
raise VerificationError("Timestamp verification failed")
|
|
||||||
|
|
||||||
|
|
||||||
def verify_application_id(candidate, records):
|
|
||||||
if candidate not in records:
|
|
||||||
raise VerificationError("Application ID verification failed")
|
|
||||||
|
|
||||||
|
|
||||||
def _valid_certificate_url(cert_url):
|
|
||||||
parsed_url = urlparse(cert_url)
|
|
||||||
if parsed_url.scheme == 'https':
|
|
||||||
if parsed_url.hostname == "s3.amazonaws.com":
|
|
||||||
if posixpath.normpath(parsed_url.path).startswith("/echo.api/"):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def _valid_certificate(cert):
|
|
||||||
not_after = cert.get_notAfter().decode('utf-8')
|
|
||||||
not_after = datetime.strptime(not_after, '%Y%m%d%H%M%SZ')
|
|
||||||
if datetime.utcnow() >= not_after:
|
|
||||||
return False
|
|
||||||
found = False
|
|
||||||
for i in range(0, cert.get_extension_count()):
|
|
||||||
extension = cert.get_extension(i)
|
|
||||||
short_name = extension.get_short_name().decode('utf-8')
|
|
||||||
value = str(extension)
|
|
||||||
if 'subjectAltName' == short_name and 'DNS:echo-api.amazon.com' == value:
|
|
||||||
found = True
|
|
||||||
break
|
|
||||||
if not found:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
BIN
f-ask/ngrok.exe
BIN
f-ask/ngrok.exe
Binary file not shown.
|
|
@ -1,5 +0,0 @@
|
||||||
-r requirements.txt
|
|
||||||
mock==2.0.0
|
|
||||||
requests==2.13.0
|
|
||||||
tox==2.7.0
|
|
||||||
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
aniso8601==1.2.0
|
|
||||||
Flask==0.12.1
|
|
||||||
cryptography==2.1.4
|
|
||||||
pyOpenSSL==17.0.0
|
|
||||||
PyYAML==3.12
|
|
||||||
six==1.11.0
|
|
||||||
|
|
||||||
|
|
@ -1,256 +0,0 @@
|
||||||
import collections
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
from copy import copy
|
|
||||||
|
|
||||||
from flask import Flask, json
|
|
||||||
from flask_ask import Ask, question, statement, audio, current_stream, logger
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
ask = Ask(app, "/")
|
|
||||||
logging.getLogger('flask_ask').setLevel(logging.INFO)
|
|
||||||
|
|
||||||
|
|
||||||
playlist = [
|
|
||||||
# 'https://www.freesound.org/data/previews/367/367142_2188-lq.mp3',
|
|
||||||
'https://archive.org/download/mailboxbadgerdrumsamplesvolume2/Ringing.mp3',
|
|
||||||
'https://archive.org/download/petescott20160927/20160927%20RC300-53-127.0bpm.mp3',
|
|
||||||
'https://archive.org/download/plpl011/plpl011_05-johnny_ripper-rain.mp3',
|
|
||||||
'https://archive.org/download/piano_by_maxmsp/beats107.mp3',
|
|
||||||
'https://archive.org/download/petescott20160927/20160927%20RC300-58-115.1bpm.mp3',
|
|
||||||
'https://archive.org/download/PianoScale/PianoScale.mp3',
|
|
||||||
# 'https://archive.org/download/FemaleVoiceSample/Female_VoiceTalent_demo.mp4',
|
|
||||||
'https://archive.org/download/mailboxbadgerdrumsamplesvolume2/Risset%20Drum%201.mp3',
|
|
||||||
'https://archive.org/download/mailboxbadgerdrumsamplesvolume2/Submarine.mp3',
|
|
||||||
# 'https://ia800203.us.archive.org/27/items/CarelessWhisper_435/CarelessWhisper.ogg'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class QueueManager(object):
|
|
||||||
"""Manages queue data in a seperate context from current_stream.
|
|
||||||
|
|
||||||
The flask-ask Local current_stream refers only to the current data from Alexa requests and Skill Responses.
|
|
||||||
Alexa Skills Kit does not provide enqueued or stream-histroy data and does not provide a session attribute
|
|
||||||
when delivering AudioPlayer Requests.
|
|
||||||
|
|
||||||
This class is used to maintain accurate control of multiple streams,
|
|
||||||
so that the user may send Intents to move throughout a queue.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, urls):
|
|
||||||
self._urls = urls
|
|
||||||
self._queued = collections.deque(urls)
|
|
||||||
self._history = collections.deque()
|
|
||||||
self._current = None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def status(self):
|
|
||||||
status = {
|
|
||||||
'Current Position': self.current_position,
|
|
||||||
'Current URL': self.current,
|
|
||||||
'Next URL': self.up_next,
|
|
||||||
'Previous': self.previous,
|
|
||||||
'History': list(self.history)
|
|
||||||
}
|
|
||||||
return status
|
|
||||||
|
|
||||||
@property
|
|
||||||
def up_next(self):
|
|
||||||
"""Returns the url at the front of the queue"""
|
|
||||||
qcopy = copy(self._queued)
|
|
||||||
try:
|
|
||||||
return qcopy.popleft()
|
|
||||||
except IndexError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def current(self):
|
|
||||||
return self._current
|
|
||||||
|
|
||||||
@current.setter
|
|
||||||
def current(self, url):
|
|
||||||
self._save_to_history()
|
|
||||||
self._current = url
|
|
||||||
|
|
||||||
@property
|
|
||||||
def history(self):
|
|
||||||
return self._history
|
|
||||||
|
|
||||||
@property
|
|
||||||
def previous(self):
|
|
||||||
history = copy(self.history)
|
|
||||||
try:
|
|
||||||
return history.pop()
|
|
||||||
except IndexError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def add(self, url):
|
|
||||||
self._urls.append(url)
|
|
||||||
self._queued.append(url)
|
|
||||||
|
|
||||||
def extend(self, urls):
|
|
||||||
self._urls.extend(urls)
|
|
||||||
self._queued.extend(urls)
|
|
||||||
|
|
||||||
def _save_to_history(self):
|
|
||||||
if self._current:
|
|
||||||
self._history.append(self._current)
|
|
||||||
|
|
||||||
def end_current(self):
|
|
||||||
self._save_to_history()
|
|
||||||
self._current = None
|
|
||||||
|
|
||||||
def step(self):
|
|
||||||
self.end_current()
|
|
||||||
self._current = self._queued.popleft()
|
|
||||||
return self._current
|
|
||||||
|
|
||||||
def step_back(self):
|
|
||||||
self._queued.appendleft(self._current)
|
|
||||||
self._current = self._history.pop()
|
|
||||||
return self._current
|
|
||||||
|
|
||||||
def reset(self):
|
|
||||||
self._queued = collections.deque(self._urls)
|
|
||||||
self._history = []
|
|
||||||
|
|
||||||
def start(self):
|
|
||||||
self.__init__(self._urls)
|
|
||||||
return self.step()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def current_position(self):
|
|
||||||
return len(self._history) + 1
|
|
||||||
|
|
||||||
|
|
||||||
queue = QueueManager(playlist)
|
|
||||||
|
|
||||||
|
|
||||||
@ask.launch
|
|
||||||
def launch():
|
|
||||||
card_title = 'Playlist Example'
|
|
||||||
text = 'Welcome to an example for playing a playlist. You can ask me to start the playlist.'
|
|
||||||
prompt = 'You can ask start playlist.'
|
|
||||||
return question(text).reprompt(prompt).simple_card(card_title, text)
|
|
||||||
|
|
||||||
|
|
||||||
@ask.intent('PlaylistDemoIntent')
|
|
||||||
def start_playlist():
|
|
||||||
speech = 'Heres a playlist of some sounds. You can ask me Next, Previous, or Start Over'
|
|
||||||
stream_url = queue.start()
|
|
||||||
return audio(speech).play(stream_url)
|
|
||||||
|
|
||||||
|
|
||||||
# QueueManager object is not stepped forward here.
|
|
||||||
# This allows for Next Intents and on_playback_finished requests to trigger the step
|
|
||||||
@ask.on_playback_nearly_finished()
|
|
||||||
def nearly_finished():
|
|
||||||
if queue.up_next:
|
|
||||||
_infodump('Alexa is now ready for a Next or Previous Intent')
|
|
||||||
# dump_stream_info()
|
|
||||||
next_stream = queue.up_next
|
|
||||||
_infodump('Enqueueing {}'.format(next_stream))
|
|
||||||
return audio().enqueue(next_stream)
|
|
||||||
else:
|
|
||||||
_infodump('Nearly finished with last song in playlist')
|
|
||||||
|
|
||||||
|
|
||||||
@ask.on_playback_finished()
|
|
||||||
def play_back_finished():
|
|
||||||
_infodump('Finished Audio stream for track {}'.format(queue.current_position))
|
|
||||||
if queue.up_next:
|
|
||||||
queue.step()
|
|
||||||
_infodump('stepped queue forward')
|
|
||||||
dump_stream_info()
|
|
||||||
else:
|
|
||||||
return statement('You have reached the end of the playlist!')
|
|
||||||
|
|
||||||
|
|
||||||
# NextIntent steps queue forward and clears enqueued streams that were already sent to Alexa
|
|
||||||
# next_stream will match queue.up_next and enqueue Alexa with the correct subsequent stream.
|
|
||||||
@ask.intent('AMAZON.NextIntent')
|
|
||||||
def next_song():
|
|
||||||
if queue.up_next:
|
|
||||||
speech = 'playing next queued song'
|
|
||||||
next_stream = queue.step()
|
|
||||||
_infodump('Stepped queue forward to {}'.format(next_stream))
|
|
||||||
dump_stream_info()
|
|
||||||
return audio(speech).play(next_stream)
|
|
||||||
else:
|
|
||||||
return audio('There are no more songs in the queue')
|
|
||||||
|
|
||||||
|
|
||||||
@ask.intent('AMAZON.PreviousIntent')
|
|
||||||
def previous_song():
|
|
||||||
if queue.previous:
|
|
||||||
speech = 'playing previously played song'
|
|
||||||
prev_stream = queue.step_back()
|
|
||||||
dump_stream_info()
|
|
||||||
return audio(speech).play(prev_stream)
|
|
||||||
|
|
||||||
else:
|
|
||||||
return audio('There are no songs in your playlist history.')
|
|
||||||
|
|
||||||
|
|
||||||
@ask.intent('AMAZON.StartOverIntent')
|
|
||||||
def restart_track():
|
|
||||||
if queue.current:
|
|
||||||
speech = 'Restarting current track'
|
|
||||||
dump_stream_info()
|
|
||||||
return audio(speech).play(queue.current, offset=0)
|
|
||||||
else:
|
|
||||||
return statement('There is no current song')
|
|
||||||
|
|
||||||
|
|
||||||
@ask.on_playback_started()
|
|
||||||
def started(offset, token, url):
|
|
||||||
_infodump('Started audio stream for track {}'.format(queue.current_position))
|
|
||||||
dump_stream_info()
|
|
||||||
|
|
||||||
|
|
||||||
@ask.on_playback_stopped()
|
|
||||||
def stopped(offset, token):
|
|
||||||
_infodump('Stopped audio stream for track {}'.format(queue.current_position))
|
|
||||||
|
|
||||||
@ask.intent('AMAZON.PauseIntent')
|
|
||||||
def pause():
|
|
||||||
seconds = current_stream.offsetInMilliseconds / 1000
|
|
||||||
msg = 'Paused the Playlist on track {}, offset at {} seconds'.format(
|
|
||||||
queue.current_position, seconds)
|
|
||||||
_infodump(msg)
|
|
||||||
dump_stream_info()
|
|
||||||
return audio(msg).stop().simple_card(msg)
|
|
||||||
|
|
||||||
|
|
||||||
@ask.intent('AMAZON.ResumeIntent')
|
|
||||||
def resume():
|
|
||||||
seconds = current_stream.offsetInMilliseconds / 1000
|
|
||||||
msg = 'Resuming the Playlist on track {}, offset at {} seconds'.format(queue.current_position, seconds)
|
|
||||||
_infodump(msg)
|
|
||||||
dump_stream_info()
|
|
||||||
return audio(msg).resume().simple_card(msg)
|
|
||||||
|
|
||||||
|
|
||||||
@ask.session_ended
|
|
||||||
def session_ended():
|
|
||||||
return "{}", 200
|
|
||||||
|
|
||||||
def dump_stream_info():
|
|
||||||
status = {
|
|
||||||
'Current Stream Status': current_stream.__dict__,
|
|
||||||
'Queue status': queue.status
|
|
||||||
}
|
|
||||||
_infodump(status)
|
|
||||||
|
|
||||||
|
|
||||||
def _infodump(obj, indent=2):
|
|
||||||
msg = json.dumps(obj, indent=indent)
|
|
||||||
logger.info(msg)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
if 'ASK_VERIFY_REQUESTS' in os.environ:
|
|
||||||
verify = str(os.environ.get('ASK_VERIFY_REQUESTS', '')).lower()
|
|
||||||
if verify == 'false':
|
|
||||||
app.config['ASK_VERIFY_REQUESTS'] = False
|
|
||||||
app.run(debug=True)
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
{
|
|
||||||
"intents": [
|
|
||||||
{
|
|
||||||
"intent": "AMAZON.PauseIntent"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"intent": "PlaylistDemoIntent"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"intent": "AMAZON.StopIntent"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"intent": "AMAZON.ResumeIntent"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
PlaylistDemoIntent start the playlist
|
|
||||||
|
|
@ -1,89 +0,0 @@
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
|
|
||||||
from flask import Flask, json, render_template
|
|
||||||
from flask_ask import Ask, request, session, question, statement, context, audio, current_stream
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
ask = Ask(app, "/")
|
|
||||||
logger = logging.getLogger()
|
|
||||||
logging.getLogger('flask_ask').setLevel(logging.INFO)
|
|
||||||
|
|
||||||
|
|
||||||
@ask.launch
|
|
||||||
def launch():
|
|
||||||
card_title = 'Audio Example'
|
|
||||||
text = 'Welcome to an audio example. You can ask to begin demo, or try asking me to play the sax.'
|
|
||||||
prompt = 'You can ask to begin demo, or try asking me to play the sax.'
|
|
||||||
return question(text).reprompt(prompt).simple_card(card_title, text)
|
|
||||||
|
|
||||||
|
|
||||||
@ask.intent('DemoIntent')
|
|
||||||
def demo():
|
|
||||||
speech = "Here's one of my favorites"
|
|
||||||
stream_url = 'https://www.vintagecomputermusic.com/mp3/s2t9_Computer_Speech_Demonstration.mp3'
|
|
||||||
return audio(speech).play(stream_url, offset=93000)
|
|
||||||
|
|
||||||
|
|
||||||
# 'ask audio_skil Play the sax
|
|
||||||
@ask.intent('SaxIntent')
|
|
||||||
def george_michael():
|
|
||||||
speech = 'yeah you got it!'
|
|
||||||
stream_url = 'https://ia800203.us.archive.org/27/items/CarelessWhisper_435/CarelessWhisper.ogg'
|
|
||||||
return audio(speech).play(stream_url)
|
|
||||||
|
|
||||||
|
|
||||||
@ask.intent('AMAZON.PauseIntent')
|
|
||||||
def pause():
|
|
||||||
return audio('Paused the stream.').stop()
|
|
||||||
|
|
||||||
|
|
||||||
@ask.intent('AMAZON.ResumeIntent')
|
|
||||||
def resume():
|
|
||||||
return audio('Resuming.').resume()
|
|
||||||
|
|
||||||
@ask.intent('AMAZON.StopIntent')
|
|
||||||
def stop():
|
|
||||||
return audio('stopping').clear_queue(stop=True)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# optional callbacks
|
|
||||||
@ask.on_playback_started()
|
|
||||||
def started(offset, token):
|
|
||||||
_infodump('STARTED Audio Stream at {} ms'.format(offset))
|
|
||||||
_infodump('Stream holds the token {}'.format(token))
|
|
||||||
_infodump('STARTED Audio stream from {}'.format(current_stream.url))
|
|
||||||
|
|
||||||
|
|
||||||
@ask.on_playback_stopped()
|
|
||||||
def stopped(offset, token):
|
|
||||||
_infodump('STOPPED Audio Stream at {} ms'.format(offset))
|
|
||||||
_infodump('Stream holds the token {}'.format(token))
|
|
||||||
_infodump('Stream stopped playing from {}'.format(current_stream.url))
|
|
||||||
|
|
||||||
|
|
||||||
@ask.on_playback_nearly_finished()
|
|
||||||
def nearly_finished():
|
|
||||||
_infodump('Stream nearly finished from {}'.format(current_stream.url))
|
|
||||||
|
|
||||||
@ask.on_playback_finished()
|
|
||||||
def stream_finished(token):
|
|
||||||
_infodump('Playback has finished for stream with token {}'.format(token))
|
|
||||||
|
|
||||||
@ask.session_ended
|
|
||||||
def session_ended():
|
|
||||||
return "{}", 200
|
|
||||||
|
|
||||||
def _infodump(obj, indent=2):
|
|
||||||
msg = json.dumps(obj, indent=indent)
|
|
||||||
logger.info(msg)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
if 'ASK_VERIFY_REQUESTS' in os.environ:
|
|
||||||
verify = str(os.environ.get('ASK_VERIFY_REQUESTS', '')).lower()
|
|
||||||
if verify == 'false':
|
|
||||||
app.config['ASK_VERIFY_REQUESTS'] = False
|
|
||||||
app.run(debug=True)
|
|
||||||
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
{
|
|
||||||
"intents": [
|
|
||||||
{
|
|
||||||
"intent": "AMAZON.PauseIntent"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"intent": "DemoIntent"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"intent": "SaxIntent"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"intent": "AMAZON.StopIntent"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"intent": "AMAZON.ResumeIntent"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
DemoIntent begin demo
|
|
||||||
SaxIntent play the sax
|
|
||||||
SaxIntent play sax
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
|
|
||||||
from flask import Flask
|
|
||||||
from helloworld import blueprint
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
app.register_blueprint(blueprint)
|
|
||||||
|
|
||||||
logging.getLogger('flask_app').setLevel(logging.DEBUG)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
if 'ASK_VERIFY_REQUESTS' in os.environ:
|
|
||||||
verify = str(os.environ.get('ASK_VERIFY_REQUESTS', '')).lower()
|
|
||||||
if verify == 'false':
|
|
||||||
app.config['ASK_VERIFY_REQUESTS'] = False
|
|
||||||
app.run(debug=True)
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
import logging
|
|
||||||
|
|
||||||
from flask import Blueprint, render_template
|
|
||||||
from flask_ask import Ask, question, statement
|
|
||||||
|
|
||||||
|
|
||||||
blueprint = Blueprint('blueprint_api', __name__, url_prefix="/ask")
|
|
||||||
ask = Ask(blueprint=blueprint)
|
|
||||||
|
|
||||||
logging.getLogger('flask_ask').setLevel(logging.DEBUG)
|
|
||||||
|
|
||||||
|
|
||||||
@ask.launch
|
|
||||||
def launch():
|
|
||||||
speech_text = render_template('welcome')
|
|
||||||
return question(speech_text).reprompt(speech_text).simple_card('HelloWorld', speech_text)
|
|
||||||
|
|
||||||
|
|
||||||
@ask.intent('HelloWorldIntent')
|
|
||||||
def hello_world():
|
|
||||||
speech_text = render_template('hello')
|
|
||||||
return statement(speech_text).simple_card('HelloWorld', speech_text)
|
|
||||||
|
|
||||||
|
|
||||||
@ask.intent('AMAZON.HelpIntent')
|
|
||||||
def help():
|
|
||||||
speech_text = render_template('help')
|
|
||||||
return question(speech_text).reprompt(speech_text).simple_card('HelloWorld', speech_text)
|
|
||||||
|
|
||||||
|
|
||||||
@ask.session_ended
|
|
||||||
def session_ended():
|
|
||||||
return "{}", 200
|
|
||||||
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
{
|
|
||||||
"intents": [
|
|
||||||
{
|
|
||||||
"intent": "HelloWorldIntent"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"intent": "AMAZON.HelpIntent"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
HelloWorldIntent say hello
|
|
||||||
HelloWorldIntent say hello world
|
|
||||||
HelloWorldIntent hello
|
|
||||||
HelloWorldIntent say hi
|
|
||||||
HelloWorldIntent say hi world
|
|
||||||
HelloWorldIntent hi
|
|
||||||
HelloWorldIntent how are you
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
welcome: "Welcome to the Alexa Skills Kit, you can say hello"
|
|
||||||
hello: "Hello world!"
|
|
||||||
help: "You can say hello to me!"
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
|
|
||||||
from flask import Flask
|
|
||||||
from flask_ask import Ask, request, session, question, statement
|
|
||||||
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
ask = Ask(app, "/")
|
|
||||||
logging.getLogger('flask_ask').setLevel(logging.DEBUG)
|
|
||||||
|
|
||||||
|
|
||||||
@ask.launch
|
|
||||||
def launch():
|
|
||||||
speech_text = 'Welcome to the Alexa Skills Kit, you can say hello'
|
|
||||||
return question(speech_text).reprompt(speech_text).simple_card('HelloWorld', speech_text)
|
|
||||||
|
|
||||||
|
|
||||||
@ask.intent('HelloWorldIntent')
|
|
||||||
def hello_world():
|
|
||||||
speech_text = 'Hello world'
|
|
||||||
return statement(speech_text).simple_card('HelloWorld', speech_text)
|
|
||||||
|
|
||||||
|
|
||||||
@ask.intent('AMAZON.HelpIntent')
|
|
||||||
def help():
|
|
||||||
speech_text = 'You can say hello to me!'
|
|
||||||
return question(speech_text).reprompt(speech_text).simple_card('HelloWorld', speech_text)
|
|
||||||
|
|
||||||
|
|
||||||
@ask.session_ended
|
|
||||||
def session_ended():
|
|
||||||
return "{}", 200
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
if 'ASK_VERIFY_REQUESTS' in os.environ:
|
|
||||||
verify = str(os.environ.get('ASK_VERIFY_REQUESTS', '')).lower()
|
|
||||||
if verify == 'false':
|
|
||||||
app.config['ASK_VERIFY_REQUESTS'] = False
|
|
||||||
app.run(debug=True)
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
{
|
|
||||||
"intents": [
|
|
||||||
{
|
|
||||||
"intent": "HelloWorldIntent"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"intent": "AMAZON.HelpIntent"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
HelloWorldIntent say hello
|
|
||||||
HelloWorldIntent say hello world
|
|
||||||
HelloWorldIntent hello
|
|
||||||
HelloWorldIntent say hi
|
|
||||||
HelloWorldIntent say hi world
|
|
||||||
HelloWorldIntent hi
|
|
||||||
HelloWorldIntent how are you
|
|
||||||
|
|
@ -1,145 +0,0 @@
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
from six.moves.urllib.request import urlopen
|
|
||||||
|
|
||||||
|
|
||||||
from flask import Flask
|
|
||||||
from flask_ask import Ask, request, session, question, statement
|
|
||||||
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
ask = Ask(app, "/")
|
|
||||||
logging.getLogger('flask_ask').setLevel(logging.DEBUG)
|
|
||||||
|
|
||||||
|
|
||||||
# URL prefix to download history content from Wikipedia.
|
|
||||||
URL_PREFIX = 'https://en.wikipedia.org/w/api.php?action=query&prop=extracts' + \
|
|
||||||
'&format=json&explaintext=&exsectionformat=plain&redirects=&titles='
|
|
||||||
|
|
||||||
# Constant defining number of events to be read at one time.
|
|
||||||
PAGINATION_SIZE = 3
|
|
||||||
|
|
||||||
# Length of the delimiter between individual events.
|
|
||||||
DELIMITER_SIZE = 2
|
|
||||||
|
|
||||||
# Size of events from Wikipedia response.
|
|
||||||
SIZE_OF_EVENTS = 10
|
|
||||||
|
|
||||||
# Constant defining session attribute key for the event index
|
|
||||||
SESSION_INDEX = 'index'
|
|
||||||
|
|
||||||
# Constant defining session attribute key for the event text key for date of events.
|
|
||||||
SESSION_TEXT = 'text'
|
|
||||||
|
|
||||||
|
|
||||||
@ask.launch
|
|
||||||
def launch():
|
|
||||||
speech_output = 'History buff. What day do you want events for?'
|
|
||||||
reprompt_text = "With History Buff, you can get historical events for any day of the year. " + \
|
|
||||||
"For example, you could say today, or August thirtieth. " + \
|
|
||||||
"Now, which day do you want?"
|
|
||||||
return question(speech_output).reprompt(reprompt_text)
|
|
||||||
|
|
||||||
|
|
||||||
@ask.intent('GetFirstEventIntent', convert={ 'day': 'date' })
|
|
||||||
def get_first_event(day):
|
|
||||||
month_name = day.strftime('%B')
|
|
||||||
day_number = day.day
|
|
||||||
events = _get_json_events_from_wikipedia(month_name, day_number)
|
|
||||||
if not events:
|
|
||||||
speech_output = "There is a problem connecting to Wikipedia at this time. Please try again later."
|
|
||||||
return statement('<speak>{}</speak>'.format(speech_output))
|
|
||||||
else:
|
|
||||||
card_title = "Events on {} {}".format(month_name, day_number)
|
|
||||||
speech_output = "<p>For {} {}</p>".format(month_name, day_number)
|
|
||||||
card_output = ""
|
|
||||||
for i in range(PAGINATION_SIZE):
|
|
||||||
speech_output += "<p>{}</p>".format(events[i])
|
|
||||||
card_output += "{}\n".format(events[i])
|
|
||||||
speech_output += " Wanna go deeper into history?"
|
|
||||||
card_output += " Wanna go deeper into history?"
|
|
||||||
reprompt_text = "With History Buff, you can get historical events for any day of the year. " + \
|
|
||||||
"For example, you could say today, or August thirtieth. " + \
|
|
||||||
"Now, which day do you want?"
|
|
||||||
session.attributes[SESSION_INDEX] = PAGINATION_SIZE
|
|
||||||
session.attributes[SESSION_TEXT] = events
|
|
||||||
speech_output = '<speak>{}</speak>'.format(speech_output)
|
|
||||||
return question(speech_output).reprompt(reprompt_text).simple_card(card_title, card_output)
|
|
||||||
|
|
||||||
|
|
||||||
@ask.intent('GetNextEventIntent')
|
|
||||||
def get_next_event():
|
|
||||||
events = session.attributes[SESSION_TEXT]
|
|
||||||
index = session.attributes[SESSION_INDEX]
|
|
||||||
card_title = "More events on this day in history"
|
|
||||||
speech_output = ""
|
|
||||||
card_output = ""
|
|
||||||
i = 0
|
|
||||||
while i < PAGINATION_SIZE and index < len(events):
|
|
||||||
speech_output += "<p>{}</p>".format(events[index])
|
|
||||||
card_output += "{}\n".format(events[index])
|
|
||||||
i += 1
|
|
||||||
index += 1
|
|
||||||
speech_output += " Wanna go deeper into history?"
|
|
||||||
reprompt_text = "Do you want to know more about what happened on this date?"
|
|
||||||
session.attributes[SESSION_INDEX] = index
|
|
||||||
speech_output = '<speak>{}</speak>'.format(speech_output)
|
|
||||||
return question(speech_output).reprompt(reprompt_text).simple_card(card_title, card_output)
|
|
||||||
|
|
||||||
|
|
||||||
@ask.intent('AMAZON.StopIntent')
|
|
||||||
def stop():
|
|
||||||
return statement("Goodbye")
|
|
||||||
|
|
||||||
|
|
||||||
@ask.intent('AMAZON.CancelIntent')
|
|
||||||
def cancel():
|
|
||||||
return statement("Goodbye")
|
|
||||||
|
|
||||||
|
|
||||||
@ask.session_ended
|
|
||||||
def session_ended():
|
|
||||||
return "{}", 200
|
|
||||||
|
|
||||||
|
|
||||||
def _get_json_events_from_wikipedia(month, date):
|
|
||||||
url = "{}{}_{}".format(URL_PREFIX, month, date)
|
|
||||||
data = urlopen(url).read().decode('utf-8')
|
|
||||||
return _parse_json(data)
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_json(text):
|
|
||||||
events = []
|
|
||||||
try:
|
|
||||||
slice_start = text.index("\\nEvents\\n") + SIZE_OF_EVENTS
|
|
||||||
slice_end = text.index("\\n\\n\\nBirths")
|
|
||||||
text = text[slice_start:slice_end];
|
|
||||||
except ValueError:
|
|
||||||
return events
|
|
||||||
start_index = end_index = 0
|
|
||||||
done = False
|
|
||||||
while not done:
|
|
||||||
try:
|
|
||||||
end_index = text.index('\\n', start_index + DELIMITER_SIZE)
|
|
||||||
event_text = text[start_index:end_index]
|
|
||||||
start_index = end_index + 2
|
|
||||||
except ValueError:
|
|
||||||
event_text = text[start_index:]
|
|
||||||
done = True
|
|
||||||
# replace dashes returned in text from Wikipedia's API
|
|
||||||
event_text = event_text.replace('\\u2013', '')
|
|
||||||
# add comma after year so Alexa pauses before continuing with the sentence
|
|
||||||
event_text = re.sub('^\d+', r'\g<0>,', event_text)
|
|
||||||
events.append(event_text)
|
|
||||||
events.reverse()
|
|
||||||
return events
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
if 'ASK_VERIFY_REQUESTS' in os.environ:
|
|
||||||
verify = str(os.environ.get('ASK_VERIFY_REQUESTS', '')).lower()
|
|
||||||
if verify == 'false':
|
|
||||||
app.config['ASK_VERIFY_REQUESTS'] = False
|
|
||||||
app.run(debug=True)
|
|
||||||
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
{
|
|
||||||
"intents": [
|
|
||||||
{
|
|
||||||
"intent": "GetFirstEventIntent",
|
|
||||||
"slots": [
|
|
||||||
{
|
|
||||||
"name": "day",
|
|
||||||
"type": "AMAZON.DATE"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"intent": "GetNextEventIntent"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"intent": "AMAZON.HelpIntent"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"intent": "AMAZON.StopIntent"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"intent": "AMAZON.CancelIntent"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
GetFirstEventIntent get events for {day}
|
|
||||||
GetFirstEventIntent give me events for {day}
|
|
||||||
GetFirstEventIntent what happened on {day}
|
|
||||||
GetFirstEventIntent what happened
|
|
||||||
GetFirstEventIntent {day}
|
|
||||||
|
|
||||||
GetNextEventIntent yes
|
|
||||||
GetNextEventIntent yup
|
|
||||||
GetNextEventIntent sure
|
|
||||||
GetNextEventIntent yes please
|
|
||||||
|
|
||||||
AMAZON.StopIntent no
|
|
||||||
AMAZON.StopIntent nope
|
|
||||||
AMAZON.StopIntent no thanks
|
|
||||||
AMAZON.StopIntent no thank you
|
|
||||||
|
|
@ -1,81 +0,0 @@
|
||||||
{
|
|
||||||
"interactionModel": {
|
|
||||||
"languageModel": {
|
|
||||||
"invocationName": "demo",
|
|
||||||
"intents": [
|
|
||||||
{
|
|
||||||
"name": "AMAZON.FallbackIntent",
|
|
||||||
"samples": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "AMAZON.CancelIntent",
|
|
||||||
"samples": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "AMAZON.HelpIntent",
|
|
||||||
"samples": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "AMAZON.StopIntent",
|
|
||||||
"samples": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "BuySkillItemIntent",
|
|
||||||
"slots": [
|
|
||||||
{
|
|
||||||
"name": "ProductName",
|
|
||||||
"type": "LIST_OF_PRODUCT_NAMES"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"samples": [
|
|
||||||
"{ProductName}",
|
|
||||||
"buy",
|
|
||||||
"shop",
|
|
||||||
"buy {ProductName}",
|
|
||||||
"purchase {ProductName}",
|
|
||||||
"want {ProductName}",
|
|
||||||
"would like {ProductName}"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "RefundSkillItemIntent",
|
|
||||||
"slots": [
|
|
||||||
{
|
|
||||||
"name": "ProductName",
|
|
||||||
"type": "LIST_OF_PRODUCT_NAMES"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"samples": [
|
|
||||||
"cancel {ProductName}",
|
|
||||||
"return {ProductName}",
|
|
||||||
"refund {ProductName}",
|
|
||||||
"want a refund for {ProductName}",
|
|
||||||
"would like to return {ProductName}"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"types": [
|
|
||||||
{
|
|
||||||
"name": "LIST_OF_PRODUCT_NAMES",
|
|
||||||
"values": [
|
|
||||||
{
|
|
||||||
"name": {
|
|
||||||
"value": "monthly subscription"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": {
|
|
||||||
"value": "start smoking"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": {
|
|
||||||
"value": "stop smoking"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,79 +0,0 @@
|
||||||
import requests
|
|
||||||
from flask import json
|
|
||||||
from flask_ask import logger
|
|
||||||
|
|
||||||
class Product():
|
|
||||||
'''
|
|
||||||
Object model for inSkillProducts and methods to access products.
|
|
||||||
|
|
||||||
{"inSkillProducts":[
|
|
||||||
{"productId":"amzn1.adg.product.your_product_id",
|
|
||||||
"referenceName":"product_name",
|
|
||||||
"type":"ENTITLEMENT",
|
|
||||||
"name":"product name",
|
|
||||||
"summary":"This product has helped many people.",
|
|
||||||
"entitled":"NOT_ENTITLED",
|
|
||||||
"purchasable":"NOT_PURCHASABLE"}],
|
|
||||||
"nextToken":null,
|
|
||||||
"truncated":false}
|
|
||||||
|
|
||||||
'''
|
|
||||||
|
|
||||||
def __init__(self, apiAccessToken):
|
|
||||||
self.token = apiAccessToken
|
|
||||||
self.product_list = self.query()
|
|
||||||
|
|
||||||
|
|
||||||
def query(self):
|
|
||||||
# Information required to invoke the API is available in the session
|
|
||||||
apiEndpoint = "https://api.amazonalexa.com"
|
|
||||||
apiPath = "/v1/users/~current/skills/~current/inSkillProducts"
|
|
||||||
token = "bearer " + self.token
|
|
||||||
language = "en-US" #self.event.request.locale
|
|
||||||
|
|
||||||
url = apiEndpoint + apiPath
|
|
||||||
headers = {
|
|
||||||
"Content-Type" : 'application/json',
|
|
||||||
"Accept-Language" : language,
|
|
||||||
"Authorization" : token
|
|
||||||
}
|
|
||||||
#Call the API
|
|
||||||
res = requests.get(url, headers=headers)
|
|
||||||
logger.info('PRODUCTS:' + '*' * 80)
|
|
||||||
logger.info(res.status_code)
|
|
||||||
logger.info(res.text)
|
|
||||||
if res.status_code == 200:
|
|
||||||
data = json.loads(res.text)
|
|
||||||
return data['inSkillProducts']
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def list(self):
|
|
||||||
""" return list of purchasable and not entitled products"""
|
|
||||||
mylist = []
|
|
||||||
for prod in self.product_list:
|
|
||||||
if self.purchasable(prod) and not self.entitled(prod):
|
|
||||||
mylist.append(prod)
|
|
||||||
return mylist
|
|
||||||
|
|
||||||
def purchasable(self, product):
|
|
||||||
""" return True if purchasable product"""
|
|
||||||
return 'PURCHASABLE' == product['purchasable']
|
|
||||||
|
|
||||||
def entitled(self, product):
|
|
||||||
""" return True if entitled product"""
|
|
||||||
return 'ENTITLED' == product['entitled']
|
|
||||||
|
|
||||||
|
|
||||||
def productId(self, name):
|
|
||||||
print(self.product_list)
|
|
||||||
for prod in self.product_list:
|
|
||||||
if name == prod['name'].lower():
|
|
||||||
return prod['productId']
|
|
||||||
return None
|
|
||||||
|
|
||||||
def productName(self, id):
|
|
||||||
for prod in self.product_list:
|
|
||||||
if id == prod['productId']:
|
|
||||||
return prod['name']
|
|
||||||
return None
|
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
import requests
|
|
||||||
|
|
||||||
from flask import Flask, json, render_template
|
|
||||||
from flask_ask import Ask, request, session, question, statement, context, buy, upsell, refund, logger
|
|
||||||
from model import Product
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
ask = Ask(app, "/")
|
|
||||||
logging.getLogger('flask_ask').setLevel(logging.DEBUG)
|
|
||||||
|
|
||||||
|
|
||||||
PRODUCT_KEY = "PRODUCT"
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ask.on_purchase_completed( mapping={'payload': 'payload','name':'name','status':'status','token':'token'})
|
|
||||||
def completed(payload, name, status, token):
|
|
||||||
products = Product(context.System.apiAccessToken)
|
|
||||||
logger.info('on-purchase-completed {}'.format( request))
|
|
||||||
logger.info('payload: {} {}'.format(payload.purchaseResult, payload.productId))
|
|
||||||
logger.info('name: {}'.format(name))
|
|
||||||
logger.info('token: {}'.format(token))
|
|
||||||
logger.info('status: {}'.format( status.code == 200))
|
|
||||||
product_name = products.productName(payload.productId)
|
|
||||||
logger.info('Product name'.format(product_name))
|
|
||||||
if status.code == '200' and ('ACCEPTED' in payload.purchaseResult):
|
|
||||||
return question('To listen it just say - play {} '.format(product_name))
|
|
||||||
else:
|
|
||||||
return question('Do you want to buy another product?')
|
|
||||||
|
|
||||||
@ask.launch
|
|
||||||
def launch():
|
|
||||||
products = Product(context.System.apiAccessToken)
|
|
||||||
question_text = render_template('welcome', products=products.list())
|
|
||||||
reprompt_text = render_template('welcome_reprompt')
|
|
||||||
return question(question_text).reprompt(reprompt_text).simple_card('Welcome', question_text)
|
|
||||||
|
|
||||||
|
|
||||||
@ask.intent('BuySkillItemIntent', mapping={'product_name': 'ProductName'})
|
|
||||||
def buy_intent(product_name):
|
|
||||||
products = Product(context.System.apiAccessToken)
|
|
||||||
logger.info("PRODUCT: {}".format(product_name))
|
|
||||||
buy_card = render_template('buy_card', product=product_name)
|
|
||||||
productId = products.productId(product_name)
|
|
||||||
if productId is not None:
|
|
||||||
session.attributes[PRODUCT_KEY] = productId
|
|
||||||
else:
|
|
||||||
return statement("I didn't find a product {}".format(product_name))
|
|
||||||
raise NotImplementedError()
|
|
||||||
return buy(productId).simple_card('Welcome', question_text)
|
|
||||||
|
|
||||||
#return upsell(product,'get this great product')
|
|
||||||
|
|
||||||
|
|
||||||
@ask.intent('RefundSkillItemIntent', mapping={'product_name': 'ProductName'})
|
|
||||||
def refund_intent(product_name):
|
|
||||||
refund_card = render_template('refund_card')
|
|
||||||
logger.info("PRODUCT: {}".format(product_name))
|
|
||||||
|
|
||||||
products = Product(context.System.apiAccessToken)
|
|
||||||
productId = products.productId(product_name)
|
|
||||||
|
|
||||||
if productId is not None:
|
|
||||||
session.attributes[PRODUCT_KEY] = productId
|
|
||||||
else:
|
|
||||||
raise NotImplementedError()
|
|
||||||
return refund(productId)
|
|
||||||
|
|
||||||
|
|
||||||
@ask.intent('AMAZON.FallbackIntent')
|
|
||||||
def fallback_intent():
|
|
||||||
return statement("FallbackIntent")
|
|
||||||
|
|
||||||
|
|
||||||
@ask.session_ended
|
|
||||||
def session_ended():
|
|
||||||
return "{}", 200
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
if 'ASK_VERIFY_REQUESTS' in os.environ:
|
|
||||||
verify = str(os.environ.get('ASK_VERIFY_REQUESTS', '')).lower()
|
|
||||||
if verify == 'false':
|
|
||||||
app.config['ASK_VERIFY_REQUESTS'] = False
|
|
||||||
app.run(debug=True)
|
|
||||||
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
welcome: |
|
|
||||||
Welcome to the Flask-ask purchase demo.
|
|
||||||
{% if products %}
|
|
||||||
Here is a list of products available:
|
|
||||||
{%for product in products%}
|
|
||||||
{{ product.name}},
|
|
||||||
{%endfor %}
|
|
||||||
Please tell me the product name you want to buy.
|
|
||||||
{%else%}
|
|
||||||
You have no products configured. Please configure products using ASK CLI.
|
|
||||||
{%endif%}
|
|
||||||
|
|
||||||
|
|
||||||
welcome_reprompt: Please tell me the product name you want to buy.
|
|
||||||
|
|
||||||
refund_card: |
|
|
||||||
Refund Intent for {{product}}
|
|
||||||
|
|
||||||
|
|
||||||
buy_card: |
|
|
||||||
Buy Intent for {{product}}
|
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
from flask import Flask
|
|
||||||
from flask_ask import Ask, request, session, question, statement
|
|
||||||
import random
|
|
||||||
import yaml
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
ask = Ask(app, "/")
|
|
||||||
logging.getLogger('flask_ask').setLevel(logging.DEBUG)
|
|
||||||
|
|
||||||
|
|
||||||
@ask.launch
|
|
||||||
def launch():
|
|
||||||
return question("Each excercise will take about 5 minutes. Are you Ready?")
|
|
||||||
|
|
||||||
|
|
||||||
def get_workoutplan():
|
|
||||||
with open("C:/Users/John/Desktop/GST/samples/reddit/workouts.yaml", 'r') as stream:
|
|
||||||
exercises = yaml.load(stream)
|
|
||||||
exercise_names = random.sample(list(exercises), 5)
|
|
||||||
workout_plan = []
|
|
||||||
for exercise_name in exercise_names:
|
|
||||||
exercise = str(exercises[exercise_name]["duration"][0]) + " " + str(exercises[exercise_name]["duration"][1] )+ " of " + exercise_name
|
|
||||||
workout_plan.append(exercise)
|
|
||||||
return workout_plan
|
|
||||||
|
|
||||||
|
|
||||||
@ask.intent('YesIntent', mapping={'part': 'Part'})
|
|
||||||
def start_workout(part):
|
|
||||||
print(part)
|
|
||||||
workout_plan = get_workoutplan()
|
|
||||||
response = "Do "
|
|
||||||
|
|
||||||
for excercise in workout_plan[:-1]:
|
|
||||||
response += excercise + ", "
|
|
||||||
response += " and " + workout_plan[-1]
|
|
||||||
print(response)
|
|
||||||
return statement(response)
|
|
||||||
|
|
||||||
@ask.intent('NoIntent')
|
|
||||||
def no():
|
|
||||||
return statement("Allright, maybe later.")
|
|
||||||
|
|
||||||
@ask.intent('AMAZON.HelpIntent')
|
|
||||||
def help():
|
|
||||||
speech_text = 'If you want a quick workout just say "workout"'
|
|
||||||
return statement(speech_text)
|
|
||||||
|
|
||||||
|
|
||||||
@ask.session_ended
|
|
||||||
def session_ended():
|
|
||||||
return "{}", 200
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
if 'ASK_VERIFY_REQUESTS' in os.environ:
|
|
||||||
verify = str(os.environ.get('ASK_VERIFY_REQUESTS', '')).lower()
|
|
||||||
if verify == 'false':
|
|
||||||
app.config['ASK_VERIFY_REQUESTS'] = False
|
|
||||||
app.run()
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
|
|
||||||
{
|
|
||||||
"interactionModel": {
|
|
||||||
"languageModel": {
|
|
||||||
"invocationName": "give me a workout",
|
|
||||||
"intents": [
|
|
||||||
{
|
|
||||||
"name": "AMAZON.CancelIntent",
|
|
||||||
"samples": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "AMAZON.HelpIntent",
|
|
||||||
"samples": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "AMAZON.StopIntent",
|
|
||||||
"samples": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "YesIntent",
|
|
||||||
"slots": [],
|
|
||||||
"samples": [
|
|
||||||
"yes"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "NoIntent",
|
|
||||||
"slots": [],
|
|
||||||
"samples": [
|
|
||||||
"no"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "HelloWorldIntent",
|
|
||||||
"slots": [],
|
|
||||||
"samples": [
|
|
||||||
"let's workout",
|
|
||||||
"give me todays workout",
|
|
||||||
"what's todays workout"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "AMAZON.NavigateHomeIntent",
|
|
||||||
"samples": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"types": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
HelloWorldIntent say hello
|
|
||||||
HelloWorldIntent say hello world
|
|
||||||
HelloWorldIntent hello
|
|
||||||
HelloWorldIntent say hi
|
|
||||||
HelloWorldIntent say hi world
|
|
||||||
HelloWorldIntent hi
|
|
||||||
HelloWorldIntent how are you
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
Chest
|
|
||||||
Legs
|
|
||||||
Torso
|
|
||||||
Upper body
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
from flask import Flask
|
|
||||||
from flask_ask import Ask, request, session, question, statement
|
|
||||||
import random
|
|
||||||
import yaml
|
|
||||||
|
|
||||||
def read_exercises():
|
|
||||||
with open("workouts.yaml", 'r') as stream:
|
|
||||||
exercises = yaml.load(stream)
|
|
||||||
exercise_names = random.sample(list(exercises), 5)
|
|
||||||
workout_plan = []
|
|
||||||
for exercise_name in exercise_names:
|
|
||||||
exercise = str(exercises[exercise_name]["duration"][0]) + " " + str(exercises[exercise_name]["duration"][1] )+ " of " + exercise_name
|
|
||||||
workout_plan.append(exercise)
|
|
||||||
print(exercise)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
read_exercises()
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
---
|
|
||||||
|
|
||||||
Run in place:
|
|
||||||
duration:
|
|
||||||
- 30
|
|
||||||
- seconds
|
|
||||||
explaination: "just run in place... dummy"
|
|
||||||
|
|
||||||
Jumping jacks:
|
|
||||||
duration:
|
|
||||||
- 30
|
|
||||||
- repetitions
|
|
||||||
explaination: "act like your Patric Star jumping of joy"
|
|
||||||
|
|
||||||
Burpees:
|
|
||||||
duration:
|
|
||||||
- 10
|
|
||||||
- repetitions
|
|
||||||
explaination: "do a push-up, then stand up and do a little jump"
|
|
||||||
|
|
||||||
Push-ups:
|
|
||||||
duration:
|
|
||||||
- 10
|
|
||||||
- repetitions
|
|
||||||
explaination: "lie on the ground, with your arms besides your toso. Angle them at 45° and push."
|
|
||||||
|
|
||||||
Mountain climbers:
|
|
||||||
duration:
|
|
||||||
- 20
|
|
||||||
- repetitions
|
|
||||||
explaination: "get in the upwards push-up position and move alternating knees to your chest"
|
|
||||||
|
|
||||||
Jump lunges:
|
|
||||||
duration:
|
|
||||||
- 15
|
|
||||||
- repetitions
|
|
||||||
explaination: "Walk in a way that your knees almost touch the ground"
|
|
||||||
|
|
||||||
Jump squats:
|
|
||||||
duration:
|
|
||||||
- 15
|
|
||||||
- repetitions
|
|
||||||
explaination: "jump and pull ypur knees to your chest"
|
|
||||||
|
|
||||||
Bicycle crunches:
|
|
||||||
duration:
|
|
||||||
- 20
|
|
||||||
- repetitions
|
|
||||||
explaination: "lay on your back with your hands behind your head, move your ellenbow to your opposite knee"
|
|
||||||
|
|
||||||
Squats:
|
|
||||||
duration:
|
|
||||||
- 15
|
|
||||||
- repetitions
|
|
||||||
explaination: "you know what a squat is, mind the 90°"
|
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
|
|
||||||
from flask import Flask, json, render_template
|
|
||||||
from flask_ask import Ask, request, session, question, statement
|
|
||||||
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
ask = Ask(app, "/")
|
|
||||||
logging.getLogger('flask_ask').setLevel(logging.DEBUG)
|
|
||||||
|
|
||||||
|
|
||||||
COLOR_KEY = "COLOR"
|
|
||||||
|
|
||||||
|
|
||||||
@ask.launch
|
|
||||||
def launch():
|
|
||||||
card_title = render_template('card_title')
|
|
||||||
question_text = render_template('welcome')
|
|
||||||
reprompt_text = render_template('welcome_reprompt')
|
|
||||||
return question(question_text).reprompt(reprompt_text).simple_card(card_title, question_text)
|
|
||||||
|
|
||||||
|
|
||||||
@ask.intent('MyColorIsIntent', mapping={'color': 'Color'})
|
|
||||||
def my_color_is(color):
|
|
||||||
card_title = render_template('card_title')
|
|
||||||
if color is not None:
|
|
||||||
session.attributes[COLOR_KEY] = color
|
|
||||||
question_text = render_template('known_color', color=color)
|
|
||||||
reprompt_text = render_template('known_color_reprompt')
|
|
||||||
else:
|
|
||||||
question_text = render_template('unknown_color')
|
|
||||||
reprompt_text = render_template('unknown_color_reprompt')
|
|
||||||
return question(question_text).reprompt(reprompt_text).simple_card(card_title, question_text)
|
|
||||||
|
|
||||||
|
|
||||||
@ask.intent('WhatsMyColorIntent')
|
|
||||||
def whats_my_color():
|
|
||||||
card_title = render_template('card_title')
|
|
||||||
color = session.attributes.get(COLOR_KEY)
|
|
||||||
if color is not None:
|
|
||||||
statement_text = render_template('known_color_bye', color=color)
|
|
||||||
return statement(statement_text).simple_card(card_title, statement_text)
|
|
||||||
else:
|
|
||||||
question_text = render_template('unknown_color_reprompt')
|
|
||||||
return question(question_text).reprompt(question_text).simple_card(card_title, question_text)
|
|
||||||
|
|
||||||
|
|
||||||
@ask.session_ended
|
|
||||||
def session_ended():
|
|
||||||
return "{}", 200
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
if 'ASK_VERIFY_REQUESTS' in os.environ:
|
|
||||||
verify = str(os.environ.get('ASK_VERIFY_REQUESTS', '')).lower()
|
|
||||||
if verify == 'false':
|
|
||||||
app.config['ASK_VERIFY_REQUESTS'] = False
|
|
||||||
app.run(debug=True)
|
|
||||||
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
{
|
|
||||||
"intents": [
|
|
||||||
{
|
|
||||||
"intent": "MyColorIsIntent",
|
|
||||||
"slots": [
|
|
||||||
{
|
|
||||||
"name": "Color",
|
|
||||||
"type": "LIST_OF_COLORS"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"intent": "WhatsMyColorIntent"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
MyColorIsIntent my color is {Color}
|
|
||||||
MyColorIsIntent my favorite color is {Color}
|
|
||||||
WhatsMyColorIntent whats my color
|
|
||||||
WhatsMyColorIntent what is my color
|
|
||||||
WhatsMyColorIntent say my color
|
|
||||||
WhatsMyColorIntent tell me my color
|
|
||||||
WhatsMyColorIntent whats my favorite color
|
|
||||||
WhatsMyColorIntent what is my favorite color
|
|
||||||
WhatsMyColorIntent say my favorite color
|
|
||||||
WhatsMyColorIntent tell me my favorite color
|
|
||||||
WhatsMyColorIntent tell me what my favorite color is
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
green
|
|
||||||
blue
|
|
||||||
purple
|
|
||||||
red
|
|
||||||
orange
|
|
||||||
yellow
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
welcome: |
|
|
||||||
Welcome to the Alexa Skills Kit sample. Please tell me your favorite color by
|
|
||||||
saying, my favorite color is red
|
|
||||||
|
|
||||||
welcome_reprompt: Please tell me your favorite color by saying, my favorite color is red
|
|
||||||
|
|
||||||
known_color: |
|
|
||||||
I now know that your favorite color is {{ color }}. You can ask me your favorite color
|
|
||||||
by saying, what's my favorite color?
|
|
||||||
|
|
||||||
known_color_reprompt: You can ask me your favorite color by saying, what's my favorite color?
|
|
||||||
|
|
||||||
known_color_bye: Your favorite color is {{ color }}. Goodbye
|
|
||||||
|
|
||||||
unknown_color: I'm not sure what your favorite color is, please try again
|
|
||||||
|
|
||||||
unknown_color_reprompt: |
|
|
||||||
I'm not sure what your favorite color is. You can tell me your favorite color by saying,
|
|
||||||
my favorite color is red
|
|
||||||
|
|
||||||
card_title: Session
|
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
from random import randint
|
|
||||||
|
|
||||||
from flask import Flask, render_template
|
|
||||||
from flask_ask import Ask, request, session, question, statement
|
|
||||||
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
ask = Ask(app, "/")
|
|
||||||
logging.getLogger('flask_ask').setLevel(logging.DEBUG)
|
|
||||||
|
|
||||||
|
|
||||||
@ask.launch
|
|
||||||
def launch():
|
|
||||||
return get_new_fact()
|
|
||||||
|
|
||||||
|
|
||||||
@ask.intent('GetNewFactIntent')
|
|
||||||
def get_new_fact():
|
|
||||||
num_facts = 13 # increment this when adding a new fact template
|
|
||||||
fact_index = randint(0, num_facts-1)
|
|
||||||
fact_text = render_template('space_fact_{}'.format(fact_index))
|
|
||||||
card_title = render_template('card_title')
|
|
||||||
return statement(fact_text).simple_card(card_title, fact_text)
|
|
||||||
|
|
||||||
|
|
||||||
@ask.intent('AMAZON.HelpIntent')
|
|
||||||
def help():
|
|
||||||
help_text = render_template('help')
|
|
||||||
return question(help_text).reprompt(help_text)
|
|
||||||
|
|
||||||
|
|
||||||
@ask.intent('AMAZON.StopIntent')
|
|
||||||
def stop():
|
|
||||||
bye_text = render_template('bye')
|
|
||||||
return statement(bye_text)
|
|
||||||
|
|
||||||
|
|
||||||
@ask.intent('AMAZON.CancelIntent')
|
|
||||||
def cancel():
|
|
||||||
bye_text = render_template('bye')
|
|
||||||
return statement(bye_text)
|
|
||||||
|
|
||||||
|
|
||||||
@ask.session_ended
|
|
||||||
def session_ended():
|
|
||||||
return "{}", 200
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
if 'ASK_VERIFY_REQUESTS' in os.environ:
|
|
||||||
verify = str(os.environ.get('ASK_VERIFY_REQUESTS', '')).lower()
|
|
||||||
if verify == 'false':
|
|
||||||
app.config['ASK_VERIFY_REQUESTS'] = False
|
|
||||||
app.run(debug=True)
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
{
|
|
||||||
"intents": [
|
|
||||||
{
|
|
||||||
"intent": "GetNewFactIntent"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"intent": "AMAZON.HelpIntent"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"intent": "AMAZON.StopIntent"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"intent": "AMAZON.CancelIntent"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
GetNewFactIntent a fact
|
|
||||||
GetNewFactIntent a space fact
|
|
||||||
GetNewFactIntent tell me a fact
|
|
||||||
GetNewFactIntent tell me a space fact
|
|
||||||
GetNewFactIntent give me a fact
|
|
||||||
GetNewFactIntent give me a space fact
|
|
||||||
GetNewFactIntent tell me trivia
|
|
||||||
GetNewFactIntent tell me a space trivia
|
|
||||||
GetNewFactIntent give me trivia
|
|
||||||
GetNewFactIntent give me a space trivia
|
|
||||||
GetNewFactIntent give me some information
|
|
||||||
GetNewFactIntent give me some space information
|
|
||||||
GetNewFactIntent tell me something
|
|
||||||
GetNewFactIntent give me something
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
space_fact_0: A year on Mercury is just 88 days long.
|
|
||||||
space_fact_1: Despite being farther from the Sun, Venus experiences higher temperatures than Mercury.
|
|
||||||
space_fact_2: Venus rotates counter-clockwise, possibly because of a collision in the past with an asteroid.
|
|
||||||
space_fact_3: On Mars, the Sun appears about half the size as it does on Earth.
|
|
||||||
space_fact_4: Earth is the only planet not named after a god.
|
|
||||||
space_fact_5: Jupiter has the shortest day of all the planets.
|
|
||||||
space_fact_6: The Milky Way galaxy will collide with the Andromeda Galaxy in about 5 billion years.
|
|
||||||
space_fact_7: The Sun contains 99.86% of the mass in the Solar System.
|
|
||||||
space_fact_8: The Sun is an almost perfect sphere.
|
|
||||||
space_fact_9: A total solar eclipse can happen once every 1 to 2 years. This makes them a rare event.
|
|
||||||
space_fact_10: Saturn radiates two and a half times more energy into space than it receives from the sun.
|
|
||||||
space_fact_11: The temperature inside the Sun can reach 15 million degrees Celsius.
|
|
||||||
space_fact_12: The Moon is moving approximately 3.8 cm away from our planet every year.
|
|
||||||
card_title: SpaceGeek
|
|
||||||
help: You can ask Space Geek tell me a space fact, or, you can say exit. What can I help you with?
|
|
||||||
bye: Goodbye
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
{
|
|
||||||
"intents": [
|
|
||||||
{
|
|
||||||
"intent": "OneshotTideIntent",
|
|
||||||
"slots": [
|
|
||||||
{
|
|
||||||
"name": "City",
|
|
||||||
"type": "LIST_OF_CITIES"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "State",
|
|
||||||
"type": "LIST_OF_STATES"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Date",
|
|
||||||
"type": "AMAZON.DATE"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"intent": "DialogTideIntent",
|
|
||||||
"slots": [
|
|
||||||
{
|
|
||||||
"name": "City",
|
|
||||||
"type": "LIST_OF_CITIES"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "State",
|
|
||||||
"type": "LIST_OF_STATES"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Date",
|
|
||||||
"type": "AMAZON.DATE"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"intent": "SupportedCitiesIntent"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"intent": "AMAZON.HelpIntent"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"intent": "AMAZON.StopIntent"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"intent": "AMAZON.CancelIntent"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -1,92 +0,0 @@
|
||||||
DialogTideIntent {City}
|
|
||||||
DialogTideIntent {City} {State}
|
|
||||||
DialogTideIntent {Date}
|
|
||||||
|
|
||||||
OneshotTideIntent get high tide
|
|
||||||
OneshotTideIntent get high tide for {City} {State}
|
|
||||||
OneshotTideIntent get high tide for {City} {State} {Date}
|
|
||||||
OneshotTideIntent get high tide for {City} {Date}
|
|
||||||
OneshotTideIntent get high tide for {Date}
|
|
||||||
OneshotTideIntent get high tide {Date}
|
|
||||||
OneshotTideIntent get the high tide for {City} {Date}
|
|
||||||
OneshotTideIntent get the next tide for {City} for {Date}
|
|
||||||
OneshotTideIntent get the tides for {Date}
|
|
||||||
OneshotTideIntent get tide information for {City}
|
|
||||||
OneshotTideIntent get tide information for {City} {State}
|
|
||||||
OneshotTideIntent get tide information for {City} {State} on {Date}
|
|
||||||
OneshotTideIntent get tide information for {City} city
|
|
||||||
OneshotTideIntent get tide information for {City} for {Date}
|
|
||||||
OneshotTideIntent get tide information for {City} on {Date}
|
|
||||||
OneshotTideIntent get tide information for {City} {Date}
|
|
||||||
OneshotTideIntent get tides for {City}
|
|
||||||
OneshotTideIntent get tides for {City} {State}
|
|
||||||
OneshotTideIntent get tides for {City} {State} {Date}
|
|
||||||
OneshotTideIntent get tides for {City} {Date}
|
|
||||||
OneshotTideIntent tide information
|
|
||||||
OneshotTideIntent tide information for {City}
|
|
||||||
OneshotTideIntent tide information for {City} {State}
|
|
||||||
OneshotTideIntent tide information for {City} on {Date}
|
|
||||||
OneshotTideIntent tide information for {Date}
|
|
||||||
OneshotTideIntent when high tide is
|
|
||||||
OneshotTideIntent when is high tide
|
|
||||||
OneshotTideIntent when is high tide for {City} {Date}
|
|
||||||
OneshotTideIntent when is high tide in {City}
|
|
||||||
OneshotTideIntent when is high tide in {City} {State}
|
|
||||||
OneshotTideIntent when is high tide in {City} city
|
|
||||||
OneshotTideIntent when is high tide in {City} on {Date}
|
|
||||||
OneshotTideIntent when is high tide on {Date}
|
|
||||||
OneshotTideIntent when is high tide {Date}
|
|
||||||
OneshotTideIntent when is next tide
|
|
||||||
OneshotTideIntent when is next tide in {City} {State}
|
|
||||||
OneshotTideIntent when is next tide in {City} {State} on {Date}
|
|
||||||
OneshotTideIntent when is next tide on {Date}
|
|
||||||
OneshotTideIntent when is the highest tide in {City}
|
|
||||||
OneshotTideIntent when is the highest tide in {City} {Date}
|
|
||||||
OneshotTideIntent when is the highest tide {Date}
|
|
||||||
OneshotTideIntent when is the next high tide {Date}
|
|
||||||
OneshotTideIntent when is the next highest water
|
|
||||||
OneshotTideIntent when is the next highest water for {City}
|
|
||||||
OneshotTideIntent when is the next highest water for {City} {State} for {Date}
|
|
||||||
OneshotTideIntent when is the next highest water for {City} {State}
|
|
||||||
OneshotTideIntent when is the next highest water for {Date}
|
|
||||||
OneshotTideIntent when is the next tide for {City}
|
|
||||||
OneshotTideIntent when is the next tide for {City} {State}
|
|
||||||
OneshotTideIntent when is the next tide for {City} city
|
|
||||||
OneshotTideIntent when is the next tide for {City} for {Date}
|
|
||||||
OneshotTideIntent when is the next tide for {Date}
|
|
||||||
OneshotTideIntent when is today's high tide
|
|
||||||
OneshotTideIntent when is today's highest tide {Date}
|
|
||||||
OneshotTideIntent when will the water be highest for {City}
|
|
||||||
OneshotTideIntent when will the water be highest for {City} for {Date}
|
|
||||||
OneshotTideIntent when will the water be highest for {Date}
|
|
||||||
OneshotTideIntent when will the water be highest {Date}
|
|
||||||
OneshotTideIntent when is high tide on {Date}
|
|
||||||
OneshotTideIntent when is the next tide for {Date}
|
|
||||||
OneshotTideIntent when is next tide on {Date}
|
|
||||||
OneshotTideIntent get the tides for {Date}
|
|
||||||
OneshotTideIntent when is the highest tide {Date}
|
|
||||||
OneshotTideIntent when is the next highest water for {Date}
|
|
||||||
OneshotTideIntent when is high tide on {Date}
|
|
||||||
OneshotTideIntent get high tide for {Date}
|
|
||||||
OneshotTideIntent get high tide {Date}
|
|
||||||
OneshotTideIntent when is high tide {Date}
|
|
||||||
OneshotTideIntent tide information for {Date}
|
|
||||||
OneshotTideIntent when is high tide in {City} on {Date}
|
|
||||||
OneshotTideIntent get the next tide for {City} for {Date}
|
|
||||||
OneshotTideIntent get high tide for {City} California {Date}
|
|
||||||
OneshotTideIntent when is high tide for {City} {Date}
|
|
||||||
OneshotTideIntent tide information for {City} on {Date}
|
|
||||||
OneshotTideIntent when is high tide in {City} on {Date}
|
|
||||||
OneshotTideIntent when is the next tide for {City} for {Date}
|
|
||||||
OneshotTideIntent when is next tide in {City} {State} on {Date}
|
|
||||||
OneshotTideIntent get high tide for {City} {Date}
|
|
||||||
OneshotTideIntent get the high tide for {City} {Date}
|
|
||||||
OneshotTideIntent tide information for {City} on {Date}
|
|
||||||
OneshotTideIntent get tide information for {City} on {Date}
|
|
||||||
OneshotTideIntent get tides for {City} {Date}
|
|
||||||
|
|
||||||
SupportedCitiesIntent what cities
|
|
||||||
SupportedCitiesIntent what cities are supported
|
|
||||||
SupportedCitiesIntent which cities are supported
|
|
||||||
SupportedCitiesIntent which cities
|
|
||||||
SupportedCitiesIntent which cities do you know
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
seattle
|
|
||||||
los angeles
|
|
||||||
monterey
|
|
||||||
san diego
|
|
||||||
san francisco
|
|
||||||
boston
|
|
||||||
new york
|
|
||||||
miami
|
|
||||||
wilmington
|
|
||||||
tampa
|
|
||||||
galveston
|
|
||||||
morehead
|
|
||||||
new orleans
|
|
||||||
beaufort
|
|
||||||
myrtle beach
|
|
||||||
virginia beach
|
|
||||||
charleston
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
california
|
|
||||||
florida
|
|
||||||
louisiana
|
|
||||||
massachusetts
|
|
||||||
new york
|
|
||||||
north carolina
|
|
||||||
south carolina
|
|
||||||
texas
|
|
||||||
virginia
|
|
||||||
washington
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
welcome: |
|
|
||||||
<speak>
|
|
||||||
Welcome to Tide Pooler.
|
|
||||||
<audio src='https://s3.amazonaws.com/ask-storage/tidePooler/OceanWaves.mp3'/>
|
|
||||||
Which city would you like tide information for?
|
|
||||||
</speak>
|
|
||||||
|
|
||||||
tide_info: |
|
|
||||||
{{ date | humanize_date }} in {{ city }}, the first high tide will be around
|
|
||||||
{{ tideinfo.first_high_tide_time | humanize_time }}, and will peak at about
|
|
||||||
{{ tideinfo.first_high_tide_height | humanize_height }}, followed by a low tide around
|
|
||||||
{{ tideinfo.low_tide_time | humanize_time }}, that will be about
|
|
||||||
{{ tideinfo.low_tide_height | humanize_height }}.
|
|
||||||
|
|
||||||
The second high tide will be around {{ tideinfo.second_high_tide_time | humanize_time }},
|
|
||||||
and will peak at about {{ tideinfo.second_high_tide_height | humanize_height }}
|
|
||||||
|
|
||||||
help: |
|
|
||||||
I can lead you through providing a city and day of the week to get tide information, or you can simply open
|
|
||||||
Tide Pooler and ask a question like, get tide information for Seattle on Saturday. For a list of supported
|
|
||||||
cities, ask what cities are supported. Which city would you like tide information for?
|
|
||||||
|
|
||||||
list_cities: |
|
|
||||||
Currently, I know tide information for these coastal cities: {{ cities }}
|
|
||||||
Which city would you like tide information for?
|
|
||||||
|
|
||||||
list_cities_reprompt: Which city would you like tide information for?
|
|
||||||
|
|
||||||
city_dialog: For which city would you like tide information for {{ date | humanize_date }}
|
|
||||||
|
|
||||||
city_dialog_reprompt: For which city?
|
|
||||||
|
|
||||||
date_dialog: For which date would you like tide information for {{ city }}?
|
|
||||||
|
|
||||||
date_dialog_reprompt: For which date?
|
|
||||||
|
|
||||||
date_dialog2: Please try again saying a day of the week, for example, Saturday
|
|
||||||
|
|
||||||
noaa_problem: Sorry, the National Oceanic tide service is experiencing a problem. Please try again later.
|
|
||||||
|
|
||||||
bye: Goodbye
|
|
||||||
|
|
@ -1,298 +0,0 @@
|
||||||
import os
|
|
||||||
import logging
|
|
||||||
import datetime
|
|
||||||
import math
|
|
||||||
import re
|
|
||||||
from six.moves.urllib.request import urlopen
|
|
||||||
from six.moves.urllib.parse import urlencode
|
|
||||||
|
|
||||||
import aniso8601
|
|
||||||
from flask import Flask, json, render_template
|
|
||||||
from flask_ask import Ask, request, session, question, statement
|
|
||||||
|
|
||||||
|
|
||||||
ENDPOINT = "http://tidesandcurrents.noaa.gov/api/datagetter"
|
|
||||||
SESSION_CITY = "city"
|
|
||||||
SESSION_DATE = "date"
|
|
||||||
|
|
||||||
# NOAA station codes
|
|
||||||
STATION_CODE_SEATTLE = "9447130"
|
|
||||||
STATION_CODE_SAN_FRANCISCO = "9414290"
|
|
||||||
STATION_CODE_MONTEREY = "9413450"
|
|
||||||
STATION_CODE_LOS_ANGELES = "9410660"
|
|
||||||
STATION_CODE_SAN_DIEGO = "9410170"
|
|
||||||
STATION_CODE_BOSTON = "8443970"
|
|
||||||
STATION_CODE_NEW_YORK = "8518750"
|
|
||||||
STATION_CODE_VIRGINIA_BEACH = "8638863"
|
|
||||||
STATION_CODE_WILMINGTON = "8658163"
|
|
||||||
STATION_CODE_CHARLESTON = "8665530"
|
|
||||||
STATION_CODE_BEAUFORT = "8656483"
|
|
||||||
STATION_CODE_MYRTLE_BEACH = "8661070"
|
|
||||||
STATION_CODE_MIAMI = "8723214"
|
|
||||||
STATION_CODE_TAMPA = "8726667"
|
|
||||||
STATION_CODE_NEW_ORLEANS = "8761927"
|
|
||||||
STATION_CODE_GALVESTON = "8771341"
|
|
||||||
|
|
||||||
STATIONS = {}
|
|
||||||
STATIONS["seattle"] = STATION_CODE_SEATTLE
|
|
||||||
STATIONS["san francisco"] = STATION_CODE_SAN_FRANCISCO
|
|
||||||
STATIONS["monterey"] = STATION_CODE_MONTEREY
|
|
||||||
STATIONS["los angeles"] = STATION_CODE_LOS_ANGELES
|
|
||||||
STATIONS["san diego"] = STATION_CODE_SAN_DIEGO
|
|
||||||
STATIONS["boston"] = STATION_CODE_BOSTON
|
|
||||||
STATIONS["new york"] = STATION_CODE_NEW_YORK
|
|
||||||
STATIONS["virginia beach"] = STATION_CODE_VIRGINIA_BEACH
|
|
||||||
STATIONS["wilmington"] = STATION_CODE_WILMINGTON
|
|
||||||
STATIONS["charleston"] = STATION_CODE_CHARLESTON
|
|
||||||
STATIONS["beaufort"] = STATION_CODE_BEAUFORT
|
|
||||||
STATIONS["myrtle beach"] = STATION_CODE_MYRTLE_BEACH
|
|
||||||
STATIONS["miami"] = STATION_CODE_MIAMI
|
|
||||||
STATIONS["tampa"] = STATION_CODE_TAMPA
|
|
||||||
STATIONS["new orleans"] = STATION_CODE_NEW_ORLEANS
|
|
||||||
STATIONS["galveston"] = STATION_CODE_GALVESTON
|
|
||||||
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
ask = Ask(app, "/")
|
|
||||||
logging.getLogger('flask_ask').setLevel(logging.DEBUG)
|
|
||||||
|
|
||||||
|
|
||||||
class TideInfo(object):
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.first_high_tide_time = None
|
|
||||||
self.first_high_tide_height = None
|
|
||||||
self.low_tide_time = None
|
|
||||||
self.low_tide_height = None
|
|
||||||
self.second_high_tide_time = None
|
|
||||||
self.second_high_tide_height = None
|
|
||||||
|
|
||||||
|
|
||||||
@ask.launch
|
|
||||||
def launch():
|
|
||||||
welcome_text = render_template('welcome')
|
|
||||||
help_text = render_template('help')
|
|
||||||
return question(welcome_text).reprompt(help_text)
|
|
||||||
|
|
||||||
|
|
||||||
@ask.intent('OneshotTideIntent',
|
|
||||||
mapping={'city': 'City', 'date': 'Date'},
|
|
||||||
convert={'date': 'date'},
|
|
||||||
default={'city': 'seattle', 'date': datetime.date.today })
|
|
||||||
def one_shot_tide(city, date):
|
|
||||||
if city.lower() not in STATIONS:
|
|
||||||
return supported_cities()
|
|
||||||
return _make_tide_request(city, date)
|
|
||||||
|
|
||||||
|
|
||||||
@ask.intent('DialogTideIntent',
|
|
||||||
mapping={'city': 'City', 'date': 'Date'},
|
|
||||||
convert={'date': 'date'})
|
|
||||||
def dialog_tide(city, date):
|
|
||||||
if city is not None:
|
|
||||||
if city.lower() not in STATIONS:
|
|
||||||
return supported_cities()
|
|
||||||
if SESSION_DATE not in session.attributes:
|
|
||||||
session.attributes[SESSION_CITY] = city
|
|
||||||
return _dialog_date(city)
|
|
||||||
date = aniso8601.parse_date(session.attributes[SESSION_DATE])
|
|
||||||
return _make_tide_request(city, date)
|
|
||||||
elif date is not None:
|
|
||||||
if SESSION_CITY not in session.attributes:
|
|
||||||
session.attributes[SESSION_DATE] = date.isoformat()
|
|
||||||
return _dialog_city(date)
|
|
||||||
city = session.attributes[SESSION_CITY]
|
|
||||||
return _make_tide_request(city, date)
|
|
||||||
else:
|
|
||||||
return _dialog_no_slot()
|
|
||||||
|
|
||||||
|
|
||||||
@ask.intent('SupportedCitiesIntent')
|
|
||||||
def supported_cities():
|
|
||||||
cities = ", ".join(sorted(STATIONS.keys()))
|
|
||||||
list_cities_text = render_template('list_cities', cities=cities)
|
|
||||||
list_cities_reprompt_text = render_template('list_cities_reprompt')
|
|
||||||
return question(list_cities_text).reprompt(list_cities_reprompt_text)
|
|
||||||
|
|
||||||
|
|
||||||
@ask.intent('AMAZON.HelpIntent')
|
|
||||||
def help():
|
|
||||||
help_text = render_template('help')
|
|
||||||
list_cities_reprompt_text = render_template('list_cities_reprompt')
|
|
||||||
return question(help_text).reprompt(list_cities_reprompt_text)
|
|
||||||
|
|
||||||
|
|
||||||
@ask.intent('AMAZON.StopIntent')
|
|
||||||
def stop():
|
|
||||||
bye_text = render_template('bye')
|
|
||||||
return statement(bye_text)
|
|
||||||
|
|
||||||
|
|
||||||
@ask.intent('AMAZON.CancelIntent')
|
|
||||||
def cancel():
|
|
||||||
bye_text = render_template('bye')
|
|
||||||
return statement(bye_text)
|
|
||||||
|
|
||||||
|
|
||||||
@ask.session_ended
|
|
||||||
def session_ended():
|
|
||||||
return "{}", 200
|
|
||||||
|
|
||||||
|
|
||||||
@app.template_filter()
|
|
||||||
def humanize_date(dt):
|
|
||||||
# http://stackoverflow.com/a/20007730/1163855
|
|
||||||
ordinal = lambda n: "%d%s" % (n,"tsnrhtdd"[(n/10%10!=1)*(n%10<4)*n%10::4])
|
|
||||||
month_and_day_of_week = dt.strftime('%A %B')
|
|
||||||
day_of_month = ordinal(dt.day)
|
|
||||||
year = dt.year if dt.year != datetime.datetime.now().year else ""
|
|
||||||
formatted_date = "{} {} {}".format(month_and_day_of_week, day_of_month, year)
|
|
||||||
formatted_date = re.sub('\s+', ' ', formatted_date)
|
|
||||||
return formatted_date
|
|
||||||
|
|
||||||
|
|
||||||
@app.template_filter()
|
|
||||||
def humanize_time(dt):
|
|
||||||
morning_threshold = 12
|
|
||||||
afternoon_threshold = 17
|
|
||||||
evening_threshold = 20
|
|
||||||
hour_24 = dt.hour
|
|
||||||
if hour_24 < morning_threshold:
|
|
||||||
period_of_day = "in the morning"
|
|
||||||
elif hour_24 < afternoon_threshold:
|
|
||||||
period_of_day = "in the afternoon"
|
|
||||||
elif hour_24 < evening_threshold:
|
|
||||||
period_of_day = "in the evening"
|
|
||||||
else:
|
|
||||||
period_of_day = " at night"
|
|
||||||
the_time = dt.strftime('%I:%M')
|
|
||||||
formatted_time = "{} {}".format(the_time, period_of_day)
|
|
||||||
return formatted_time
|
|
||||||
|
|
||||||
|
|
||||||
@app.template_filter()
|
|
||||||
def humanize_height(height):
|
|
||||||
round_down_threshold = 0.25
|
|
||||||
round_to_half_threshold = 0.75
|
|
||||||
is_negative = False
|
|
||||||
if height < 0:
|
|
||||||
height = abs(height)
|
|
||||||
is_negative = True
|
|
||||||
remainder = height % 1
|
|
||||||
if remainder < round_down_threshold:
|
|
||||||
remainder_text = ""
|
|
||||||
feet = int(math.floor(height))
|
|
||||||
elif remainder < round_to_half_threshold:
|
|
||||||
remainder_text = "and a half"
|
|
||||||
feet = int(math.floor(height))
|
|
||||||
else:
|
|
||||||
remainder_text = ""
|
|
||||||
feet = int(math.floor(height))
|
|
||||||
if is_negative:
|
|
||||||
feet *= -1
|
|
||||||
formatted_height = "{} {} feet".format(feet, remainder_text)
|
|
||||||
formatted_height = re.sub('\s+', ' ', formatted_height)
|
|
||||||
return formatted_height
|
|
||||||
|
|
||||||
|
|
||||||
def _dialog_no_slot():
|
|
||||||
if SESSION_CITY in session.attributes:
|
|
||||||
date_dialog2_text = render_template('date_dialog2')
|
|
||||||
return question(date_dialog2_text).reprompt(date_dialog2_text)
|
|
||||||
else:
|
|
||||||
return supported_cities()
|
|
||||||
|
|
||||||
|
|
||||||
def _dialog_date(city):
|
|
||||||
date_dialog_text = render_template('date_dialog', city=city)
|
|
||||||
date_dialog_reprompt_text = render_template('date_dialog_reprompt')
|
|
||||||
return question(date_dialog_text).reprompt(date_dialog_reprompt_text)
|
|
||||||
|
|
||||||
|
|
||||||
def _dialog_city(date):
|
|
||||||
session.attributes[SESSION_DATE] = date
|
|
||||||
session.attributes_encoder = _json_date_handler
|
|
||||||
city_dialog_text = render_template('city_dialog', date=date)
|
|
||||||
city_dialog_reprompt_text = render_template('city_dialog_reprompt')
|
|
||||||
return question(city_dialog_text).reprompt(city_dialog_reprompt_text)
|
|
||||||
|
|
||||||
|
|
||||||
def _json_date_handler(obj):
|
|
||||||
if isinstance(obj, datetime.date):
|
|
||||||
return obj.isoformat()
|
|
||||||
|
|
||||||
|
|
||||||
def _make_tide_request(city, date):
|
|
||||||
station = STATIONS.get(city.lower())
|
|
||||||
noaa_api_params = {
|
|
||||||
'station': station,
|
|
||||||
'product': 'predictions',
|
|
||||||
'datum': 'MLLW',
|
|
||||||
'units': 'english',
|
|
||||||
'time_zone': 'lst_ldt',
|
|
||||||
'format': 'json'
|
|
||||||
}
|
|
||||||
if date == datetime.date.today():
|
|
||||||
noaa_api_params['date'] = 'today'
|
|
||||||
else:
|
|
||||||
noaa_api_params['begin_date'] = date.strftime('%Y%m%d')
|
|
||||||
noaa_api_params['range'] = 24
|
|
||||||
url = ENDPOINT + "?" + urlencode(noaa_api_params)
|
|
||||||
resp_body = urlopen(url).read()
|
|
||||||
if len(resp_body) == 0:
|
|
||||||
statement_text = render_template('noaa_problem')
|
|
||||||
else:
|
|
||||||
noaa_response_obj = json.loads(resp_body)
|
|
||||||
predictions = noaa_response_obj['predictions']
|
|
||||||
tideinfo = _find_tide_info(predictions)
|
|
||||||
statement_text = render_template('tide_info', date=date, city=city, tideinfo=tideinfo)
|
|
||||||
return statement(statement_text).simple_card("Tide Pooler", statement_text)
|
|
||||||
|
|
||||||
|
|
||||||
def _find_tide_info(predictions):
|
|
||||||
"""
|
|
||||||
Algorithm to find the 2 high tides for the day, the first of which is smaller and occurs
|
|
||||||
mid-day, the second of which is larger and typically in the evening.
|
|
||||||
"""
|
|
||||||
|
|
||||||
last_prediction = None
|
|
||||||
first_high_tide = None
|
|
||||||
second_high_tide = None
|
|
||||||
low_tide = None
|
|
||||||
first_tide_done = False
|
|
||||||
for prediction in predictions:
|
|
||||||
if last_prediction is None:
|
|
||||||
last_prediction = prediction
|
|
||||||
continue
|
|
||||||
if last_prediction['v'] < prediction['v']:
|
|
||||||
if not first_tide_done:
|
|
||||||
first_high_tide = prediction
|
|
||||||
else:
|
|
||||||
second_high_tide = prediction
|
|
||||||
else: # we're decreasing
|
|
||||||
if not first_tide_done and first_high_tide is not None:
|
|
||||||
first_tide_done = True
|
|
||||||
elif second_high_tide is not None:
|
|
||||||
break # we're decreasing after having found the 2nd tide. We're done.
|
|
||||||
if first_tide_done:
|
|
||||||
low_tide = prediction
|
|
||||||
last_prediction = prediction
|
|
||||||
|
|
||||||
fmt = '%Y-%m-%d %H:%M'
|
|
||||||
parse = datetime.datetime.strptime
|
|
||||||
tideinfo = TideInfo()
|
|
||||||
tideinfo.first_high_tide_time = parse(first_high_tide['t'], fmt)
|
|
||||||
tideinfo.first_high_tide_height = float(first_high_tide['v'])
|
|
||||||
tideinfo.second_high_tide_time = parse(second_high_tide['t'], fmt)
|
|
||||||
tideinfo.second_high_tide_height = float(second_high_tide['v'])
|
|
||||||
tideinfo.low_tide_time = parse(low_tide['t'], fmt)
|
|
||||||
tideinfo.low_tide_height = float(low_tide['v'])
|
|
||||||
return tideinfo
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
if 'ASK_VERIFY_REQUESTS' in os.environ:
|
|
||||||
verify = str(os.environ.get('ASK_VERIFY_REQUESTS', '')).lower()
|
|
||||||
if verify == 'false':
|
|
||||||
app.config['ASK_VERIFY_REQUESTS'] = False
|
|
||||||
app.run(debug=True)
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
[bdist_wheel]
|
|
||||||
universal = 1
|
|
||||||
|
|
||||||
[metadata]
|
|
||||||
description-file = README.md
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
"""
|
|
||||||
Flask-Ask
|
|
||||||
-------------
|
|
||||||
|
|
||||||
Easy Alexa Skills Kit integration for Flask
|
|
||||||
"""
|
|
||||||
from setuptools import setup
|
|
||||||
|
|
||||||
def parse_requirements(filename):
|
|
||||||
""" load requirements from a pip requirements file """
|
|
||||||
lineiter = (line.strip() for line in open(filename))
|
|
||||||
return [line for line in lineiter if line and not line.startswith("#")]
|
|
||||||
|
|
||||||
setup(
|
|
||||||
name='Flask-Ask',
|
|
||||||
version='0.9.7',
|
|
||||||
url='https://github.com/johnwheeler/flask-ask',
|
|
||||||
license='Apache 2.0',
|
|
||||||
author='John Wheeler',
|
|
||||||
author_email='john@johnwheeler.org',
|
|
||||||
description='Rapid Alexa Skills Kit Development for Amazon Echo Devices in Python',
|
|
||||||
long_description=__doc__,
|
|
||||||
packages=['flask_ask'],
|
|
||||||
zip_safe=False,
|
|
||||||
include_package_data=True,
|
|
||||||
platforms='any',
|
|
||||||
install_requires=parse_requirements('requirements.txt'),
|
|
||||||
test_requires=[
|
|
||||||
'mock',
|
|
||||||
'requests'
|
|
||||||
],
|
|
||||||
test_suite='tests',
|
|
||||||
classifiers=[
|
|
||||||
'License :: OSI Approved :: Apache Software License',
|
|
||||||
'Framework :: Flask',
|
|
||||||
'Programming Language :: Python',
|
|
||||||
'Environment :: Web Environment',
|
|
||||||
'Intended Audience :: Developers',
|
|
||||||
'Operating System :: OS Independent',
|
|
||||||
'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
|
|
||||||
'Topic :: Software Development :: Libraries :: Python Modules'
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
@ -1,67 +0,0 @@
|
||||||
import unittest
|
|
||||||
from mock import patch, MagicMock
|
|
||||||
from flask import Flask
|
|
||||||
from flask_ask import Ask, audio
|
|
||||||
from flask_ask.models import _Field
|
|
||||||
|
|
||||||
|
|
||||||
class AudioUnitTests(unittest.TestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.ask_patcher = patch('flask_ask.core.find_ask', return_value=Ask())
|
|
||||||
self.ask_patcher.start()
|
|
||||||
self.context_patcher = patch('flask_ask.models.context', return_value=MagicMock())
|
|
||||||
self.context_patcher.start()
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
self.ask_patcher.stop()
|
|
||||||
self.context_patcher.stop()
|
|
||||||
|
|
||||||
def test_token_generation(self):
|
|
||||||
""" Confirm we get a new token when setting a stream url """
|
|
||||||
audio_item = audio()._audio_item(stream_url='https://fakestream', offset=123)
|
|
||||||
self.assertEqual(36, len(audio_item['stream']['token']))
|
|
||||||
self.assertEqual(123, audio_item['stream']['offsetInMilliseconds'])
|
|
||||||
|
|
||||||
def test_custom_token(self):
|
|
||||||
""" Check to see that the provided opaque token remains constant"""
|
|
||||||
token = "hello_world"
|
|
||||||
audio_item = audio()._audio_item(stream_url='https://fakestream', offset=10, opaque_token=token)
|
|
||||||
self.assertEqual(token, audio_item['stream']['token'])
|
|
||||||
self.assertEqual(10, audio_item['stream']['offsetInMilliseconds'])
|
|
||||||
|
|
||||||
|
|
||||||
class AskStreamHandlingTests(unittest.TestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
fake_context = {'System': {'user': {'userId': 'dave'}}}
|
|
||||||
self.context_patcher = patch.object(Ask, 'context', return_value=fake_context)
|
|
||||||
self.context_patcher.start()
|
|
||||||
self.request_patcher = patch.object(Ask, 'request', return_value=MagicMock())
|
|
||||||
self.request_patcher.start()
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
self.context_patcher.stop()
|
|
||||||
self.request_patcher.stop()
|
|
||||||
|
|
||||||
def test_setting_and_getting_current_stream(self):
|
|
||||||
ask = Ask()
|
|
||||||
with patch('flask_ask.core.find_ask', return_value=ask):
|
|
||||||
self.assertEqual(_Field(), ask.current_stream)
|
|
||||||
|
|
||||||
stream = _Field()
|
|
||||||
stream.__dict__.update({'token': 'asdf', 'offsetInMilliseconds': 123, 'url': 'junk'})
|
|
||||||
with patch('flask_ask.core.top_stream', return_value=stream):
|
|
||||||
self.assertEqual(stream, ask.current_stream)
|
|
||||||
|
|
||||||
def test_from_directive_call(self):
|
|
||||||
ask = Ask()
|
|
||||||
fake_stream = _Field()
|
|
||||||
fake_stream.__dict__.update({'token':'fake'})
|
|
||||||
with patch('flask_ask.core.top_stream', return_value=fake_stream):
|
|
||||||
from_buffer = ask._from_directive()
|
|
||||||
self.assertEqual(fake_stream, from_buffer)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
unittest.main()
|
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
import unittest
|
|
||||||
from mock import patch, Mock
|
|
||||||
from werkzeug.contrib.cache import SimpleCache
|
|
||||||
from flask_ask.core import Ask
|
|
||||||
from flask_ask.cache import push_stream, pop_stream, top_stream, set_stream
|
|
||||||
|
|
||||||
|
|
||||||
class CacheTests(unittest.TestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.patcher = patch('flask_ask.core.find_ask', return_value=Ask())
|
|
||||||
self.ask = self.patcher.start()
|
|
||||||
self.user_id = 'dave'
|
|
||||||
self.token = '123-abc'
|
|
||||||
self.cache = SimpleCache()
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
self.patcher.stop()
|
|
||||||
|
|
||||||
def test_adding_removing_stream(self):
|
|
||||||
self.assertTrue(push_stream(self.cache, self.user_id, self.token))
|
|
||||||
|
|
||||||
# peak at the top
|
|
||||||
self.assertEqual(self.token, top_stream(self.cache, self.user_id))
|
|
||||||
self.assertIsNone(top_stream(self.cache, 'not dave'))
|
|
||||||
|
|
||||||
# pop it off
|
|
||||||
self.assertEqual(self.token, pop_stream(self.cache, self.user_id))
|
|
||||||
self.assertIsNone(top_stream(self.cache, self.user_id))
|
|
||||||
|
|
||||||
def test_pushing_works_like_a_stack(self):
|
|
||||||
push_stream(self.cache, self.user_id, 'junk')
|
|
||||||
push_stream(self.cache, self.user_id, self.token)
|
|
||||||
|
|
||||||
self.assertEqual(self.token, pop_stream(self.cache, self.user_id))
|
|
||||||
self.assertEqual('junk', pop_stream(self.cache, self.user_id))
|
|
||||||
self.assertIsNone(pop_stream(self.cache, self.user_id))
|
|
||||||
|
|
||||||
def test_cannot_push_nones_into_stack(self):
|
|
||||||
self.assertIsNone(push_stream(self.cache, self.user_id, None))
|
|
||||||
|
|
||||||
def test_set_overrides_stack(self):
|
|
||||||
push_stream(self.cache, self.user_id, '1')
|
|
||||||
push_stream(self.cache, self.user_id, '2')
|
|
||||||
self.assertEqual('2', top_stream(self.cache, self.user_id))
|
|
||||||
|
|
||||||
set_stream(self.cache, self.user_id, '3')
|
|
||||||
self.assertEqual('3', pop_stream(self.cache, self.user_id))
|
|
||||||
self.assertIsNone(pop_stream(self.cache, self.user_id))
|
|
||||||
|
|
||||||
def test_calls_to_top_with_no_user_return_none(self):
|
|
||||||
""" RedisCache implementation doesn't like None key values. """
|
|
||||||
mock = Mock()
|
|
||||||
result = top_stream(mock, None)
|
|
||||||
self.assertFalse(mock.get.called)
|
|
||||||
self.assertIsNone(result)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
unittest.main()
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import unittest
|
|
||||||
from aniso8601.timezone import UTCOffset, build_utcoffset
|
|
||||||
from flask_ask.core import Ask
|
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from mock import patch, MagicMock
|
|
||||||
import json
|
|
||||||
|
|
||||||
|
|
||||||
class FakeRequest(object):
|
|
||||||
""" Fake out a Flask request for testing purposes for now """
|
|
||||||
|
|
||||||
headers = {'Signaturecertchainurl': None, 'Signature': None}
|
|
||||||
|
|
||||||
def __init__(self, data):
|
|
||||||
self.data = json.dumps(data)
|
|
||||||
|
|
||||||
|
|
||||||
class TestCoreRoutines(unittest.TestCase):
|
|
||||||
""" Tests for core Flask Ask functionality """
|
|
||||||
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.mock_app = MagicMock()
|
|
||||||
self.mock_app.debug = True
|
|
||||||
self.mock_app.config = {'ASK_VERIFY_TIMESTAMP_DEBUG': False}
|
|
||||||
|
|
||||||
# XXX: this mess implies we should think about tidying up Ask._alexa_request
|
|
||||||
self.patch_current_app = patch('flask_ask.core.current_app', new=self.mock_app)
|
|
||||||
self.patch_load_cert = patch('flask_ask.core.verifier.load_certificate')
|
|
||||||
self.patch_verify_sig = patch('flask_ask.core.verifier.verify_signature')
|
|
||||||
self.patch_current_app.start()
|
|
||||||
self.patch_load_cert.start()
|
|
||||||
self.patch_verify_sig.start()
|
|
||||||
|
|
||||||
@patch('flask_ask.core.flask_request',
|
|
||||||
new=FakeRequest({'request': {'timestamp': 1234},
|
|
||||||
'session': {'application': {'applicationId': 1}}}))
|
|
||||||
def test_alexa_request_parsing(self):
|
|
||||||
ask = Ask()
|
|
||||||
ask._alexa_request()
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_timestamp(self):
|
|
||||||
utc = build_utcoffset('UTC', timedelta(hours=0))
|
|
||||||
result = Ask._parse_timestamp('2017-07-08T07:38:00Z')
|
|
||||||
self.assertEqual(datetime(2017, 7, 8, 7, 38, 0, 0, utc), result)
|
|
||||||
|
|
||||||
result = Ask._parse_timestamp(1234567890)
|
|
||||||
self.assertEqual(datetime(2009, 2, 13, 23, 31, 30), result)
|
|
||||||
|
|
||||||
with self.assertRaises(ValueError):
|
|
||||||
Ask._parse_timestamp(None)
|
|
||||||
|
|
||||||
def test_tries_parsing_on_valueerror(self):
|
|
||||||
max_timestamp = 253402300800
|
|
||||||
|
|
||||||
# should cause a ValueError normally
|
|
||||||
with self.assertRaises(ValueError):
|
|
||||||
datetime.utcfromtimestamp(max_timestamp)
|
|
||||||
|
|
||||||
# should safely parse, assuming scale change needed
|
|
||||||
# note: this assert looks odd, but Py2 handles the parsing
|
|
||||||
# differently, resulting in a differing timestamp
|
|
||||||
# due to more granularity of microseconds
|
|
||||||
result = Ask._parse_timestamp(max_timestamp)
|
|
||||||
self.assertEqual(datetime(1978, 1, 11, 21, 31, 40).timetuple()[0:6],
|
|
||||||
result.timetuple()[0:6])
|
|
||||||
|
|
||||||
with self.assertRaises(ValueError):
|
|
||||||
# still raise an error if too large
|
|
||||||
Ask._parse_timestamp(max_timestamp * 1000)
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
self.patch_current_app.stop()
|
|
||||||
self.patch_load_cert.stop()
|
|
||||||
self.patch_verify_sig.stop()
|
|
||||||
|
|
@ -1,122 +0,0 @@
|
||||||
import unittest
|
|
||||||
import json
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
from flask_ask import Ask, audio
|
|
||||||
from flask import Flask
|
|
||||||
|
|
||||||
|
|
||||||
play_request = {
|
|
||||||
"version": "1.0",
|
|
||||||
"session": {
|
|
||||||
"new": True,
|
|
||||||
"sessionId": "amzn1.echo-api.session.0000000-0000-0000-0000-00000000000",
|
|
||||||
"application": {
|
|
||||||
"applicationId": "fake-application-id"
|
|
||||||
},
|
|
||||||
"attributes": {},
|
|
||||||
"user": {
|
|
||||||
"userId": "amzn1.account.AM3B00000000000000000000000"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"context": {
|
|
||||||
"System": {
|
|
||||||
"application": {
|
|
||||||
"applicationId": "fake-application-id"
|
|
||||||
},
|
|
||||||
"user": {
|
|
||||||
"userId": "amzn1.account.AM3B00000000000000000000000"
|
|
||||||
},
|
|
||||||
"device": {
|
|
||||||
"supportedInterfaces": {
|
|
||||||
"AudioPlayer": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"AudioPlayer": {
|
|
||||||
"offsetInMilliseconds": 0,
|
|
||||||
"playerActivity": "IDLE"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"request": {
|
|
||||||
"type": "IntentRequest",
|
|
||||||
"requestId": "string",
|
|
||||||
"timestamp": "string",
|
|
||||||
"locale": "string",
|
|
||||||
"intent": {
|
|
||||||
"name": "TestPlay",
|
|
||||||
"slots": {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class AudioIntegrationTests(unittest.TestCase):
|
|
||||||
""" Integration tests of the Audio Directives """
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.app = Flask(__name__)
|
|
||||||
self.app.config['ASK_VERIFY_REQUESTS'] = False
|
|
||||||
self.ask = Ask(app=self.app, route='/ask')
|
|
||||||
self.client = self.app.test_client()
|
|
||||||
self.stream_url = 'https://fakestream'
|
|
||||||
self.custom_token = 'custom_uuid_{0}'.format(str(uuid.uuid4()))
|
|
||||||
|
|
||||||
@self.ask.intent('TestPlay')
|
|
||||||
def play():
|
|
||||||
return audio('playing').play(self.stream_url)
|
|
||||||
|
|
||||||
@self.ask.intent('TestCustomTokenIntents')
|
|
||||||
def custom_token_intents():
|
|
||||||
return audio('playing with custom token').play(self.stream_url,
|
|
||||||
opaque_token=self.custom_token)
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_play_intent(self):
|
|
||||||
""" Test to see if we can properly play a stream """
|
|
||||||
response = self.client.post('/ask', data=json.dumps(play_request))
|
|
||||||
self.assertEqual(200, response.status_code)
|
|
||||||
|
|
||||||
data = json.loads(response.data.decode('utf-8'))
|
|
||||||
self.assertEqual('playing',
|
|
||||||
data['response']['outputSpeech']['text'])
|
|
||||||
|
|
||||||
directive = data['response']['directives'][0]
|
|
||||||
self.assertEqual('AudioPlayer.Play', directive['type'])
|
|
||||||
|
|
||||||
stream = directive['audioItem']['stream']
|
|
||||||
self.assertIsNotNone(stream['token'])
|
|
||||||
self.assertEqual(self.stream_url, stream['url'])
|
|
||||||
self.assertEqual(0, stream['offsetInMilliseconds'])
|
|
||||||
|
|
||||||
def test_play_intent_with_custom_token(self):
|
|
||||||
""" Test to check that custom token supplied is returned """
|
|
||||||
|
|
||||||
# change the intent name to route to our custom token for play_request
|
|
||||||
original_intent_name = play_request['request']['intent']['name']
|
|
||||||
play_request['request']['intent']['name'] = 'TestCustomTokenIntents'
|
|
||||||
|
|
||||||
response = self.client.post('/ask', data=json.dumps(play_request))
|
|
||||||
self.assertEqual(200, response.status_code)
|
|
||||||
|
|
||||||
data = json.loads(response.data.decode('utf-8'))
|
|
||||||
self.assertEqual('playing with custom token',
|
|
||||||
data['response']['outputSpeech']['text'])
|
|
||||||
|
|
||||||
directive = data['response']['directives'][0]
|
|
||||||
self.assertEqual('AudioPlayer.Play', directive['type'])
|
|
||||||
|
|
||||||
stream = directive['audioItem']['stream']
|
|
||||||
self.assertEqual(stream['token'], self.custom_token)
|
|
||||||
self.assertEqual(self.stream_url, stream['url'])
|
|
||||||
self.assertEqual(0, stream['offsetInMilliseconds'])
|
|
||||||
|
|
||||||
# reset our play_request
|
|
||||||
play_request['request']['intent']['name'] = original_intent_name
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
unittest.main()
|
|
||||||
|
|
@ -1,114 +0,0 @@
|
||||||
import unittest
|
|
||||||
import json
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
from flask_ask import Ask, statement
|
|
||||||
from flask import Flask
|
|
||||||
|
|
||||||
|
|
||||||
play_request = {
|
|
||||||
"version": "1.0",
|
|
||||||
"session": {
|
|
||||||
"new": False,
|
|
||||||
"sessionId": "amzn1.echo-api.session.f6ebc0ba-9d7a-4c3f-b056-b6c3f9da0713",
|
|
||||||
"application": {
|
|
||||||
"applicationId": "amzn1.ask.skill.26338c44-65da-4d58-aa75-c86b21271eb7"
|
|
||||||
},
|
|
||||||
"user": {
|
|
||||||
"userId": "amzn1.ask.account.AHR7KBC3MFCX7LYT6HJBGDLIGQUU3FLANWCZ",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"context": {
|
|
||||||
"AudioPlayer": {
|
|
||||||
"playerActivity": "IDLE"
|
|
||||||
},
|
|
||||||
"Display": {
|
|
||||||
"token": ""
|
|
||||||
},
|
|
||||||
"System": {
|
|
||||||
"application": {
|
|
||||||
"applicationId": "amzn1.ask.skill.26338c44-65da-4d58-aa75-c86b21271eb7"
|
|
||||||
},
|
|
||||||
"user": {
|
|
||||||
"userId": "amzn1.ask.account.AHR7KBC3MFCX7LYT6HJBGDLIGQUU3FLANWCZ",
|
|
||||||
},
|
|
||||||
"device": {
|
|
||||||
"deviceId": "amzn1.ask.device.AELNXV4JQJMF5QALYUQXHOZJ",
|
|
||||||
"supportedInterfaces": {
|
|
||||||
"AudioPlayer": {},
|
|
||||||
"Display": {
|
|
||||||
"templateVersion": "1.0",
|
|
||||||
"markupVersion": "1.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"apiEndpoint": "https://api.amazonalexa.com",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"request": {
|
|
||||||
"type": "IntentRequest",
|
|
||||||
"requestId": "amzn1.echo-api.request.4859a7e3-1960-4ed9-ac7b-854309346916",
|
|
||||||
"timestamp": "2018-04-04T06:28:23Z",
|
|
||||||
"locale": "en-US",
|
|
||||||
"intent": {
|
|
||||||
"name": "TestCustomSlotTypeIntents",
|
|
||||||
"confirmationStatus": "NONE",
|
|
||||||
"slots": {
|
|
||||||
"child_info": {
|
|
||||||
"name": "child_info",
|
|
||||||
"value": "friends info",
|
|
||||||
"resolutions": {
|
|
||||||
"resolutionsPerAuthority": [
|
|
||||||
{
|
|
||||||
"authority": "amzn1.er-authority.echo-sdk.amzn1.ask.skill.26338c44-65da-4d58-aa75-c86b21271eb7.child_info_type",
|
|
||||||
"status": {
|
|
||||||
"code": "ER_SUCCESS_MATCH"
|
|
||||||
},
|
|
||||||
"values": [
|
|
||||||
{
|
|
||||||
"value": {
|
|
||||||
"name": "friend_info",
|
|
||||||
"id": "FRIEND_INFO"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"confirmationStatus": "NONE"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dialogState": "STARTED"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class CustomSlotTypeIntegrationTests(unittest.TestCase):
|
|
||||||
""" Integration tests of the custom slot type """
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.app = Flask(__name__)
|
|
||||||
self.app.config['ASK_VERIFY_REQUESTS'] = False
|
|
||||||
self.ask = Ask(app=self.app, route='/ask')
|
|
||||||
self.client = self.app.test_client()
|
|
||||||
|
|
||||||
@self.ask.intent('TestCustomSlotTypeIntents')
|
|
||||||
def custom_slot_type_intents(child_info):
|
|
||||||
return statement(child_info)
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_custom_slot_type_intent(self):
|
|
||||||
""" Test to see if custom slot type value is correct """
|
|
||||||
response = self.client.post('/ask', data=json.dumps(play_request))
|
|
||||||
self.assertEqual(200, response.status_code)
|
|
||||||
|
|
||||||
data = json.loads(response.data.decode('utf-8'))
|
|
||||||
self.assertEqual('friend_info',
|
|
||||||
data['response']['outputSpeech']['text'])
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
unittest.main()
|
|
||||||
|
|
@ -1,171 +0,0 @@
|
||||||
"""
|
|
||||||
Smoke test using the samples.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import unittest
|
|
||||||
import os
|
|
||||||
import six
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
import subprocess
|
|
||||||
|
|
||||||
from requests import post
|
|
||||||
|
|
||||||
import flask_ask
|
|
||||||
|
|
||||||
|
|
||||||
launch = {
|
|
||||||
"version": "1.0",
|
|
||||||
"session": {
|
|
||||||
"new": True,
|
|
||||||
"sessionId": "amzn1.echo-api.session.0000000-0000-0000-0000-00000000000",
|
|
||||||
"application": {
|
|
||||||
"applicationId": "fake-application-id"
|
|
||||||
},
|
|
||||||
"attributes": {},
|
|
||||||
"user": {
|
|
||||||
"userId": "amzn1.account.AM3B00000000000000000000000"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"context": {
|
|
||||||
"System": {
|
|
||||||
"application": {
|
|
||||||
"applicationId": "fake-application-id"
|
|
||||||
},
|
|
||||||
"user": {
|
|
||||||
"userId": "amzn1.account.AM3B00000000000000000000000"
|
|
||||||
},
|
|
||||||
"device": {
|
|
||||||
"supportedInterfaces": {
|
|
||||||
"AudioPlayer": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"AudioPlayer": {
|
|
||||||
"offsetInMilliseconds": 0,
|
|
||||||
"playerActivity": "IDLE"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"request": {
|
|
||||||
"type": "LaunchRequest",
|
|
||||||
"requestId": "string",
|
|
||||||
"timestamp": "string",
|
|
||||||
"locale": "string",
|
|
||||||
"intent": {
|
|
||||||
"name": "TestPlay",
|
|
||||||
"slots": {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
project_root = os.path.abspath(os.path.join(flask_ask.__file__, '../..'))
|
|
||||||
|
|
||||||
|
|
||||||
@unittest.skipIf(six.PY2, "Not yet supported on Python 2.x")
|
|
||||||
class SmokeTestUsingSamples(unittest.TestCase):
|
|
||||||
""" Try launching each sample and sending some requests to them. """
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.python = sys.executable
|
|
||||||
self.env = {'PYTHONPATH': project_root,
|
|
||||||
'ASK_VERIFY_REQUESTS': 'false'}
|
|
||||||
if os.name == 'nt':
|
|
||||||
self.env['SYSTEMROOT'] = os.getenv('SYSTEMROOT')
|
|
||||||
self.env['PATH'] = os.getenv('PATH')
|
|
||||||
|
|
||||||
def _launch(self, sample):
|
|
||||||
prefix = os.path.join(project_root, 'samples/')
|
|
||||||
path = prefix + sample
|
|
||||||
process = subprocess.Popen([self.python, path], env=self.env)
|
|
||||||
time.sleep(1)
|
|
||||||
self.assertIsNone(process.poll(),
|
|
||||||
msg='Poll should work,'
|
|
||||||
'otherwise we failed to launch')
|
|
||||||
self.process = process
|
|
||||||
|
|
||||||
def _post(self, route='/', data={}):
|
|
||||||
url = 'http://127.0.0.1:5000' + str(route)
|
|
||||||
print('POSTing to %s' % url)
|
|
||||||
response = post(url, json=data)
|
|
||||||
self.assertEqual(200, response.status_code)
|
|
||||||
return response
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _get_text(http_response):
|
|
||||||
data = http_response.json()
|
|
||||||
return data.get('response', {})\
|
|
||||||
.get('outputSpeech', {})\
|
|
||||||
.get('text', None)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _get_reprompt(http_response):
|
|
||||||
data = http_response.json()
|
|
||||||
return data.get('response', {})\
|
|
||||||
.get('reprompt', {})\
|
|
||||||
.get('outputSpeech', {})\
|
|
||||||
.get('text', None)
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
try:
|
|
||||||
self.process.terminate()
|
|
||||||
self.process.communicate(timeout=1)
|
|
||||||
except Exception as e:
|
|
||||||
try:
|
|
||||||
print('[%s]...trying to kill.' % str(e))
|
|
||||||
self.process.kill()
|
|
||||||
self.process.communicate(timeout=1)
|
|
||||||
except Exception as e:
|
|
||||||
print('Error killing test python process: %s' % str(e))
|
|
||||||
print('*** it is recommended you manually kill with PID %s',
|
|
||||||
self.process.pid)
|
|
||||||
|
|
||||||
def test_helloworld(self):
|
|
||||||
""" Test the HelloWorld sample project """
|
|
||||||
self._launch('helloworld/helloworld.py')
|
|
||||||
response = self._post(data=launch)
|
|
||||||
self.assertTrue('hello' in self._get_text(response))
|
|
||||||
|
|
||||||
def test_session_sample(self):
|
|
||||||
""" Test the Session sample project """
|
|
||||||
self._launch('session/session.py')
|
|
||||||
response = self._post(data=launch)
|
|
||||||
self.assertTrue('favorite color' in self._get_text(response))
|
|
||||||
|
|
||||||
def test_audio_simple_demo(self):
|
|
||||||
""" Test the SimpleDemo Audio sample project """
|
|
||||||
self._launch('audio/simple_demo/ask_audio.py')
|
|
||||||
response = self._post(data=launch)
|
|
||||||
self.assertTrue('audio example' in self._get_text(response))
|
|
||||||
|
|
||||||
def test_audio_playlist_demo(self):
|
|
||||||
""" Test the Playlist Audio sample project """
|
|
||||||
self._launch('audio/playlist_demo/playlist.py')
|
|
||||||
response = self._post(data=launch)
|
|
||||||
self.assertTrue('playlist' in self._get_text(response))
|
|
||||||
|
|
||||||
def test_blueprints_demo(self):
|
|
||||||
""" Test the sample project using Flask Blueprints """
|
|
||||||
self._launch('blueprint_demo/demo.py')
|
|
||||||
response = self._post(route='/ask', data=launch)
|
|
||||||
self.assertTrue('hello' in self._get_text(response))
|
|
||||||
|
|
||||||
def test_history_buff(self):
|
|
||||||
""" Test the History Buff sample """
|
|
||||||
self._launch('historybuff/historybuff.py')
|
|
||||||
response = self._post(data=launch)
|
|
||||||
self.assertTrue('History buff' in self._get_text(response))
|
|
||||||
|
|
||||||
def test_spacegeek(self):
|
|
||||||
""" Test the Spacegeek sample """
|
|
||||||
self._launch('spacegeek/spacegeek.py')
|
|
||||||
response = self._post(data=launch)
|
|
||||||
# response is random
|
|
||||||
self.assertTrue(len(self._get_text(response)) > 1)
|
|
||||||
|
|
||||||
def test_tidepooler(self):
|
|
||||||
""" Test the Tide Pooler sample """
|
|
||||||
self._launch('tidepooler/tidepooler.py')
|
|
||||||
response = self._post(data=launch)
|
|
||||||
self.assertTrue('Which city' in self._get_reprompt(response))
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import unittest
|
|
||||||
from flask_ask import statement, question
|
|
||||||
|
|
||||||
|
|
||||||
class UnicodeTests(unittest.TestCase):
|
|
||||||
""" Test using Unicode in responses. (Issue #147) """
|
|
||||||
|
|
||||||
unicode_string = u"Was kann ich für dich tun?"
|
|
||||||
|
|
||||||
def test_unicode_statements(self):
|
|
||||||
""" Test unicode statement responses """
|
|
||||||
stmt = statement(self.unicode_string)
|
|
||||||
speech = stmt._response['outputSpeech']['text']
|
|
||||||
print(speech)
|
|
||||||
self.assertTrue(self.unicode_string in speech)
|
|
||||||
|
|
||||||
def test_unicode_questions(self):
|
|
||||||
""" Test unicode in question responses """
|
|
||||||
q = question(self.unicode_string)
|
|
||||||
speech = q._response['outputSpeech']['text']
|
|
||||||
self.assertTrue(self.unicode_string in speech)
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
[tox]
|
|
||||||
envlist = py27,py3
|
|
||||||
skipsdist = True
|
|
||||||
|
|
||||||
[testenv]
|
|
||||||
deps = -rrequirements-dev.txt
|
|
||||||
commands = python setup.py test
|
|
||||||
|
|
||||||
Loading…
Reference in New Issue