├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── MANIFEST.in ├── Makefile ├── Pipfile ├── Pipfile.lock ├── README.rst ├── django_alexa ├── __init__.py ├── alexa.py ├── api │ ├── __init__.py │ └── fields.py ├── apps.py ├── internal │ ├── __init__.py │ ├── exceptions.py │ ├── fields.py │ ├── intents_schema.py │ ├── response_builder.py │ └── validation.py ├── management │ ├── __init__.py │ ├── base.py │ └── commands │ │ ├── __init__.py │ │ ├── alexa.py │ │ ├── alexa_custom_slots.py │ │ ├── alexa_intents.py │ │ └── alexa_utterances.py ├── models.py ├── serializers.py ├── urls.py └── views.py ├── django_alexa_tests ├── __init__.py ├── internal_tests │ ├── test_response_builder.py │ └── test_validation.py └── test_django_alexa.py ├── docs ├── make.bat └── source │ ├── about-django-alexa.rst │ ├── conf.py │ └── index.rst ├── make.bat ├── readthedocs.yml ├── requirements.txt ├── setup.cfg ├── setup.py ├── test-requirements.txt └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | AUTHORS 59 | ChangeLog 60 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: python 3 | branches: 4 | only: 5 | - master 6 | before_cache: 7 | - rm -f $HOME/.cache/pip/log/debug.log 8 | cache: 9 | directories: 10 | - $HOME/.cache/pip 11 | python: 12 | - "3.6" 13 | install: 14 | - "pip install pipenv" 15 | - "pipenv install --dev" 16 | - "python setup.py develop" 17 | script: 18 | - "flake8 django_alexa/" 19 | - "py.test" 20 | after_success: 21 | coveralls 22 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at Pycontrib@Ling-li.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Kyle 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS 2 | include ChangeLog 3 | include README.rst 4 | recursive-include django_alexa *.html -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SOURCEDIR = source 8 | BUILDDIR = build 9 | 10 | # Put it first so that "make" without argument is like "make help". 11 | help: 12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 13 | 14 | .PHONY: help Makefile 15 | 16 | # Catch-all target: route all unknown targets to Sphinx using the new 17 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 18 | %: Makefile 19 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | pytest = "*" 8 | pytest-cov = "*" 9 | mock = "*" 10 | tox = "*" 11 | flake8 = "*" 12 | flake8-colors = "*" 13 | pyversion3 = "*" 14 | 15 | [packages] 16 | PyCryptodome = "*" 17 | Django = "*" 18 | djangorestframework = "*" 19 | pyOpenSSL = "*" 20 | pytz = "*" 21 | requests = "*" 22 | pyversion3 = "*" 23 | sphinx = "*" 24 | sphinx-rtd-theme = "*" 25 | 26 | [requires] 27 | python_version = "3.6" 28 | 29 | [pipenv] 30 | allow_prereleases = true 31 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "43f11894bfe6e45758b2c5454c591e2b0c63e49a0809e8d1de21596b5013646b" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.6" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "alabaster": { 20 | "hashes": [ 21 | "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359", 22 | "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02" 23 | ], 24 | "version": "==0.7.12" 25 | }, 26 | "ansimarkup": { 27 | "hashes": [ 28 | "sha256:06365e3ef89a12734fc408b2449cb4642d5fe2e603e95e7296eff9e98a0fe0b4", 29 | "sha256:174d920481416cec8d5a707af542d6fba25a1df1c21d8996479c32ba453649a4" 30 | ], 31 | "version": "==1.4.0" 32 | }, 33 | "babel": { 34 | "hashes": [ 35 | "sha256:1aac2ae2d0d8ea368fa90906567f5c08463d98ade155c0c4bfedd6a0f7160e38", 36 | "sha256:d670ea0b10f8b723672d3a6abeb87b565b244da220d76b4dba1b66269ec152d4" 37 | ], 38 | "version": "==2.8.0" 39 | }, 40 | "better-exceptions-fork": { 41 | "hashes": [ 42 | "sha256:5f0983da51e956dbdaf8b9a3d10e2774b382ce6c6ff2e54685c33e2dbe8f1472" 43 | ], 44 | "version": "==0.2.1.post6" 45 | }, 46 | "certifi": { 47 | "hashes": [ 48 | "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", 49 | "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" 50 | ], 51 | "version": "==2018.11.29" 52 | }, 53 | "cffi": { 54 | "hashes": [ 55 | "sha256:001bf3242a1bb04d985d63e138230802c6c8d4db3668fb545fb5005ddf5bb5ff", 56 | "sha256:00789914be39dffba161cfc5be31b55775de5ba2235fe49aa28c148236c4e06b", 57 | "sha256:028a579fc9aed3af38f4892bdcc7390508adabc30c6af4a6e4f611b0c680e6ac", 58 | "sha256:14491a910663bf9f13ddf2bc8f60562d6bc5315c1f09c704937ef17293fb85b0", 59 | "sha256:1cae98a7054b5c9391eb3249b86e0e99ab1e02bb0cc0575da191aedadbdf4384", 60 | "sha256:2089ed025da3919d2e75a4d963d008330c96751127dd6f73c8dc0c65041b4c26", 61 | "sha256:2d384f4a127a15ba701207f7639d94106693b6cd64173d6c8988e2c25f3ac2b6", 62 | "sha256:337d448e5a725bba2d8293c48d9353fc68d0e9e4088d62a9571def317797522b", 63 | "sha256:399aed636c7d3749bbed55bc907c3288cb43c65c4389964ad5ff849b6370603e", 64 | "sha256:3b911c2dbd4f423b4c4fcca138cadde747abdb20d196c4a48708b8a2d32b16dd", 65 | "sha256:3d311bcc4a41408cf5854f06ef2c5cab88f9fded37a3b95936c9879c1640d4c2", 66 | "sha256:62ae9af2d069ea2698bf536dcfe1e4eed9090211dbaafeeedf5cb6c41b352f66", 67 | "sha256:66e41db66b47d0d8672d8ed2708ba91b2f2524ece3dee48b5dfb36be8c2f21dc", 68 | "sha256:675686925a9fb403edba0114db74e741d8181683dcf216be697d208857e04ca8", 69 | "sha256:7e63cbcf2429a8dbfe48dcc2322d5f2220b77b2e17b7ba023d6166d84655da55", 70 | "sha256:8a6c688fefb4e1cd56feb6c511984a6c4f7ec7d2a1ff31a10254f3c817054ae4", 71 | "sha256:8c0ffc886aea5df6a1762d0019e9cb05f825d0eec1f520c51be9d198701daee5", 72 | "sha256:95cd16d3dee553f882540c1ffe331d085c9e629499ceadfbda4d4fde635f4b7d", 73 | "sha256:99f748a7e71ff382613b4e1acc0ac83bf7ad167fb3802e35e90d9763daba4d78", 74 | "sha256:b8c78301cefcf5fd914aad35d3c04c2b21ce8629b5e4f4e45ae6812e461910fa", 75 | "sha256:c420917b188a5582a56d8b93bdd8e0f6eca08c84ff623a4c16e809152cd35793", 76 | "sha256:c43866529f2f06fe0edc6246eb4faa34f03fe88b64a0a9a942561c8e22f4b71f", 77 | "sha256:cab50b8c2250b46fe738c77dbd25ce017d5e6fb35d3407606e7a4180656a5a6a", 78 | "sha256:cef128cb4d5e0b3493f058f10ce32365972c554572ff821e175dbc6f8ff6924f", 79 | "sha256:cf16e3cf6c0a5fdd9bc10c21687e19d29ad1fe863372b5543deaec1039581a30", 80 | "sha256:e56c744aa6ff427a607763346e4170629caf7e48ead6921745986db3692f987f", 81 | "sha256:e577934fc5f8779c554639376beeaa5657d54349096ef24abe8c74c5d9c117c3", 82 | "sha256:f2b0fa0c01d8a0c7483afd9f31d7ecf2d71760ca24499c8697aeb5ca37dc090c" 83 | ], 84 | "version": "==1.14.0" 85 | }, 86 | "chardet": { 87 | "hashes": [ 88 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", 89 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" 90 | ], 91 | "version": "==3.0.4" 92 | }, 93 | "colorama": { 94 | "hashes": [ 95 | "sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d", 96 | "sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48" 97 | ], 98 | "version": "==0.4.1" 99 | }, 100 | "coverage": { 101 | "hashes": [ 102 | "sha256:09e47c529ff77bf042ecfe858fb55c3e3eb97aac2c87f0349ab5a7efd6b3939f", 103 | "sha256:0a1f9b0eb3aa15c990c328535655847b3420231af299386cfe5efc98f9c250fe", 104 | "sha256:0cc941b37b8c2ececfed341444a456912e740ecf515d560de58b9a76562d966d", 105 | "sha256:10e8af18d1315de936d67775d3a814cc81d0747a1a0312d84e27ae5610e313b0", 106 | "sha256:1b4276550b86caa60606bd3572b52769860a81a70754a54acc8ba789ce74d607", 107 | "sha256:1e8a2627c48266c7b813975335cfdea58c706fe36f607c97d9392e61502dc79d", 108 | "sha256:2b224052bfd801beb7478b03e8a66f3f25ea56ea488922e98903914ac9ac930b", 109 | "sha256:447c450a093766744ab53bf1e7063ec82866f27bcb4f4c907da25ad293bba7e3", 110 | "sha256:46101fc20c6f6568561cdd15a54018bb42980954b79aa46da8ae6f008066a30e", 111 | "sha256:4710dc676bb4b779c4361b54eb308bc84d64a2fa3d78e5f7228921eccce5d815", 112 | "sha256:510986f9a280cd05189b42eee2b69fecdf5bf9651d4cd315ea21d24a964a3c36", 113 | "sha256:5535dda5739257effef56e49a1c51c71f1d37a6e5607bb25a5eee507c59580d1", 114 | "sha256:5a7524042014642b39b1fcae85fb37556c200e64ec90824ae9ecf7b667ccfc14", 115 | "sha256:5f55028169ef85e1fa8e4b8b1b91c0b3b0fa3297c4fb22990d46ff01d22c2d6c", 116 | "sha256:6694d5573e7790a0e8d3d177d7a416ca5f5c150742ee703f3c18df76260de794", 117 | "sha256:6831e1ac20ac52634da606b658b0b2712d26984999c9d93f0c6e59fe62ca741b", 118 | "sha256:77f0d9fa5e10d03aa4528436e33423bfa3718b86c646615f04616294c935f840", 119 | "sha256:828ad813c7cdc2e71dcf141912c685bfe4b548c0e6d9540db6418b807c345ddd", 120 | "sha256:85a06c61598b14b015d4df233d249cd5abfa61084ef5b9f64a48e997fd829a82", 121 | "sha256:8cb4febad0f0b26c6f62e1628f2053954ad2c555d67660f28dfb1b0496711952", 122 | "sha256:a5c58664b23b248b16b96253880b2868fb34358911400a7ba39d7f6399935389", 123 | "sha256:aaa0f296e503cda4bc07566f592cd7a28779d433f3a23c48082af425d6d5a78f", 124 | "sha256:ab235d9fe64833f12d1334d29b558aacedfbca2356dfb9691f2d0d38a8a7bfb4", 125 | "sha256:b3b0c8f660fae65eac74fbf003f3103769b90012ae7a460863010539bb7a80da", 126 | "sha256:bab8e6d510d2ea0f1d14f12642e3f35cefa47a9b2e4c7cea1852b52bc9c49647", 127 | "sha256:c45297bbdbc8bb79b02cf41417d63352b70bcb76f1bbb1ee7d47b3e89e42f95d", 128 | "sha256:d19bca47c8a01b92640c614a9147b081a1974f69168ecd494687c827109e8f42", 129 | "sha256:d64b4340a0c488a9e79b66ec9f9d77d02b99b772c8b8afd46c1294c1d39ca478", 130 | "sha256:da969da069a82bbb5300b59161d8d7c8d423bc4ccd3b410a9b4d8932aeefc14b", 131 | "sha256:ed02c7539705696ecb7dc9d476d861f3904a8d2b7e894bd418994920935d36bb", 132 | "sha256:ee5b8abc35b549012e03a7b1e86c09491457dba6c94112a2482b18589cc2bdb9" 133 | ], 134 | "version": "==4.5.2" 135 | }, 136 | "coveralls": { 137 | "hashes": [ 138 | "sha256:ab638e88d38916a6cedbf80a9cd8992d5fa55c77ab755e262e00b36792b7cd6d", 139 | "sha256:b2388747e2529fa4c669fb1e3e2756e4e07b6ee56c7d9fce05f35ccccc913aa0" 140 | ], 141 | "version": "==1.5.1" 142 | }, 143 | "cryptography": { 144 | "hashes": [ 145 | "sha256:02079a6addc7b5140ba0825f542c0869ff4df9a69c360e339ecead5baefa843c", 146 | "sha256:1df22371fbf2004c6f64e927668734070a8953362cd8370ddd336774d6743595", 147 | "sha256:369d2346db5934345787451504853ad9d342d7f721ae82d098083e1f49a582ad", 148 | "sha256:3cda1f0ed8747339bbdf71b9f38ca74c7b592f24f65cdb3ab3765e4b02871651", 149 | "sha256:44ff04138935882fef7c686878e1c8fd80a723161ad6a98da31e14b7553170c2", 150 | "sha256:4b1030728872c59687badcca1e225a9103440e467c17d6d1730ab3d2d64bfeff", 151 | "sha256:58363dbd966afb4f89b3b11dfb8ff200058fbc3b947507675c19ceb46104b48d", 152 | "sha256:6ec280fb24d27e3d97aa731e16207d58bd8ae94ef6eab97249a2afe4ba643d42", 153 | "sha256:7270a6c29199adc1297776937a05b59720e8a782531f1f122f2eb8467f9aab4d", 154 | "sha256:73fd30c57fa2d0a1d7a49c561c40c2f79c7d6c374cc7750e9ac7c99176f6428e", 155 | "sha256:7f09806ed4fbea8f51585231ba742b58cbcfbfe823ea197d8c89a5e433c7e912", 156 | "sha256:90df0cc93e1f8d2fba8365fb59a858f51a11a394d64dbf3ef844f783844cc793", 157 | "sha256:971221ed40f058f5662a604bd1ae6e4521d84e6cad0b7b170564cc34169c8f13", 158 | "sha256:a518c153a2b5ed6b8cc03f7ae79d5ffad7315ad4569b2d5333a13c38d64bd8d7", 159 | "sha256:b0de590a8b0979649ebeef8bb9f54394d3a41f66c5584fff4220901739b6b2f0", 160 | "sha256:b43f53f29816ba1db8525f006fa6f49292e9b029554b3eb56a189a70f2a40879", 161 | "sha256:d31402aad60ed889c7e57934a03477b572a03af7794fa8fb1780f21ea8f6551f", 162 | "sha256:de96157ec73458a7f14e3d26f17f8128c959084931e8997b9e655a39c8fde9f9", 163 | "sha256:df6b4dca2e11865e6cfbfb708e800efb18370f5a46fd601d3755bc7f85b3a8a2", 164 | "sha256:ecadccc7ba52193963c0475ac9f6fa28ac01e01349a2ca48509667ef41ffd2cf", 165 | "sha256:fb81c17e0ebe3358486cd8cc3ad78adbae58af12fc2bf2bc0bb84e8090fa5ce8" 166 | ], 167 | "version": "==2.8" 168 | }, 169 | "django": { 170 | "hashes": [ 171 | "sha256:1226168be1b1c7efd0e66ee79b0e0b58b2caa7ed87717909cd8a57bb13a7079a", 172 | "sha256:9a4635813e2d498a3c01b10c701fe4a515d76dd290aaa792ccb65ca4ccb6b038" 173 | ], 174 | "index": "pypi", 175 | "version": "==2.2.10" 176 | }, 177 | "djangorestframework": { 178 | "hashes": [ 179 | "sha256:607865b0bb1598b153793892101d881466bd5a991de12bd6229abb18b1c86136", 180 | "sha256:63f76cbe1e7d12b94c357d7e54401103b2e52aef0f7c1650d6c820ad708776e5" 181 | ], 182 | "index": "pypi", 183 | "version": "==3.9.0" 184 | }, 185 | "docopt": { 186 | "hashes": [ 187 | "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491" 188 | ], 189 | "version": "==0.6.2" 190 | }, 191 | "docutils": { 192 | "hashes": [ 193 | "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af", 194 | "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc" 195 | ], 196 | "version": "==0.16" 197 | }, 198 | "idna": { 199 | "hashes": [ 200 | "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", 201 | "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" 202 | ], 203 | "version": "==2.8" 204 | }, 205 | "imagesize": { 206 | "hashes": [ 207 | "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1", 208 | "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1" 209 | ], 210 | "version": "==1.2.0" 211 | }, 212 | "jinja2": { 213 | "hashes": [ 214 | "sha256:c10142f819c2d22bdcd17548c46fa9b77cf4fda45097854c689666bf425e7484", 215 | "sha256:c922560ac46888d47384de1dbdc3daaa2ea993af4b26a436dec31fa2c19ec668" 216 | ], 217 | "version": "==3.0.0a1" 218 | }, 219 | "loguru": { 220 | "hashes": [ 221 | "sha256:5697a878f0addbafc896698ed3cfa12e2515e9b641d1cb2021745fa2625c3ff7", 222 | "sha256:966b4c48c98abc6c8edb3fd76f44cbce969eaddfa8555f0c08252ff6dcf083ca" 223 | ], 224 | "version": "==0.2.4" 225 | }, 226 | "markupsafe": { 227 | "hashes": [ 228 | "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", 229 | "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", 230 | "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", 231 | "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", 232 | "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42", 233 | "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", 234 | "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", 235 | "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", 236 | "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", 237 | "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", 238 | "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", 239 | "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b", 240 | "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", 241 | "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15", 242 | "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", 243 | "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", 244 | "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", 245 | "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", 246 | "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", 247 | "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", 248 | "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", 249 | "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", 250 | "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", 251 | "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", 252 | "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", 253 | "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", 254 | "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", 255 | "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", 256 | "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", 257 | "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", 258 | "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2", 259 | "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", 260 | "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be" 261 | ], 262 | "version": "==1.1.1" 263 | }, 264 | "packaging": { 265 | "hashes": [ 266 | "sha256:0886227f54515e592aaa2e5a553332c73962917f2831f1b0f9b9f4380a4b9807", 267 | "sha256:f95a1e147590f204328170981833854229bb2912ac3d5f89e2a8ccd2834800c9" 268 | ], 269 | "version": "==18.0" 270 | }, 271 | "pbr": { 272 | "hashes": [ 273 | "sha256:f59d71442f9ece3dffc17bc36575768e1ee9967756e6b6535f0ee1f0054c3d68", 274 | "sha256:f6d5b23f226a2ba58e14e49aa3b1bfaf814d0199144b95d78458212444de1387" 275 | ], 276 | "version": "==5.1.1" 277 | }, 278 | "pycparser": { 279 | "hashes": [ 280 | "sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3" 281 | ], 282 | "version": "==2.19" 283 | }, 284 | "pycryptodome": { 285 | "hashes": [ 286 | "sha256:08dcfd52a6784c9ca6b8d098301326ec86a33b94e44759dac031ba71407a1a2e", 287 | "sha256:08de8132a11fe3df5a60ffc9292eabd713b77250650190bb5beeb01ef2593e51", 288 | "sha256:148349c2dfbe80c3dfe598c60147f7875ae9a1dc91beb79c15eade734262a1ab", 289 | "sha256:185c091af54f90d038efc7eeca586161e603bdcbcbaaef2bc7454147f66669d2", 290 | "sha256:1d0d94c09d032538a7b33eeb52eca21eb66db6f00689000066baf307cb7091c2", 291 | "sha256:249d4301eb1e41dce29550a6c8693d4a7d23a06cb2d8afb51f1f42680dd00de1", 292 | "sha256:2c7fe7b081f257d51138369ce3f8675cbae6d2b94f19b5abbf127b2b61db6b99", 293 | "sha256:3210d8ee57f92055b7c6c393e8770b331dd125b371007dcbcddca5dfc7d8c8ce", 294 | "sha256:331e93fdddf8e2779e85cc2e0cbb2bb173a9ebcfbd0eb77390f875e5db0f9940", 295 | "sha256:3b295dc48de69a8055c73d5d49b1355c9479ffeeff72d0c746fb25e205189fe1", 296 | "sha256:4617d3925bdd77e6930d2d3d343324062a3ebd87652808158f8d6f4be4e2161c", 297 | "sha256:500d932db4c418932510237911fb36f85d2452bd444bd0bee96c4a05223a0c81", 298 | "sha256:56857d04dadf51dfcc8223bea4127d739704c11a5aef365d373f8999a34d3c33", 299 | "sha256:5d8d9dd7ba37bb84773160ebb65ad7794517723a4a549367227bb1325ebb8925", 300 | "sha256:5e6ab7478243f56fb51a89b8946fbd6853e924cd2aba3c22513bc508d3807a27", 301 | "sha256:6650d66a513736d61bca9ca2b1c09deb72bf2dcdf47151507ec0c05595a5b0aa", 302 | "sha256:7a0ad14c046c7fe4f60d597f15fd58af41d25f143ff5c8742df3bd80b9008c7d", 303 | "sha256:7c360b9f8b01e704ca70404001cf298505df9b2158a0c29021361ddf7f73117f", 304 | "sha256:8365fbf5254f086e2ad9f589f026506b04e7cf7819a851c91a864bb2d7b35369", 305 | "sha256:9048ef02431b19d823bd758dcd30bef6b29f0a92e49efc3dbec30c8b96e77570", 306 | "sha256:a378c1aaddc8874a71205c4eee3aaddda99afbc62f213e065ac06df0686d42dc", 307 | "sha256:bf60769ef3fd33023cb10ab277903f84f07819465f463cbdae66f732054f90dc", 308 | "sha256:cbfa5f741ba3dc8e07d5beb7c8cacce629f47a15bb31d4625cec3b8b171c489d", 309 | "sha256:de3e9bb4d356a8bc72f848b7691ec760c8abfbbf368fcd7642240c3e6126e740", 310 | "sha256:e39b956d8dfa3377b8cafc90649fa715d5a17c12f7e7f117920664eddc410803", 311 | "sha256:f0377ce5ce4df524394e0745c807932895bb8f25d791ab24b47687d2e049d691", 312 | "sha256:f09ea14afb0b811cdfdaf2de01ad1a7f8c46faee81291d34044eff409b713cee", 313 | "sha256:f5fc7e3b2d29552f0383063408ce2bd295e9d3c7ef13377599aa300a3d2baef7" 314 | ], 315 | "index": "pypi", 316 | "version": "==3.7.2" 317 | }, 318 | "pygments": { 319 | "hashes": [ 320 | "sha256:5ffada19f6203563680669ee7f53b64dabbeb100eb51b61996085e99c03b284a", 321 | "sha256:e8218dd399a61674745138520d0d4cf2621d7e032439341bc3f647bff125818d" 322 | ], 323 | "version": "==2.3.1" 324 | }, 325 | "pyopenssl": { 326 | "hashes": [ 327 | "sha256:26ff56a6b5ecaf3a2a59f132681e2a80afcc76b4f902f612f518f92c2a1bf854", 328 | "sha256:6488f1423b00f73b7ad5167885312bb0ce410d3312eb212393795b53c8caa580" 329 | ], 330 | "index": "pypi", 331 | "version": "==18.0.0" 332 | }, 333 | "pyparsing": { 334 | "hashes": [ 335 | "sha256:40856e74d4987de5d01761a22d1621ae1c7f8774585acae358aa5c5936c6c90b", 336 | "sha256:f353aab21fd474459d97b709e527b5571314ee5f067441dc9f88e33eecd96592" 337 | ], 338 | "version": "==2.3.0" 339 | }, 340 | "pytz": { 341 | "hashes": [ 342 | "sha256:32b0891edff07e28efe91284ed9c31e123d84bea3fd98e1f72be2508f43ef8d9", 343 | "sha256:d5f05e487007e29e03409f9398d074e158d920d36eb82eaf66fb1136b0c5374c" 344 | ], 345 | "index": "pypi", 346 | "version": "==2018.9" 347 | }, 348 | "pyversion3": { 349 | "hashes": [ 350 | "sha256:9664d2a93f7664fefe3ff78ad4a714265f169bcf2e6a6bed961aca1d813ea02f", 351 | "sha256:b6954d9f4b796002017f93d2a4fd20c9adb90fc6355cdf0fc84be968349b31a8" 352 | ], 353 | "index": "pypi", 354 | "version": "==0.5.12" 355 | }, 356 | "requests": { 357 | "hashes": [ 358 | "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", 359 | "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" 360 | ], 361 | "index": "pypi", 362 | "version": "==2.21.0" 363 | }, 364 | "six": { 365 | "hashes": [ 366 | "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", 367 | "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" 368 | ], 369 | "version": "==1.12.0" 370 | }, 371 | "snowballstemmer": { 372 | "hashes": [ 373 | "sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0", 374 | "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52" 375 | ], 376 | "version": "==2.0.0" 377 | }, 378 | "sphinx": { 379 | "hashes": [ 380 | "sha256:429e3172466df289f0f742471d7e30ba3ee11f3b5aecd9a840480d03f14bcfe5", 381 | "sha256:c4cb17ba44acffae3d3209646b6baec1e215cad3065e852c68cc569d4df1b9f8" 382 | ], 383 | "index": "pypi", 384 | "version": "==1.8.3" 385 | }, 386 | "sphinx-rtd-theme": { 387 | "hashes": [ 388 | "sha256:02f02a676d6baabb758a20c7a479d58648e0f64f13e07d1b388e9bb2afe86a09", 389 | "sha256:d0f6bc70f98961145c5b0e26a992829363a197321ba571b31b24ea91879e0c96" 390 | ], 391 | "index": "pypi", 392 | "version": "==0.4.2" 393 | }, 394 | "sphinxcontrib-websupport": { 395 | "hashes": [ 396 | "sha256:50fb98fcb8ff2a8869af2afa6b8ee51b3baeb0b17dacd72505105bf15d506ead", 397 | "sha256:bad3fbd312bc36a31841e06e7617471587ef642bdacdbdddaa8cc30cf251b5ea" 398 | ], 399 | "version": "==1.2.0" 400 | }, 401 | "sqlparse": { 402 | "hashes": [ 403 | "sha256:40afe6b8d4b1117e7dff5504d7a8ce07d9a1b15aeeade8a2d10f130a834f8177", 404 | "sha256:7c3dca29c022744e95b547e867cee89f4fce4373f3549ccd8797d8eb52cdb873" 405 | ], 406 | "version": "==0.3.0" 407 | }, 408 | "urllib3": { 409 | "hashes": [ 410 | "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", 411 | "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" 412 | ], 413 | "version": "==1.24.1" 414 | } 415 | }, 416 | "develop": { 417 | "ansimarkup": { 418 | "hashes": [ 419 | "sha256:06365e3ef89a12734fc408b2449cb4642d5fe2e603e95e7296eff9e98a0fe0b4", 420 | "sha256:174d920481416cec8d5a707af542d6fba25a1df1c21d8996479c32ba453649a4" 421 | ], 422 | "version": "==1.4.0" 423 | }, 424 | "appdirs": { 425 | "hashes": [ 426 | "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", 427 | "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e" 428 | ], 429 | "version": "==1.4.3" 430 | }, 431 | "atomicwrites": { 432 | "hashes": [ 433 | "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4", 434 | "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6" 435 | ], 436 | "version": "==1.3.0" 437 | }, 438 | "attrs": { 439 | "hashes": [ 440 | "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", 441 | "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" 442 | ], 443 | "version": "==19.3.0" 444 | }, 445 | "better-exceptions-fork": { 446 | "hashes": [ 447 | "sha256:5f0983da51e956dbdaf8b9a3d10e2774b382ce6c6ff2e54685c33e2dbe8f1472" 448 | ], 449 | "version": "==0.2.1.post6" 450 | }, 451 | "certifi": { 452 | "hashes": [ 453 | "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", 454 | "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" 455 | ], 456 | "version": "==2018.11.29" 457 | }, 458 | "chardet": { 459 | "hashes": [ 460 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", 461 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" 462 | ], 463 | "version": "==3.0.4" 464 | }, 465 | "colorama": { 466 | "hashes": [ 467 | "sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d", 468 | "sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48" 469 | ], 470 | "version": "==0.4.1" 471 | }, 472 | "coverage": { 473 | "hashes": [ 474 | "sha256:09e47c529ff77bf042ecfe858fb55c3e3eb97aac2c87f0349ab5a7efd6b3939f", 475 | "sha256:0a1f9b0eb3aa15c990c328535655847b3420231af299386cfe5efc98f9c250fe", 476 | "sha256:0cc941b37b8c2ececfed341444a456912e740ecf515d560de58b9a76562d966d", 477 | "sha256:10e8af18d1315de936d67775d3a814cc81d0747a1a0312d84e27ae5610e313b0", 478 | "sha256:1b4276550b86caa60606bd3572b52769860a81a70754a54acc8ba789ce74d607", 479 | "sha256:1e8a2627c48266c7b813975335cfdea58c706fe36f607c97d9392e61502dc79d", 480 | "sha256:2b224052bfd801beb7478b03e8a66f3f25ea56ea488922e98903914ac9ac930b", 481 | "sha256:447c450a093766744ab53bf1e7063ec82866f27bcb4f4c907da25ad293bba7e3", 482 | "sha256:46101fc20c6f6568561cdd15a54018bb42980954b79aa46da8ae6f008066a30e", 483 | "sha256:4710dc676bb4b779c4361b54eb308bc84d64a2fa3d78e5f7228921eccce5d815", 484 | "sha256:510986f9a280cd05189b42eee2b69fecdf5bf9651d4cd315ea21d24a964a3c36", 485 | "sha256:5535dda5739257effef56e49a1c51c71f1d37a6e5607bb25a5eee507c59580d1", 486 | "sha256:5a7524042014642b39b1fcae85fb37556c200e64ec90824ae9ecf7b667ccfc14", 487 | "sha256:5f55028169ef85e1fa8e4b8b1b91c0b3b0fa3297c4fb22990d46ff01d22c2d6c", 488 | "sha256:6694d5573e7790a0e8d3d177d7a416ca5f5c150742ee703f3c18df76260de794", 489 | "sha256:6831e1ac20ac52634da606b658b0b2712d26984999c9d93f0c6e59fe62ca741b", 490 | "sha256:77f0d9fa5e10d03aa4528436e33423bfa3718b86c646615f04616294c935f840", 491 | "sha256:828ad813c7cdc2e71dcf141912c685bfe4b548c0e6d9540db6418b807c345ddd", 492 | "sha256:85a06c61598b14b015d4df233d249cd5abfa61084ef5b9f64a48e997fd829a82", 493 | "sha256:8cb4febad0f0b26c6f62e1628f2053954ad2c555d67660f28dfb1b0496711952", 494 | "sha256:a5c58664b23b248b16b96253880b2868fb34358911400a7ba39d7f6399935389", 495 | "sha256:aaa0f296e503cda4bc07566f592cd7a28779d433f3a23c48082af425d6d5a78f", 496 | "sha256:ab235d9fe64833f12d1334d29b558aacedfbca2356dfb9691f2d0d38a8a7bfb4", 497 | "sha256:b3b0c8f660fae65eac74fbf003f3103769b90012ae7a460863010539bb7a80da", 498 | "sha256:bab8e6d510d2ea0f1d14f12642e3f35cefa47a9b2e4c7cea1852b52bc9c49647", 499 | "sha256:c45297bbdbc8bb79b02cf41417d63352b70bcb76f1bbb1ee7d47b3e89e42f95d", 500 | "sha256:d19bca47c8a01b92640c614a9147b081a1974f69168ecd494687c827109e8f42", 501 | "sha256:d64b4340a0c488a9e79b66ec9f9d77d02b99b772c8b8afd46c1294c1d39ca478", 502 | "sha256:da969da069a82bbb5300b59161d8d7c8d423bc4ccd3b410a9b4d8932aeefc14b", 503 | "sha256:ed02c7539705696ecb7dc9d476d861f3904a8d2b7e894bd418994920935d36bb", 504 | "sha256:ee5b8abc35b549012e03a7b1e86c09491457dba6c94112a2482b18589cc2bdb9" 505 | ], 506 | "version": "==4.5.2" 507 | }, 508 | "coveralls": { 509 | "hashes": [ 510 | "sha256:ab638e88d38916a6cedbf80a9cd8992d5fa55c77ab755e262e00b36792b7cd6d", 511 | "sha256:b2388747e2529fa4c669fb1e3e2756e4e07b6ee56c7d9fce05f35ccccc913aa0" 512 | ], 513 | "version": "==1.5.1" 514 | }, 515 | "distlib": { 516 | "hashes": [ 517 | "sha256:2e166e231a26b36d6dfe35a48c4464346620f8645ed0ace01ee31822b288de21" 518 | ], 519 | "version": "==0.3.0" 520 | }, 521 | "docopt": { 522 | "hashes": [ 523 | "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491" 524 | ], 525 | "version": "==0.6.2" 526 | }, 527 | "filelock": { 528 | "hashes": [ 529 | "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59", 530 | "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836" 531 | ], 532 | "version": "==3.0.12" 533 | }, 534 | "flake8": { 535 | "hashes": [ 536 | "sha256:6a35f5b8761f45c5513e3405f110a86bea57982c3b75b766ce7b65217abe1670", 537 | "sha256:c01f8a3963b3571a8e6bd7a4063359aff90749e160778e03817cd9b71c9e07d2" 538 | ], 539 | "index": "pypi", 540 | "version": "==3.6.0" 541 | }, 542 | "flake8-colors": { 543 | "hashes": [ 544 | "sha256:508fcf6efc15826f2146b42172ab41999555e07af43fcfb3e6a28ad596189560" 545 | ], 546 | "index": "pypi", 547 | "version": "==0.1.6" 548 | }, 549 | "idna": { 550 | "hashes": [ 551 | "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", 552 | "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" 553 | ], 554 | "version": "==2.8" 555 | }, 556 | "importlib-metadata": { 557 | "hashes": [ 558 | "sha256:06f5b3a99029c7134207dd882428a66992a9de2bef7c2b699b5641f9886c3302", 559 | "sha256:b97607a1a18a5100839aec1dc26a1ea17ee0d93b20b0f008d80a5a050afb200b" 560 | ], 561 | "markers": "python_version < '3.8'", 562 | "version": "==1.5.0" 563 | }, 564 | "importlib-resources": { 565 | "hashes": [ 566 | "sha256:6e2783b2538bd5a14678284a3962b0660c715e5a0f10243fd5e00a4b5974f50b", 567 | "sha256:d3279fd0f6f847cced9f7acc19bd3e5df54d34f93a2e7bb5f238f81545787078" 568 | ], 569 | "markers": "python_version < '3.7'", 570 | "version": "==1.0.2" 571 | }, 572 | "loguru": { 573 | "hashes": [ 574 | "sha256:5697a878f0addbafc896698ed3cfa12e2515e9b641d1cb2021745fa2625c3ff7", 575 | "sha256:966b4c48c98abc6c8edb3fd76f44cbce969eaddfa8555f0c08252ff6dcf083ca" 576 | ], 577 | "version": "==0.2.4" 578 | }, 579 | "mccabe": { 580 | "hashes": [ 581 | "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", 582 | "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" 583 | ], 584 | "version": "==0.6.1" 585 | }, 586 | "mock": { 587 | "hashes": [ 588 | "sha256:5ce3c71c5545b472da17b72268978914d0252980348636840bd34a00b5cc96c1", 589 | "sha256:b158b6df76edd239b8208d481dc46b6afd45a846b7812ff0ce58971cf5bc8bba" 590 | ], 591 | "index": "pypi", 592 | "version": "==2.0.0" 593 | }, 594 | "more-itertools": { 595 | "hashes": [ 596 | "sha256:5dd8bcf33e5f9513ffa06d5ad33d78f31e1931ac9a18f33d37e77a180d393a7c", 597 | "sha256:b1ddb932186d8a6ac451e1d95844b382f55e12686d51ca0c68b6f61f2ab7a507" 598 | ], 599 | "version": "==8.2.0" 600 | }, 601 | "packaging": { 602 | "hashes": [ 603 | "sha256:0886227f54515e592aaa2e5a553332c73962917f2831f1b0f9b9f4380a4b9807", 604 | "sha256:f95a1e147590f204328170981833854229bb2912ac3d5f89e2a8ccd2834800c9" 605 | ], 606 | "version": "==18.0" 607 | }, 608 | "pbr": { 609 | "hashes": [ 610 | "sha256:f59d71442f9ece3dffc17bc36575768e1ee9967756e6b6535f0ee1f0054c3d68", 611 | "sha256:f6d5b23f226a2ba58e14e49aa3b1bfaf814d0199144b95d78458212444de1387" 612 | ], 613 | "version": "==5.1.1" 614 | }, 615 | "pluggy": { 616 | "hashes": [ 617 | "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", 618 | "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" 619 | ], 620 | "version": "==0.13.1" 621 | }, 622 | "py": { 623 | "hashes": [ 624 | "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa", 625 | "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0" 626 | ], 627 | "version": "==1.8.1" 628 | }, 629 | "pycodestyle": { 630 | "hashes": [ 631 | "sha256:cbc619d09254895b0d12c2c691e237b2e91e9b2ecf5e84c26b35400f93dcfb83", 632 | "sha256:cbfca99bd594a10f674d0cd97a3d802a1fdef635d4361e1a2658de47ed261e3a" 633 | ], 634 | "version": "==2.4.0" 635 | }, 636 | "pyflakes": { 637 | "hashes": [ 638 | "sha256:9a7662ec724d0120012f6e29d6248ae3727d821bba522a0e6b356eff19126a49", 639 | "sha256:f661252913bc1dbe7fcfcbf0af0db3f42ab65aabd1a6ca68fe5d466bace94dae" 640 | ], 641 | "version": "==2.0.0" 642 | }, 643 | "pygments": { 644 | "hashes": [ 645 | "sha256:5ffada19f6203563680669ee7f53b64dabbeb100eb51b61996085e99c03b284a", 646 | "sha256:e8218dd399a61674745138520d0d4cf2621d7e032439341bc3f647bff125818d" 647 | ], 648 | "version": "==2.3.1" 649 | }, 650 | "pyparsing": { 651 | "hashes": [ 652 | "sha256:40856e74d4987de5d01761a22d1621ae1c7f8774585acae358aa5c5936c6c90b", 653 | "sha256:f353aab21fd474459d97b709e527b5571314ee5f067441dc9f88e33eecd96592" 654 | ], 655 | "version": "==2.3.0" 656 | }, 657 | "pytest": { 658 | "hashes": [ 659 | "sha256:41568ea7ecb4a68d7f63837cf65b92ce8d0105e43196ff2b26622995bb3dc4b2", 660 | "sha256:c3c573a29d7c9547fb90217ece8a8843aa0c1328a797e200290dc3d0b4b823be" 661 | ], 662 | "index": "pypi", 663 | "version": "==4.1.1" 664 | }, 665 | "pytest-cov": { 666 | "hashes": [ 667 | "sha256:0ab664b25c6aa9716cbf203b17ddb301932383046082c081b9848a0edf5add33", 668 | "sha256:230ef817450ab0699c6cc3c9c8f7a829c34674456f2ed8df1fe1d39780f7c87f" 669 | ], 670 | "index": "pypi", 671 | "version": "==2.6.1" 672 | }, 673 | "pyversion3": { 674 | "hashes": [ 675 | "sha256:9664d2a93f7664fefe3ff78ad4a714265f169bcf2e6a6bed961aca1d813ea02f", 676 | "sha256:b6954d9f4b796002017f93d2a4fd20c9adb90fc6355cdf0fc84be968349b31a8" 677 | ], 678 | "index": "pypi", 679 | "version": "==0.5.12" 680 | }, 681 | "requests": { 682 | "hashes": [ 683 | "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", 684 | "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" 685 | ], 686 | "index": "pypi", 687 | "version": "==2.21.0" 688 | }, 689 | "six": { 690 | "hashes": [ 691 | "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", 692 | "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" 693 | ], 694 | "version": "==1.12.0" 695 | }, 696 | "toml": { 697 | "hashes": [ 698 | "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", 699 | "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" 700 | ], 701 | "version": "==0.10.0" 702 | }, 703 | "tox": { 704 | "hashes": [ 705 | "sha256:04f8f1aa05de8e76d7a266ccd14e0d665d429977cd42123bc38efa9b59964e9e", 706 | "sha256:25ef928babe88c71e3ed3af0c464d1160b01fca2dd1870a5bb26c2dea61a17fc" 707 | ], 708 | "index": "pypi", 709 | "version": "==3.7.0" 710 | }, 711 | "urllib3": { 712 | "hashes": [ 713 | "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", 714 | "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" 715 | ], 716 | "version": "==1.24.1" 717 | }, 718 | "virtualenv": { 719 | "hashes": [ 720 | "sha256:7e4e234e1f27755685dc54063d989756790b0682d60e304db589fa1604938013", 721 | "sha256:e0099edd03c77e14a8ac9be62e45af28759984075d9409bb2c3a4edeb7420a23" 722 | ], 723 | "version": "==20.0.2" 724 | }, 725 | "zipp": { 726 | "hashes": [ 727 | "sha256:5c56e330306215cd3553342cfafc73dda2c60792384117893f3a83f8a1209f50", 728 | "sha256:d65287feb793213ffe11c0f31b81602be31448f38aeb8ffc2eb286c4f6f6657e" 729 | ], 730 | "version": "==2.2.0" 731 | } 732 | } 733 | } 734 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | django-alexa 2 | ============ 3 | 4 | .. image:: https://badge.fury.io/py/django-alexa.svg 5 | :target: https://badge.fury.io/py/django-alexa 6 | :alt: Current Version 7 | 8 | .. image:: https://travis-ci.com/pycontribs/django-alexa.svg?branch=master 9 | :target: https://travis-ci.com/pycontribs/django-alexa 10 | :alt: Build Status 11 | 12 | .. image:: https://coveralls.io/repos/github/pycontribs/django-alexa/badge.svg?branch=master 13 | :target: https://coveralls.io/github/pycontribs/django-alexa?branch=master 14 | 15 | .. image:: https://pyup.io/repos/github/pycontribs/django-alexa/shield.svg 16 | :target: https://pyup.io/repos/github/pycontribs/django-alexa/ 17 | :alt: Updates 18 | 19 | .. image:: https://pyup.io/repos/github/pycontribs/django-alexa/python-3-shield.svg 20 | :target: https://pyup.io/repos/github/pycontribs/django-alexa/ 21 | :alt: Python 3 22 | 23 | .. image:: https://requires.io/github/pycontribs/django-alexa/requirements.svg?branch=master 24 | :target: https://requires.io/github/pycontribs/django-alexa/requirements/?branch=master 25 | :alt: Requirements Status 26 | 27 | .. image:: https://snyk.io/test/github/pycontribs/django-alexa/badge.svg?targetFile=requirements.txt 28 | :target: https://snyk.io/test/github/pycontribs/django-alexa/badge.svg?targetFile=requirements.txt 29 | :alt: Vulnerbilities Status 30 | 31 | .. image:: https://img.shields.io/badge/code%20style-black-000000.svg 32 | :target: https://github.com/ambv/black 33 | :alt: Code Style: black 34 | 35 | Amazon Alexa Skills Kit integration for Django 36 | 37 | The django-alexa framework leverages the django-rest-framework package to support 38 | the REST API that alexa skills need to use, but wraps up the bolierplate for intent 39 | routing and response creation that you'd need to write yourself. 40 | 41 | Freeing you up to just write your alexa intents and utterances. 42 | 43 | Full Documentation 44 | ------------------ 45 | https://django-alexa.readthedocs.io/en/latest/ 46 | 47 | Quickstart 48 | ---------- 49 | 50 | Feeling impatient? I like your style. 51 | 52 | .. code-block:: bash 53 | 54 | $ pip install django-alexa 55 | 56 | In your django settings.py add the following: 57 | 58 | .. code-block:: python 59 | 60 | INSTALLED_APPS = [ 61 | 'django_alexa', 62 | 'rest_framework', # don't forget to add this too 63 | ... 64 | ] 65 | 66 | In your django urls.py add the following: 67 | 68 | .. code-block:: python 69 | 70 | urlpatterns = [ 71 | url(r'^', include('django_alexa.urls')), 72 | ... 73 | ] 74 | 75 | Your django app will now have a new REST api endpoint at `/alexa/ask/` 76 | that will handle all the incoming request and route them to the intents defined 77 | in any "alexa.py" file. 78 | 79 | Set environment variables to configure the validation needs: 80 | 81 | .. code-block:: bash 82 | 83 | ALEXA_APP_ID_DEFAULT="Your Amazon Alexa App ID DEFAULT" 84 | ALEXA_APP_ID_OTHER="Your Amazon Alexa App ID OTHER" # for each app 85 | ALEXA_REQUEST_VERIFICATON=True # Enables/Disable request verification 86 | 87 | 88 | You can service multiple alexa skills by organizing your intents by an app name. 89 | See the intent decorator's "app" argument for more information. 90 | 91 | If you set your django project to DEBUG=True django-alexa will also do some 92 | helpful debugging for you during request ingestion, such as catch all exceptions 93 | and give you back a stacktrace and error type in the alexa phone app. 94 | 95 | django-alexa is also configured to log useful information such as request body, 96 | request headers, validation as well as response data, this is all configured 97 | through the standard python logging setup using the logger 'django-alexa' 98 | 99 | In your django project make an `alexa.py` file. 100 | This file is where you define all your alexa intents and utterances. 101 | Each intent must return a valid alexa response dictionary. To aid in this the 102 | django-alexa api provides a helper class called `ResponseBuilder`. 103 | This class has a function to speed up building these dictionaries for responses. 104 | 105 | Please see the documentation on the class for a summary of the details or head 106 | to https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/alexa-skills-kit-interface-reference 107 | and checkout the more verbose documentation on proper alexa responses 108 | 109 | Example: 110 | 111 | .. code-block:: python 112 | 113 | from django_alexa.api import fields, intent, ResponseBuilder 114 | 115 | HOUSES = ("gryffindor", "hufflepuff", "ravenclaw", "slytherin") 116 | 117 | @intent 118 | def LaunchRequest(session): 119 | """ 120 | Hogwarts is a go 121 | --- 122 | launch 123 | start 124 | run 125 | begin 126 | open 127 | """ 128 | return ResponseBuilder.create_response(message="Welcome to Hog warts school of witchcraft and wizardry!", 129 | reprompt="What house would you like to give points to?", 130 | end_session=False, 131 | launched=True) 132 | 133 | 134 | class PointsForHouseSlots(fields.AmazonSlots): 135 | house = fields.AmazonCustom(label="HOUSE_LIST", choices=HOUSES) 136 | points = fields.AmazonNumber() 137 | 138 | 139 | @intent(slots=PointsForHouseSlots) 140 | def AddPointsToHouse(session, house, points): 141 | """ 142 | Direct response to add points to a house 143 | --- 144 | {points} {house} 145 | {points} points {house} 146 | add {points} points to {house} 147 | give {points} points to {house} 148 | """ 149 | kwargs = {} 150 | kwargs['message'] = "{0} points added to house {1}.".format(points, house) 151 | if session.get('launched'): 152 | kwargs['reprompt'] = "What house would you like to give points to?" 153 | kwargs['end_session'] = False 154 | kwargs['launched'] = session['launched'] 155 | return ResponseBuilder.create_response(**kwargs) 156 | 157 | The django-alexa framework also provides two django management commands that 158 | will build your intents and utterances schema for you by inspecting the code. 159 | The django-alexa framework also defines some best practice intents to help 160 | get you up and running even faster, but allows you to easily override them, 161 | as seen above with the custom LaunchRequest. 162 | 163 | .. code-block:: bash 164 | 165 | >>> python manage.py alexa_intents 166 | { 167 | "intents": [ 168 | { 169 | "intent": "StopIntent", 170 | "slots": [] 171 | }, 172 | { 173 | "intent": "PointsForHouse", 174 | "slots": [ 175 | { 176 | "name": "points", 177 | "type": "AMAZON.NUMBER" 178 | }, 179 | { 180 | "name": "house", 181 | "type": "HOUSE_LIST" 182 | } 183 | ] 184 | }, 185 | { 186 | "intent": "HelpIntent", 187 | "slots": [] 188 | }, 189 | { 190 | "intent": "LaunchRequest", 191 | "slots": [] 192 | }, 193 | { 194 | "intent": "SessionEndedRequest", 195 | "slots": [] 196 | }, 197 | { 198 | "intent": "UnforgivableCurses", 199 | "slots": [] 200 | }, 201 | { 202 | "intent": "CancelIntent", 203 | "slots": [] 204 | } 205 | ] 206 | } 207 | 208 | .. code-block:: bash 209 | 210 | >>> python manage.py alexa_utterances 211 | StopIntent stop 212 | StopIntent end 213 | HelpIntent help 214 | HelpIntent info 215 | HelpIntent information 216 | LaunchRequest launch 217 | LaunchRequest start 218 | LaunchRequest run 219 | LaunchRequest begin 220 | LaunchRequest open 221 | PointsForHouse {points} {house} 222 | PointsForHouse {points} points {house} 223 | PointsForHouse add {points} points to {house} 224 | PointsForHouse give {points} points to {house} 225 | SessionEndedRequest quit 226 | SessionEndedRequest nevermind 227 | CancelIntent cancel 228 | 229 | .. code-block:: bash 230 | 231 | >>> python manage.py alexa_custom_slots 232 | HOUSE_LIST: 233 | gryffindor 234 | hufflepuff 235 | ravenclaw 236 | slytherin 237 | 238 | There is also a convience that will print each of this grouped by app name 239 | 240 | .. code-block:: bash 241 | 242 | >>> python manage.py alexa 243 | ... All of the above data output ... 244 | 245 | 246 | 247 | Utterances can be added to your function's docstring seperating them from the 248 | regular docstring by placing them after '---'. 249 | 250 | Each line after '---' will be added as an utterance. 251 | 252 | When defining utterances with variables in them make sure all of the requested 253 | variables in any of the utterances are defined as fields in the slots 254 | for that intent. 255 | 256 | The django-alexa framework will throw errors when these management commands run 257 | if things seem to be out of place or incorrect. 258 | 259 | 260 | Contributing 261 | ------------ 262 | 263 | - The master branch is meant to be stable. I usually work on unstable stuff on a personal branch. 264 | - Fork the master branch ( https://github.com/pycontribs/django-alexa/fork ) 265 | - Create your branch (`git checkout -b my-branch`) 266 | - Install required dependencies via pipenv install 267 | - Run the unit tests via pytest or tox 268 | - Run `tox`, this will run black (for formatting code), flake8 for linting and pytests 269 | - Commit your changes (git commit -am 'added fixes for something') 270 | - Push to the branch (git push origin my-branch) 271 | - If you want to merge code from the master branch you can set the upstream like this: 272 | `git remote add upstream https://github.com/pycontribs/django-alexa.git` 273 | - Create a new Pull Request (Travis CI will test your changes) 274 | - And you're done! 275 | 276 | - Features, Bug fixes, bug reports and new documentation are all appreciated! 277 | - See the github issues page for outstanding things that could be worked on. 278 | 279 | 280 | Credits: Kyle Rockman 2016 281 | -------------------------------------------------------------------------------- /django_alexa/__init__.py: -------------------------------------------------------------------------------- 1 | from django.utils.module_loading import autodiscover_modules 2 | 3 | 4 | default_app_config = "django_alexa.apps.AlexaAppConfig" 5 | 6 | 7 | def autodiscover(): 8 | autodiscover_modules("alexa") 9 | -------------------------------------------------------------------------------- /django_alexa/alexa.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from .api import intent, ResponseBuilder 3 | 4 | 5 | @intent 6 | def LaunchRequest(**kwargs): 7 | """ 8 | Default Start Session Intent 9 | --- 10 | launch 11 | open 12 | resume 13 | start 14 | run 15 | load 16 | begin 17 | """ 18 | return ResponseBuilder.create_response( 19 | message="Welcome.", 20 | reprompt="What would you like to do next?", 21 | end_session=False, 22 | ) 23 | 24 | 25 | @intent 26 | def CancelIntent(**kwargs): 27 | """ 28 | Default Cancel Intent 29 | --- 30 | cancel 31 | """ 32 | return ResponseBuilder.create_response( 33 | message="Canceling actions not configured!", 34 | reprompt="What would you like to do next?", 35 | end_session=False, 36 | ) 37 | 38 | 39 | @intent 40 | def StopIntent(**kwargs): 41 | """ 42 | Default Stop Intent 43 | --- 44 | stop 45 | end 46 | nevermind 47 | """ 48 | return ResponseBuilder.create_response(message="Stopping actions not configured!") 49 | 50 | 51 | @intent 52 | def HelpIntent(**kwargs): 53 | """ 54 | Default Help Intent 55 | --- 56 | help 57 | info 58 | information 59 | """ 60 | return ResponseBuilder.create_response(message="No help was configured!") 61 | 62 | 63 | @intent 64 | def SessionEndedRequest(**kwargs): 65 | """ 66 | Default End Session Intent 67 | --- 68 | quit 69 | """ 70 | return ResponseBuilder.create_response() 71 | -------------------------------------------------------------------------------- /django_alexa/api/__init__.py: -------------------------------------------------------------------------------- 1 | from django_alexa.internal import intent, ResponseBuilder # NOQA 2 | -------------------------------------------------------------------------------- /django_alexa/api/fields.py: -------------------------------------------------------------------------------- 1 | """This maps DRF serializer fields to ASK fields""" 2 | import six 3 | from rest_framework import serializers 4 | from django_alexa.internal import fields 5 | 6 | 7 | class AmazonSlots(fields.AmazonSlots, serializers.Serializer): 8 | pass 9 | 10 | 11 | class AmazonCustom(fields.AmazonCustom, serializers.ChoiceField): 12 | def get_slot_name(self): 13 | return self.label 14 | 15 | def get_choices(self): 16 | return [six.text_type(key) for key in self.choices.keys()] 17 | 18 | 19 | class AmazonLiteral(fields.AmazonLiteral, serializers.CharField): 20 | pass 21 | 22 | 23 | class AmazonNumber(fields.AmazonNumber, serializers.IntegerField): 24 | pass 25 | 26 | 27 | class AmazonDate(fields.AmazonDate, serializers.DateField): 28 | pass 29 | 30 | 31 | class AmazonTime(fields.AmazonTime, serializers.TimeField): 32 | pass 33 | 34 | 35 | class AmazonDuration(fields.AmazonDuration, serializers.DurationField): 36 | pass 37 | 38 | 39 | class AmazonUSCity(fields.AmazonUSCity, serializers.CharField): 40 | pass 41 | 42 | 43 | class AmazonFirstName(fields.AmazonFirstName, serializers.CharField): 44 | pass 45 | 46 | 47 | class AmazonUSState(fields.AmazonUSState, serializers.CharField): 48 | pass 49 | 50 | 51 | class AmazonFourDigitNumber(fields.AmazonFourDigitNumber, serializers.IntegerField): 52 | pass 53 | -------------------------------------------------------------------------------- /django_alexa/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AlexaAppConfig(AppConfig): 5 | name = "django_alexa" 6 | 7 | def ready(self): 8 | super(AlexaAppConfig, self).ready() 9 | self.module.autodiscover() 10 | -------------------------------------------------------------------------------- /django_alexa/internal/__init__.py: -------------------------------------------------------------------------------- 1 | """This is a common internal api that could probably be turned into a seperate package""" 2 | from __future__ import absolute_import 3 | 4 | __all__ = [ 5 | "InternalError", 6 | "validate_response_limit", 7 | "validate_alexa_request", 8 | "validate_char_limit", 9 | "validate_app_ids", 10 | "ALEXA_APP_IDS", 11 | "intent", 12 | "IntentsSchema", 13 | "ResponseBuilder", 14 | ] 15 | from .exceptions import InternalError # flake8: noqa 16 | from .validation import ( 17 | validate_response_limit, 18 | validate_alexa_request, 19 | validate_char_limit, 20 | validate_app_ids, 21 | ALEXA_APP_IDS, 22 | ) # flake8: noqa 23 | from .intents_schema import intent, IntentsSchema # flake8: noqa 24 | from .response_builder import ResponseBuilder # flake8: noqa 25 | -------------------------------------------------------------------------------- /django_alexa/internal/exceptions.py: -------------------------------------------------------------------------------- 1 | class InternalError(Exception): 2 | pass 3 | -------------------------------------------------------------------------------- /django_alexa/internal/fields.py: -------------------------------------------------------------------------------- 1 | """These are the only fields supported by the Alexa skills kit""" 2 | from __future__ import absolute_import 3 | 4 | 5 | class AmazonSlots(object): 6 | """Base for all amazon slots""" 7 | 8 | pass 9 | 10 | 11 | class AmazonField(object): 12 | """Base for all amazon fields""" 13 | 14 | amazon_name = None 15 | 16 | def get_slot_name(self): 17 | return self.amazon_name 18 | 19 | 20 | class AmazonCustom(AmazonField): 21 | def get_choices(self): 22 | return [] 23 | 24 | 25 | class AmazonLiteral(AmazonField): 26 | amazon_name = "AMAZON.LITERAL" 27 | 28 | 29 | class AmazonNumber(AmazonField): 30 | amazon_name = "AMAZON.NUMBER" 31 | 32 | 33 | class AmazonDate(AmazonField): 34 | amazon_name = "AMAZON.DATE" 35 | 36 | 37 | class AmazonTime(AmazonField): 38 | amazon_name = "AMAZON.TIME" 39 | 40 | 41 | class AmazonDuration(AmazonField): 42 | amazon_name = "AMAZON.DURATION" 43 | 44 | 45 | class AmazonUSCity(AmazonField): 46 | amazon_name = "AMAZON.US_CITY" 47 | 48 | 49 | class AmazonFirstName(AmazonField): 50 | amazon_name = "AMAZON.US_FIRST_NAME" 51 | 52 | 53 | class AmazonUSState(AmazonField): 54 | amazon_name = "AMAZON.US_STATE" 55 | 56 | 57 | class AmazonFourDigitNumber(AmazonField): 58 | amazon_name = "AMAZON.FOUR_DIGIT_NUMBER" 59 | -------------------------------------------------------------------------------- /django_alexa/internal/intents_schema.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import logging 3 | from string import Formatter 4 | from .exceptions import InternalError 5 | from .fields import AmazonSlots, AmazonField, AmazonCustom 6 | 7 | log = logging.getLogger(__name__) 8 | 9 | DEFAULT_INTENTS = [ 10 | "LaunchRequest", 11 | "CancelIntent", 12 | "StopIntent", 13 | "HelpIntent", 14 | "SessionEndedRequest", 15 | ] 16 | 17 | 18 | class IntentsSchema: 19 | apps = {} 20 | intents = {} 21 | 22 | @classmethod 23 | def get_intent(cls, app, intent): 24 | key_name = app + "." + intent 25 | if key_name not in cls.intents.keys(): 26 | if intent in DEFAULT_INTENTS: 27 | return cls.get_intent("base", intent) 28 | else: 29 | msg = "Unable to find an intent defined for '{0}'".format(key_name) 30 | raise InternalError(msg) 31 | return cls.intents[key_name] 32 | 33 | @classmethod 34 | def route(cls, session, app, intent, intent_kwargs): 35 | """Routes an intent to the proper method""" 36 | func, slot = cls.get_intent(app, intent) 37 | if slot and bool(intent_kwargs) is False: 38 | msg = "Intent '{0}.{1}' requires slots data and none was provided".format( 39 | app, intent 40 | ) 41 | raise InternalError(msg) 42 | intent_kwargs["session"] = session 43 | msg = "Routing: '{0}.{1}' with args {2} to '{3}.{4}'".format( 44 | app, intent, intent_kwargs, func.__module__, func.__name__ 45 | ) 46 | log.info(msg) 47 | return func(**intent_kwargs) 48 | 49 | @classmethod 50 | def register(cls, func, intent, slots=None, app="base"): 51 | if slots: 52 | if not issubclass(slots, AmazonSlots): 53 | msg = "'{0}' slot is not a valid alexa slot".format(slots.__name__) 54 | logging.warn(msg) 55 | slots = None 56 | else: 57 | s = slots() 58 | for field_name, field in s.get_fields().items(): 59 | if issubclass(field.__class__, AmazonField) is not True: 60 | msg = "'{0}' on slot '{1}' is not a valid alexa slot field type" 61 | msg = msg.format(field_name, s.__class__.__name__) 62 | raise InternalError(msg) 63 | cls.intents[app + "." + intent] = (func, slots) 64 | if app not in cls.apps: 65 | cls.apps[app] = [] 66 | cls.apps[app] += [intent] 67 | 68 | @classmethod 69 | def generate_schema(cls, app="base"): 70 | """Generates the alexa intents schema json for an app""" 71 | intents = [] 72 | for intent_name in cls.apps[app]: 73 | intent_data = {"intent": intent_name, "slots": []} 74 | _, slot = cls.get_intent(app, intent_name) 75 | if slot: 76 | s = slot() 77 | for field_name, field in s.get_fields().items(): 78 | slot_type = field.get_slot_name() 79 | if slot_type is None: 80 | msg = "Intent '{0}.{1}' slot '{2}' does not have a valid slot_type" 81 | raise InternalError(msg.format(app, intent_name, field_name)) 82 | if slot_type == "AMAZON.LITERAL": 83 | msg = "Please upgrade intent '{0}.{1}' slot '{2}' to a AmazonCustom field with choices!" 84 | log.warning(msg.format(app, intent_name, field_name)) 85 | slot_data = {"name": field_name, "type": slot_type} 86 | intent_data["slots"].append(slot_data) 87 | intents.append(intent_data) 88 | return {"intents": intents} 89 | 90 | @classmethod 91 | def generate_utterances(cls, app="base"): 92 | """Generates the alexa utterances schema for all intents for an app""" 93 | utterance_format = "{0} {1}" 94 | utterances = [] 95 | for intent_name in cls.apps[app]: 96 | func, slot = cls.get_intent(app, intent_name) 97 | fields = [] 98 | if slot: 99 | s = slot() 100 | fields = s.get_fields().keys() 101 | docstring = """""" 102 | if func.__doc__: 103 | if "---\n" in func.__doc__: 104 | docstring = func.__doc__.split("---")[-1].strip() 105 | for line in docstring.splitlines(): 106 | line = line.strip() 107 | for key in [i[1] for i in Formatter().parse(line) if i[1]]: 108 | if "|" in key: 109 | key = key.split("|")[-1] 110 | if key not in fields: 111 | msg = "Intent '{0}.{1}' utterance '{2}' has a missing the key in the slot '{3}'" 112 | raise ValueError( 113 | msg.format(app, intent_name, line, s.__class__.__name__) 114 | ) 115 | utterances.append(utterance_format.format(intent_name, line.lower())) 116 | return utterances 117 | 118 | @classmethod 119 | def generate_custom_slots(cls, app="base"): 120 | slots = [] 121 | for intent_name in cls.apps[app]: 122 | func, slot = cls.get_intent(app, intent_name) 123 | if slot: 124 | s = slot() 125 | for field_name, field in s.get_fields().items(): 126 | if issubclass(field.__class__, AmazonCustom): 127 | msg = "\n" + field.get_slot_name() + ":\n" 128 | for choice in field.get_choices(): 129 | msg += " " + choice + "\n" 130 | msg += "\n" 131 | slots.append(msg) 132 | return slots 133 | 134 | 135 | def intent(*args, **kwargs): 136 | """ 137 | Decorator that registers a function to the IntentsSchema 138 | app - The specific app grouping you'd like to register this intent to - Default: base 139 | intent - The intent you'd like to give this intent - Default: 140 | slots - A slot object with a set of fields to determine the argument needs of the intent 141 | """ 142 | invoked = bool(not args or kwargs) 143 | if not invoked: 144 | func, args = args[0], () 145 | 146 | def register(func): 147 | app = kwargs.get("app", "base") 148 | intent = kwargs.get("intent", func.__name__) 149 | slots = kwargs.get("slots", None) 150 | IntentsSchema.register(func, intent, slots, app) 151 | return func 152 | 153 | return register if invoked else register(func) 154 | -------------------------------------------------------------------------------- /django_alexa/internal/response_builder.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import logging 3 | 4 | log = logging.getLogger(__name__) 5 | 6 | 7 | class ResponseBuilder(object): 8 | """ 9 | Simple class to help users to build alexa response data 10 | """ 11 | 12 | version = "" 13 | 14 | @classmethod 15 | def set_version(cls, version): 16 | cls.version = version 17 | 18 | @classmethod 19 | def create_response( 20 | cls, 21 | message=None, 22 | message_is_ssml=False, 23 | reprompt=None, 24 | reprompt_is_ssml=False, 25 | reprompt_append=True, 26 | title=None, 27 | content=None, 28 | card_type=None, 29 | card_image_small=None, 30 | card_image_large=None, 31 | card_text=None, 32 | end_session=True, 33 | **kwargs 34 | ): 35 | """ 36 | Shortcut to create the data structure for an alexa response 37 | 38 | Output Speech: 39 | message - text message to be spoken out by the Echo 40 | message_is_ssml - If true the "message" is ssml formated and should be treated as such 41 | 42 | Reprompt Speech: 43 | reprompt - text message to be spoken out by the Echo 44 | reprompt_is_ssml - If true the "reprompt" is ssml formated and should be treated as such 45 | reprompt_append - If true the "reprompt" is append to the end of "message" for best practice voice interface design 46 | 47 | Card: 48 | card_type - A string describing the type of card to render. ("Simple", "Standard", "LinkAccount") 49 | title - A string containing the title of the card. (not applicable for cards of type LinkAccount). 50 | content - A string containing the contents of the card (not applicable for cards of type Standard or LinkAccount). 51 | Note that you can include line breaks in the content for a card of type Simple. 52 | card_text - A string containing the contents of the card of type "Standard" 53 | card_image_small - A URL to image to be shown in card (not applicable for cards of type Simple or LinkAccount) 54 | card_image_large - A URL to image to be shown in card (not applicable for cards of type Simple or LinkAccount) 55 | 56 | end_session - flag to determine whether this interaction should end the session 57 | 58 | kwargs - Anything added here will be persisted across requests if end_session is False 59 | 60 | For more comprehensive documentation see: 61 | https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/alexa-skills-kit-interface-reference 62 | 63 | For images in cards please see: 64 | https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/providing-home-cards-for-the-amazon-alexa-app#image_hosting 65 | """ 66 | data = {} 67 | data["version"] = cls.version 68 | data["response"] = cls._create_response( 69 | message, 70 | message_is_ssml, 71 | reprompt, 72 | reprompt_is_ssml, 73 | reprompt_append, 74 | title, 75 | content, 76 | card_type, 77 | card_image_small, 78 | card_image_large, 79 | card_text, 80 | end_session, 81 | ) 82 | data["sessionAttributes"] = kwargs 83 | log.debug("Response Data: {0}".format(data)) 84 | return data 85 | 86 | @classmethod 87 | def _create_response( 88 | cls, 89 | message=None, 90 | message_is_ssml=False, 91 | reprompt=None, 92 | reprompt_is_ssml=False, 93 | reprompt_append=True, 94 | title=None, 95 | content=None, 96 | card_type=None, 97 | card_image_small=None, 98 | card_image_large=None, 99 | card_text=None, 100 | end_session=True, 101 | ): 102 | data = {} 103 | data["shouldEndSession"] = end_session 104 | if message: 105 | if reprompt_append and reprompt is not None: 106 | message += " " + reprompt 107 | message_is_ssml = ( 108 | True if any([message_is_ssml, reprompt_is_ssml]) else False 109 | ) 110 | data["outputSpeech"] = cls._create_speech( 111 | message=message, is_ssml=message_is_ssml 112 | ) 113 | if title or content or card_type == "LinkAccount": 114 | data["card"] = cls._create_card( 115 | title=title, 116 | content=content, 117 | card_type=card_type, 118 | card_image_small=card_image_small, 119 | card_image_large=card_image_large, 120 | card_text=card_text, 121 | ) 122 | if reprompt: 123 | data["reprompt"] = cls._create_reprompt( 124 | message=reprompt, is_ssml=reprompt_is_ssml 125 | ) 126 | return data 127 | 128 | @classmethod 129 | def _create_speech(cls, message=None, is_ssml=False): 130 | data = {} 131 | if is_ssml: 132 | data["type"] = "SSML" 133 | data["ssml"] = "" + message + "" 134 | else: 135 | data["type"] = "PlainText" 136 | data["text"] = message 137 | return data 138 | 139 | @classmethod 140 | def _create_reprompt(cls, message=None, is_ssml=False): 141 | data = {} 142 | data["outputSpeech"] = cls._create_speech(message=message, is_ssml=is_ssml) 143 | return data 144 | 145 | @classmethod 146 | def _create_card( 147 | cls, 148 | title=None, 149 | content=None, 150 | card_type=None, 151 | card_image_small=None, 152 | card_image_large=None, 153 | card_text=None, 154 | ): 155 | data = {"type": card_type or "Simple"} 156 | if title: 157 | data["title"] = title 158 | if content: 159 | data["content"] = content 160 | if card_text: 161 | data["text"] = card_text 162 | if card_image_small: 163 | data["image"] = { 164 | "smallImageUrl": card_image_small, 165 | "largeImageUrl": card_image_large, 166 | } 167 | return data 168 | -------------------------------------------------------------------------------- /django_alexa/internal/validation.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import os 3 | import sys 4 | import ast 5 | import logging 6 | import json 7 | import requests 8 | import base64 9 | import pytz 10 | from datetime import datetime, timedelta 11 | from OpenSSL import crypto 12 | from .exceptions import InternalError 13 | 14 | # Test for python 3 15 | if sys.version_info > (3, 0): 16 | from urllib.parse import urlparse 17 | else: 18 | from urlparse import urlparse 19 | 20 | log = logging.getLogger(__name__) 21 | 22 | ALEXA_APP_IDS = dict( 23 | [ 24 | (str(os.environ[envvar]), envvar.replace("ALEXA_APP_ID_", "")) 25 | for envvar in os.environ.keys() 26 | if envvar.startswith("ALEXA_APP_ID_") 27 | ] 28 | ) 29 | ALEXA_REQUEST_VERIFICATON = ast.literal_eval( 30 | os.environ.get("ALEXA_REQUEST_VERIFICATON", "True") 31 | ) 32 | 33 | 34 | def validate_response_limit(value): 35 | """ 36 | value - response content 37 | """ 38 | if sys.getsizeof(value) >= 1000 * 1000 * 24 + sys.getsizeof("a"): 39 | msg = "Alexa response content is bigger then 24 kilobytes: {0}".format(value) 40 | raise InternalError(msg) 41 | 42 | 43 | def validate_app_ids(value): 44 | """ 45 | value - an alexa app id 46 | """ 47 | if value not in ALEXA_APP_IDS.keys(): 48 | msg = "{0} is not one of the valid alexa skills application ids for this service".format( 49 | value 50 | ) 51 | raise InternalError(msg) 52 | 53 | 54 | def validate_current_timestamp(value): 55 | """ 56 | value - a timestamp formatted in ISO 8601 (for example, 2015-05-13T12:34:56Z). 57 | """ 58 | timestamp = datetime.strptime(value, "%Y-%m-%dT%H:%M:%SZ") 59 | utc_timestamp = pytz.utc.localize(timestamp) 60 | utc_timestamp_now = pytz.utc.localize(datetime.utcnow()) 61 | delta = utc_timestamp - utc_timestamp_now 62 | log.debug("DATE TIME CHECK!") 63 | log.debug("Alexa: {0}".format(utc_timestamp)) 64 | log.debug("Server: {0}".format(utc_timestamp_now)) 65 | log.debug("Delta: {0}".format(delta)) 66 | if abs(delta) > timedelta(minutes=2, seconds=30): 67 | return False 68 | else: 69 | return True 70 | 71 | 72 | def validate_char_limit(value): 73 | """ 74 | value - a serializer to check to make sure the character limit is not excceed 75 | """ 76 | data = json.dumps(value) 77 | if len(data) > 8000: 78 | msg = "exceeded the total character limit of 8000: {}".format(data) 79 | raise InternalError(msg) 80 | 81 | 82 | def verify_cert_url(cert_url): 83 | """ 84 | Verify the URL location of the certificate 85 | """ 86 | if cert_url is None: 87 | return False 88 | parsed_url = urlparse(cert_url) 89 | if parsed_url.scheme == "https": 90 | if parsed_url.hostname == "s3.amazonaws.com": 91 | # normpath in windows converts forwards slashes to backslashes, hence replace 92 | if ( 93 | os.path.normpath(parsed_url.path) 94 | .replace("\\", "/") 95 | .startswith("/echo.api/") 96 | ): 97 | if parsed_url.port is None: 98 | return True 99 | elif parsed_url.port == 443: 100 | return True 101 | return False 102 | 103 | 104 | def verify_signature(request_body, signature, cert_url): 105 | """ 106 | Verify the request signature is valid. 107 | """ 108 | if signature is None or cert_url is None: 109 | return False 110 | if len(signature) == 0: 111 | return False 112 | cert_str = requests.get(cert_url) 113 | certificate = crypto.load_certificate(crypto.FILETYPE_PEM, str(cert_str.text)) 114 | if certificate.has_expired() is True: 115 | return False 116 | if certificate.get_subject().CN != "echo-api.amazon.com": 117 | return False 118 | decoded_signature = base64.b64decode(signature) 119 | try: 120 | if crypto.verify(certificate, decoded_signature, request_body, "sha1") is None: 121 | return True 122 | except Exception as ex: 123 | raise InternalError( 124 | f"Error occurred during signature validation: {ex}", {"error": 400} 125 | ) 126 | return False 127 | 128 | 129 | def validate_alexa_request(request_headers, request_body): 130 | """ 131 | Validates this is a valid alexa request 132 | value - a django request object 133 | """ 134 | if ALEXA_REQUEST_VERIFICATON is True: 135 | timestamp = json.loads(request_body.decode("utf8"))["request"]["timestamp"] 136 | # For each of the following errors, the alexa service expects an HTTP error code. This isn't well documented. 137 | # I'm going to return 403 forbidden just to be safe (but need to pass a message to the custom error handler, 138 | # hence why I'm adding an argument when raising the error) 139 | if validate_current_timestamp(timestamp) is False: 140 | raise InternalError("Invalid Request Timestamp", {"error": 400}) 141 | if verify_cert_url(request_headers.get("HTTP_SIGNATURECERTCHAINURL")) is False: 142 | raise InternalError("Invalid Certificate Chain URL", {"error": 400}) 143 | if ( 144 | verify_signature( 145 | request_body, 146 | request_headers.get("HTTP_SIGNATURE"), 147 | request_headers.get("HTTP_SIGNATURECERTCHAINURL"), 148 | ) 149 | is False 150 | ): 151 | raise InternalError("Invalid Request Signature", {"error": 400}) 152 | -------------------------------------------------------------------------------- /django_alexa/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pycontribs/django-alexa/459f4ce21ebaadfd86ec62d23e50cafb140c6406/django_alexa/management/__init__.py -------------------------------------------------------------------------------- /django_alexa/management/base.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from django.core.management.base import BaseCommand 3 | from ..internal import IntentsSchema 4 | 5 | 6 | class AlexaBaseCommand(BaseCommand): 7 | def add_arguments(self, parser): 8 | parser.add_argument( 9 | "args", 10 | metavar="app_label", 11 | nargs="*", 12 | help="Restricts data to the specified app_label", 13 | ) 14 | parser.add_argument( 15 | "-a", 16 | "--all", 17 | action="store_true", 18 | dest="do_all_apps", 19 | default=False, 20 | help="If specified will return all apps schema's", 21 | ) 22 | 23 | def handle(self, *app_labels, **options): 24 | do_all_apps = options.get("do_all_apps") 25 | if len(app_labels) == 0: 26 | if do_all_apps: 27 | app_labels = IntentsSchema.apps.keys() 28 | else: 29 | app_labels = ["base"] 30 | for app in app_labels: 31 | self.do_work(app) 32 | -------------------------------------------------------------------------------- /django_alexa/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pycontribs/django-alexa/459f4ce21ebaadfd86ec62d23e50cafb140c6406/django_alexa/management/commands/__init__.py -------------------------------------------------------------------------------- /django_alexa/management/commands/alexa.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from django.core.management import call_command 3 | from ..base import AlexaBaseCommand 4 | 5 | 6 | class Command(AlexaBaseCommand): 7 | help = "Prints the Alexa Skills Kit schema's for an app" 8 | 9 | def do_work(self, app): 10 | self.stdout.write("\n#### SCHEMAS FOR {0} ####\n".format(app)) 11 | call_command("alexa_intents", app) 12 | call_command("alexa_custom_slots", app) 13 | call_command("alexa_utterances", app) 14 | self.stdout.write("\n#####################################\n") 15 | -------------------------------------------------------------------------------- /django_alexa/management/commands/alexa_custom_slots.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from ..base import AlexaBaseCommand 3 | from ...internal import IntentsSchema 4 | 5 | 6 | class Command(AlexaBaseCommand): 7 | help = "Prints the Alexa Skills Kit custom slot schema for an app" 8 | 9 | def do_work(self, app): 10 | data = IntentsSchema.generate_custom_slots(app=app) 11 | self.stdout.write("\n".join(data)) 12 | -------------------------------------------------------------------------------- /django_alexa/management/commands/alexa_intents.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import json 3 | from ..base import AlexaBaseCommand 4 | from ...internal import IntentsSchema 5 | 6 | 7 | class Command(AlexaBaseCommand): 8 | help = "Prints the Alexa Skills Kit intents schema for an app" 9 | 10 | def do_work(self, app): 11 | data = IntentsSchema.generate_schema(app=app) 12 | self.stdout.write(json.dumps(data, indent=4, sort_keys=True) + "\n") 13 | -------------------------------------------------------------------------------- /django_alexa/management/commands/alexa_utterances.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from ..base import AlexaBaseCommand 3 | from ...internal import IntentsSchema 4 | 5 | 6 | class Command(AlexaBaseCommand): 7 | help = "Prints the Alexa Skills Kit utterances schema" 8 | 9 | def do_work(self, app): 10 | data = IntentsSchema.generate_utterances(app=app) 11 | self.stdout.write("\n".join(data)) 12 | -------------------------------------------------------------------------------- /django_alexa/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pycontribs/django-alexa/459f4ce21ebaadfd86ec62d23e50cafb140c6406/django_alexa/models.py -------------------------------------------------------------------------------- /django_alexa/serializers.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import logging 3 | from rest_framework import serializers 4 | from .internal import validate_app_ids, validate_char_limit 5 | 6 | log = logging.getLogger(__name__) 7 | 8 | 9 | class Obj(object): 10 | def __init__(self, data): 11 | self.__dict__.update(data) 12 | 13 | 14 | class BaseASKSerializer(serializers.Serializer): 15 | def create(self, validated_data): 16 | return Obj(data=validated_data) 17 | 18 | 19 | class ASKApplicationSerializer(BaseASKSerializer): 20 | applicationId = serializers.CharField(validators=[validate_app_ids]) 21 | 22 | 23 | class ASKUserSerializer(BaseASKSerializer): 24 | userId = serializers.CharField() 25 | accessToken = serializers.CharField(required=False, allow_null=True) 26 | 27 | 28 | class ASKSessionSerializer(BaseASKSerializer): 29 | sessionId = serializers.CharField() 30 | application = ASKApplicationSerializer() 31 | attributes = serializers.DictField(required=False, allow_null=True) 32 | user = ASKUserSerializer() 33 | new = serializers.BooleanField() 34 | 35 | 36 | class ASKIntentSerializer(BaseASKSerializer): 37 | name = serializers.CharField() 38 | slots = serializers.DictField(required=False, allow_null=True) 39 | 40 | 41 | class ASKRequestSerializer(BaseASKSerializer): 42 | type = serializers.CharField() 43 | requestId = serializers.CharField() 44 | timestamp = serializers.DateTimeField(format="%Y-%m-%dT%H:%M:%SZ") 45 | intent = ASKIntentSerializer(required=False) 46 | reason = serializers.CharField(required=False) 47 | 48 | 49 | class ASKOutputSpeechSerializer(BaseASKSerializer): 50 | # TODO: Choice validation to check if text and ssml are not both empty 51 | type = serializers.ChoiceField(choices=("PlainText", "SSML")) 52 | text = serializers.CharField(required=False) 53 | ssml = serializers.CharField(required=False) 54 | 55 | 56 | class ASKCardSerializer(BaseASKSerializer): 57 | type = serializers.ChoiceField(default="Simple", choices=("Simple", "LinkAccount")) 58 | title = serializers.CharField(required=False) 59 | content = serializers.CharField(required=False) 60 | 61 | 62 | class ASKRepromptSerializer(BaseASKSerializer): 63 | outputSpeech = ASKOutputSpeechSerializer(required=False) 64 | 65 | 66 | class ASKResponseSerializer(BaseASKSerializer): 67 | outputSpeech = ASKOutputSpeechSerializer( 68 | required=False, validators=[validate_char_limit] 69 | ) 70 | card = ASKCardSerializer(required=False, validators=[validate_char_limit]) 71 | reprompt = ASKRepromptSerializer(required=False) 72 | shouldEndSession = serializers.BooleanField() 73 | 74 | 75 | class ASKInputSerializer(BaseASKSerializer): 76 | version = serializers.FloatField(required=True) 77 | session = ASKSessionSerializer() 78 | request = ASKRequestSerializer() 79 | 80 | 81 | class ASKOutputSerializer(BaseASKSerializer): 82 | version = serializers.FloatField(required=True) 83 | sessionAttributes = serializers.DictField(required=False) 84 | response = ASKResponseSerializer() 85 | -------------------------------------------------------------------------------- /django_alexa/urls.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from django.conf.urls import url 3 | from .views import ASKView 4 | 5 | urlpatterns = [url(r"^alexa/ask/$", ASKView.as_view(), name="alexa")] 6 | -------------------------------------------------------------------------------- /django_alexa/views.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import logging 3 | import traceback 4 | from django.conf import settings 5 | from rest_framework.response import Response 6 | from rest_framework.status import HTTP_200_OK 7 | from django.http import HttpResponseBadRequest, HttpResponseForbidden 8 | from rest_framework.views import APIView 9 | from .serializers import ASKInputSerializer 10 | from .internal import ( 11 | ALEXA_APP_IDS, 12 | ResponseBuilder, 13 | IntentsSchema, 14 | validate_alexa_request, 15 | validate_response_limit, 16 | ) 17 | 18 | 19 | log = logging.getLogger(__name__) 20 | 21 | 22 | class ASKView(APIView): 23 | def handle_exception(self, exc): 24 | if settings.DEBUG: 25 | log.exception("An error occured in your skill.") 26 | msg = "An error occured in your skill. Please check the response card for details." 27 | title = exc.__class__.__name__ 28 | content = traceback.format_exc() 29 | data = ResponseBuilder.create_response( 30 | message=msg, title=title, content=content 31 | ) 32 | else: 33 | msg = "An internal error occured in the skill." 34 | log.exception(msg) 35 | data = ResponseBuilder.create_response(message=msg) 36 | # If we need to return an error code, do so. 37 | # There is probably a better way of doing this, but this works. If anyone knows of a better way, please - 38 | # submit a correction 39 | try: 40 | error = exc.args[1] 41 | if error["error"] == 403: 42 | log.debug(data) 43 | return HttpResponseForbidden() 44 | elif error["error"] == 400: 45 | log.debug(data) 46 | return HttpResponseBadRequest() 47 | else: 48 | # If we are passed an error code we should probably do something more here, but for now - this works. 49 | return Response(data=data, status=HTTP_200_OK) 50 | except Exception as ex: 51 | log.exception(f"caught unhandled error: {ex}") 52 | return Response(data=data, status=HTTP_200_OK) 53 | 54 | def handle_request(self, validated_data): 55 | log.info("Alexa Request Body: {0}".format(validated_data)) 56 | intent_kwargs = {} 57 | session = validated_data["session"] 58 | app = ALEXA_APP_IDS[session["application"]["applicationId"]] 59 | if validated_data["request"]["type"] == "IntentRequest": 60 | intent_name = validated_data["request"]["intent"]["name"] 61 | for slot, slot_data in ( 62 | validated_data["request"]["intent"].get("slots", {}).items() 63 | ): 64 | slot_key = slot_data["name"] 65 | try: 66 | slot_value = slot_data["value"] 67 | except KeyError: 68 | slot_value = None 69 | intent_kwargs[slot_key] = slot_value 70 | else: 71 | intent_name = validated_data["request"]["type"] 72 | _, slot = IntentsSchema.get_intent(app, intent_name) 73 | if slot: 74 | slots = slot(data=intent_kwargs) 75 | slots.is_valid() 76 | intent_kwargs = slots.data 77 | data = IntentsSchema.route(session, app, intent_name, intent_kwargs) 78 | return Response(data=data, status=HTTP_200_OK) 79 | 80 | def post(self, request, *args, **kwargs): 81 | # we have to save the request body because we need to set the version 82 | # before we do anything dangerous so that we can properly send exception 83 | # reponses and the DRF request object doesn't allow you to access the 84 | # body after you have accessed the "data" stream 85 | body = request.body 86 | ResponseBuilder.set_version(request.data["version"]) 87 | validate_alexa_request(request.META, body) 88 | serializer = ASKInputSerializer(data=request.data) 89 | serializer.is_valid(raise_exception=True) 90 | return self.handle_request(serializer.validated_data) 91 | 92 | def dispatch(self, request, *args, **kwargs): 93 | log.debug("#" * 10 + "Start Alexa Request" + "#" * 10) 94 | response = super(ASKView, self).dispatch(request, *args, **kwargs) 95 | if response.status_code == 200: 96 | validate_response_limit(response.render().content) 97 | log.debug("#" * 10 + "End Alexa Request" + "#" * 10) 98 | return response 99 | -------------------------------------------------------------------------------- /django_alexa_tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pycontribs/django-alexa/459f4ce21ebaadfd86ec62d23e50cafb140c6406/django_alexa_tests/__init__.py -------------------------------------------------------------------------------- /django_alexa_tests/internal_tests/test_response_builder.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import mock 3 | 4 | from django_alexa.internal import ResponseBuilder 5 | 6 | 7 | class TestResponseBuilder: 8 | def test_card_simple(self): 9 | rb = ResponseBuilder.create_response( 10 | card_type="Simple", 11 | content="Some content", title="Title") 12 | assert rb['response']['card'] == {'content': 'Some content', 'title': 'Title', 'type': 'Simple'} 13 | 14 | def test_card_standard(self): 15 | rb = ResponseBuilder.create_response( 16 | card_type="Standard", card_text="Some text", title="Some title", 17 | card_image_small="https://example.org/pic_small.jpg", 18 | card_image_large="https://example.org/pic_large.jpg") 19 | assert rb['response']['card'] == {'type': 'Standard', 'title': 'Some title', 'image': {'smallImageUrl': 'https://example.org/pic_small.jpg', 'largeImageUrl': 'https://example.org/pic_large.jpg'}, 'text': 'Some text'} 20 | -------------------------------------------------------------------------------- /django_alexa_tests/internal_tests/test_validation.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import mock 3 | from django_alexa.internal import validation 4 | from imp import reload 5 | from datetime import datetime, timedelta 6 | import pytz 7 | import os 8 | 9 | 10 | class TestValidation: 11 | def test_validate_response_limit(self): 12 | valid_value = 'x' * 1000 * 1000 * 24 13 | validation.validate_response_limit(valid_value) 14 | 15 | with pytest.raises(Exception) as exc_info: 16 | invalid_value = 'x' * 1000 * 1000 * 24 + 'x' 17 | validation.validate_response_limit(invalid_value) 18 | msg = "Should raise exception, response > 24 kb" 19 | assert "InternalError" in str(exc_info), msg 20 | 21 | @mock.patch.dict(os.environ, {'ALEXA_APP_ID_DEFAULT': 'valid_app_id_default'}) 22 | def test_validate_app_ids(self): 23 | reload(validation) 24 | valid_app_id = 'valid_app_id_default' 25 | validation.validate_app_ids(valid_app_id) 26 | 27 | with pytest.raises(Exception) as exc_info: 28 | invalid_app_id = 'non_valid_app_id' 29 | validation.validate_app_ids(invalid_app_id) 30 | msg = "Should raise exception, app id not valid" 31 | assert "InternalError" in str(exc_info), msg 32 | 33 | def test_validate_current_timestamp(self): 34 | current_time = datetime.utcnow() 35 | timestamp = current_time.strftime("%Y-%m-%dT%H:%M:%SZ") 36 | msg = 'Should be a valid timestamp' 37 | assert validation.validate_current_timestamp(timestamp), msg 38 | 39 | invalid_time = current_time - timedelta(minutes=2, seconds=30) 40 | invalid_timestamp = invalid_time.strftime("%Y-%m-%dT%H:%M:%SZ") 41 | msg = 'Shoud not be a valid timestamp' 42 | assert not validation.validate_current_timestamp(invalid_timestamp), msg 43 | 44 | def test_validate_char_limit(self): 45 | valid_value = 'x' * 7998 46 | validation.validate_char_limit(valid_value) 47 | 48 | with pytest.raises(Exception) as exc_info: 49 | invalid_value = 'x' * 8000 + 'x' 50 | validation.validate_char_limit(invalid_value) 51 | msg = "Should raise exception, too many characters" 52 | assert "InternalError" in str(exc_info), msg 53 | 54 | def test_verify_cert_url(self): 55 | url = "https://s3.amazonaws.com/echo.api/test" 56 | assert validation.verify_cert_url(url), 'Should be a valid url without port' 57 | 58 | url = "https://s3.amazonaws.com:443/echo.api/test" 59 | assert validation.verify_cert_url(url), 'Should be a valid url with port' 60 | 61 | url = " " 62 | assert not validation.verify_cert_url(url), 'Should be an invalid url' 63 | 64 | url = None 65 | assert not validation.verify_cert_url(url), 'Should be an invalid url, given None' 66 | 67 | def test_verify_signature(self): 68 | request_body = 'request_body' 69 | signature = None 70 | cert_url = ' ' 71 | msg = 'Should fail, signature = None' 72 | assert not validation.verify_signature(request_body, signature, cert_url), msg 73 | 74 | signature = '' 75 | msg = 'Should fail, signature is empty' 76 | assert not validation.verify_signature(request_body, signature, cert_url), msg 77 | 78 | def test_validate_alexa_request(self): 79 | pass 80 | -------------------------------------------------------------------------------- /django_alexa_tests/test_django_alexa.py: -------------------------------------------------------------------------------- 1 | import json 2 | import unittest as unittest 3 | 4 | # from django_alexa.api import ResponseBuilder 5 | 6 | 7 | class TestAlexaApi(unittest.TestCase): 8 | 9 | def test_response_builder(self): 10 | # data = { 11 | # 'outputSpeech': { 12 | # 'type': "PlainText", 13 | # 'text': "This is a test!", 14 | # }, 15 | # 'shouldEndSession': True, 16 | # } 17 | # response = ResponseBuilder(message="This is a test!") 18 | # self.assertEquals(json.dumps(response.data), 19 | # json.dumps(data)) 20 | pass 21 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=docs/source 11 | set BUILDDIR=docs/build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/source/about-django-alexa.rst: -------------------------------------------------------------------------------- 1 | ################## 2 | About django-alexa 3 | ################## 4 | 5 | django-alexa allows you to rapidly build Amazon alexa skills using the power of django restframework. 6 | 7 | Author: Kyle Rockman 8 | 9 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/master/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | # import os 16 | # import sys 17 | # sys.path.insert(0, os.path.abspath('.')) 18 | 19 | 20 | # -- Project information ----------------------------------------------------- 21 | 22 | project = 'django-alexa documentation' 23 | copyright = '2019, Kyle Rockman' 24 | author = 'Kyle Rockman' 25 | 26 | # The short X.Y version 27 | version = '0.0.7' 28 | # The full version, including alpha/beta/rc tags 29 | release = '0.0.7' 30 | 31 | 32 | # -- General configuration --------------------------------------------------- 33 | 34 | # If your documentation needs a minimal Sphinx version, state it here. 35 | # 36 | # needs_sphinx = '1.0' 37 | 38 | # Add any Sphinx extension module names here, as strings. They can be 39 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 40 | # ones. 41 | extensions = [ 42 | 'sphinx.ext.doctest', 43 | 'sphinx.ext.todo', 44 | 'sphinx.ext.coverage', 45 | 'sphinx.ext.githubpages', 46 | 'sphinx.ext.autodoc', 47 | ] 48 | 49 | # Add any paths that contain templates here, relative to this directory. 50 | templates_path = ['_templates'] 51 | 52 | # The suffix(es) of source filenames. 53 | # You can specify multiple suffix as a list of string: 54 | # 55 | # source_suffix = ['.rst', '.md'] 56 | source_suffix = '.rst' 57 | 58 | # The master toctree document. 59 | master_doc = 'index' 60 | 61 | # The language for content autogenerated by Sphinx. Refer to documentation 62 | # for a list of supported languages. 63 | # 64 | # This is also used if you do content translation via gettext catalogs. 65 | # Usually you set "language" from the command line for these cases. 66 | language = None 67 | 68 | # List of patterns, relative to source directory, that match files and 69 | # directories to ignore when looking for source files. 70 | # This pattern also affects html_static_path and html_extra_path. 71 | exclude_patterns = [] 72 | 73 | # The name of the Pygments (syntax highlighting) style to use. 74 | pygments_style = None 75 | 76 | 77 | # -- Options for HTML output ------------------------------------------------- 78 | 79 | # The theme to use for HTML and HTML Help pages. See the documentation for 80 | # a list of builtin themes. 81 | # 82 | html_theme = 'sphinx_rtd_theme' 83 | 84 | # Theme options are theme-specific and customize the look and feel of a theme 85 | # further. For a list of options available for each theme, see the 86 | # documentation. 87 | # 88 | # html_theme_options = {} 89 | 90 | # Add any paths that contain custom static files (such as style sheets) here, 91 | # relative to this directory. They are copied after the builtin static files, 92 | # so a file named "default.css" will overwrite the builtin "default.css". 93 | html_static_path = ['_static'] 94 | 95 | # Custom sidebar templates, must be a dictionary that maps document names 96 | # to template names. 97 | # 98 | # The default sidebars (for documents that don't match any pattern) are 99 | # defined by theme itself. Builtin themes are using these templates by 100 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 101 | # 'searchbox.html']``. 102 | # 103 | # html_sidebars = {} 104 | 105 | 106 | # -- Options for HTMLHelp output --------------------------------------------- 107 | 108 | # Output file base name for HTML help builder. 109 | htmlhelp_basename = 'django-alexadocumentationdoc' 110 | 111 | 112 | # -- Options for LaTeX output ------------------------------------------------ 113 | 114 | latex_elements = { 115 | # The paper size ('letterpaper' or 'a4paper'). 116 | # 117 | # 'papersize': 'letterpaper', 118 | 119 | # The font size ('10pt', '11pt' or '12pt'). 120 | # 121 | # 'pointsize': '10pt', 122 | 123 | # Additional stuff for the LaTeX preamble. 124 | # 125 | # 'preamble': '', 126 | 127 | # Latex figure (float) alignment 128 | # 129 | # 'figure_align': 'htbp', 130 | } 131 | 132 | # Grouping the document tree into LaTeX files. List of tuples 133 | # (source start file, target name, title, 134 | # author, documentclass [howto, manual, or own class]). 135 | latex_documents = [ 136 | (master_doc, 'django-alexadocumentation.tex', 'django-alexa documentation Documentation', 137 | 'Kyle Rockman / Ling Li', 'manual'), 138 | ] 139 | 140 | 141 | # -- Options for manual page output ------------------------------------------ 142 | 143 | # One entry per manual page. List of tuples 144 | # (source start file, name, description, authors, manual section). 145 | man_pages = [ 146 | (master_doc, 'django-alexadocumentation', 'django-alexa documentation Documentation', 147 | [author], 1) 148 | ] 149 | 150 | 151 | # -- Options for Texinfo output ---------------------------------------------- 152 | 153 | # Grouping the document tree into Texinfo files. List of tuples 154 | # (source start file, target name, title, author, 155 | # dir menu entry, description, category) 156 | texinfo_documents = [ 157 | (master_doc, 'django-alexadocumentation', 'django-alexa documentation Documentation', 158 | author, 'django-alexadocumentation', 'One line description of project.', 159 | 'Miscellaneous'), 160 | ] 161 | 162 | 163 | # -- Options for Epub output ------------------------------------------------- 164 | 165 | # Bibliographic Dublin Core info. 166 | epub_title = project 167 | 168 | # The unique identifier of the text. This can be a ISBN number 169 | # or the project homepage. 170 | # 171 | # epub_identifier = '' 172 | 173 | # A unique identification for the text. 174 | # 175 | # epub_uid = '' 176 | 177 | # A list of files that should not be packed into the epub file. 178 | epub_exclude_files = ['search.html'] 179 | 180 | 181 | # -- Extension configuration ------------------------------------------------- 182 | 183 | # -- Options for todo extension ---------------------------------------------- 184 | 185 | # If true, `todo` and `todoList` produce output, else they produce nothing. 186 | todo_include_todos = True 187 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. django-alexa documentation documentation master file, created by 2 | sphinx-quickstart on Mon Jan 14 22:23:21 2019. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to django-alexa documentation's documentation! 7 | ====================================================== 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | about-django-alexa 14 | 15 | 16 | 17 | Indices and tables 18 | ================== 19 | 20 | * :ref:`genindex` 21 | * :ref:`modindex` 22 | * :ref:`search` 23 | -------------------------------------------------------------------------------- /make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=docs/source 11 | set BUILDDIR=docs/build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /readthedocs.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # .readthedocs.tml 3 | 4 | build: 5 | image: latest 6 | 7 | python: 8 | version: 3.6 9 | setup_py_install: true 10 | requirements_file: requirements.txt -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | PyCryptodome>=3.7.2 2 | Django>=2.1.6 3 | djangorestframework>=3.9.1 4 | pyOpenSSL>=18.0.0 5 | pytz>=2018.9 6 | requests>=2.21.0 7 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal=1 3 | 4 | [flake8] 5 | max-line-length=88 6 | ignore = E121,E122,E123,E124,E125,E126,E127,E128,E129,E131,E501,E701 7 | exclude = django_alexa_tests/* 8 | format = pylint 9 | 10 | [metadata] 11 | name = django-alexa 12 | author = Kyle Rockman 13 | author_email = kyle.rockman@mac.com 14 | summary = Amazon Alexa Skills Kit integration for Django 15 | url = https://github.com/rocktavious/django-alexa 16 | home-page = https://github.com/pycontribs/django-alexa 17 | description-file = README.rst 18 | license-file = LICENSE 19 | classifiers = 20 | Environment :: Console 21 | Intended Audience :: Developers 22 | Intended Audience :: Information Technology 23 | License :: OSI Approved :: Apache Software License 24 | Operating System :: OS Independent 25 | Programming Language :: Python 26 | Programming Language :: Python :: 3 :: Only 27 | 28 | [files] 29 | packages = 30 | django_alexa 31 | 32 | [pbr] 33 | warnerrors = True 34 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | from os import path 3 | from sys import version_info 4 | 5 | 6 | def open_file(fname): 7 | return open(path.join(path.dirname(__file__), fname)) 8 | 9 | setup_requires = ['pbr', 'pyversion3'] 10 | 11 | setup( 12 | license='MIT', 13 | setup_requires=setup_requires, 14 | pbr=True, 15 | auto_version="PBR", 16 | install_requires=open(path.join(path.dirname(__file__), 'requirements.txt')).readlines(), 17 | ) 18 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | coveralls>=1.5.1 2 | flake8>=3.6.0 3 | flake8-colors>=0.1.6 4 | pytest-django>=3.4.5 5 | pytest-cov>=2.6.1 6 | pytest>=4.1.0 7 | mock==3.0.5 8 | unittest2==1.1.0 9 | 10 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | minversion = 2.2.0 3 | skipsdist = True 4 | envlist = black, lint, py36 5 | 6 | [testenv] 7 | usedevelop = True 8 | deps = pipenv 9 | commands = 10 | pipenv install --dev -ignore-pipfile 11 | pipenv run py.test 12 | 13 | [pytest] 14 | addopts = --cov-report term --cov django_alexa django_alexa_tests/ 15 | 16 | [testenv:sphinx] 17 | basepython = python3 18 | skip_install = true 19 | deps = sphinx 20 | commands = 21 | make html 22 | make linkcheck 23 | 24 | [testenv:black] 25 | basepython = python3 26 | skip_install = true 27 | deps = 28 | black 29 | commands = 30 | black -N django_alexa/ 31 | 32 | [testenv:lint] 33 | basepython=python3 34 | deps= 35 | flake8 36 | flake8-colors 37 | commands = 38 | flake8 django_alexa/ 39 | 40 | [testenv:testrelease] 41 | usedevelop = False 42 | deps = 43 | wheel 44 | twine 45 | commands = 46 | python setup.py increment tag sdist bdist_egg bdist_wheel 47 | twine upload --repository-url https://test.pypi.org/legacy/ dist/* 48 | 49 | [testenv:release] 50 | usedevelop = False 51 | deps = 52 | wheel 53 | twine 54 | commands = 55 | python setup.py increment tag sdist bdist_egg bdist_wheel 56 | twine upload dist/* 57 | 58 | [flake8] 59 | max-line-length = 88 60 | ignore = D203, W503, E203, E501 61 | select = C,E,F,W,B,B950 62 | exclude = 63 | .tox, 64 | .git, 65 | __pycache__, 66 | docs/source/conf.py, 67 | build, 68 | dist, 69 | tests/fixtures/*, 70 | *.pyc, 71 | *.egg-info, 72 | .cache, 73 | .eggs 74 | max-complexity = 10 75 | import-order-style = google 76 | application-import-names = flake8 77 | format = ${cyan}%(path)s${reset}:${yellow_bold}%(row)d${reset}:${green_bold}%(col)d${reset}: ${red_bold}%(code)s${reset} %(text)s 78 | --------------------------------------------------------------------------------