├── .gitignore ├── .travis.yml ├── LICENSE ├── docs ├── .nojekyll ├── Makefile ├── _static │ └── .gitkeep ├── api.rst ├── build │ ├── doctrees │ │ ├── api.doctree │ │ ├── environment.pickle │ │ ├── examples.doctree │ │ └── index.doctree │ └── html │ │ ├── .buildinfo │ │ ├── _modules │ │ ├── index.html │ │ └── interactive_python │ │ │ ├── _util.html │ │ │ ├── connection.html │ │ │ ├── discovery.html │ │ │ ├── encoding.html │ │ │ ├── errors.html │ │ │ ├── oauth.html │ │ │ ├── scene.html │ │ │ └── state.html │ │ ├── _sources │ │ ├── api.rst.txt │ │ ├── examples.rst.txt │ │ └── index.rst.txt │ │ ├── _static │ │ ├── ajax-loader.gif │ │ ├── alabaster.css │ │ ├── basic.css │ │ ├── comment-bright.png │ │ ├── comment-close.png │ │ ├── comment.png │ │ ├── custom.css │ │ ├── doctools.js │ │ ├── down-pressed.png │ │ ├── down.png │ │ ├── file.png │ │ ├── jquery-3.1.0.js │ │ ├── jquery.js │ │ ├── minus.png │ │ ├── plus.png │ │ ├── pygments.css │ │ ├── searchtools.js │ │ ├── underscore-1.3.1.js │ │ ├── underscore.js │ │ ├── up-pressed.png │ │ ├── up.png │ │ └── websupport.js │ │ ├── api.html │ │ ├── examples.html │ │ ├── genindex.html │ │ ├── index.html │ │ ├── objects.inv │ │ ├── py-modindex.html │ │ ├── search.html │ │ └── searchindex.js ├── conf.py ├── examples.rst ├── index.html ├── index.rst └── make.bat ├── examples ├── 0_init │ ├── __init__.py │ └── pong.py ├── 1_viewer_controlled │ └── pong.py ├── 2_shortcode_oauth │ └── oauth.py ├── __init__.py └── engine.py ├── interactive_python ├── __init__.py ├── _util.py ├── connection.py ├── discovery.py ├── encoding.py ├── errors.py ├── keycodes.py ├── log.py ├── oauth.py ├── scene.py └── state.py ├── readme.md ├── setup.cfg ├── setup.py └── tests ├── __init__.py ├── _util.py ├── connection_test.py ├── encoding_test.py ├── fixture ├── gzipped_square_reply ├── sample0_decoded ├── sample0_encoded ├── sample1_decoded ├── sample1_encoded ├── sample2_decoded └── sample2_encoded ├── keycode_test.py └── resource_test.py /.gitignore: -------------------------------------------------------------------------------- 1 | #### joe made this: http://goel.io/joe 2 | 3 | #####=== Python ===##### 4 | 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | env/ 15 | venv/ 16 | /build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | /.idea 62 | *.iml 63 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - 3.5 4 | - 3.6 5 | install: 6 | - python setup.py -q install 7 | script: nosetests 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft 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 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mixer/interactive-python/348628aec594bf48cf503647e41ec080751fac09/docs/.nojekyll -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " applehelp to make an Apple Help Book" 34 | @echo " devhelp to make HTML files and a Devhelp project" 35 | @echo " epub to make an epub" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | html: 55 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 56 | @echo 57 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 58 | 59 | dirhtml: 60 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 61 | @echo 62 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 63 | 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | pickle: 70 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 71 | @echo 72 | @echo "Build finished; now you can process the pickle files." 73 | 74 | json: 75 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 76 | @echo 77 | @echo "Build finished; now you can process the JSON files." 78 | 79 | htmlhelp: 80 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 81 | @echo 82 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 83 | ".hhp project file in $(BUILDDIR)/htmlhelp." 84 | 85 | qthelp: 86 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 87 | @echo 88 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 89 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 90 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/interactive-python.qhcp" 91 | @echo "To view the help file:" 92 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/interactive-python.qhc" 93 | 94 | applehelp: 95 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 96 | @echo 97 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 98 | @echo "N.B. You won't be able to view it unless you put it in" \ 99 | "~/Library/Documentation/Help or install it in your application" \ 100 | "bundle." 101 | 102 | devhelp: 103 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 104 | @echo 105 | @echo "Build finished." 106 | @echo "To view the help file:" 107 | @echo "# mkdir -p $$HOME/.local/share/devhelp/interactive-python" 108 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/interactive-python" 109 | @echo "# devhelp" 110 | 111 | epub: 112 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 113 | @echo 114 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 115 | 116 | latex: 117 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 118 | @echo 119 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 120 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 121 | "(use \`make latexpdf' here to do that automatically)." 122 | 123 | latexpdf: 124 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 125 | @echo "Running LaTeX files through pdflatex..." 126 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 127 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 128 | 129 | latexpdfja: 130 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 131 | @echo "Running LaTeX files through platex and dvipdfmx..." 132 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 133 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 134 | 135 | text: 136 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 137 | @echo 138 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 139 | 140 | man: 141 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 142 | @echo 143 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 144 | 145 | texinfo: 146 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 147 | @echo 148 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 149 | @echo "Run \`make' in that directory to run these through makeinfo" \ 150 | "(use \`make info' here to do that automatically)." 151 | 152 | info: 153 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 154 | @echo "Running Texinfo files through makeinfo..." 155 | make -C $(BUILDDIR)/texinfo info 156 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 157 | 158 | gettext: 159 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 160 | @echo 161 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 162 | 163 | changes: 164 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 165 | @echo 166 | @echo "The overview file is in $(BUILDDIR)/changes." 167 | 168 | linkcheck: 169 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 170 | @echo 171 | @echo "Link check complete; look for any errors in the above output " \ 172 | "or in $(BUILDDIR)/linkcheck/output.txt." 173 | 174 | doctest: 175 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 176 | @echo "Testing of doctests in the sources finished, look at the " \ 177 | "results in $(BUILDDIR)/doctest/output.txt." 178 | 179 | coverage: 180 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 181 | @echo "Testing of coverage in the sources finished, look at the " \ 182 | "results in $(BUILDDIR)/coverage/python.txt." 183 | 184 | xml: 185 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 186 | @echo 187 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 188 | 189 | pseudoxml: 190 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 191 | @echo 192 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 193 | -------------------------------------------------------------------------------- /docs/_static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mixer/interactive-python/348628aec594bf48cf503647e41ec080751fac09/docs/_static/.gitkeep -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | API 2 | === 3 | 4 | This document describes the Python API to connect to Mixer's Interactive service. We assume you're already somewhat familiar with Interactive. If you're not, check our `reference guide `_. 5 | 6 | To connect to Interactive, you first need to have an Interactive project and some means of authentication. You'll want to `create an Interactive project `_ on Mixer and register an `OAuth client `_ while you're there. For most integrations, you can leave the OAuth "redirect URLs" empty, and you should not request a secret key. 7 | 8 | You can now use the OAuthClient class with your OAuth client ID to get an access token. 9 | 10 | OAuth 11 | ----- 12 | 13 | .. autoclass:: interactive_python.OAuthClient 14 | :members: 15 | :undoc-members: 16 | 17 | .. autoclass:: interactive_python.OAuthShortCode 18 | :members: 19 | :undoc-members: 20 | 21 | .. attribute:: code 22 | 23 | The short six-digit code to be displayed to the user. They should be prompted to enter it on `mixer.com/go `_. 24 | 25 | .. autoclass:: interactive_python.OAuthTokens 26 | :members: 27 | :undoc-members: 28 | 29 | .. attribute:: access 30 | 31 | The OAuth access token to use in :func:`~interactive_python.State.connect`. 32 | 33 | .. attribute:: refresh 34 | 35 | The OAuth refresh token that can be used to re-grant the access token. 36 | 37 | .. attribute:: expires_at 38 | 39 | The datetime at which the access token expires. 40 | 41 | State 42 | ----- 43 | 44 | .. autoclass:: interactive_python.State 45 | :members: 46 | :undoc-members: 47 | :show-inheritance: 48 | 49 | .. attribute:: connection 50 | 51 | The underlying :class:`~interactive_python.Connection` to the Interactive service. You should not need to deal with this most of the time. 52 | 53 | .. autoclass:: interactive_python.Discovery 54 | :members: 55 | :undoc-members: 56 | :show-inheritance: 57 | 58 | Scenes 59 | ------ 60 | 61 | .. autoclass:: interactive_python.Scene 62 | :members: 63 | :show-inheritance: 64 | 65 | .. attribute:: controls 66 | 67 | A dict of control IDs to :class:`~interactive_python.Control`s in the scene. 68 | 69 | 70 | .. autoclass:: interactive_python.Control 71 | :members: 72 | :undoc-members: 73 | :show-inheritance: 74 | 75 | .. autoclass:: interactive_python.Button 76 | :members: 77 | :undoc-members: 78 | :show-inheritance: 79 | 80 | .. autoclass:: interactive_python.Joystick 81 | :members: 82 | :undoc-members: 83 | :show-inheritance: 84 | 85 | Utilities 86 | --------- 87 | 88 | .. data:: interactive_python.keycode 89 | 90 | Keycode is an instance of a class that helps translate keycodes from their textual representation to their corresponding numeric code, as represented on the protocol. For example:: 91 | 92 | from interactive_python import keycode 93 | 94 | print(keycode.up) # => 38 95 | print(keycode.a) # => 65 96 | getattr(keycode, '⌘') # => 91 97 | 98 | 99 | .. autoclass:: interactive_python._util.ChangeTracker 100 | :members: 101 | :undoc-members: 102 | :show-inheritance: 103 | 104 | .. autoclass:: interactive_python._util.Resource 105 | :members: 106 | :undoc-members: 107 | :show-inheritance: 108 | 109 | Errors 110 | ------ 111 | 112 | .. autoclass:: interactive_python.DiscoveryError 113 | :show-inheritance: 114 | 115 | .. autoclass:: interactive_python.NoServersAvailableError 116 | :show-inheritance: 117 | 118 | .. autoclass:: interactive_python.ShortCodeError 119 | :show-inheritance: 120 | 121 | .. autoclass:: interactive_python.UnknownShortCodeError 122 | :show-inheritance: 123 | 124 | .. autoclass:: interactive_python.ShortCodeAccessDeniedError 125 | :show-inheritance: 126 | 127 | .. autoclass:: interactive_python.ShortCodeTimeoutError 128 | :show-inheritance: 129 | 130 | Low-Level Protocol 131 | ------------------ 132 | 133 | .. autoclass:: interactive_python.Connection 134 | :members: 135 | :undoc-members: 136 | :show-inheritance: 137 | 138 | .. autoclass:: interactive_python.Call 139 | :members: 140 | :undoc-members: 141 | :show-inheritance: 142 | 143 | 144 | .. automodule:: interactive_python.encoding 145 | :members: 146 | :undoc-members: 147 | :show-inheritance: 148 | -------------------------------------------------------------------------------- /docs/build/doctrees/api.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mixer/interactive-python/348628aec594bf48cf503647e41ec080751fac09/docs/build/doctrees/api.doctree -------------------------------------------------------------------------------- /docs/build/doctrees/environment.pickle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mixer/interactive-python/348628aec594bf48cf503647e41ec080751fac09/docs/build/doctrees/environment.pickle -------------------------------------------------------------------------------- /docs/build/doctrees/examples.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mixer/interactive-python/348628aec594bf48cf503647e41ec080751fac09/docs/build/doctrees/examples.doctree -------------------------------------------------------------------------------- /docs/build/doctrees/index.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mixer/interactive-python/348628aec594bf48cf503647e41ec080751fac09/docs/build/doctrees/index.doctree -------------------------------------------------------------------------------- /docs/build/html/.buildinfo: -------------------------------------------------------------------------------- 1 | # Sphinx build info version 1 2 | # This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. 3 | config: 0230582b10b603459a61363b5f1dd01c 4 | tags: 645f666f9bcd5a90fca523b33c5a78b7 5 | -------------------------------------------------------------------------------- /docs/build/html/_modules/index.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | Overview: module code — interactive-python 1.0.0 documentation 9 | 10 | 11 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 |
37 | 55 | 75 |
76 |
77 | 85 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /docs/build/html/_modules/interactive_python/discovery.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | interactive_python.discovery — interactive-python 1.0.0 documentation 9 | 10 | 11 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 |
37 |
38 |
39 |
40 | 41 |

Source code for interactive_python.discovery

 42 | import aiohttp
 43 | 
 44 | from .errors import DiscoveryError, NoServersAvailableError
 45 | 
 46 | 
 47 | 
[docs]class Discovery: 48 | """Discovery is a simple service discovery class which retrieves an 49 | Interactive host to connect to. This is passed into the State by default; 50 | you usually should not need to override it. 51 | """ 52 | def __init__(self, host='https://mixer.com', 53 | path='/api/v1/interactive/hosts', loop=None, timeout=10): 54 | self._host = host 55 | self._path = path 56 | self._timeout = timeout 57 | self._loop = None 58 | 59 |
[docs] async def find(self): 60 | """Returns the websocket address of an interactive server to connect 61 | to, or raises a NoServersAvailableError. 62 | """ 63 | 64 | # Technically it might be nice to asyncio this, but I'm not eager to 65 | # add another dependency and Python doesn't seem to have an asyncio 66 | # http client in its standard library yet. 67 | 68 | async with aiohttp.ClientSession(loop=self._loop) as session: 69 | async with session.get(self._host + self._path) as res: 70 | if res.status >= 300: 71 | raise DiscoveryError('Expected a 2xx status code, but' 72 | 'got {}'.format(res.status)) 73 | 74 | servers = await res.json() 75 | if len(servers) == 0: 76 | raise NoServersAvailableError() 77 | print(servers[0]['address']) 78 | return servers[0]['address']
79 |
80 | 81 |
82 |
83 |
84 | 106 |
107 |
108 | 116 | 117 | 118 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /docs/build/html/_modules/interactive_python/errors.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | interactive_python.errors — interactive-python 1.0.0 documentation 9 | 10 | 11 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 |
37 |
38 |
39 |
40 | 41 |

Source code for interactive_python.errors

 42 | 
[docs]class DiscoveryError(Exception): 43 | """Raised if some error occurs during service discovery 44 | that we didn't anticipate. 45 | """ 46 | pass
47 | 48 | 49 |
[docs]class NoServersAvailableError(Exception): 50 | """Raised if Beam reports that no servers are available.""" 51 | pass
52 | 53 | 54 |
[docs]class ShortCodeError(Exception): 55 | """Base exception raised when some unexpected event occurs in the shortcode 56 | OAuth flow.""" 57 | pass
58 | 59 | 60 |
[docs]class UnknownShortCodeError(ShortCodeError): 61 | """Exception raised when an unknown error happens while running shortcode 62 | OAuth. 63 | """ 64 | pass
65 | 66 | 67 |
[docs]class ShortCodeAccessDeniedError(ShortCodeError): 68 | """Exception raised when the user denies access to the client in shortcode 69 | OAuth.""" 70 | pass
71 | 72 | 73 |
[docs]class ShortCodeTimeoutError(ShortCodeError): 74 | """Exception raised when the shortcode expires without being accepted.""" 75 | pass
76 |
77 | 78 |
79 |
80 |
81 | 103 |
104 |
105 | 113 | 114 | 115 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /docs/build/html/_sources/api.rst.txt: -------------------------------------------------------------------------------- 1 | API 2 | === 3 | 4 | This document describes the Python API to connect to Mixer's Interactive service. We assume you're already somewhat familiar with Interactive. If you're not, check our `reference guide `_. 5 | 6 | To connect to Interactive, you first need to have an Interactive project and some means of authentication. You'll want to `create an Interactive project `_ on Mixer and register an `OAuth client `_ while you're there. For most integrations, you can leave the OAuth "redirect URLs" empty, and you should not request a secret key. 7 | 8 | You can now use the OAuthClient class with your OAuth client ID to get an access token. 9 | 10 | OAuth 11 | ----- 12 | 13 | .. autoclass:: interactive_python.OAuthClient 14 | :members: 15 | :undoc-members: 16 | 17 | .. autoclass:: interactive_python.OAuthShortCode 18 | :members: 19 | :undoc-members: 20 | 21 | .. attribute:: code 22 | 23 | The short six-digit code to be displayed to the user. They should be prompted to enter it on `mixer.com/go `_. 24 | 25 | .. autoclass:: interactive_python.OAuthTokens 26 | :members: 27 | :undoc-members: 28 | 29 | .. attribute:: access 30 | 31 | The OAuth access token to use in :func:`~interactive_python.State.connect`. 32 | 33 | .. attribute:: refresh 34 | 35 | The OAuth refresh token that can be used to re-grant the access token. 36 | 37 | .. attribute:: expires_at 38 | 39 | The datetime at which the access token expires. 40 | 41 | State 42 | ----- 43 | 44 | .. autoclass:: interactive_python.State 45 | :members: 46 | :undoc-members: 47 | :show-inheritance: 48 | 49 | .. attribute:: connection 50 | 51 | The underlying :class:`~interactive_python.Connection` to the Interactive service. You should not need to deal with this most of the time. 52 | 53 | .. autoclass:: interactive_python.Discovery 54 | :members: 55 | :undoc-members: 56 | :show-inheritance: 57 | 58 | Scenes 59 | ------ 60 | 61 | .. autoclass:: interactive_python.Scene 62 | :members: 63 | :show-inheritance: 64 | 65 | .. attribute:: controls 66 | 67 | A dict of control IDs to :class:`~interactive_python.Control`s in the scene. 68 | 69 | 70 | .. autoclass:: interactive_python.Control 71 | :members: 72 | :undoc-members: 73 | :show-inheritance: 74 | 75 | .. autoclass:: interactive_python.Button 76 | :members: 77 | :undoc-members: 78 | :show-inheritance: 79 | 80 | .. autoclass:: interactive_python.Joystick 81 | :members: 82 | :undoc-members: 83 | :show-inheritance: 84 | 85 | Utilities 86 | --------- 87 | 88 | .. data:: interactive_python.keycode 89 | 90 | Keycode is an instance of a class that helps translate keycodes from their textual representation to their corresponding numeric code, as represented on the protocol. For example:: 91 | 92 | from interactive_python import keycode 93 | 94 | print(keycode.up) # => 38 95 | print(keycode.a) # => 65 96 | getattr(keycode, '⌘') # => 91 97 | 98 | 99 | .. autoclass:: interactive_python._util.ChangeTracker 100 | :members: 101 | :undoc-members: 102 | :show-inheritance: 103 | 104 | .. autoclass:: interactive_python._util.Resource 105 | :members: 106 | :undoc-members: 107 | :show-inheritance: 108 | 109 | Errors 110 | ------ 111 | 112 | .. autoclass:: interactive_python.DiscoveryError 113 | :show-inheritance: 114 | 115 | .. autoclass:: interactive_python.NoServersAvailableError 116 | :show-inheritance: 117 | 118 | .. autoclass:: interactive_python.ShortCodeError 119 | :show-inheritance: 120 | 121 | .. autoclass:: interactive_python.UnknownShortCodeError 122 | :show-inheritance: 123 | 124 | .. autoclass:: interactive_python.ShortCodeAccessDeniedError 125 | :show-inheritance: 126 | 127 | .. autoclass:: interactive_python.ShortCodeTimeoutError 128 | :show-inheritance: 129 | 130 | Low-Level Protocol 131 | ------------------ 132 | 133 | .. autoclass:: interactive_python.Connection 134 | :members: 135 | :undoc-members: 136 | :show-inheritance: 137 | 138 | .. autoclass:: interactive_python.Call 139 | :members: 140 | :undoc-members: 141 | :show-inheritance: 142 | 143 | 144 | .. automodule:: interactive_python.encoding 145 | :members: 146 | :undoc-members: 147 | :show-inheritance: 148 | -------------------------------------------------------------------------------- /docs/build/html/_sources/examples.rst.txt: -------------------------------------------------------------------------------- 1 | Runnable Examples 2 | ================= 3 | 4 | OAuth Flow 5 | ---------- 6 | 7 | .. NOTE:: 8 | 9 | The runnable source for this can be found in the `examples/2_shortcode_oauth `_ folder of this repo. 10 | 11 | This example shows you how to use the shortcode OAuth flow to get an access token to connect to Interactive. For development you can `manually request a token `_, but the shortcode flow provides a better experience to users! 12 | 13 | .. include:: ../examples/2_shortcode_oauth/oauth.py 14 | :start-line: 12 15 | :end-line: 34 16 | :code: python 17 | 18 | 19 | Pong 20 | ---- 21 | 22 | .. NOTE:: 23 | 24 | The runnable source for this can be found in the `examples/1_viewer_controlled `_ folder of this repo. 25 | 26 | For an initial example, we'll create a small pong game. We already wrote a tiny game engine that can be used to create a two player game like so: 27 | 28 | .. include:: ../examples/0_init/pong.py 29 | :start-line: 15 30 | :end-line: 33 31 | :code: python 32 | 33 | Let's make it Interactive! 34 | 35 | .. include:: ../examples/1_viewer_controlled/pong.py 36 | :start-line: 22 37 | :end-line: 86 38 | :code: python 39 | -------------------------------------------------------------------------------- /docs/build/html/_sources/index.rst.txt: -------------------------------------------------------------------------------- 1 | .. interactive-python documentation master file, created by 2 | sphinx-quickstart on Tue Aug 1 18:55:17 2017. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Mixer Interactive Python API 7 | ============================ 8 | 9 | This API is a reference implementation for connecting to Mixer's Interactive service as a game client. This documentation and library is stable, but is still actively developed and further expansions are planned. 10 | 11 | Contents: 12 | 13 | .. toctree:: 14 | :maxdepth: 2 15 | 16 | api 17 | examples 18 | 19 | 20 | 21 | Indices and tables 22 | ================== 23 | 24 | * :ref:`genindex` 25 | * :ref:`modindex` 26 | * :ref:`search` 27 | 28 | -------------------------------------------------------------------------------- /docs/build/html/_static/ajax-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mixer/interactive-python/348628aec594bf48cf503647e41ec080751fac09/docs/build/html/_static/ajax-loader.gif -------------------------------------------------------------------------------- /docs/build/html/_static/alabaster.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | @import url("basic.css"); 54 | 55 | /* -- page layout ----------------------------------------------------------- */ 56 | 57 | body { 58 | font-family: 'goudy old style', 'minion pro', 'bell mt', Georgia, 'Hiragino Mincho Pro', serif; 59 | font-size: 17px; 60 | background-color: #fff; 61 | color: #000; 62 | margin: 0; 63 | padding: 0; 64 | } 65 | 66 | 67 | div.document { 68 | width: 940px; 69 | margin: 30px auto 0 auto; 70 | } 71 | 72 | div.documentwrapper { 73 | float: left; 74 | width: 100%; 75 | } 76 | 77 | div.bodywrapper { 78 | margin: 0 0 0 220px; 79 | } 80 | 81 | div.sphinxsidebar { 82 | width: 220px; 83 | font-size: 14px; 84 | line-height: 1.5; 85 | } 86 | 87 | hr { 88 | border: 1px solid #B1B4B6; 89 | } 90 | 91 | div.body { 92 | background-color: #fff; 93 | color: #3E4349; 94 | padding: 0 30px 0 30px; 95 | } 96 | 97 | div.body > .section { 98 | text-align: left; 99 | } 100 | 101 | div.footer { 102 | width: 940px; 103 | margin: 20px auto 30px auto; 104 | font-size: 14px; 105 | color: #888; 106 | text-align: right; 107 | } 108 | 109 | div.footer a { 110 | color: #888; 111 | } 112 | 113 | p.caption { 114 | font-family: inherit; 115 | font-size: inherit; 116 | } 117 | 118 | 119 | div.relations { 120 | display: none; 121 | } 122 | 123 | 124 | div.sphinxsidebar a { 125 | color: #444; 126 | text-decoration: none; 127 | border-bottom: 1px dotted #999; 128 | } 129 | 130 | div.sphinxsidebar a:hover { 131 | border-bottom: 1px solid #999; 132 | } 133 | 134 | div.sphinxsidebarwrapper { 135 | padding: 18px 10px; 136 | } 137 | 138 | div.sphinxsidebarwrapper p.logo { 139 | padding: 0; 140 | margin: -10px 0 0 0px; 141 | text-align: center; 142 | } 143 | 144 | div.sphinxsidebarwrapper h1.logo { 145 | margin-top: -10px; 146 | text-align: center; 147 | margin-bottom: 5px; 148 | text-align: left; 149 | } 150 | 151 | div.sphinxsidebarwrapper h1.logo-name { 152 | margin-top: 0px; 153 | } 154 | 155 | div.sphinxsidebarwrapper p.blurb { 156 | margin-top: 0; 157 | font-style: normal; 158 | } 159 | 160 | div.sphinxsidebar h3, 161 | div.sphinxsidebar h4 { 162 | font-family: 'Garamond', 'Georgia', serif; 163 | color: #444; 164 | font-size: 24px; 165 | font-weight: normal; 166 | margin: 0 0 5px 0; 167 | padding: 0; 168 | } 169 | 170 | div.sphinxsidebar h4 { 171 | font-size: 20px; 172 | } 173 | 174 | div.sphinxsidebar h3 a { 175 | color: #444; 176 | } 177 | 178 | div.sphinxsidebar p.logo a, 179 | div.sphinxsidebar h3 a, 180 | div.sphinxsidebar p.logo a:hover, 181 | div.sphinxsidebar h3 a:hover { 182 | border: none; 183 | } 184 | 185 | div.sphinxsidebar p { 186 | color: #555; 187 | margin: 10px 0; 188 | } 189 | 190 | div.sphinxsidebar ul { 191 | margin: 10px 0; 192 | padding: 0; 193 | color: #000; 194 | } 195 | 196 | div.sphinxsidebar ul li.toctree-l1 > a { 197 | font-size: 120%; 198 | } 199 | 200 | div.sphinxsidebar ul li.toctree-l2 > a { 201 | font-size: 110%; 202 | } 203 | 204 | div.sphinxsidebar input { 205 | border: 1px solid #CCC; 206 | font-family: 'goudy old style', 'minion pro', 'bell mt', Georgia, 'Hiragino Mincho Pro', serif; 207 | font-size: 1em; 208 | } 209 | 210 | div.sphinxsidebar hr { 211 | border: none; 212 | height: 1px; 213 | color: #AAA; 214 | background: #AAA; 215 | 216 | text-align: left; 217 | margin-left: 0; 218 | width: 50%; 219 | } 220 | 221 | /* -- body styles ----------------------------------------------------------- */ 222 | 223 | a { 224 | color: #004B6B; 225 | text-decoration: underline; 226 | } 227 | 228 | a:hover { 229 | color: #6D4100; 230 | text-decoration: underline; 231 | } 232 | 233 | div.body h1, 234 | div.body h2, 235 | div.body h3, 236 | div.body h4, 237 | div.body h5, 238 | div.body h6 { 239 | font-family: 'Garamond', 'Georgia', serif; 240 | font-weight: normal; 241 | margin: 30px 0px 10px 0px; 242 | padding: 0; 243 | } 244 | 245 | div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; } 246 | div.body h2 { font-size: 180%; } 247 | div.body h3 { font-size: 150%; } 248 | div.body h4 { font-size: 130%; } 249 | div.body h5 { font-size: 100%; } 250 | div.body h6 { font-size: 100%; } 251 | 252 | a.headerlink { 253 | color: #DDD; 254 | padding: 0 4px; 255 | text-decoration: none; 256 | } 257 | 258 | a.headerlink:hover { 259 | color: #444; 260 | background: #EAEAEA; 261 | } 262 | 263 | div.body p, div.body dd, div.body li { 264 | line-height: 1.4em; 265 | } 266 | 267 | div.admonition { 268 | margin: 20px 0px; 269 | padding: 10px 30px; 270 | background-color: #EEE; 271 | border: 1px solid #CCC; 272 | } 273 | 274 | div.admonition tt.xref, div.admonition code.xref, div.admonition a tt { 275 | background-color: #FBFBFB; 276 | border-bottom: 1px solid #fafafa; 277 | } 278 | 279 | div.admonition p.admonition-title { 280 | font-family: 'Garamond', 'Georgia', serif; 281 | font-weight: normal; 282 | font-size: 24px; 283 | margin: 0 0 10px 0; 284 | padding: 0; 285 | line-height: 1; 286 | } 287 | 288 | div.admonition p.last { 289 | margin-bottom: 0; 290 | } 291 | 292 | div.highlight { 293 | background-color: #fff; 294 | } 295 | 296 | dt:target, .highlight { 297 | background: #FAF3E8; 298 | } 299 | 300 | div.warning { 301 | background-color: #FCC; 302 | border: 1px solid #FAA; 303 | } 304 | 305 | div.danger { 306 | background-color: #FCC; 307 | border: 1px solid #FAA; 308 | -moz-box-shadow: 2px 2px 4px #D52C2C; 309 | -webkit-box-shadow: 2px 2px 4px #D52C2C; 310 | box-shadow: 2px 2px 4px #D52C2C; 311 | } 312 | 313 | div.error { 314 | background-color: #FCC; 315 | border: 1px solid #FAA; 316 | -moz-box-shadow: 2px 2px 4px #D52C2C; 317 | -webkit-box-shadow: 2px 2px 4px #D52C2C; 318 | box-shadow: 2px 2px 4px #D52C2C; 319 | } 320 | 321 | div.caution { 322 | background-color: #FCC; 323 | border: 1px solid #FAA; 324 | } 325 | 326 | div.attention { 327 | background-color: #FCC; 328 | border: 1px solid #FAA; 329 | } 330 | 331 | div.important { 332 | background-color: #EEE; 333 | border: 1px solid #CCC; 334 | } 335 | 336 | div.note { 337 | background-color: #EEE; 338 | border: 1px solid #CCC; 339 | } 340 | 341 | div.tip { 342 | background-color: #EEE; 343 | border: 1px solid #CCC; 344 | } 345 | 346 | div.hint { 347 | background-color: #EEE; 348 | border: 1px solid #CCC; 349 | } 350 | 351 | div.seealso { 352 | background-color: #EEE; 353 | border: 1px solid #CCC; 354 | } 355 | 356 | div.topic { 357 | background-color: #EEE; 358 | } 359 | 360 | p.admonition-title { 361 | display: inline; 362 | } 363 | 364 | p.admonition-title:after { 365 | content: ":"; 366 | } 367 | 368 | pre, tt, code { 369 | font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; 370 | font-size: 0.9em; 371 | } 372 | 373 | .hll { 374 | background-color: #FFC; 375 | margin: 0 -12px; 376 | padding: 0 12px; 377 | display: block; 378 | } 379 | 380 | img.screenshot { 381 | } 382 | 383 | tt.descname, tt.descclassname, code.descname, code.descclassname { 384 | font-size: 0.95em; 385 | } 386 | 387 | tt.descname, code.descname { 388 | padding-right: 0.08em; 389 | } 390 | 391 | img.screenshot { 392 | -moz-box-shadow: 2px 2px 4px #EEE; 393 | -webkit-box-shadow: 2px 2px 4px #EEE; 394 | box-shadow: 2px 2px 4px #EEE; 395 | } 396 | 397 | table.docutils { 398 | border: 1px solid #888; 399 | -moz-box-shadow: 2px 2px 4px #EEE; 400 | -webkit-box-shadow: 2px 2px 4px #EEE; 401 | box-shadow: 2px 2px 4px #EEE; 402 | } 403 | 404 | table.docutils td, table.docutils th { 405 | border: 1px solid #888; 406 | padding: 0.25em 0.7em; 407 | } 408 | 409 | table.field-list, table.footnote { 410 | border: none; 411 | -moz-box-shadow: none; 412 | -webkit-box-shadow: none; 413 | box-shadow: none; 414 | } 415 | 416 | table.footnote { 417 | margin: 15px 0; 418 | width: 100%; 419 | border: 1px solid #EEE; 420 | background: #FDFDFD; 421 | font-size: 0.9em; 422 | } 423 | 424 | table.footnote + table.footnote { 425 | margin-top: -15px; 426 | border-top: none; 427 | } 428 | 429 | table.field-list th { 430 | padding: 0 0.8em 0 0; 431 | } 432 | 433 | table.field-list td { 434 | padding: 0; 435 | } 436 | 437 | table.field-list p { 438 | margin-bottom: 0.8em; 439 | } 440 | 441 | /* Cloned from 442 | * https://github.com/sphinx-doc/sphinx/commit/ef60dbfce09286b20b7385333d63a60321784e68 443 | */ 444 | .field-name { 445 | -moz-hyphens: manual; 446 | -ms-hyphens: manual; 447 | -webkit-hyphens: manual; 448 | hyphens: manual; 449 | } 450 | 451 | table.footnote td.label { 452 | width: .1px; 453 | padding: 0.3em 0 0.3em 0.5em; 454 | } 455 | 456 | table.footnote td { 457 | padding: 0.3em 0.5em; 458 | } 459 | 460 | dl { 461 | margin: 0; 462 | padding: 0; 463 | } 464 | 465 | dl dd { 466 | margin-left: 30px; 467 | } 468 | 469 | blockquote { 470 | margin: 0 0 0 30px; 471 | padding: 0; 472 | } 473 | 474 | ul, ol { 475 | /* Matches the 30px from the narrow-screen "li > ul" selector below */ 476 | margin: 10px 0 10px 30px; 477 | padding: 0; 478 | } 479 | 480 | pre { 481 | background: #EEE; 482 | padding: 7px 30px; 483 | margin: 15px 0px; 484 | line-height: 1.3em; 485 | } 486 | 487 | div.viewcode-block:target { 488 | background: #ffd; 489 | } 490 | 491 | dl pre, blockquote pre, li pre { 492 | margin-left: 0; 493 | padding-left: 30px; 494 | } 495 | 496 | tt, code { 497 | background-color: #ecf0f3; 498 | color: #222; 499 | /* padding: 1px 2px; */ 500 | } 501 | 502 | tt.xref, code.xref, a tt { 503 | background-color: #FBFBFB; 504 | border-bottom: 1px solid #fff; 505 | } 506 | 507 | a.reference { 508 | text-decoration: none; 509 | border-bottom: 1px dotted #004B6B; 510 | } 511 | 512 | /* Don't put an underline on images */ 513 | a.image-reference, a.image-reference:hover { 514 | border-bottom: none; 515 | } 516 | 517 | a.reference:hover { 518 | border-bottom: 1px solid #6D4100; 519 | } 520 | 521 | a.footnote-reference { 522 | text-decoration: none; 523 | font-size: 0.7em; 524 | vertical-align: top; 525 | border-bottom: 1px dotted #004B6B; 526 | } 527 | 528 | a.footnote-reference:hover { 529 | border-bottom: 1px solid #6D4100; 530 | } 531 | 532 | a:hover tt, a:hover code { 533 | background: #EEE; 534 | } 535 | 536 | 537 | @media screen and (max-width: 870px) { 538 | 539 | div.sphinxsidebar { 540 | display: none; 541 | } 542 | 543 | div.document { 544 | width: 100%; 545 | 546 | } 547 | 548 | div.documentwrapper { 549 | margin-left: 0; 550 | margin-top: 0; 551 | margin-right: 0; 552 | margin-bottom: 0; 553 | } 554 | 555 | div.bodywrapper { 556 | margin-top: 0; 557 | margin-right: 0; 558 | margin-bottom: 0; 559 | margin-left: 0; 560 | } 561 | 562 | ul { 563 | margin-left: 0; 564 | } 565 | 566 | li > ul { 567 | /* Matches the 30px from the "ul, ol" selector above */ 568 | margin-left: 30px; 569 | } 570 | 571 | .document { 572 | width: auto; 573 | } 574 | 575 | .footer { 576 | width: auto; 577 | } 578 | 579 | .bodywrapper { 580 | margin: 0; 581 | } 582 | 583 | .footer { 584 | width: auto; 585 | } 586 | 587 | .github { 588 | display: none; 589 | } 590 | 591 | 592 | 593 | } 594 | 595 | 596 | 597 | @media screen and (max-width: 875px) { 598 | 599 | body { 600 | margin: 0; 601 | padding: 20px 30px; 602 | } 603 | 604 | div.documentwrapper { 605 | float: none; 606 | background: #fff; 607 | } 608 | 609 | div.sphinxsidebar { 610 | display: block; 611 | float: none; 612 | width: 102.5%; 613 | margin: 50px -30px -20px -30px; 614 | padding: 10px 20px; 615 | background: #333; 616 | color: #FFF; 617 | } 618 | 619 | div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p, 620 | div.sphinxsidebar h3 a { 621 | color: #fff; 622 | } 623 | 624 | div.sphinxsidebar a { 625 | color: #AAA; 626 | } 627 | 628 | div.sphinxsidebar p.logo { 629 | display: none; 630 | } 631 | 632 | div.document { 633 | width: 100%; 634 | margin: 0; 635 | } 636 | 637 | div.footer { 638 | display: none; 639 | } 640 | 641 | div.bodywrapper { 642 | margin: 0; 643 | } 644 | 645 | div.body { 646 | min-height: 0; 647 | padding: 0; 648 | } 649 | 650 | .rtd_doc_footer { 651 | display: none; 652 | } 653 | 654 | .document { 655 | width: auto; 656 | } 657 | 658 | .footer { 659 | width: auto; 660 | } 661 | 662 | .footer { 663 | width: auto; 664 | } 665 | 666 | .github { 667 | display: none; 668 | } 669 | } 670 | 671 | 672 | /* misc. */ 673 | 674 | .revsys-inline { 675 | display: none!important; 676 | } 677 | 678 | /* Make nested-list/multi-paragraph items look better in Releases changelog 679 | * pages. Without this, docutils' magical list fuckery causes inconsistent 680 | * formatting between different release sub-lists. 681 | */ 682 | div#changelog > div.section > ul > li > p:only-child { 683 | margin-bottom: 0; 684 | } 685 | 686 | /* Hide fugly table cell borders in ..bibliography:: directive output */ 687 | table.docutils.citation, table.docutils.citation td, table.docutils.citation th { 688 | border: none; 689 | /* Below needed in some edge cases; if not applied, bottom shadows appear */ 690 | -moz-box-shadow: none; 691 | -webkit-box-shadow: none; 692 | box-shadow: none; 693 | } -------------------------------------------------------------------------------- /docs/build/html/_static/basic.css: -------------------------------------------------------------------------------- 1 | /* 2 | * basic.css 3 | * ~~~~~~~~~ 4 | * 5 | * Sphinx stylesheet -- basic theme. 6 | * 7 | * :copyright: Copyright 2007-2017 by the Sphinx team, see AUTHORS. 8 | * :license: BSD, see LICENSE for details. 9 | * 10 | */ 11 | 12 | /* -- main layout ----------------------------------------------------------- */ 13 | 14 | div.clearer { 15 | clear: both; 16 | } 17 | 18 | /* -- relbar ---------------------------------------------------------------- */ 19 | 20 | div.related { 21 | width: 100%; 22 | font-size: 90%; 23 | } 24 | 25 | div.related h3 { 26 | display: none; 27 | } 28 | 29 | div.related ul { 30 | margin: 0; 31 | padding: 0 0 0 10px; 32 | list-style: none; 33 | } 34 | 35 | div.related li { 36 | display: inline; 37 | } 38 | 39 | div.related li.right { 40 | float: right; 41 | margin-right: 5px; 42 | } 43 | 44 | /* -- sidebar --------------------------------------------------------------- */ 45 | 46 | div.sphinxsidebarwrapper { 47 | padding: 10px 5px 0 10px; 48 | } 49 | 50 | div.sphinxsidebar { 51 | float: left; 52 | width: 230px; 53 | margin-left: -100%; 54 | font-size: 90%; 55 | word-wrap: break-word; 56 | overflow-wrap : break-word; 57 | } 58 | 59 | div.sphinxsidebar ul { 60 | list-style: none; 61 | } 62 | 63 | div.sphinxsidebar ul ul, 64 | div.sphinxsidebar ul.want-points { 65 | margin-left: 20px; 66 | list-style: square; 67 | } 68 | 69 | div.sphinxsidebar ul ul { 70 | margin-top: 0; 71 | margin-bottom: 0; 72 | } 73 | 74 | div.sphinxsidebar form { 75 | margin-top: 10px; 76 | } 77 | 78 | div.sphinxsidebar input { 79 | border: 1px solid #98dbcc; 80 | font-family: sans-serif; 81 | font-size: 1em; 82 | } 83 | 84 | div.sphinxsidebar #searchbox input[type="text"] { 85 | width: 170px; 86 | } 87 | 88 | img { 89 | border: 0; 90 | max-width: 100%; 91 | } 92 | 93 | /* -- search page ----------------------------------------------------------- */ 94 | 95 | ul.search { 96 | margin: 10px 0 0 20px; 97 | padding: 0; 98 | } 99 | 100 | ul.search li { 101 | padding: 5px 0 5px 20px; 102 | background-image: url(file.png); 103 | background-repeat: no-repeat; 104 | background-position: 0 7px; 105 | } 106 | 107 | ul.search li a { 108 | font-weight: bold; 109 | } 110 | 111 | ul.search li div.context { 112 | color: #888; 113 | margin: 2px 0 0 30px; 114 | text-align: left; 115 | } 116 | 117 | ul.keywordmatches li.goodmatch a { 118 | font-weight: bold; 119 | } 120 | 121 | /* -- index page ------------------------------------------------------------ */ 122 | 123 | table.contentstable { 124 | width: 90%; 125 | margin-left: auto; 126 | margin-right: auto; 127 | } 128 | 129 | table.contentstable p.biglink { 130 | line-height: 150%; 131 | } 132 | 133 | a.biglink { 134 | font-size: 1.3em; 135 | } 136 | 137 | span.linkdescr { 138 | font-style: italic; 139 | padding-top: 5px; 140 | font-size: 90%; 141 | } 142 | 143 | /* -- general index --------------------------------------------------------- */ 144 | 145 | table.indextable { 146 | width: 100%; 147 | } 148 | 149 | table.indextable td { 150 | text-align: left; 151 | vertical-align: top; 152 | } 153 | 154 | table.indextable ul { 155 | margin-top: 0; 156 | margin-bottom: 0; 157 | list-style-type: none; 158 | } 159 | 160 | table.indextable > tbody > tr > td > ul { 161 | padding-left: 0em; 162 | } 163 | 164 | table.indextable tr.pcap { 165 | height: 10px; 166 | } 167 | 168 | table.indextable tr.cap { 169 | margin-top: 10px; 170 | background-color: #f2f2f2; 171 | } 172 | 173 | img.toggler { 174 | margin-right: 3px; 175 | margin-top: 3px; 176 | cursor: pointer; 177 | } 178 | 179 | div.modindex-jumpbox { 180 | border-top: 1px solid #ddd; 181 | border-bottom: 1px solid #ddd; 182 | margin: 1em 0 1em 0; 183 | padding: 0.4em; 184 | } 185 | 186 | div.genindex-jumpbox { 187 | border-top: 1px solid #ddd; 188 | border-bottom: 1px solid #ddd; 189 | margin: 1em 0 1em 0; 190 | padding: 0.4em; 191 | } 192 | 193 | /* -- domain module index --------------------------------------------------- */ 194 | 195 | table.modindextable td { 196 | padding: 2px; 197 | border-collapse: collapse; 198 | } 199 | 200 | /* -- general body styles --------------------------------------------------- */ 201 | 202 | div.body p, div.body dd, div.body li, div.body blockquote { 203 | -moz-hyphens: auto; 204 | -ms-hyphens: auto; 205 | -webkit-hyphens: auto; 206 | hyphens: auto; 207 | } 208 | 209 | a.headerlink { 210 | visibility: hidden; 211 | } 212 | 213 | h1:hover > a.headerlink, 214 | h2:hover > a.headerlink, 215 | h3:hover > a.headerlink, 216 | h4:hover > a.headerlink, 217 | h5:hover > a.headerlink, 218 | h6:hover > a.headerlink, 219 | dt:hover > a.headerlink, 220 | caption:hover > a.headerlink, 221 | p.caption:hover > a.headerlink, 222 | div.code-block-caption:hover > a.headerlink { 223 | visibility: visible; 224 | } 225 | 226 | div.body p.caption { 227 | text-align: inherit; 228 | } 229 | 230 | div.body td { 231 | text-align: left; 232 | } 233 | 234 | .first { 235 | margin-top: 0 !important; 236 | } 237 | 238 | p.rubric { 239 | margin-top: 30px; 240 | font-weight: bold; 241 | } 242 | 243 | img.align-left, .figure.align-left, object.align-left { 244 | clear: left; 245 | float: left; 246 | margin-right: 1em; 247 | } 248 | 249 | img.align-right, .figure.align-right, object.align-right { 250 | clear: right; 251 | float: right; 252 | margin-left: 1em; 253 | } 254 | 255 | img.align-center, .figure.align-center, object.align-center { 256 | display: block; 257 | margin-left: auto; 258 | margin-right: auto; 259 | } 260 | 261 | .align-left { 262 | text-align: left; 263 | } 264 | 265 | .align-center { 266 | text-align: center; 267 | } 268 | 269 | .align-right { 270 | text-align: right; 271 | } 272 | 273 | /* -- sidebars -------------------------------------------------------------- */ 274 | 275 | div.sidebar { 276 | margin: 0 0 0.5em 1em; 277 | border: 1px solid #ddb; 278 | padding: 7px 7px 0 7px; 279 | background-color: #ffe; 280 | width: 40%; 281 | float: right; 282 | } 283 | 284 | p.sidebar-title { 285 | font-weight: bold; 286 | } 287 | 288 | /* -- topics ---------------------------------------------------------------- */ 289 | 290 | div.topic { 291 | border: 1px solid #ccc; 292 | padding: 7px 7px 0 7px; 293 | margin: 10px 0 10px 0; 294 | } 295 | 296 | p.topic-title { 297 | font-size: 1.1em; 298 | font-weight: bold; 299 | margin-top: 10px; 300 | } 301 | 302 | /* -- admonitions ----------------------------------------------------------- */ 303 | 304 | div.admonition { 305 | margin-top: 10px; 306 | margin-bottom: 10px; 307 | padding: 7px; 308 | } 309 | 310 | div.admonition dt { 311 | font-weight: bold; 312 | } 313 | 314 | div.admonition dl { 315 | margin-bottom: 0; 316 | } 317 | 318 | p.admonition-title { 319 | margin: 0px 10px 5px 0px; 320 | font-weight: bold; 321 | } 322 | 323 | div.body p.centered { 324 | text-align: center; 325 | margin-top: 25px; 326 | } 327 | 328 | /* -- tables ---------------------------------------------------------------- */ 329 | 330 | table.docutils { 331 | border: 0; 332 | border-collapse: collapse; 333 | } 334 | 335 | table caption span.caption-number { 336 | font-style: italic; 337 | } 338 | 339 | table caption span.caption-text { 340 | } 341 | 342 | table.docutils td, table.docutils th { 343 | padding: 1px 8px 1px 5px; 344 | border-top: 0; 345 | border-left: 0; 346 | border-right: 0; 347 | border-bottom: 1px solid #aaa; 348 | } 349 | 350 | table.footnote td, table.footnote th { 351 | border: 0 !important; 352 | } 353 | 354 | th { 355 | text-align: left; 356 | padding-right: 5px; 357 | } 358 | 359 | table.citation { 360 | border-left: solid 1px gray; 361 | margin-left: 1px; 362 | } 363 | 364 | table.citation td { 365 | border-bottom: none; 366 | } 367 | 368 | /* -- figures --------------------------------------------------------------- */ 369 | 370 | div.figure { 371 | margin: 0.5em; 372 | padding: 0.5em; 373 | } 374 | 375 | div.figure p.caption { 376 | padding: 0.3em; 377 | } 378 | 379 | div.figure p.caption span.caption-number { 380 | font-style: italic; 381 | } 382 | 383 | div.figure p.caption span.caption-text { 384 | } 385 | 386 | /* -- field list styles ----------------------------------------------------- */ 387 | 388 | table.field-list td, table.field-list th { 389 | border: 0 !important; 390 | } 391 | 392 | .field-list ul { 393 | margin: 0; 394 | padding-left: 1em; 395 | } 396 | 397 | .field-list p { 398 | margin: 0; 399 | } 400 | 401 | .field-name { 402 | -moz-hyphens: manual; 403 | -ms-hyphens: manual; 404 | -webkit-hyphens: manual; 405 | hyphens: manual; 406 | } 407 | 408 | /* -- other body styles ----------------------------------------------------- */ 409 | 410 | ol.arabic { 411 | list-style: decimal; 412 | } 413 | 414 | ol.loweralpha { 415 | list-style: lower-alpha; 416 | } 417 | 418 | ol.upperalpha { 419 | list-style: upper-alpha; 420 | } 421 | 422 | ol.lowerroman { 423 | list-style: lower-roman; 424 | } 425 | 426 | ol.upperroman { 427 | list-style: upper-roman; 428 | } 429 | 430 | dl { 431 | margin-bottom: 15px; 432 | } 433 | 434 | dd p { 435 | margin-top: 0px; 436 | } 437 | 438 | dd ul, dd table { 439 | margin-bottom: 10px; 440 | } 441 | 442 | dd { 443 | margin-top: 3px; 444 | margin-bottom: 10px; 445 | margin-left: 30px; 446 | } 447 | 448 | dt:target, .highlighted { 449 | background-color: #fbe54e; 450 | } 451 | 452 | dl.glossary dt { 453 | font-weight: bold; 454 | font-size: 1.1em; 455 | } 456 | 457 | .optional { 458 | font-size: 1.3em; 459 | } 460 | 461 | .sig-paren { 462 | font-size: larger; 463 | } 464 | 465 | .versionmodified { 466 | font-style: italic; 467 | } 468 | 469 | .system-message { 470 | background-color: #fda; 471 | padding: 5px; 472 | border: 3px solid red; 473 | } 474 | 475 | .footnote:target { 476 | background-color: #ffa; 477 | } 478 | 479 | .line-block { 480 | display: block; 481 | margin-top: 1em; 482 | margin-bottom: 1em; 483 | } 484 | 485 | .line-block .line-block { 486 | margin-top: 0; 487 | margin-bottom: 0; 488 | margin-left: 1.5em; 489 | } 490 | 491 | .guilabel, .menuselection { 492 | font-family: sans-serif; 493 | } 494 | 495 | .accelerator { 496 | text-decoration: underline; 497 | } 498 | 499 | .classifier { 500 | font-style: oblique; 501 | } 502 | 503 | abbr, acronym { 504 | border-bottom: dotted 1px; 505 | cursor: help; 506 | } 507 | 508 | /* -- code displays --------------------------------------------------------- */ 509 | 510 | pre { 511 | overflow: auto; 512 | overflow-y: hidden; /* fixes display issues on Chrome browsers */ 513 | } 514 | 515 | span.pre { 516 | -moz-hyphens: none; 517 | -ms-hyphens: none; 518 | -webkit-hyphens: none; 519 | hyphens: none; 520 | } 521 | 522 | td.linenos pre { 523 | padding: 5px 0px; 524 | border: 0; 525 | background-color: transparent; 526 | color: #aaa; 527 | } 528 | 529 | table.highlighttable { 530 | margin-left: 0.5em; 531 | } 532 | 533 | table.highlighttable td { 534 | padding: 0 0.5em 0 0.5em; 535 | } 536 | 537 | div.code-block-caption { 538 | padding: 2px 5px; 539 | font-size: small; 540 | } 541 | 542 | div.code-block-caption code { 543 | background-color: transparent; 544 | } 545 | 546 | div.code-block-caption + div > div.highlight > pre { 547 | margin-top: 0; 548 | } 549 | 550 | div.code-block-caption span.caption-number { 551 | padding: 0.1em 0.3em; 552 | font-style: italic; 553 | } 554 | 555 | div.code-block-caption span.caption-text { 556 | } 557 | 558 | div.literal-block-wrapper { 559 | padding: 1em 1em 0; 560 | } 561 | 562 | div.literal-block-wrapper div.highlight { 563 | margin: 0; 564 | } 565 | 566 | code.descname { 567 | background-color: transparent; 568 | font-weight: bold; 569 | font-size: 1.2em; 570 | } 571 | 572 | code.descclassname { 573 | background-color: transparent; 574 | } 575 | 576 | code.xref, a code { 577 | background-color: transparent; 578 | font-weight: bold; 579 | } 580 | 581 | h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { 582 | background-color: transparent; 583 | } 584 | 585 | .viewcode-link { 586 | float: right; 587 | } 588 | 589 | .viewcode-back { 590 | float: right; 591 | font-family: sans-serif; 592 | } 593 | 594 | div.viewcode-block:target { 595 | margin: -1px -10px; 596 | padding: 0 10px; 597 | } 598 | 599 | /* -- math display ---------------------------------------------------------- */ 600 | 601 | img.math { 602 | vertical-align: middle; 603 | } 604 | 605 | div.body div.math p { 606 | text-align: center; 607 | } 608 | 609 | span.eqno { 610 | float: right; 611 | } 612 | 613 | span.eqno a.headerlink { 614 | position: relative; 615 | left: 0px; 616 | z-index: 1; 617 | } 618 | 619 | div.math:hover a.headerlink { 620 | visibility: visible; 621 | } 622 | 623 | /* -- printout stylesheet --------------------------------------------------- */ 624 | 625 | @media print { 626 | div.document, 627 | div.documentwrapper, 628 | div.bodywrapper { 629 | margin: 0 !important; 630 | width: 100%; 631 | } 632 | 633 | div.sphinxsidebar, 634 | div.related, 635 | div.footer, 636 | #top-link { 637 | display: none; 638 | } 639 | } -------------------------------------------------------------------------------- /docs/build/html/_static/comment-bright.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mixer/interactive-python/348628aec594bf48cf503647e41ec080751fac09/docs/build/html/_static/comment-bright.png -------------------------------------------------------------------------------- /docs/build/html/_static/comment-close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mixer/interactive-python/348628aec594bf48cf503647e41ec080751fac09/docs/build/html/_static/comment-close.png -------------------------------------------------------------------------------- /docs/build/html/_static/comment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mixer/interactive-python/348628aec594bf48cf503647e41ec080751fac09/docs/build/html/_static/comment.png -------------------------------------------------------------------------------- /docs/build/html/_static/custom.css: -------------------------------------------------------------------------------- 1 | /* This file intentionally left blank. */ 2 | -------------------------------------------------------------------------------- /docs/build/html/_static/doctools.js: -------------------------------------------------------------------------------- 1 | /* 2 | * doctools.js 3 | * ~~~~~~~~~~~ 4 | * 5 | * Sphinx JavaScript utilities for all documentation. 6 | * 7 | * :copyright: Copyright 2007-2017 by the Sphinx team, see AUTHORS. 8 | * :license: BSD, see LICENSE for details. 9 | * 10 | */ 11 | 12 | /** 13 | * select a different prefix for underscore 14 | */ 15 | $u = _.noConflict(); 16 | 17 | /** 18 | * make the code below compatible with browsers without 19 | * an installed firebug like debugger 20 | if (!window.console || !console.firebug) { 21 | var names = ["log", "debug", "info", "warn", "error", "assert", "dir", 22 | "dirxml", "group", "groupEnd", "time", "timeEnd", "count", "trace", 23 | "profile", "profileEnd"]; 24 | window.console = {}; 25 | for (var i = 0; i < names.length; ++i) 26 | window.console[names[i]] = function() {}; 27 | } 28 | */ 29 | 30 | /** 31 | * small helper function to urldecode strings 32 | */ 33 | jQuery.urldecode = function(x) { 34 | return decodeURIComponent(x).replace(/\+/g, ' '); 35 | }; 36 | 37 | /** 38 | * small helper function to urlencode strings 39 | */ 40 | jQuery.urlencode = encodeURIComponent; 41 | 42 | /** 43 | * This function returns the parsed url parameters of the 44 | * current request. Multiple values per key are supported, 45 | * it will always return arrays of strings for the value parts. 46 | */ 47 | jQuery.getQueryParameters = function(s) { 48 | if (typeof s == 'undefined') 49 | s = document.location.search; 50 | var parts = s.substr(s.indexOf('?') + 1).split('&'); 51 | var result = {}; 52 | for (var i = 0; i < parts.length; i++) { 53 | var tmp = parts[i].split('=', 2); 54 | var key = jQuery.urldecode(tmp[0]); 55 | var value = jQuery.urldecode(tmp[1]); 56 | if (key in result) 57 | result[key].push(value); 58 | else 59 | result[key] = [value]; 60 | } 61 | return result; 62 | }; 63 | 64 | /** 65 | * highlight a given string on a jquery object by wrapping it in 66 | * span elements with the given class name. 67 | */ 68 | jQuery.fn.highlightText = function(text, className) { 69 | function highlight(node) { 70 | if (node.nodeType == 3) { 71 | var val = node.nodeValue; 72 | var pos = val.toLowerCase().indexOf(text); 73 | if (pos >= 0 && !jQuery(node.parentNode).hasClass(className)) { 74 | var span = document.createElement("span"); 75 | span.className = className; 76 | span.appendChild(document.createTextNode(val.substr(pos, text.length))); 77 | node.parentNode.insertBefore(span, node.parentNode.insertBefore( 78 | document.createTextNode(val.substr(pos + text.length)), 79 | node.nextSibling)); 80 | node.nodeValue = val.substr(0, pos); 81 | } 82 | } 83 | else if (!jQuery(node).is("button, select, textarea")) { 84 | jQuery.each(node.childNodes, function() { 85 | highlight(this); 86 | }); 87 | } 88 | } 89 | return this.each(function() { 90 | highlight(this); 91 | }); 92 | }; 93 | 94 | /* 95 | * backward compatibility for jQuery.browser 96 | * This will be supported until firefox bug is fixed. 97 | */ 98 | if (!jQuery.browser) { 99 | jQuery.uaMatch = function(ua) { 100 | ua = ua.toLowerCase(); 101 | 102 | var match = /(chrome)[ \/]([\w.]+)/.exec(ua) || 103 | /(webkit)[ \/]([\w.]+)/.exec(ua) || 104 | /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) || 105 | /(msie) ([\w.]+)/.exec(ua) || 106 | ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) || 107 | []; 108 | 109 | return { 110 | browser: match[ 1 ] || "", 111 | version: match[ 2 ] || "0" 112 | }; 113 | }; 114 | jQuery.browser = {}; 115 | jQuery.browser[jQuery.uaMatch(navigator.userAgent).browser] = true; 116 | } 117 | 118 | /** 119 | * Small JavaScript module for the documentation. 120 | */ 121 | var Documentation = { 122 | 123 | init : function() { 124 | this.fixFirefoxAnchorBug(); 125 | this.highlightSearchWords(); 126 | this.initIndexTable(); 127 | 128 | }, 129 | 130 | /** 131 | * i18n support 132 | */ 133 | TRANSLATIONS : {}, 134 | PLURAL_EXPR : function(n) { return n == 1 ? 0 : 1; }, 135 | LOCALE : 'unknown', 136 | 137 | // gettext and ngettext don't access this so that the functions 138 | // can safely bound to a different name (_ = Documentation.gettext) 139 | gettext : function(string) { 140 | var translated = Documentation.TRANSLATIONS[string]; 141 | if (typeof translated == 'undefined') 142 | return string; 143 | return (typeof translated == 'string') ? translated : translated[0]; 144 | }, 145 | 146 | ngettext : function(singular, plural, n) { 147 | var translated = Documentation.TRANSLATIONS[singular]; 148 | if (typeof translated == 'undefined') 149 | return (n == 1) ? singular : plural; 150 | return translated[Documentation.PLURALEXPR(n)]; 151 | }, 152 | 153 | addTranslations : function(catalog) { 154 | for (var key in catalog.messages) 155 | this.TRANSLATIONS[key] = catalog.messages[key]; 156 | this.PLURAL_EXPR = new Function('n', 'return +(' + catalog.plural_expr + ')'); 157 | this.LOCALE = catalog.locale; 158 | }, 159 | 160 | /** 161 | * add context elements like header anchor links 162 | */ 163 | addContextElements : function() { 164 | $('div[id] > :header:first').each(function() { 165 | $('\u00B6'). 166 | attr('href', '#' + this.id). 167 | attr('title', _('Permalink to this headline')). 168 | appendTo(this); 169 | }); 170 | $('dt[id]').each(function() { 171 | $('\u00B6'). 172 | attr('href', '#' + this.id). 173 | attr('title', _('Permalink to this definition')). 174 | appendTo(this); 175 | }); 176 | }, 177 | 178 | /** 179 | * workaround a firefox stupidity 180 | * see: https://bugzilla.mozilla.org/show_bug.cgi?id=645075 181 | */ 182 | fixFirefoxAnchorBug : function() { 183 | if (document.location.hash) 184 | window.setTimeout(function() { 185 | document.location.href += ''; 186 | }, 10); 187 | }, 188 | 189 | /** 190 | * highlight the search words provided in the url in the text 191 | */ 192 | highlightSearchWords : function() { 193 | var params = $.getQueryParameters(); 194 | var terms = (params.highlight) ? params.highlight[0].split(/\s+/) : []; 195 | if (terms.length) { 196 | var body = $('div.body'); 197 | if (!body.length) { 198 | body = $('body'); 199 | } 200 | window.setTimeout(function() { 201 | $.each(terms, function() { 202 | body.highlightText(this.toLowerCase(), 'highlighted'); 203 | }); 204 | }, 10); 205 | $('') 207 | .appendTo($('#searchbox')); 208 | } 209 | }, 210 | 211 | /** 212 | * init the domain index toggle buttons 213 | */ 214 | initIndexTable : function() { 215 | var togglers = $('img.toggler').click(function() { 216 | var src = $(this).attr('src'); 217 | var idnum = $(this).attr('id').substr(7); 218 | $('tr.cg-' + idnum).toggle(); 219 | if (src.substr(-9) == 'minus.png') 220 | $(this).attr('src', src.substr(0, src.length-9) + 'plus.png'); 221 | else 222 | $(this).attr('src', src.substr(0, src.length-8) + 'minus.png'); 223 | }).css('display', ''); 224 | if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) { 225 | togglers.click(); 226 | } 227 | }, 228 | 229 | /** 230 | * helper function to hide the search marks again 231 | */ 232 | hideSearchWords : function() { 233 | $('#searchbox .highlight-link').fadeOut(300); 234 | $('span.highlighted').removeClass('highlighted'); 235 | }, 236 | 237 | /** 238 | * make the url absolute 239 | */ 240 | makeURL : function(relativeURL) { 241 | return DOCUMENTATION_OPTIONS.URL_ROOT + '/' + relativeURL; 242 | }, 243 | 244 | /** 245 | * get the current relative url 246 | */ 247 | getCurrentURL : function() { 248 | var path = document.location.pathname; 249 | var parts = path.split(/\//); 250 | $.each(DOCUMENTATION_OPTIONS.URL_ROOT.split(/\//), function() { 251 | if (this == '..') 252 | parts.pop(); 253 | }); 254 | var url = parts.join('/'); 255 | return path.substring(url.lastIndexOf('/') + 1, path.length - 1); 256 | }, 257 | 258 | initOnKeyListeners: function() { 259 | $(document).keyup(function(event) { 260 | var activeElementType = document.activeElement.tagName; 261 | // don't navigate when in search box or textarea 262 | if (activeElementType !== 'TEXTAREA' && activeElementType !== 'INPUT' && activeElementType !== 'SELECT') { 263 | switch (event.keyCode) { 264 | case 37: // left 265 | var prevHref = $('link[rel="prev"]').prop('href'); 266 | if (prevHref) { 267 | window.location.href = prevHref; 268 | return false; 269 | } 270 | case 39: // right 271 | var nextHref = $('link[rel="next"]').prop('href'); 272 | if (nextHref) { 273 | window.location.href = nextHref; 274 | return false; 275 | } 276 | } 277 | } 278 | }); 279 | } 280 | }; 281 | 282 | // quick alias for translations 283 | _ = Documentation.gettext; 284 | 285 | $(document).ready(function() { 286 | Documentation.init(); 287 | }); -------------------------------------------------------------------------------- /docs/build/html/_static/down-pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mixer/interactive-python/348628aec594bf48cf503647e41ec080751fac09/docs/build/html/_static/down-pressed.png -------------------------------------------------------------------------------- /docs/build/html/_static/down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mixer/interactive-python/348628aec594bf48cf503647e41ec080751fac09/docs/build/html/_static/down.png -------------------------------------------------------------------------------- /docs/build/html/_static/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mixer/interactive-python/348628aec594bf48cf503647e41ec080751fac09/docs/build/html/_static/file.png -------------------------------------------------------------------------------- /docs/build/html/_static/minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mixer/interactive-python/348628aec594bf48cf503647e41ec080751fac09/docs/build/html/_static/minus.png -------------------------------------------------------------------------------- /docs/build/html/_static/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mixer/interactive-python/348628aec594bf48cf503647e41ec080751fac09/docs/build/html/_static/plus.png -------------------------------------------------------------------------------- /docs/build/html/_static/pygments.css: -------------------------------------------------------------------------------- 1 | .highlight .hll { background-color: #ffffcc } 2 | .highlight { background: #eeffcc; } 3 | .highlight .c { color: #408090; font-style: italic } /* Comment */ 4 | .highlight .err { border: 1px solid #FF0000 } /* Error */ 5 | .highlight .k { color: #007020; font-weight: bold } /* Keyword */ 6 | .highlight .o { color: #666666 } /* Operator */ 7 | .highlight .ch { color: #408090; font-style: italic } /* Comment.Hashbang */ 8 | .highlight .cm { color: #408090; font-style: italic } /* Comment.Multiline */ 9 | .highlight .cp { color: #007020 } /* Comment.Preproc */ 10 | .highlight .cpf { color: #408090; font-style: italic } /* Comment.PreprocFile */ 11 | .highlight .c1 { color: #408090; font-style: italic } /* Comment.Single */ 12 | .highlight .cs { color: #408090; background-color: #fff0f0 } /* Comment.Special */ 13 | .highlight .gd { color: #A00000 } /* Generic.Deleted */ 14 | .highlight .ge { font-style: italic } /* Generic.Emph */ 15 | .highlight .gr { color: #FF0000 } /* Generic.Error */ 16 | .highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ 17 | .highlight .gi { color: #00A000 } /* Generic.Inserted */ 18 | .highlight .go { color: #333333 } /* Generic.Output */ 19 | .highlight .gp { color: #c65d09; font-weight: bold } /* Generic.Prompt */ 20 | .highlight .gs { font-weight: bold } /* Generic.Strong */ 21 | .highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ 22 | .highlight .gt { color: #0044DD } /* Generic.Traceback */ 23 | .highlight .kc { color: #007020; font-weight: bold } /* Keyword.Constant */ 24 | .highlight .kd { color: #007020; font-weight: bold } /* Keyword.Declaration */ 25 | .highlight .kn { color: #007020; font-weight: bold } /* Keyword.Namespace */ 26 | .highlight .kp { color: #007020 } /* Keyword.Pseudo */ 27 | .highlight .kr { color: #007020; font-weight: bold } /* Keyword.Reserved */ 28 | .highlight .kt { color: #902000 } /* Keyword.Type */ 29 | .highlight .m { color: #208050 } /* Literal.Number */ 30 | .highlight .s { color: #4070a0 } /* Literal.String */ 31 | .highlight .na { color: #4070a0 } /* Name.Attribute */ 32 | .highlight .nb { color: #007020 } /* Name.Builtin */ 33 | .highlight .nc { color: #0e84b5; font-weight: bold } /* Name.Class */ 34 | .highlight .no { color: #60add5 } /* Name.Constant */ 35 | .highlight .nd { color: #555555; font-weight: bold } /* Name.Decorator */ 36 | .highlight .ni { color: #d55537; font-weight: bold } /* Name.Entity */ 37 | .highlight .ne { color: #007020 } /* Name.Exception */ 38 | .highlight .nf { color: #06287e } /* Name.Function */ 39 | .highlight .nl { color: #002070; font-weight: bold } /* Name.Label */ 40 | .highlight .nn { color: #0e84b5; font-weight: bold } /* Name.Namespace */ 41 | .highlight .nt { color: #062873; font-weight: bold } /* Name.Tag */ 42 | .highlight .nv { color: #bb60d5 } /* Name.Variable */ 43 | .highlight .ow { color: #007020; font-weight: bold } /* Operator.Word */ 44 | .highlight .w { color: #bbbbbb } /* Text.Whitespace */ 45 | .highlight .mb { color: #208050 } /* Literal.Number.Bin */ 46 | .highlight .mf { color: #208050 } /* Literal.Number.Float */ 47 | .highlight .mh { color: #208050 } /* Literal.Number.Hex */ 48 | .highlight .mi { color: #208050 } /* Literal.Number.Integer */ 49 | .highlight .mo { color: #208050 } /* Literal.Number.Oct */ 50 | .highlight .sa { color: #4070a0 } /* Literal.String.Affix */ 51 | .highlight .sb { color: #4070a0 } /* Literal.String.Backtick */ 52 | .highlight .sc { color: #4070a0 } /* Literal.String.Char */ 53 | .highlight .dl { color: #4070a0 } /* Literal.String.Delimiter */ 54 | .highlight .sd { color: #4070a0; font-style: italic } /* Literal.String.Doc */ 55 | .highlight .s2 { color: #4070a0 } /* Literal.String.Double */ 56 | .highlight .se { color: #4070a0; font-weight: bold } /* Literal.String.Escape */ 57 | .highlight .sh { color: #4070a0 } /* Literal.String.Heredoc */ 58 | .highlight .si { color: #70a0d0; font-style: italic } /* Literal.String.Interpol */ 59 | .highlight .sx { color: #c65d09 } /* Literal.String.Other */ 60 | .highlight .sr { color: #235388 } /* Literal.String.Regex */ 61 | .highlight .s1 { color: #4070a0 } /* Literal.String.Single */ 62 | .highlight .ss { color: #517918 } /* Literal.String.Symbol */ 63 | .highlight .bp { color: #007020 } /* Name.Builtin.Pseudo */ 64 | .highlight .fm { color: #06287e } /* Name.Function.Magic */ 65 | .highlight .vc { color: #bb60d5 } /* Name.Variable.Class */ 66 | .highlight .vg { color: #bb60d5 } /* Name.Variable.Global */ 67 | .highlight .vi { color: #bb60d5 } /* Name.Variable.Instance */ 68 | .highlight .vm { color: #bb60d5 } /* Name.Variable.Magic */ 69 | .highlight .il { color: #208050 } /* Literal.Number.Integer.Long */ -------------------------------------------------------------------------------- /docs/build/html/_static/underscore.js: -------------------------------------------------------------------------------- 1 | // Underscore.js 1.3.1 2 | // (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc. 3 | // Underscore is freely distributable under the MIT license. 4 | // Portions of Underscore are inspired or borrowed from Prototype, 5 | // Oliver Steele's Functional, and John Resig's Micro-Templating. 6 | // For all details and documentation: 7 | // http://documentcloud.github.com/underscore 8 | (function(){function q(a,c,d){if(a===c)return a!==0||1/a==1/c;if(a==null||c==null)return a===c;if(a._chain)a=a._wrapped;if(c._chain)c=c._wrapped;if(a.isEqual&&b.isFunction(a.isEqual))return a.isEqual(c);if(c.isEqual&&b.isFunction(c.isEqual))return c.isEqual(a);var e=l.call(a);if(e!=l.call(c))return false;switch(e){case "[object String]":return a==String(c);case "[object Number]":return a!=+a?c!=+c:a==0?1/a==1/c:a==+c;case "[object Date]":case "[object Boolean]":return+a==+c;case "[object RegExp]":return a.source== 9 | c.source&&a.global==c.global&&a.multiline==c.multiline&&a.ignoreCase==c.ignoreCase}if(typeof a!="object"||typeof c!="object")return false;for(var f=d.length;f--;)if(d[f]==a)return true;d.push(a);var f=0,g=true;if(e=="[object Array]"){if(f=a.length,g=f==c.length)for(;f--;)if(!(g=f in a==f in c&&q(a[f],c[f],d)))break}else{if("constructor"in a!="constructor"in c||a.constructor!=c.constructor)return false;for(var h in a)if(b.has(a,h)&&(f++,!(g=b.has(c,h)&&q(a[h],c[h],d))))break;if(g){for(h in c)if(b.has(c, 10 | h)&&!f--)break;g=!f}}d.pop();return g}var r=this,G=r._,n={},k=Array.prototype,o=Object.prototype,i=k.slice,H=k.unshift,l=o.toString,I=o.hasOwnProperty,w=k.forEach,x=k.map,y=k.reduce,z=k.reduceRight,A=k.filter,B=k.every,C=k.some,p=k.indexOf,D=k.lastIndexOf,o=Array.isArray,J=Object.keys,s=Function.prototype.bind,b=function(a){return new m(a)};if(typeof exports!=="undefined"){if(typeof module!=="undefined"&&module.exports)exports=module.exports=b;exports._=b}else r._=b;b.VERSION="1.3.1";var j=b.each= 11 | b.forEach=function(a,c,d){if(a!=null)if(w&&a.forEach===w)a.forEach(c,d);else if(a.length===+a.length)for(var e=0,f=a.length;e2;a== 12 | null&&(a=[]);if(y&&a.reduce===y)return e&&(c=b.bind(c,e)),f?a.reduce(c,d):a.reduce(c);j(a,function(a,b,i){f?d=c.call(e,d,a,b,i):(d=a,f=true)});if(!f)throw new TypeError("Reduce of empty array with no initial value");return d};b.reduceRight=b.foldr=function(a,c,d,e){var f=arguments.length>2;a==null&&(a=[]);if(z&&a.reduceRight===z)return e&&(c=b.bind(c,e)),f?a.reduceRight(c,d):a.reduceRight(c);var g=b.toArray(a).reverse();e&&!f&&(c=b.bind(c,e));return f?b.reduce(g,c,d,e):b.reduce(g,c)};b.find=b.detect= 13 | function(a,c,b){var e;E(a,function(a,g,h){if(c.call(b,a,g,h))return e=a,true});return e};b.filter=b.select=function(a,c,b){var e=[];if(a==null)return e;if(A&&a.filter===A)return a.filter(c,b);j(a,function(a,g,h){c.call(b,a,g,h)&&(e[e.length]=a)});return e};b.reject=function(a,c,b){var e=[];if(a==null)return e;j(a,function(a,g,h){c.call(b,a,g,h)||(e[e.length]=a)});return e};b.every=b.all=function(a,c,b){var e=true;if(a==null)return e;if(B&&a.every===B)return a.every(c,b);j(a,function(a,g,h){if(!(e= 14 | e&&c.call(b,a,g,h)))return n});return e};var E=b.some=b.any=function(a,c,d){c||(c=b.identity);var e=false;if(a==null)return e;if(C&&a.some===C)return a.some(c,d);j(a,function(a,b,h){if(e||(e=c.call(d,a,b,h)))return n});return!!e};b.include=b.contains=function(a,c){var b=false;if(a==null)return b;return p&&a.indexOf===p?a.indexOf(c)!=-1:b=E(a,function(a){return a===c})};b.invoke=function(a,c){var d=i.call(arguments,2);return b.map(a,function(a){return(b.isFunction(c)?c||a:a[c]).apply(a,d)})};b.pluck= 15 | function(a,c){return b.map(a,function(a){return a[c]})};b.max=function(a,c,d){if(!c&&b.isArray(a))return Math.max.apply(Math,a);if(!c&&b.isEmpty(a))return-Infinity;var e={computed:-Infinity};j(a,function(a,b,h){b=c?c.call(d,a,b,h):a;b>=e.computed&&(e={value:a,computed:b})});return e.value};b.min=function(a,c,d){if(!c&&b.isArray(a))return Math.min.apply(Math,a);if(!c&&b.isEmpty(a))return Infinity;var e={computed:Infinity};j(a,function(a,b,h){b=c?c.call(d,a,b,h):a;bd?1:0}),"value")};b.groupBy=function(a,c){var d={},e=b.isFunction(c)?c:function(a){return a[c]};j(a,function(a,b){var c=e(a,b);(d[c]||(d[c]=[])).push(a)});return d};b.sortedIndex=function(a, 17 | c,d){d||(d=b.identity);for(var e=0,f=a.length;e>1;d(a[g])=0})})};b.difference=function(a){var c=b.flatten(i.call(arguments,1));return b.filter(a,function(a){return!b.include(c,a)})};b.zip=function(){for(var a=i.call(arguments),c=b.max(b.pluck(a,"length")),d=Array(c),e=0;e=0;d--)b=[a[d].apply(this,b)];return b[0]}}; 24 | b.after=function(a,b){return a<=0?b():function(){if(--a<1)return b.apply(this,arguments)}};b.keys=J||function(a){if(a!==Object(a))throw new TypeError("Invalid object");var c=[],d;for(d in a)b.has(a,d)&&(c[c.length]=d);return c};b.values=function(a){return b.map(a,b.identity)};b.functions=b.methods=function(a){var c=[],d;for(d in a)b.isFunction(a[d])&&c.push(d);return c.sort()};b.extend=function(a){j(i.call(arguments,1),function(b){for(var d in b)a[d]=b[d]});return a};b.defaults=function(a){j(i.call(arguments, 25 | 1),function(b){for(var d in b)a[d]==null&&(a[d]=b[d])});return a};b.clone=function(a){return!b.isObject(a)?a:b.isArray(a)?a.slice():b.extend({},a)};b.tap=function(a,b){b(a);return a};b.isEqual=function(a,b){return q(a,b,[])};b.isEmpty=function(a){if(b.isArray(a)||b.isString(a))return a.length===0;for(var c in a)if(b.has(a,c))return false;return true};b.isElement=function(a){return!!(a&&a.nodeType==1)};b.isArray=o||function(a){return l.call(a)=="[object Array]"};b.isObject=function(a){return a===Object(a)}; 26 | b.isArguments=function(a){return l.call(a)=="[object Arguments]"};if(!b.isArguments(arguments))b.isArguments=function(a){return!(!a||!b.has(a,"callee"))};b.isFunction=function(a){return l.call(a)=="[object Function]"};b.isString=function(a){return l.call(a)=="[object String]"};b.isNumber=function(a){return l.call(a)=="[object Number]"};b.isNaN=function(a){return a!==a};b.isBoolean=function(a){return a===true||a===false||l.call(a)=="[object Boolean]"};b.isDate=function(a){return l.call(a)=="[object Date]"}; 27 | b.isRegExp=function(a){return l.call(a)=="[object RegExp]"};b.isNull=function(a){return a===null};b.isUndefined=function(a){return a===void 0};b.has=function(a,b){return I.call(a,b)};b.noConflict=function(){r._=G;return this};b.identity=function(a){return a};b.times=function(a,b,d){for(var e=0;e/g,">").replace(/"/g,""").replace(/'/g,"'").replace(/\//g,"/")};b.mixin=function(a){j(b.functions(a), 28 | function(c){K(c,b[c]=a[c])})};var L=0;b.uniqueId=function(a){var b=L++;return a?a+b:b};b.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var t=/.^/,u=function(a){return a.replace(/\\\\/g,"\\").replace(/\\'/g,"'")};b.template=function(a,c){var d=b.templateSettings,d="var __p=[],print=function(){__p.push.apply(__p,arguments);};with(obj||{}){__p.push('"+a.replace(/\\/g,"\\\\").replace(/'/g,"\\'").replace(d.escape||t,function(a,b){return"',_.escape("+ 29 | u(b)+"),'"}).replace(d.interpolate||t,function(a,b){return"',"+u(b)+",'"}).replace(d.evaluate||t,function(a,b){return"');"+u(b).replace(/[\r\n\t]/g," ")+";__p.push('"}).replace(/\r/g,"\\r").replace(/\n/g,"\\n").replace(/\t/g,"\\t")+"');}return __p.join('');",e=new Function("obj","_",d);return c?e(c,b):function(a){return e.call(this,a,b)}};b.chain=function(a){return b(a).chain()};var m=function(a){this._wrapped=a};b.prototype=m.prototype;var v=function(a,c){return c?b(a).chain():a},K=function(a,c){m.prototype[a]= 30 | function(){var a=i.call(arguments);H.call(a,this._wrapped);return v(c.apply(b,a),this._chain)}};b.mixin(b);j("pop,push,reverse,shift,sort,splice,unshift".split(","),function(a){var b=k[a];m.prototype[a]=function(){var d=this._wrapped;b.apply(d,arguments);var e=d.length;(a=="shift"||a=="splice")&&e===0&&delete d[0];return v(d,this._chain)}});j(["concat","join","slice"],function(a){var b=k[a];m.prototype[a]=function(){return v(b.apply(this._wrapped,arguments),this._chain)}});m.prototype.chain=function(){this._chain= 31 | true;return this};m.prototype.value=function(){return this._wrapped}}).call(this); 32 | -------------------------------------------------------------------------------- /docs/build/html/_static/up-pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mixer/interactive-python/348628aec594bf48cf503647e41ec080751fac09/docs/build/html/_static/up-pressed.png -------------------------------------------------------------------------------- /docs/build/html/_static/up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mixer/interactive-python/348628aec594bf48cf503647e41ec080751fac09/docs/build/html/_static/up.png -------------------------------------------------------------------------------- /docs/build/html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | Mixer Interactive Python API — interactive-python 1.0.0 documentation 9 | 10 | 11 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
38 |
39 |
40 |
41 | 42 |
43 |

Mixer Interactive Python API

44 |

This API is a reference implementation for connecting to Mixer’s Interactive service as a game client. This documentation and library is stable, but is still actively developed and further expansions are planned.

45 |

Contents:

46 |
47 | 63 |
64 |
65 |
66 |

Indices and tables

67 | 72 |
73 | 74 | 75 |
76 |
77 |
78 | 112 |
113 |
114 | 125 | 126 | 127 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /docs/build/html/objects.inv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mixer/interactive-python/348628aec594bf48cf503647e41ec080751fac09/docs/build/html/objects.inv -------------------------------------------------------------------------------- /docs/build/html/py-modindex.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | Python Module Index — interactive-python 1.0.0 documentation 9 | 10 | 11 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 |
41 |
42 |
43 | 44 | 45 |

Python Module Index

46 | 47 |
48 | i 49 |
50 | 51 | 52 | 53 | 55 | 56 | 58 | 61 | 62 | 63 | 66 |
 
54 | i
59 | interactive_python 60 |
    64 | interactive_python.encoding 65 |
67 | 68 | 69 |
70 |
71 |
72 | 92 |
93 |
94 | 102 | 103 | 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /docs/build/html/search.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | Search — interactive-python 1.0.0 documentation 9 | 10 | 11 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 |
45 |
46 |
47 |
48 | 49 |

Search

50 |
51 | 52 |

53 | Please activate JavaScript to enable the search 54 | functionality. 55 |

56 |
57 |

58 | From here you can search these documents. Enter your search 59 | words into the box below and click "search". Note that the search 60 | function will automatically search for all of the words. Pages 61 | containing fewer words won't appear in the result list. 62 |

63 |
64 | 65 | 66 | 67 |
68 | 69 |
70 | 71 |
72 | 73 |
74 |
75 |
76 | 86 |
87 |
88 | 96 | 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /docs/build/html/searchindex.js: -------------------------------------------------------------------------------- 1 | Search.setIndex({docnames:["api","examples","index"],envversion:53,filenames:["api.rst","examples.rst","index.rst"],objects:{"interactive_python.Call":{data:[0,0,1,""],name:[0,0,1,""],reply_error:[0,2,1,""]},"interactive_python.Connection":{call:[0,2,1,""],close:[0,2,1,""],connect:[0,2,1,""],get_packet:[0,2,1,""],has_packet:[0,2,1,""],reply:[0,2,1,""],set_compression:[0,2,1,""]},"interactive_python.Control":{"delete":[0,2,1,""],update:[0,2,1,""]},"interactive_python.Discovery":{find:[0,2,1,""]},"interactive_python.OAuthClient":{get_code:[0,2,1,""]},"interactive_python.OAuthShortCode":{accepted:[0,2,1,""]},"interactive_python.Scene":{"delete":[0,2,1,""],create_controls:[0,2,1,""],update:[0,2,1,""]},"interactive_python.State":{connect:[0,2,1,""],create_scenes:[0,2,1,""],pump:[0,2,1,""],pump_async:[0,2,1,""],scene:[0,2,1,""],set_ready:[0,2,1,""]},"interactive_python._util":{ChangeTracker:[0,1,1,""],Resource:[0,1,1,""]},"interactive_python._util.ChangeTracker":{assign:[0,2,1,""],has_changed:[0,2,1,""],to_json:[0,2,1,""]},"interactive_python._util.Resource":{id:[0,0,1,""]},"interactive_python.encoding":{Encoding:[0,1,1,""],EncodingException:[0,4,1,""],GzipEncoding:[0,1,1,""],TextEncoding:[0,1,1,""],reset_buffer:[0,5,1,""]},"interactive_python.encoding.Encoding":{decode:[0,2,1,""],encode:[0,2,1,""],name:[0,2,1,""]},"interactive_python.encoding.GzipEncoding":{decode:[0,2,1,""],encode:[0,2,1,""],name:[0,2,1,""]},"interactive_python.encoding.TextEncoding":{decode:[0,2,1,""],encode:[0,2,1,""],name:[0,2,1,""]},OAuthShortCode:{code:[0,0,1,""]},OAuthTokens:{access:[0,0,1,""],expires_at:[0,0,1,""],refresh:[0,0,1,""]},Scene:{controls:[0,0,1,""]},State:{connection:[0,0,1,""]},interactive_python:{Button:[0,1,1,""],Call:[0,1,1,""],Connection:[0,1,1,""],Control:[0,1,1,""],Discovery:[0,1,1,""],DiscoveryError:[0,1,1,""],Joystick:[0,1,1,""],NoServersAvailableError:[0,1,1,""],OAuthClient:[0,1,1,""],OAuthShortCode:[0,1,1,""],OAuthTokens:[0,1,1,""],Scene:[0,1,1,""],ShortCodeAccessDeniedError:[0,1,1,""],ShortCodeError:[0,1,1,""],ShortCodeTimeoutError:[0,1,1,""],State:[0,1,1,""],UnknownShortCodeError:[0,1,1,""],encoding:[0,3,0,"-"],keycode:[0,6,1,""]}},objnames:{"0":["py","attribute","Python attribute"],"1":["py","class","Python class"],"2":["py","method","Python method"],"3":["py","module","Python module"],"4":["py","exception","Python exception"],"5":["py","function","Python function"],"6":["py","data","Python data"]},objtypes:{"0":"py:attribute","1":"py:class","2":"py:method","3":"py:module","4":"py:exception","5":"py:function","6":"py:data"},terms:{"1_viewer_control":1,"2_shortcode_oauth":1,"abstract":0,"byte":0,"class":0,"default":[0,1],"import":[0,1],"int":0,"new":0,"return":[0,1],"short":0,"static":0,"super":1,"switch":0,"throw":0,"true":0,"try":[0,1],"while":0,For:[0,1],IDs:0,The:[0,1],__init__:1,_ball:1,_create_paddl:1,_interact:1,_player_1:1,_player_2:1,_screen_height:1,_screen_width:1,_setup_control:1,_unixselectoreventloop:0,_util:0,accept:[0,1],access:[0,1],activ:2,add:0,address:0,again:[0,1],agre:0,algorithm:0,all:[0,1],allow:0,alreadi:[0,1],also:0,altern:[0,1],alwai:0,ani:0,anticip:0,app:0,appear:0,appli:0,applic:0,argument:0,argv:1,assign:0,assum:0,async:[0,1],asynchron:0,asyncio:[0,1],attach:0,authent:0,author:[0,1],automat:[0,1],avail:0,await:[0,1],back:0,background:0,base:0,beam:0,bearer:[0,1],been:0,befor:0,being:0,better:1,blank:1,block:0,bodi:0,bool:0,boot:1,both:0,btn:0,buffer:0,bulk:0,button:[0,1],call:[0,1],call_id:0,can:[0,1],cancel:0,caus:0,chang:0,changetrack:0,check:0,check_interv:0,click:0,click_m:0,client:[0,1,2],client_id:0,client_secret:0,close:0,closederror:0,code:[0,1],com:[0,1],come:0,compress:0,compression_level:0,connect:[0,1,2],consol:0,construct:0,constructor:0,contain:0,content:2,control:[0,1],control_id:[0,1],coroutin:0,correspond:0,creat:[0,1],create_control:[0,1],create_scen:0,data:0,datetim:0,deal:0,debug:0,decod:0,def:[0,1],defin:0,delet:0,deliv:0,deliveri:0,demonstr:1,deni:[0,1],describ:0,design:1,desir:0,develop:[1,2],dict:0,didn:0,digit:0,discard:0,discoveri:0,discoveryerror:0,dispatch:0,dispatch_cal:0,displai:0,do_the_th:0,document:[0,2],doe:0,doesn:0,doing:0,don:0,down:[0,1],durat:0,dure:0,either:0,elif:1,emit:0,empti:0,encod:0,encodingexcept:0,engin:1,enter:[0,1],error:[1,2],establish:0,event:0,eventemitt:0,exampl:[0,2],except:[0,1],execut:0,exist:0,expans:2,experi:1,expir:0,expires_at:0,extra_head:0,fail:0,fallback:0,fals:0,familiar:0,fatal_error:1,find:0,fire:0,first:0,flow:[0,2],folder:1,forev:0,form:0,format:[0,1],found:1,from:[0,1],fulfil:0,full:0,fulli:0,further:2,futur:0,game:[0,1,2],gener:0,get:[0,1],get_access_token:[0,1],get_cod:[0,1],get_event_loop:0,get_interactive_address:0,get_packet:0,getattr:0,giveinput:0,given:0,going:0,gracefulli:0,grant:0,greet:0,group:0,guid:0,gzipencod:0,handl:0,happen:0,hard:1,has:0,has_chang:0,has_packet:0,have:0,height:[0,1],help:0,here:[0,1],host:0,how:1,http:0,id_properti:0,implement:[0,2],incom:0,index:2,inform:0,initi:1,input:0,insert:0,instanc:0,integr:0,interact:[0,1],interactive_python:0,interactive_version_id:0,interfac:0,intrinsic_properti:0,is_readi:0,iter:0,its:0,joystick:0,json:0,keep:0,kei:0,keycod:[0,1],kind:0,kwarg:0,lambda:[0,1],larg:[0,1],leav:0,let:[0,1],level:2,librari:[0,2],like:[0,1],list:0,listen:0,log:0,look:0,loop:0,low:2,made:0,make:[0,1],manual:[0,1],mark:0,mean:0,metadata:0,method:0,might:0,mixer:[0,1],mode:0,modul:2,more:0,most:0,mousedown:[0,1],move:[0,1],must:0,my_client_id:0,my_game_loop:0,my_oauth_token:0,my_version_id:0,name:0,need:0,none:[0,1],noserversavailableerror:0,now:0,numer:0,oauth:2,oauth_token:0,oauthclient:[0,1],oauthshortcod:0,oauthtoken:0,object:0,occur:[0,1],onc:0,oncontroldelet:0,oncontrolupd:0,one:0,onli:0,onparticipantjoin:0,onscenecr:0,onscenedelet:0,onsceneupd:0,option:0,ord:1,other:0,otherwis:0,our:[0,1],outgo:0,overrid:0,packet:0,page:2,param:0,paramet:0,particip:0,pass:0,path:0,payload:0,perspect:0,plain:0,plan:2,player:1,point:0,pong:2,posit:[0,1],prefer:0,pressed_kei:1,print:[0,1],prioriti:0,process:0,programmat:1,project:[0,1],project_sharecod:[0,1],project_version_id:[0,1],prompt:0,properti:0,protocol:2,protocol_vers:0,provid:[0,1],pump:0,pump_async:[0,1],purpos:1,pyee:0,python:0,queu:0,queue:0,rais:0,raw:0,read:0,readi:0,reassign:0,reassign_scene_id:0,redirect:0,refer:[0,2],refresh:0,regist:0,repli:0,reply_error:0,repo:1,report:0,repres:0,represent:0,request:[0,1],requir:0,reset_buff:0,resolut:0,resolv:0,resourc:0,respons:0,result:0,retriev:0,rheo1hr:1,robot:0,rpc:0,rtype:0,run:[0,1],run_until_complet:0,runnabl:2,save:0,scene:[1,2],scene_id:0,scheme:0,scope:0,search:2,second:0,secret:0,see:0,self:[0,1],send:0,serial:0,server:0,servic:[0,2],session:0,set_compress:0,set_readi:[0,1],setup:1,shortcod:[0,1],shortcodeaccessdeniederror:[0,1],shortcodeerror:0,shortcodetimeouterror:[0,1],should:0,show:1,signal:0,simpl:0,singl:0,six:0,size:[0,1],slice:0,slow:[0,1],small:1,socket:0,some:0,someth:0,somewhat:0,soon:0,sourc:[0,1],space:0,stabl:2,start:0,state:[1,2],step:1,still:2,stop:0,str:0,string:0,structur:0,studio:1,submit:0,subsequ:0,success:0,sync:0,synchron:0,sys:1,taggabl:0,take:0,tetrisd:0,text:[0,1],textencod:0,textual:0,thei:0,them:[0,1],thi:[0,1,2],though:0,through:0,tick:0,time:0,timeout:0,timeouterror:0,tini:1,to_json:0,token:[0,1],too:[0,1],track:0,translat:0,trigger:0,two:[0,1],type:0,underli:0,unexpect:0,unknown:[0,1],unknownshortcodeerror:[0,1],until:0,updat:[0,1],upgrad:0,url:0,usag:0,use:[0,1],used:[0,1],user:[0,1],usernam:0,usual:0,util:2,valu:0,via:0,wait:0,want:0,websit:0,websocket:0,welcom:0,what:0,when:0,whenev:0,where:0,whether:0,which:0,who:0,width:[0,1],within:0,without:0,work:0,wrote:1,you:[0,1],your:0},titles:["API","Runnable Examples","Mixer Interactive Python API"],titleterms:{api:[0,2],error:0,exampl:1,flow:1,indic:2,interact:2,level:0,low:0,mixer:2,oauth:[0,1],pong:1,protocol:0,python:2,runnabl:1,scene:0,state:0,tabl:2,util:0}}) -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # interactive-python documentation build configuration file, created by 5 | # sphinx-quickstart on Tue Aug 1 18:55:17 2017. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | import sys 17 | import os 18 | import shlex 19 | 20 | # If extensions (or modules to document with autodoc) are in another directory, 21 | # add these directories to sys.path here. If the directory is relative to the 22 | # documentation root, use os.path.abspath to make it absolute, like shown here. 23 | sys.path.insert(0, os.path.abspath('..')) 24 | 25 | # -- General configuration ------------------------------------------------ 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | #needs_sphinx = '1.0' 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [ 34 | 'sphinx.ext.autodoc', 35 | 'sphinx.ext.coverage', 36 | 'sphinx.ext.viewcode', 37 | 'sphinxcontrib.asyncio', 38 | ] 39 | 40 | # Add any paths that contain templates here, relative to this directory. 41 | templates_path = ['_templates'] 42 | 43 | # The suffix(es) of source filenames. 44 | # You can specify multiple suffix as a list of string: 45 | # source_suffix = ['.rst', '.md'] 46 | source_suffix = '.rst' 47 | 48 | # The encoding of source files. 49 | #source_encoding = 'utf-8-sig' 50 | 51 | # The master toctree document. 52 | master_doc = 'index' 53 | 54 | # General information about the project. 55 | project = 'interactive-python' 56 | copyright = 'Microsoft' 57 | author = 'Microsoft' 58 | 59 | # The version info for the project you're documenting, acts as replacement for 60 | # |version| and |release|, also used in various other places throughout the 61 | # built documents. 62 | # 63 | # The short X.Y version. 64 | version = '1.0' 65 | # The full version, including alpha/beta/rc tags. 66 | release = '1.0.0' 67 | 68 | # The language for content autogenerated by Sphinx. Refer to documentation 69 | # for a list of supported languages. 70 | # 71 | # This is also used if you do content translation via gettext catalogs. 72 | # Usually you set "language" from the command line for these cases. 73 | language = None 74 | 75 | # There are two options for replacing |today|: either, you set today to some 76 | # non-false value, then it is used: 77 | #today = '' 78 | # Else, today_fmt is used as the format for a strftime call. 79 | #today_fmt = '%B %d, %Y' 80 | 81 | # List of patterns, relative to source directory, that match files and 82 | # directories to ignore when looking for source files. 83 | exclude_patterns = ['../docs'] 84 | 85 | # The reST default role (used for this markup: `text`) to use for all 86 | # documents. 87 | #default_role = None 88 | 89 | # If true, '()' will be appended to :func: etc. cross-reference text. 90 | #add_function_parentheses = True 91 | 92 | # If true, the current module name will be prepended to all description 93 | # unit titles (such as .. function::). 94 | #add_module_names = True 95 | 96 | # If true, sectionauthor and moduleauthor directives will be shown in the 97 | # output. They are ignored by default. 98 | #show_authors = False 99 | 100 | # The name of the Pygments (syntax highlighting) style to use. 101 | pygments_style = 'sphinx' 102 | highlight_language = 'python' 103 | 104 | # A list of ignored prefixes for module index sorting. 105 | #modindex_common_prefix = [] 106 | 107 | # If true, keep warnings as "system message" paragraphs in the built documents. 108 | #keep_warnings = False 109 | 110 | # If true, `todo` and `todoList` produce output, else they produce nothing. 111 | todo_include_todos = False 112 | 113 | 114 | # -- Options for HTML output ---------------------------------------------- 115 | 116 | # The theme to use for HTML and HTML Help pages. See the documentation for 117 | # a list of builtin themes. 118 | html_theme = 'alabaster' 119 | 120 | # Theme options are theme-specific and customize the look and feel of a theme 121 | # further. For a list of options available for each theme, see the 122 | # documentation. 123 | #html_theme_options = {} 124 | 125 | # Add any paths that contain custom themes here, relative to this directory. 126 | #html_theme_path = [] 127 | 128 | # The name for this set of Sphinx documents. If None, it defaults to 129 | # " v documentation". 130 | #html_title = None 131 | 132 | # A shorter title for the navigation bar. Default is the same as html_title. 133 | #html_short_title = None 134 | 135 | # The name of an image file (relative to this directory) to place at the top 136 | # of the sidebar. 137 | #html_logo = None 138 | 139 | # The name of an image file (within the static path) to use as favicon of the 140 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 141 | # pixels large. 142 | #html_favicon = None 143 | 144 | # Add any paths that contain custom static files (such as style sheets) here, 145 | # relative to this directory. They are copied after the builtin static files, 146 | # so a file named "default.css" will overwrite the builtin "default.css". 147 | html_static_path = ['_static'] 148 | 149 | # Add any extra paths that contain custom files (such as robots.txt or 150 | # .htaccess) here, relative to this directory. These files are copied 151 | # directly to the root of the documentation. 152 | #html_extra_path = [] 153 | 154 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 155 | # using the given strftime format. 156 | #html_last_updated_fmt = '%b %d, %Y' 157 | 158 | # If true, SmartyPants will be used to convert quotes and dashes to 159 | # typographically correct entities. 160 | #html_use_smartypants = True 161 | 162 | # Custom sidebar templates, maps document names to template names. 163 | #html_sidebars = {} 164 | 165 | # Additional templates that should be rendered to pages, maps page names to 166 | # template names. 167 | #html_additional_pages = {} 168 | 169 | # If false, no module index is generated. 170 | #html_domain_indices = True 171 | 172 | # If false, no index is generated. 173 | #html_use_index = True 174 | 175 | # If true, the index is split into individual pages for each letter. 176 | #html_split_index = False 177 | 178 | # If true, links to the reST sources are added to the pages. 179 | #html_show_sourcelink = True 180 | 181 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 182 | #html_show_sphinx = True 183 | 184 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 185 | #html_show_copyright = True 186 | 187 | # If true, an OpenSearch description file will be output, and all pages will 188 | # contain a tag referring to it. The value of this option must be the 189 | # base URL from which the finished HTML is served. 190 | #html_use_opensearch = '' 191 | 192 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 193 | #html_file_suffix = None 194 | 195 | # Language to be used for generating the HTML full-text search index. 196 | # Sphinx supports the following languages: 197 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' 198 | # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr' 199 | #html_search_language = 'en' 200 | 201 | # A dictionary with options for the search language support, empty by default. 202 | # Now only 'ja' uses this config value 203 | #html_search_options = {'type': 'default'} 204 | 205 | # The name of a javascript file (relative to the configuration directory) that 206 | # implements a search results scorer. If empty, the default will be used. 207 | #html_search_scorer = 'scorer.js' 208 | 209 | # Output file base name for HTML help builder. 210 | htmlhelp_basename = 'interactive-pythondoc' 211 | 212 | # -- Options for LaTeX output --------------------------------------------- 213 | 214 | latex_elements = { 215 | # The paper size ('letterpaper' or 'a4paper'). 216 | #'papersize': 'letterpaper', 217 | 218 | # The font size ('10pt', '11pt' or '12pt'). 219 | #'pointsize': '10pt', 220 | 221 | # Additional stuff for the LaTeX preamble. 222 | #'preamble': '', 223 | 224 | # Latex figure (float) alignment 225 | #'figure_align': 'htbp', 226 | } 227 | 228 | # Grouping the document tree into LaTeX files. List of tuples 229 | # (source start file, target name, title, 230 | # author, documentclass [howto, manual, or own class]). 231 | latex_documents = [ 232 | (master_doc, 'interactive-python.tex', 'interactive-python Documentation', 233 | 'Microsoft', 'manual'), 234 | ] 235 | 236 | # The name of an image file (relative to this directory) to place at the top of 237 | # the title page. 238 | #latex_logo = None 239 | 240 | # For "manual" documents, if this is true, then toplevel headings are parts, 241 | # not chapters. 242 | #latex_use_parts = False 243 | 244 | # If true, show page references after internal links. 245 | #latex_show_pagerefs = False 246 | 247 | # If true, show URL addresses after external links. 248 | #latex_show_urls = False 249 | 250 | # Documents to append as an appendix to all manuals. 251 | #latex_appendices = [] 252 | 253 | # If false, no module index is generated. 254 | #latex_domain_indices = True 255 | 256 | 257 | # -- Options for manual page output --------------------------------------- 258 | 259 | # One entry per manual page. List of tuples 260 | # (source start file, name, description, authors, manual section). 261 | man_pages = [ 262 | (master_doc, 'interactive-python', 'interactive-python Documentation', 263 | [author], 1) 264 | ] 265 | 266 | # If true, show URL addresses after external links. 267 | #man_show_urls = False 268 | 269 | 270 | # -- Options for Texinfo output ------------------------------------------- 271 | 272 | # Grouping the document tree into Texinfo files. List of tuples 273 | # (source start file, target name, title, author, 274 | # dir menu entry, description, category) 275 | texinfo_documents = [ 276 | (master_doc, 'interactive-python', 'interactive-python Documentation', 277 | author, 'interactive-python', 'One line description of project.', 278 | 'Miscellaneous'), 279 | ] 280 | 281 | # Documents to append as an appendix to all manuals. 282 | #texinfo_appendices = [] 283 | 284 | # If false, no module index is generated. 285 | #texinfo_domain_indices = True 286 | 287 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 288 | #texinfo_show_urls = 'footnote' 289 | 290 | # If true, do not generate a @detailmenu in the "Top" node's menu. 291 | #texinfo_no_detailmenu = False 292 | -------------------------------------------------------------------------------- /docs/examples.rst: -------------------------------------------------------------------------------- 1 | Runnable Examples 2 | ================= 3 | 4 | OAuth Flow 5 | ---------- 6 | 7 | .. NOTE:: 8 | 9 | The runnable source for this can be found in the `examples/2_shortcode_oauth `_ folder of this repo. 10 | 11 | This example shows you how to use the shortcode OAuth flow to get an access token to connect to Interactive. For development you can `manually request a token `_, but the shortcode flow provides a better experience to users! 12 | 13 | .. include:: ../examples/2_shortcode_oauth/oauth.py 14 | :start-line: 12 15 | :end-line: 34 16 | :code: python 17 | 18 | 19 | Pong 20 | ---- 21 | 22 | .. NOTE:: 23 | 24 | The runnable source for this can be found in the `examples/1_viewer_controlled `_ folder of this repo. 25 | 26 | For an initial example, we'll create a small pong game. We already wrote a tiny game engine that can be used to create a two player game like so: 27 | 28 | .. include:: ../examples/0_init/pong.py 29 | :start-line: 15 30 | :end-line: 33 31 | :code: python 32 | 33 | Let's make it Interactive! 34 | 35 | .. include:: ../examples/1_viewer_controlled/pong.py 36 | :start-line: 22 37 | :end-line: 86 38 | :code: python 39 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Redirecting... 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. interactive-python documentation master file, created by 2 | sphinx-quickstart on Tue Aug 1 18:55:17 2017. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Mixer Interactive Python API 7 | ============================ 8 | 9 | This API is a reference implementation for connecting to Mixer's Interactive service as a game client. This documentation and library is stable, but is still actively developed and further expansions are planned. 10 | 11 | Contents: 12 | 13 | .. toctree:: 14 | :maxdepth: 2 15 | 16 | api 17 | examples 18 | 19 | 20 | 21 | Indices and tables 22 | ================== 23 | 24 | * :ref:`genindex` 25 | * :ref:`modindex` 26 | * :ref:`search` 27 | 28 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | echo. coverage to run coverage check of the documentation if enabled 41 | goto end 42 | ) 43 | 44 | if "%1" == "clean" ( 45 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 46 | del /q /s %BUILDDIR%\* 47 | goto end 48 | ) 49 | 50 | 51 | REM Check if sphinx-build is available and fallback to Python version if any 52 | %SPHINXBUILD% 2> nul 53 | if errorlevel 9009 goto sphinx_python 54 | goto sphinx_ok 55 | 56 | :sphinx_python 57 | 58 | set SPHINXBUILD=python -m sphinx.__init__ 59 | %SPHINXBUILD% 2> nul 60 | if errorlevel 9009 ( 61 | echo. 62 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 63 | echo.installed, then set the SPHINXBUILD environment variable to point 64 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 65 | echo.may add the Sphinx directory to PATH. 66 | echo. 67 | echo.If you don't have Sphinx installed, grab it from 68 | echo.http://sphinx-doc.org/ 69 | exit /b 1 70 | ) 71 | 72 | :sphinx_ok 73 | 74 | 75 | if "%1" == "html" ( 76 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 77 | if errorlevel 1 exit /b 1 78 | echo. 79 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 80 | goto end 81 | ) 82 | 83 | if "%1" == "dirhtml" ( 84 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 85 | if errorlevel 1 exit /b 1 86 | echo. 87 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 88 | goto end 89 | ) 90 | 91 | if "%1" == "singlehtml" ( 92 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 93 | if errorlevel 1 exit /b 1 94 | echo. 95 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 96 | goto end 97 | ) 98 | 99 | if "%1" == "pickle" ( 100 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 101 | if errorlevel 1 exit /b 1 102 | echo. 103 | echo.Build finished; now you can process the pickle files. 104 | goto end 105 | ) 106 | 107 | if "%1" == "json" ( 108 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 109 | if errorlevel 1 exit /b 1 110 | echo. 111 | echo.Build finished; now you can process the JSON files. 112 | goto end 113 | ) 114 | 115 | if "%1" == "htmlhelp" ( 116 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 117 | if errorlevel 1 exit /b 1 118 | echo. 119 | echo.Build finished; now you can run HTML Help Workshop with the ^ 120 | .hhp project file in %BUILDDIR%/htmlhelp. 121 | goto end 122 | ) 123 | 124 | if "%1" == "qthelp" ( 125 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 129 | .qhcp project file in %BUILDDIR%/qthelp, like this: 130 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\interactive-python.qhcp 131 | echo.To view the help file: 132 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\interactive-python.ghc 133 | goto end 134 | ) 135 | 136 | if "%1" == "devhelp" ( 137 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 138 | if errorlevel 1 exit /b 1 139 | echo. 140 | echo.Build finished. 141 | goto end 142 | ) 143 | 144 | if "%1" == "epub" ( 145 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 146 | if errorlevel 1 exit /b 1 147 | echo. 148 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 149 | goto end 150 | ) 151 | 152 | if "%1" == "latex" ( 153 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 154 | if errorlevel 1 exit /b 1 155 | echo. 156 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 157 | goto end 158 | ) 159 | 160 | if "%1" == "latexpdf" ( 161 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 162 | cd %BUILDDIR%/latex 163 | make all-pdf 164 | cd %~dp0 165 | echo. 166 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdfja" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf-ja 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "text" ( 181 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 182 | if errorlevel 1 exit /b 1 183 | echo. 184 | echo.Build finished. The text files are in %BUILDDIR%/text. 185 | goto end 186 | ) 187 | 188 | if "%1" == "man" ( 189 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 190 | if errorlevel 1 exit /b 1 191 | echo. 192 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 193 | goto end 194 | ) 195 | 196 | if "%1" == "texinfo" ( 197 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 198 | if errorlevel 1 exit /b 1 199 | echo. 200 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 201 | goto end 202 | ) 203 | 204 | if "%1" == "gettext" ( 205 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 206 | if errorlevel 1 exit /b 1 207 | echo. 208 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 209 | goto end 210 | ) 211 | 212 | if "%1" == "changes" ( 213 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 214 | if errorlevel 1 exit /b 1 215 | echo. 216 | echo.The overview file is in %BUILDDIR%/changes. 217 | goto end 218 | ) 219 | 220 | if "%1" == "linkcheck" ( 221 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 222 | if errorlevel 1 exit /b 1 223 | echo. 224 | echo.Link check complete; look for any errors in the above output ^ 225 | or in %BUILDDIR%/linkcheck/output.txt. 226 | goto end 227 | ) 228 | 229 | if "%1" == "doctest" ( 230 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 231 | if errorlevel 1 exit /b 1 232 | echo. 233 | echo.Testing of doctests in the sources finished, look at the ^ 234 | results in %BUILDDIR%/doctest/output.txt. 235 | goto end 236 | ) 237 | 238 | if "%1" == "coverage" ( 239 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 240 | if errorlevel 1 exit /b 1 241 | echo. 242 | echo.Testing of coverage in the sources finished, look at the ^ 243 | results in %BUILDDIR%/coverage/python.txt. 244 | goto end 245 | ) 246 | 247 | if "%1" == "xml" ( 248 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 249 | if errorlevel 1 exit /b 1 250 | echo. 251 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 252 | goto end 253 | ) 254 | 255 | if "%1" == "pseudoxml" ( 256 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 257 | if errorlevel 1 exit /b 1 258 | echo. 259 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 260 | goto end 261 | ) 262 | 263 | :end 264 | -------------------------------------------------------------------------------- /examples/0_init/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mixer/interactive-python/348628aec594bf48cf503647e41ec080751fac09/examples/0_init/__init__.py -------------------------------------------------------------------------------- /examples/0_init/pong.py: -------------------------------------------------------------------------------- 1 | """ 2 | This is the simple 'base' pong game. One person can control the paddles 3 | with "W" and "S", and another person with "I" and "K". Nothing crazy here! 4 | 5 | Press 'q' to quit. 6 | 7 | Run this with:: 8 | 9 | python -m examples.0_init.pong 10 | """ 11 | 12 | from ..engine import BaseGame, run 13 | 14 | 15 | class Game(BaseGame): 16 | def __init__(self): 17 | super().__init__() 18 | self._player_1 = self._create_paddle(x=0, height=self._screen_height//6) 19 | self._player_2 = self._create_paddle(x=self._screen_width-1, 20 | height=self._screen_height//6) 21 | 22 | def update(self, pressed_key=None): 23 | if pressed_key == ord('s'): 24 | self._player_1.move(1) 25 | elif pressed_key == ord('w'): 26 | self._player_1.move(-1) 27 | 28 | if pressed_key == ord('k'): 29 | self._player_2.move(1) 30 | elif pressed_key == ord('i'): 31 | self._player_2.move(-1) 32 | 33 | self._ball.step(self._player_1, self._player_2) 34 | 35 | 36 | run(Game()) 37 | -------------------------------------------------------------------------------- /examples/1_viewer_controlled/pong.py: -------------------------------------------------------------------------------- 1 | """ 2 | We've modified our base pong game to run an interactive where the viewers 3 | control the paddle on the right. We handle all the control setup within our 4 | client, but you can also design your controls in the Interactive Studio! 5 | 6 | You should provide an OAuth token to connect to interactive on the comment line. 7 | You can get this token by going to https://interactive.mixer.com/request 8 | 9 | Press 'q' to quit. 10 | 11 | Run this with:: 12 | 13 | python -m examples.1_viewer_controlled.pong 14 | """ 15 | 16 | from interactive_python import State, Button, keycode 17 | from sys import argv 18 | 19 | from ..engine import BaseGame, run 20 | 21 | 22 | class Game(BaseGame): 23 | def __init__(self): 24 | super().__init__() 25 | self._player_1 = self._create_paddle(x=0, height=self._screen_height//6) 26 | self._player_2 = self._create_paddle(x=self._screen_width-1, 27 | height=self._screen_height//4) 28 | self._interactive = None 29 | 30 | async def setup(self): 31 | """ 32 | Called automatically by our game engine to boot the game. We'll create 33 | an interactive connection here! I've hard-coded a blank project to use. 34 | """ 35 | try: 36 | interactive = await State.connect(authorization="Bearer " + argv[1], 37 | project_version_id=42489, 38 | project_sharecode='rheo1hre') 39 | except Exception as e: 40 | print("Error connecting to interactive", e) 41 | return 42 | 43 | self._interactive = interactive 44 | interactive.on('error', lambda e: self.fatal_error(e)) 45 | 46 | interactive.pump_async() 47 | await self._setup_controls() 48 | 49 | async def _setup_controls(self): 50 | """ 51 | All the control setup! Alternately, you can design the controls in 52 | the Interactive Studio, but we'll do them programmatically 53 | for demonstration purposes. 54 | """ 55 | up = Button( 56 | control_id='up', 57 | text='Up', 58 | keycode=keycode.up, 59 | position=[ 60 | {'size': 'large', 'width': 5, 'height': 5, 'x': 0, 'y': 0}, 61 | ], 62 | ) 63 | 64 | up.on('mousedown', lambda call: self._player_2.move(-1)) 65 | 66 | down = Button( 67 | control_id='down', 68 | text='Down', 69 | keycode=keycode.down, 70 | position=[ 71 | {'size': 'large', 'width': 5, 'height': 5, 'x': 0, 'y': 6}, 72 | ], 73 | ) 74 | 75 | down.on('mousedown', lambda call: self._player_2.move(1)) 76 | 77 | await self._interactive.scenes['default'].create_controls(up, down) 78 | await self._interactive.set_ready() 79 | 80 | def update(self, pressed_key=None): 81 | if pressed_key == ord('s'): 82 | self._player_1.move(1) 83 | elif pressed_key == ord('w'): 84 | self._player_1.move(-1) 85 | 86 | self._ball.step(self._player_1, self._player_2) 87 | 88 | 89 | run(Game()) 90 | -------------------------------------------------------------------------------- /examples/2_shortcode_oauth/oauth.py: -------------------------------------------------------------------------------- 1 | """ 2 | This is an example of "shortcode" oauth that you can use to get an access token 3 | to connect to Interactive. You want to create an OAuth client first on 4 | https://mixer.com/lab, then use its client ID to run this script. You can leave 5 | the "redirect urls" blank. 6 | 7 | Run this with:: 8 | 9 | python -m examples.2_shortcode_oauth.oauth 10 | """ 11 | 12 | import interactive_python as interactive 13 | import asyncio 14 | from sys import argv 15 | 16 | async def get_access_token(client): 17 | code = await client.get_code() 18 | print("Go to mixer.com/go and enter {}".format(code.code)) 19 | 20 | try: 21 | return await code.accepted() 22 | except interactive.ShortCodeAccessDeniedError: 23 | print("The user denied access to our client") 24 | except interactive.ShortCodeTimeoutError: 25 | print("Yo, you're too slow! Let's try again...") 26 | return await get_access_token(client) 27 | 28 | async def run(): 29 | try: 30 | with interactive.OAuthClient(argv[1]) as client: 31 | token = await get_access_token(client) 32 | print("Access token: {}".format(token.access)) 33 | except interactive.UnknownShortCodeError as e: 34 | print("An unknown error occurred in Mixer: {}".format(e)) 35 | 36 | if __name__ == '__main__': 37 | asyncio.get_event_loop().run_until_complete(run()) 38 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mixer/interactive-python/348628aec594bf48cf503647e41ec080751fac09/examples/__init__.py -------------------------------------------------------------------------------- /examples/engine.py: -------------------------------------------------------------------------------- 1 | """This is the core of the pong example. You can read through it if you'd 2 | like, but it's pretty boring. I try to shove as much boring game logic into 3 | here so that the interactive demos can focus on what matters--interactive! 4 | """ 5 | 6 | import curses 7 | import random 8 | import math 9 | import asyncio 10 | 11 | 12 | class Sprite: 13 | def __init__(self, screen): 14 | self._screen = screen 15 | self._screen_height, self._screen_width = screen.getmaxyx() 16 | 17 | def _draw_rect(self, x, y, width, height, char): 18 | for dx in range(width): 19 | for dy in range(height): 20 | self._screen.insch(y + dy, x + dx, char) 21 | 22 | def draw(self): 23 | raise NotImplementedError() 24 | 25 | 26 | class Paddle(Sprite): 27 | def __init__(self, screen, x, height, width=1): 28 | super(Paddle, self).__init__(screen) 29 | self.x = x 30 | self.y = (self._screen_height - height) // 2 31 | self.height = height 32 | self.width = width 33 | 34 | def move(self, amount): 35 | self.y = min(self._screen_height - self.height, max(0, self.y + amount)) 36 | 37 | def draw(self): 38 | self._draw_rect(self.x, self.y, self.width, self.height, 'x') 39 | 40 | 41 | class Ball(Sprite): 42 | def __init__(self, screen, speed_per_frame=2, size=1): 43 | super(Ball, self).__init__(screen) 44 | self._size = size 45 | self._speed = speed_per_frame 46 | self._reset() 47 | 48 | def _reset(self): 49 | self.x = self._screen_width // 2 50 | self.y = self._screen_height // 2 51 | self._angle = 0 52 | if random.random() > 0.5: 53 | self._angle = math.pi 54 | 55 | def _try_bounce(self, paddles): 56 | """Checks if the ball is bouncing against and of the padds and, if so, 57 | changes the angle to bounce off them. 58 | """ 59 | for paddle in paddles: 60 | if self.x > paddle.x + paddle.width or self.x + self._size < paddle.x: 61 | continue 62 | if self.y > paddle.y + paddle.height or self.y < paddle.y: 63 | continue 64 | 65 | # Set the angle to be the angle from the paddle center to the ball 66 | dy = (self.y + self._size / 2) - (paddle.y + paddle.height / 2) 67 | dx = (self.x + self._size / 2) - (paddle.x + paddle.width / 2) 68 | self._angle = math.atan2(dy, dx) 69 | 70 | def step(self, *paddles): 71 | self._try_bounce(paddles) 72 | self.x += math.cos(self._angle) * self._speed 73 | self.y += math.sin(self._angle) * self._speed 74 | 75 | if self.y < 0 or self.y > self._screen_height - 1: 76 | self.y = max(0, min(self._screen_height - self._size, self.y)) 77 | self._angle = -self._angle 78 | 79 | if self.x < 0 or self.x > self._screen_width - self._size: 80 | self._reset() 81 | 82 | def draw(self): 83 | self._draw_rect(round(self.x), round(self.y), self._size, self._size, 'o') 84 | 85 | 86 | class QuitException(Exception): 87 | pass 88 | 89 | 90 | class BaseGame: 91 | def __init__(self): 92 | self._screen = curses.initscr() 93 | self._screen_height, self._screen_width = self._screen.getmaxyx() 94 | self._ball = Ball(self._screen) 95 | self._sprites = [self._ball] 96 | self._fps = 15 97 | self._running = True 98 | 99 | def _create_paddle(self, **kwargs): 100 | paddle = Paddle(screen=self._screen, **kwargs) 101 | self._sprites.append(paddle) 102 | return paddle 103 | 104 | def update(self, pressed_key=None): 105 | raise NotImplementedError() 106 | 107 | def fatal_error(self, e): 108 | self._running = False 109 | raise e 110 | 111 | async def setup(self): 112 | pass 113 | 114 | async def loop(self): 115 | await self.setup() 116 | 117 | while self._running: 118 | self._screen.timeout(1) 119 | pressed_key = self._screen.getch() 120 | if pressed_key == ord('q'): 121 | raise QuitException() 122 | 123 | self.update(pressed_key) 124 | 125 | self._screen.clear() 126 | drawees = sorted(self._sprites, key=lambda o: o.x) 127 | for d in drawees: 128 | d.draw() 129 | self._screen.refresh() 130 | curses.flushinp() 131 | 132 | await asyncio.sleep(1 / self._fps) 133 | 134 | 135 | def run(game): 136 | loop = asyncio.get_event_loop() 137 | try: 138 | loop.run_until_complete(game.loop()) 139 | except (KeyboardInterrupt, QuitException): 140 | curses.endwin() 141 | finally: 142 | loop.stop() 143 | -------------------------------------------------------------------------------- /interactive_python/__init__.py: -------------------------------------------------------------------------------- 1 | from .connection import * 2 | from .encoding import * 3 | from .errors import * 4 | from .oauth import * 5 | from .scene import * 6 | from .state import * 7 | from .keycodes import keycode 8 | from ._util import until_event 9 | -------------------------------------------------------------------------------- /interactive_python/_util.py: -------------------------------------------------------------------------------- 1 | import string 2 | import random 3 | import json 4 | import asyncio 5 | 6 | from pyee import EventEmitter 7 | 8 | metadata_prop_name = "meta" 9 | 10 | 11 | def random_string(length, source=string.ascii_letters): 12 | return ''.join(random.choice(source) for x in range(length)) 13 | 14 | 15 | def until_event(emitter, name, loop=asyncio.get_event_loop()): 16 | fut = asyncio.Future(loop=loop) 17 | emitter.once(name, lambda result: fut.set_result(result)) 18 | return fut 19 | 20 | 21 | class OverridableJSONEncoder(json.JSONEncoder): 22 | """ 23 | OverridableJSONEncoder extends the default JSON encoder to look for a method 24 | 'to_json' on objects to be serialized, calling that when possible. 25 | """ 26 | 27 | def default(self, obj): 28 | if callable(getattr(obj, 'to_json', None)): 29 | return obj.to_json() 30 | return super().default(obj) 31 | 32 | json_encoder = OverridableJSONEncoder(check_circular=False, allow_nan=False, 33 | separators=(',', ':')) 34 | 35 | 36 | class ChangeTracker: 37 | """ 38 | ChangeTracker is a simple structure that keeps track of changes made to 39 | properties of the object. 40 | """ 41 | 42 | def __init__(self, data={}, intrinsic_properties=[]): 43 | self._intrinsic_properties = intrinsic_properties 44 | self._nested_trackers = [(key, value) for key, value in data.items() 45 | if isinstance(value, ChangeTracker)] 46 | self._data = data 47 | self._changes = set() 48 | 49 | def assign(self, **kwargs): 50 | """ 51 | Assigns data in-bulk to the resource, returns the resource. 52 | :rtype: Resource 53 | """ 54 | for key, value in kwargs.items(): 55 | self._set_and_track_property(key, value) 56 | 57 | return self 58 | 59 | def to_json(self): 60 | """ 61 | to_json will be called when serializing the resource to JSON. It returns 62 | the raw data. 63 | :return: 64 | """ 65 | return self._data 66 | 67 | def _capture_changes(self): 68 | """ 69 | Returns a dict of changes and resets the "changed" state. 70 | :rtype: dict 71 | """ 72 | output = {} 73 | for prop in self._intrinsic_properties: 74 | output[prop] = self._data[prop] 75 | 76 | for key, tracker in self._nested_trackers: 77 | if tracker.has_changed(): 78 | output[key] = tracker._capture_changes() 79 | 80 | for key in self._changes: 81 | output[key] = self._data.get(key, None) 82 | 83 | self._changes.clear() 84 | return output 85 | 86 | def has_changed(self): 87 | """ 88 | Returns whether any metadata properties have changed. 89 | :rtype: bool 90 | """ 91 | if len(self._changes) > 0: 92 | return True 93 | 94 | if any(tracker.has_changed() for key, tracker in self._nested_trackers): 95 | return True 96 | 97 | return False 98 | 99 | def _mark_synced(self): 100 | """ 101 | Marks the changed properties on the resource as having been saved. 102 | """ 103 | self._changes.clear() 104 | 105 | def _set_and_track_property(self, key, value): 106 | previous_value = self._data.get(key, None) 107 | 108 | if isinstance(previous_value, ChangeTracker): 109 | previous_value.assign(**value) 110 | elif value != previous_value: 111 | if value is None: 112 | del self._data[key] 113 | else: 114 | self._data[key] = value 115 | 116 | self._changes.add(key) 117 | 118 | def __setattr__(self, key, value): 119 | if key[0] == '_': 120 | self.__dict__[key] = value 121 | return 122 | 123 | self._set_and_track_property(key, value) 124 | 125 | def __getattr__(self, key): 126 | if key not in self._data: 127 | raise AttributeError(key) 128 | 129 | return self._data[key] 130 | 131 | 132 | class Resource(EventEmitter, ChangeTracker): 133 | """Resource represents some taggable, metadata-attachable construct in 134 | Interactive. Scenes, groups, and participants are resources. 135 | """ 136 | 137 | def __init__(self, id, id_property): 138 | ChangeTracker.__init__( 139 | self, 140 | intrinsic_properties=[id_property], 141 | data={id_property: id, 'meta': ChangeTracker()} 142 | ) 143 | EventEmitter.__init__(self) 144 | 145 | self._id_property = id_property 146 | self._connection = None 147 | 148 | @property 149 | def id(self): 150 | """ 151 | :rtype: int 152 | """ 153 | return self._data[self._id_property] 154 | 155 | def _attach_connection(self, connection): 156 | """ 157 | Called by the State when it gets or creates a new instance of this 158 | resource. Used for RPC calls. 159 | """ 160 | self._connection = connection 161 | 162 | def _apply_changes(self, change, call): 163 | """ 164 | Applies a complete update of properties from the remote server. 165 | :type change: dict 166 | :type call: Call 167 | """ 168 | self.assign(**change) 169 | self._mark_synced() 170 | self.emit('update', call) 171 | 172 | def _on_deleted(self, call): 173 | """ 174 | Called when a scene is deleted. 175 | :type call: Call 176 | """ 177 | self.emit('delete', call) 178 | -------------------------------------------------------------------------------- /interactive_python/connection.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | import websockets 4 | import collections 5 | 6 | from .log import logger 7 | from .encoding import Encoding, TextEncoding 8 | from ._util import json_encoder 9 | 10 | 11 | class Call: 12 | def __init__(self, connection, payload): 13 | """ 14 | A Call is an incoming message from the Interactive service. 15 | :param connection: the connection 16 | :param payload: 17 | """ 18 | self._connection = connection 19 | self._payload = payload 20 | 21 | @property 22 | def name(self): 23 | """ 24 | :return: The name of the method being called. 25 | :rtype: str 26 | """ 27 | return self._payload['method'] 28 | 29 | @property 30 | def data(self): 31 | """ 32 | :return: The payload of the method being called. 33 | :rtype: dict 34 | """ 35 | return self._payload['params'] 36 | 37 | def reply_error(self, result): 38 | """ 39 | Submits a successful reply for the call. 40 | :param result: The result to send to tetrisd 41 | """ 42 | self._connection.reply(self._id, result=result) 43 | 44 | def reply_error(self, error): 45 | """ 46 | Submits an errorful reply for the call. 47 | :param error: The error to send to tetrisd 48 | """ 49 | self._connection.reply(self._id, error=error) 50 | 51 | 52 | class Connection: 53 | """ 54 | The Connection is used to connect to the Interactive server. It connects 55 | to a provided socket address and provides an interface for making RPC 56 | calls. Example usage:: 57 | 58 | connection = Connection( 59 | address=get_interactive_address(), 60 | authorization="Bearer {}".format(my_oauth_token), 61 | interactive_version_id=1234) 62 | """ 63 | 64 | def __init__(self, address=None, authorization=None, 65 | project_version_id=None, project_sharecode=None, 66 | extra_headers={}, loop=asyncio.get_event_loop(), socket=None, 67 | protocol_version="2.0"): 68 | 69 | if authorization is not None: 70 | extra_headers['Authorization'] = authorization 71 | if project_version_id is not None: 72 | extra_headers['X-Interactive-Version'] = project_version_id 73 | if project_sharecode is not None: 74 | extra_headers['X-Interactive-Sharecode'] = project_sharecode 75 | extra_headers['X-Protocol-Version'] = protocol_version 76 | 77 | self._socket_or_connector = socket or websockets.client.connect( 78 | address, loop=loop, extra_headers=extra_headers) 79 | self._socket = None 80 | 81 | self._loop = loop 82 | self._encoding = TextEncoding() 83 | self._awaiting_replies = {} 84 | self._call_counter = 0 85 | self._last_sequence_number = 0 86 | 87 | self._recv_queue = collections.deque() 88 | self._recv_await = None 89 | self._recv_task = None 90 | 91 | async def connect(self): 92 | """ 93 | Connects to the Interactive server, waiting until the connection 94 | if fully established before returning. Can throw a ClosedError 95 | if something, such as authentication, fails. 96 | """ 97 | 98 | if not hasattr(self._socket_or_connector, '__await__'): 99 | self._socket = self._socket_or_connector 100 | else: 101 | self._socket = await self._socket_or_connector 102 | 103 | # await a hello event 104 | while True: 105 | packet = await self._read_single() 106 | if packet['type'] == 'method' and packet['method'] == 'hello': 107 | break 108 | 109 | self._recv_queue.append(packet) 110 | 111 | self._recv_task = asyncio.ensure_future(self._read(), loop=self._loop) 112 | 113 | def _fallback_to_plain_text(self): 114 | if isinstance(self._encoding, TextEncoding): 115 | return # we're already falling back 116 | 117 | self._encoding = TextEncoding() 118 | asyncio.ensure_future( 119 | self.set_compression(TextEncoding()), loop=self._loop) 120 | 121 | def _decode(self, data): 122 | """ 123 | Converts the packet data to a string, 124 | decompressing it if necessary. Always returns a string. 125 | """ 126 | if isinstance(data, str): 127 | return data 128 | 129 | try: 130 | return self._encoding.decode(data) 131 | except Exception as e: 132 | self._fallback_to_plain_text() 133 | logger.info("error decoding Interactive message, falling back to" 134 | "plain text", extra=e) 135 | 136 | def _encode(self, data): 137 | """ 138 | Converts the packet data to a string or byte array, 139 | compressing it if necessary. 140 | """ 141 | try: 142 | return self._encoding.encode(data) 143 | except Exception as e: 144 | self._fallback_to_plain_text() 145 | logger.warn("error encoding Interactive message, falling back to" 146 | "plain text", extra=e) 147 | 148 | return data 149 | 150 | def _handle_recv(self, data): 151 | """ 152 | Handles a single received packet from the Interactive service. 153 | """ 154 | 155 | if 'seq' in data: 156 | self._last_sequence_number = data['seq'] 157 | 158 | if data['type'] == 'reply': 159 | if data['id'] in self._awaiting_replies: 160 | self._awaiting_replies[data['id']]. \ 161 | set_result(data['result']) 162 | del self._awaiting_replies[data['id']] 163 | 164 | return 165 | 166 | self._recv_queue.append(Call(self, data)) 167 | if self._recv_await is not None: 168 | self._recv_await.set_result(True) 169 | self._recv_await = None 170 | 171 | def _send(self, payload): 172 | """ 173 | Encodes and sends a dict payload. 174 | """ 175 | future = self._socket.send(self._encode(json_encoder.encode(payload))) 176 | asyncio.ensure_future(future, loop=self._loop) 177 | 178 | async def _read_single(self): 179 | """ 180 | Reads a single event off the websocket. 181 | """ 182 | try: 183 | raw_data = await self._socket.recv() 184 | except (asyncio.CancelledError, websockets.ConnectionClosed) as e: 185 | if self._recv_await is None: 186 | self._recv_await = asyncio.Future(loop=self._loop) 187 | self._recv_await.set_result(False) 188 | raise e 189 | 190 | return json.loads(self._decode(raw_data)) 191 | 192 | async def _read(self): 193 | """ 194 | Endless read loop that runs until the socket is closed. 195 | """ 196 | while True: 197 | try: 198 | data = await self._read_single() 199 | except (asyncio.CancelledError, websockets.ConnectionClosed): 200 | break # will already be handled 201 | except Exception as e: 202 | logger.error("error in interactive read loop", extra=e) 203 | break 204 | 205 | if isinstance(data, list): 206 | for item in data: 207 | self._handle_recv(item) 208 | else: 209 | self._handle_recv(data) 210 | 211 | async def set_compression(self, scheme): 212 | """Updates the compression used on the websocket this should be 213 | called with an instance of the Encoding class, for example:: 214 | 215 | connection.set_compression(GzipEncoding()) 216 | 217 | You can, optionally, await on the resolution of method, though 218 | doing so it not at all required. Returns True if the server agreed 219 | on and executed the switch. 220 | 221 | :param scheme: The compression scheme to use 222 | :type scheme: Encoding 223 | :return: Whether the upgrade was successful 224 | :rtype: bool 225 | """ 226 | result = await self.call("setCompression", {'scheme': [scheme.name()]}) 227 | if result['scheme'] == scheme.name(): 228 | self._encoding = scheme 229 | return True 230 | 231 | return False 232 | 233 | def reply(self, call_id, result=None, error=None): 234 | """ 235 | Sends a reply for a packet id. Either the result or error should 236 | be fulfilled. 237 | 238 | :param call_id: The ID of the call being replied to. 239 | :type call_id: int 240 | :param result: The successful result of the call. 241 | :param error: The errorful result of the call. 242 | """ 243 | packet = {'type': 'reply', 'id': call_id} 244 | 245 | if result is not None: 246 | packet['result'] = result 247 | if error is not None: 248 | packet['error'] = result 249 | 250 | self._send(packet) 251 | 252 | async def call(self, method, params, discard=False, timeout=10): 253 | """ 254 | Sends a method call to the interactive socket. If discard 255 | is false, we'll wait for a response before returning, up to the 256 | timeout duration in seconds, at which point it raises an 257 | asyncio.TimeoutError. If the timeout is None, we'll wait forever. 258 | 259 | :param method: Method name to call 260 | :type method: str 261 | :param params: Parameters to insert into the method, generally a dict. 262 | :param discard: ``True`` to not request any reply to the method. 263 | :type discard: bool 264 | :param timeout: Call timeout duration, in seconds. 265 | :type timeout: int 266 | :return: The call response, or None if it was discarded. 267 | :raises: asyncio.TimeoutError 268 | """ 269 | 270 | packet = { 271 | 'type': 'method', 272 | 'method': method, 273 | 'params': params, 274 | 'id': self._call_counter, 275 | 'seq': self._last_sequence_number, 276 | } 277 | 278 | if discard: 279 | packet['discard'] = True 280 | 281 | self._call_counter += 1 282 | self._send(packet) 283 | 284 | if discard: 285 | return None 286 | 287 | future = asyncio.Future(loop=self._loop) 288 | self._awaiting_replies[packet['id']] = future 289 | 290 | try: 291 | return await asyncio.wait_for(future, timeout, loop=self._loop) 292 | except Exception as e: 293 | del self._awaiting_replies[packet['id']] 294 | raise e 295 | 296 | def get_packet(self): 297 | """ 298 | Synchronously reads a packet from the connection. Returns None if 299 | there are no more packets in the queue. Example:: 300 | 301 | while await connection.has_packet(): 302 | dispatch_call(connection.get_packet()) 303 | 304 | :rtype: Call 305 | """ 306 | if len(self._recv_queue) > 0: 307 | return self._recv_queue.popleft() 308 | 309 | return None 310 | 311 | async def has_packet(self): 312 | """ 313 | Blocks until a packet is read. Returns true if a packet is then 314 | available, or false if the connection is subsequently closed. Example:: 315 | 316 | while await connection.has_packet(): 317 | dispatch_call(connection.get_packet()) 318 | 319 | :rtype: bool 320 | """ 321 | if len(self._recv_queue) > 0: 322 | return 323 | 324 | if self._recv_await is None: 325 | self._recv_await = asyncio.Future(loop=self._loop) 326 | 327 | return await self._recv_await 328 | 329 | async def close(self): 330 | """Closes the socket connection gracefully""" 331 | self._recv_task.cancel() 332 | await self._socket.close() 333 | -------------------------------------------------------------------------------- /interactive_python/discovery.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | 3 | from .errors import DiscoveryError, NoServersAvailableError 4 | 5 | 6 | class Discovery: 7 | """Discovery is a simple service discovery class which retrieves an 8 | Interactive host to connect to. This is passed into the State by default; 9 | you usually should not need to override it. 10 | """ 11 | def __init__(self, host='https://mixer.com', 12 | path='/api/v1/interactive/hosts', loop=None, timeout=10): 13 | self._host = host 14 | self._path = path 15 | self._timeout = timeout 16 | self._loop = None 17 | 18 | async def find(self): 19 | """Returns the websocket address of an interactive server to connect 20 | to, or raises a NoServersAvailableError. 21 | """ 22 | 23 | # Technically it might be nice to asyncio this, but I'm not eager to 24 | # add another dependency and Python doesn't seem to have an asyncio 25 | # http client in its standard library yet. 26 | 27 | async with aiohttp.ClientSession(loop=self._loop) as session: 28 | async with session.get(self._host + self._path) as res: 29 | if res.status >= 300: 30 | raise DiscoveryError('Expected a 2xx status code, but' 31 | 'got {}'.format(res.status)) 32 | 33 | servers = await res.json() 34 | if len(servers) == 0: 35 | raise NoServersAvailableError() 36 | print(servers[0]['address']) 37 | return servers[0]['address'] 38 | -------------------------------------------------------------------------------- /interactive_python/encoding.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | from gzip import GzipFile 3 | import varint 4 | import io 5 | import zlib 6 | 7 | 8 | class EncodingException(Exception): 9 | """An EncodingException is raised if an error occurs in an encoding or 10 | decoding algorithm. Raising this exception triggers a fallback to 11 | plain text encoding. 12 | """ 13 | pass 14 | 15 | 16 | class Encoding: 17 | """Encoding is an abstract class that defines methods for decoding incoming 18 | and encoding outgoing websocket calls. Both encode() and decode() are 19 | allowed to raise EncodingExceptions, which will trigger a fallback to 20 | plain-text encoding. 21 | """ 22 | 23 | @abstractmethod 24 | def name(self): 25 | pass 26 | 27 | @abstractmethod 28 | def encode(self, data): 29 | """ encode takes a string of data and returns its encoded form """ 30 | pass 31 | 32 | @abstractmethod 33 | def decode(self, data): 34 | """ decode takes a byte slice of data and 35 | returns it decoded, string form """ 36 | pass 37 | 38 | 39 | def reset_buffer(buffer, value=None): 40 | buffer.truncate(0) 41 | buffer.seek(0) 42 | 43 | if value is not None: 44 | buffer.write(value) 45 | 46 | 47 | def reset_buffer(buffer, value=None): 48 | buffer.truncate(0) 49 | buffer.seek(0) 50 | 51 | if value is not None: 52 | buffer.write(value) 53 | 54 | 55 | class TextEncoding(Encoding): 56 | 57 | def name(self): 58 | return 'text' 59 | 60 | def encode(self, data): 61 | return data 62 | 63 | def decode(self, data): 64 | return data 65 | 66 | 67 | class GzipEncoding(Encoding): 68 | def __init__(self, compression_level=6): 69 | super() 70 | self._encoder_buffer = io.BytesIO() 71 | self._encoder = None 72 | self._decoder_buffer = io.BytesIO() 73 | self._decoder = zlib.decompressobj(16 + zlib.MAX_WBITS) 74 | self._compression_level = compression_level 75 | 76 | def name(self): 77 | return 'gzip' 78 | 79 | def encode(self, data): 80 | data = data.encode('utf-8') 81 | self._encoder_buffer.write(varint.encode(len(data))) 82 | 83 | # Don't initialize the encoder before the first call to encode(), since 84 | # it writes the gzip header immediately and we need to insert the 85 | # message length prior to that happening. 86 | if self._encoder is None: 87 | self._encoder = GzipFile(fileobj=self._encoder_buffer, mode='wb', 88 | compresslevel=self._compression_level) 89 | 90 | self._encoder.write(data) 91 | self._encoder.flush() 92 | 93 | output = self._encoder_buffer.getvalue() 94 | reset_buffer(self._encoder_buffer) 95 | 96 | return output 97 | 98 | def decode(self, data): 99 | # Decode the varuint prefix off the data first, then smash the remaining 100 | # data into the decode buffer and reset it to read any previous tail. 101 | prefix_stream = io.BytesIO(data) 102 | decoded_bytes = varint.decode_stream(prefix_stream) 103 | self._decoder_buffer.write(data[prefix_stream.tell():]) 104 | self._decoder_buffer.seek(0) 105 | 106 | decoded_data = self._decoder.decompress( 107 | self._decoder_buffer.getbuffer(), decoded_bytes) 108 | reset_buffer(self._decoder_buffer, self._decoder.unconsumed_tail) 109 | 110 | return decoded_data.decode('utf-8') 111 | -------------------------------------------------------------------------------- /interactive_python/errors.py: -------------------------------------------------------------------------------- 1 | class DiscoveryError(Exception): 2 | """Raised if some error occurs during service discovery 3 | that we didn't anticipate. 4 | """ 5 | pass 6 | 7 | 8 | class NoServersAvailableError(Exception): 9 | """Raised if Beam reports that no servers are available.""" 10 | pass 11 | 12 | 13 | class ShortCodeError(Exception): 14 | """Base exception raised when some unexpected event occurs in the shortcode 15 | OAuth flow.""" 16 | pass 17 | 18 | 19 | class UnknownShortCodeError(ShortCodeError): 20 | """Exception raised when an unknown error happens while running shortcode 21 | OAuth. 22 | """ 23 | pass 24 | 25 | 26 | class ShortCodeAccessDeniedError(ShortCodeError): 27 | """Exception raised when the user denies access to the client in shortcode 28 | OAuth.""" 29 | pass 30 | 31 | 32 | class ShortCodeTimeoutError(ShortCodeError): 33 | """Exception raised when the shortcode expires without being accepted.""" 34 | pass 35 | -------------------------------------------------------------------------------- /interactive_python/keycodes.py: -------------------------------------------------------------------------------- 1 | """ 2 | The code_aliases map is from the project at https://github.com/timoxley/keycode 3 | and licensed under the following terms: 4 | 5 | Copyright (c) 2014 Tim Oxley 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | """ 25 | 26 | code_aliases = { 27 | # base mapping: 28 | 'backspace': 8, 29 | 'tab': 9, 30 | 'enter': 13, 31 | 'shift': 16, 32 | 'ctrl': 17, 33 | 'alt': 18, 34 | 'pause/break': 19, 35 | 'caps lock': 20, 36 | 'esc': 27, 37 | 'space': 32, 38 | 'page up': 33, 39 | 'page down': 34, 40 | 'end': 35, 41 | 'home': 36, 42 | 'left': 37, 43 | 'up': 38, 44 | 'right': 39, 45 | 'down': 40, 46 | 'insert': 45, 47 | 'delete': 46, 48 | 'command': 91, 49 | ';': 186, 50 | '=': 187, 51 | ',': 188, 52 | '-': 189, 53 | '.': 190, 54 | '/': 191, 55 | '`': 192, 56 | '[': 219, 57 | '\\': 220, 58 | ']': 221, 59 | "'": 222, 60 | 61 | # aliases: 62 | 'windows': 91, 63 | '⇧': 16, 64 | '⌥': 18, 65 | '⌃': 17, 66 | '⌘': 91, 67 | 'ctl': 17, 68 | 'control': 17, 69 | 'option': 18, 70 | 'pause': 19, 71 | 'break': 19, 72 | 'caps': 20, 73 | 'return': 13, 74 | 'escape': 27, 75 | 'spc': 32, 76 | 'pgup': 33, 77 | 'pgdn': 34, 78 | 'ins': 45, 79 | 'del': 46, 80 | 'cmd': 91 81 | } 82 | 83 | 84 | class KeyCoder: 85 | """ 86 | Simple class one which all the keycode aliases are defined and which 87 | manually calls `ord` to generate codes for unknown letters. 88 | """ 89 | def __init__(self): 90 | for key, value in code_aliases.items(): 91 | setattr(self, key, value) 92 | 93 | def __getattr__(self, item): 94 | if len(item) != 1: 95 | raise AttributeError('"{}" is not a known keycode'.format(item)) 96 | 97 | upper = item.upper() 98 | 99 | # Although we actually use the upper version to correlate with Js' 100 | # keycodes, callers should know that if they *ask* for uppercase 101 | # letters they won't map properly. 102 | if upper == item: 103 | raise AttributeError('Cannot create a keycode for {}: Interactive' 104 | 'does not distinguish between uppercase and' 105 | 'lowercase key presses. Please use "{}"' 106 | 'instead'.format(item, item.lower())) 107 | 108 | return ord(upper) 109 | 110 | keycode = KeyCoder() 111 | -------------------------------------------------------------------------------- /interactive_python/log.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | logger = logging.getLogger(__package__) 4 | -------------------------------------------------------------------------------- /interactive_python/oauth.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | import asyncio 3 | import datetime 4 | 5 | from .errors import UnknownShortCodeError, ShortCodeAccessDeniedError, \ 6 | ShortCodeTimeoutError 7 | 8 | 9 | class OAuthGrant: 10 | """Internal DTO for an OAuth grant""" 11 | 12 | def __init__(self, client_id, client_secret, scopes, host): 13 | self.client_id = client_id 14 | self.client_secret = client_secret 15 | self.scopes = scopes 16 | self.host = host 17 | 18 | def url(self, path): 19 | return self.host + '/api/v1' + path 20 | 21 | 22 | class OAuthTokens: 23 | """OAuthTokens is a bearer from an OAuth access and refresh token retrieved 24 | via the :func:`~interactive_python.OAuthShortCode.accepted` method. 25 | """ 26 | 27 | def __init__(self, body): 28 | self.access = body['access_token'] 29 | self.refresh = body['refresh_token'] 30 | self.expires_at = datetime.datetime.now() + \ 31 | datetime.timedelta(seconds=body['expires_in']) 32 | 33 | 34 | class OAuthShortCode: 35 | """ 36 | OAuthShortCode is the shortcode handle returned by the `OAuthClient`. See 37 | documentation on that class for more information and usage examples. 38 | """ 39 | 40 | def __init__(self, grant, client, body, check_interval=1): 41 | self._handle = body['handle'] 42 | self._client = client 43 | self._grant = grant 44 | self._check_interval = check_interval 45 | 46 | self.code = body['code'] 47 | self.expires_at = datetime.datetime.now() + \ 48 | datetime.timedelta(seconds=body['expires_in']) 49 | 50 | async def accepted(self): 51 | """ 52 | Waits until the user enters the shortcode on the Mixer website. Throws 53 | if they deny access or don't enter the code in time. 54 | 55 | :raise ShortCodeAccessDeniedError: if the user denies access 56 | :raise ShortCodeTimeoutError: if the user doesn't enter 57 | :rtype: OAuthTokens 58 | """ 59 | address = self._grant.url('/oauth/shortcode/check/' + self._handle) 60 | while True: 61 | await asyncio.sleep(self._check_interval) 62 | 63 | async with self._client.get(address) as res: 64 | if res.status == 200: 65 | return await self._get_tokens(await res.json()) 66 | elif res.status == 204: 67 | continue 68 | elif res.status == 403: 69 | raise ShortCodeAccessDeniedError('User has denied access') 70 | elif res.status == 404: 71 | raise ShortCodeTimeoutError('Timeout waiting for the user ' 72 | 'to enter the OAuth shortcode.') 73 | 74 | async def _get_tokens(self, body): 75 | address = self._grant.url('/oauth/token') 76 | payload = { 77 | 'client_id': self._grant.client_id, 78 | 'client_secret': self._grant.client_secret, 79 | 'grant_type': 'authorization_code', 80 | 'code': body['code'], 81 | 'redirect_uri': '' 82 | } 83 | headers = { 84 | 'content-type': 'application/json', 85 | 'client-id': self._grant.client_id 86 | } 87 | 88 | async with self._client.post(address, json=payload, headers=headers) as res: 89 | if res.status != 200: 90 | raise UnknownShortCodeError('Expected a 2xx status code, but ' 91 | 'got {0} : reason {1}'.format(res.status, await res.text())) 92 | 93 | return OAuthTokens(await res.json()) 94 | 95 | 96 | class OAuthClient: 97 | """ 98 | The OAuth client implements our shortcode OAuth flow. From a user's 99 | perspective, your app or game displays a 6-digit code to them and prompts 100 | them to go to `mixer.com/go `_. This library will 101 | resolve back into your application once they enter that code. 102 | 103 | Here's a full example of what a full usage might 104 | look like, with error handling: 105 | 106 | .. code:: python 107 | 108 | import interactive_python as interactive 109 | import asyncio 110 | 111 | async def get_access_token(client): 112 | code = await client.get_code() 113 | print("Go to mixer.com/go and enter {}".format(code.code)) 114 | 115 | try: 116 | return await code.accepted() 117 | except interactive.ShortCodeAccessDeniedError: 118 | print("The user denied access to our client") 119 | except interactive.ShortCodeTimeoutError: 120 | print("Yo, you're too slow! Let's try again...") 121 | return await get_access_token(client) 122 | 123 | async def run(): 124 | with interactive.OAuthClient(my_client_id) as client: 125 | token = await get_access_token(client) 126 | print("Access token: {}".format(token.access)) 127 | 128 | asyncio.get_event_loop().run_until_complete(run()) 129 | 130 | :param client_id: Your OAuth client ID 131 | :type client_id: string 132 | :param client_secret: Your OAuth client secret, if any 133 | :type client_secret: string 134 | :param host: Base address of the Mixer servers 135 | :type host: string 136 | :param scopes: A list of scopes to request. For Interactive, you only 137 | need the 'interactive:robot:self' scope 138 | :type scopes: list[string] 139 | :param loop: asyncio event loop to attach to 140 | :type loop: asyncio.Loop 141 | """ 142 | 143 | def __init__(self, client_id, client_secret=None, host='https://mixer.com', 144 | scopes=['interactive:robot:self'], loop=None): 145 | self._loop = loop 146 | self._grant = OAuthGrant(client_id, client_secret, scopes, host) 147 | 148 | def __enter__(self): 149 | self._client = aiohttp.ClientSession(loop=self._loop) 150 | return self 151 | 152 | def __exit__(self, *args): 153 | self._client.close() 154 | 155 | async def get_code(self): 156 | """ 157 | Requests a shortcode from the Mixer servers and returns an 158 | OAuthShortCode handle. 159 | 160 | :rtype: OAuthShortCode 161 | """ 162 | address = self._grant.url('/oauth/shortcode') 163 | payload = { 164 | 'client_id': self._grant.client_id, 165 | 'client_secret': self._grant.client_secret, 166 | 'scope': ' '.join(self._grant.scopes) 167 | } 168 | 169 | async with self._client.post(address, json=payload) as res: 170 | if res.status >= 300: 171 | raise UnknownShortCodeError('Expected a 2xx status code, but ' 172 | 'got {0} : reason {1}'.format(res.status, await res.text())) 173 | 174 | return OAuthShortCode(self._grant, self._client, await res.json()) 175 | -------------------------------------------------------------------------------- /interactive_python/scene.py: -------------------------------------------------------------------------------- 1 | from ._util import Resource 2 | 3 | 4 | class Scene(Resource): 5 | """ 6 | Scene is a container for controls in interactive. Groups can be assigned 7 | to scenes. It emits: 8 | 9 | - A ``delete`` event when the scene is deleted, with the 10 | :class:`~interactive_python.Call` from "onSceneDelete". 11 | 12 | - An ``update`` event when the scene is updated, with the 13 | :class:`~interactive_python.Call` from "onSceneUpdate". 14 | """ 15 | 16 | def __init__(self, scene_id, **kwargs): 17 | super(Scene, self).__init__(scene_id, id_property='sceneID') 18 | self.assign(**kwargs) 19 | self.controls = {} 20 | self._control_kinds = { 21 | 'button': Button, 22 | 'joystick': Joystick, 23 | } 24 | 25 | async def delete(self, reassign_scene_id='default'): 26 | """ 27 | Deletes the scene from Interactive. Takes the id of the scene 28 | to reassign any groups who are on that scene to. 29 | :param reassign_scene_id: 30 | :type reassign_scene_id: str 31 | :rtype: None 32 | """ 33 | await self._connection.call('deleteScene', { 34 | 'sceneID': self.id, 35 | 'reassignSceneID': reassign_scene_id, 36 | }) 37 | 38 | async def update(self, priority=0): 39 | """ 40 | Saves all changes updates made to the scene. 41 | """ 42 | return await self._connection.call( 43 | 'updateScenes', 44 | {'scenes': [self._capture_changes()], 'priority': priority} 45 | ) 46 | 47 | def attach_controls(self, *controls): 48 | """ 49 | Can be called with one or more Controls to add them to to the scene. 50 | :param controls: list of controls to create 51 | :type controls: List[Control] 52 | """ 53 | for control in controls: 54 | self.controls[control.id] = control 55 | control._attach_scene(self) 56 | 57 | async def create_controls(self, *controls): 58 | """ 59 | Define and create one or more controls, and add them to the scene. 60 | :param controls: list of controls to create 61 | :type controls: List[Control] 62 | """ 63 | self.attach_controls(controls) 64 | return await self._connection.call('createControls', { 65 | 'sceneID': self.id, 66 | 'controls': controls, 67 | }) 68 | 69 | def to_json(self): 70 | props = super().to_json() 71 | props['controls'] = [c.to_json() for c in self.controls] 72 | return props 73 | 74 | def _on_deleted(self, call): 75 | super()._on_deleted(call) 76 | for control in self.controls: 77 | control._on_deleted(call) 78 | 79 | def _on_control_delete(self, call): 80 | for control_id in call.data['controlIDs']: 81 | if control_id in self.controls: 82 | self.controls[control_id]._on_deleted(call) 83 | del self.controls[control_id] 84 | 85 | def _on_control_update_or_create(self, call): 86 | for update in call.data['controls']: 87 | if update['controlID'] not in self.controls: 88 | c = self._control_kinds[update['kind']](update['controlID']) 89 | c._attach_connection(self._connection) 90 | self.controls[update['controlID']] = c 91 | 92 | self.controls[update['controlID']]._apply_changes(update, call) 93 | 94 | 95 | class Control(Resource): 96 | """ 97 | Control is a structure on which participants in interactive provide input. 98 | It emits: 99 | 100 | - A ``delete`` event when the control is deleted, with the 101 | :class:`~interactive_python.Call` from "onControlDelete". 102 | 103 | - An ``update`` event when the control is updated, with the 104 | :class:`~interactive_python.Call` from "onControlUpdate". 105 | 106 | - Other kinds events are fired when input is given on the control, like 107 | ``mousedown`` for buttons or ``move`` for joysticks. They're fired with 108 | the :class:`~interactive_python.Call` that triggered them. 109 | 110 | Here's an example of creating a button and listening for clicks:: 111 | 112 | btn = Button( 113 | control_id='click_me', 114 | text='Click Me!', 115 | keycode=keycode.space, 116 | position=[ 117 | {'size': 'large', 'width': 5, 'height': 5, 'x': 0, 'y': 0}, 118 | ], 119 | ) 120 | 121 | # Logs the call to the console whenever the button is clicked 122 | btn.on('mousedown', lambda call: print(call)) 123 | 124 | await state.scene('default').create_controls(btn) 125 | 126 | When using controls defined in the interactive studio editor, you only need to 127 | define the control_id, and the events then attach them to the scene. 128 | 129 | js = Joystick(control_id='joystick_1') 130 | btn = Button(control_id='click_me') 131 | 132 | js.on('move', lambda call: print(call.data['input'].x, call.data['input'].y)) 133 | btn.on('mousedown', lambda call: print(call)) 134 | 135 | state.scene('default').attach_controls(js, btn) 136 | 137 | """ 138 | 139 | def __init__(self, control_id, **kwargs): 140 | super(Control, self).__init__(control_id, id_property='controlID') 141 | self._scene = None 142 | self.assign(**kwargs) 143 | 144 | def _attach_scene(self, scene): 145 | self._scene = scene 146 | self._attach_connection(scene._connection) 147 | 148 | def _give_input(self, call): 149 | self.emit(call.data['input']['event'], call) 150 | 151 | async def delete(self): 152 | """ 153 | Deletes the control 154 | :return: None 155 | """ 156 | await self._connection.call('deleteControls', { 157 | 'sceneID': self._scene.id, 158 | 'controlIDs': [self.id], 159 | }) 160 | 161 | async def update(self): 162 | """ 163 | Saves all changes updates made to the control. 164 | """ 165 | await self._connection.call('updateControls', { 166 | 'sceneID': self._scene.id, 167 | 'controls': [self._capture_changes()], 168 | }) 169 | 170 | 171 | class Button(Control): 172 | def __init__(self, control_id, **kwargs): 173 | super().__init__(control_id) 174 | kwargs['kind'] = 'button' 175 | self.assign(**kwargs) 176 | 177 | 178 | class Joystick(Control): 179 | def __init__(self, control_id, **kwargs): 180 | super().__init__(control_id) 181 | kwargs['kind'] = 'joystick' 182 | self.assign(**kwargs) 183 | -------------------------------------------------------------------------------- /interactive_python/state.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import asyncio 3 | from pyee import EventEmitter 4 | 5 | from .connection import Connection 6 | from .discovery import Discovery 7 | from .scene import Scene 8 | 9 | 10 | class State(EventEmitter): 11 | """State is the state container for a single interactive session. 12 | It should usually be created via the static 13 | :func:`~interactive_python.State.connect` method:: 14 | 15 | connection = State.connect( 16 | project_version_id=my_version_id, 17 | authorization="Bearer " + oauth_token) 18 | 19 | The Scene is a pyee.EventEmitter. When calls come down, they're always 20 | emitted on the State by their method name. So, for instance, you can 21 | listen to "onSceneCreate" or "onParticipantJoin" on the scene:: 22 | 23 | def greet(call): 24 | for participant in call.data['participants']: 25 | print('Welcome {}!', participant['username']) 26 | 27 | scene.on('onParticipantJoin', greet) 28 | 29 | The state can work in two modes for handling delivery of events and updates. 30 | You can use `pump()` calls synchronously within your game loop to apply 31 | updates that have been queued. Alternately, you can call ``pump_async()`` to 32 | signal to that state that you want updates delivered asynchronously, as soon 33 | as they come in. For example:: 34 | 35 | # Async delivery. `giveInput` is emitted as soon as any input comes in. 36 | state.on('giveInput', lambda call: do_the_thing(call)) 37 | state.pump_async() 38 | 39 | # Sync delivery. `giveInput` is emitted only during calls to pump() 40 | state.on('giveInput', lambda call: do_the_thing(call)) 41 | while True: 42 | my_game_loop.tick() 43 | state.pump() 44 | 45 | # You can also read queues of changes from pump(), if you prefer 46 | # to dispatch changes manually: 47 | # for call in pump(): ... 48 | 49 | In both modes, all incoming call are emitted as events on the State 50 | instance. 51 | 52 | :param connection: The websocket connection to interactive. 53 | :type connection: Connection 54 | """ 55 | 56 | def __init__(self, connection): 57 | super(State, self).__init__() 58 | self._scenes = {'default': Scene('default')} 59 | self.connection = connection 60 | self._enable_event_queue = True 61 | self._event_queue = collections.deque() 62 | self._scenes['default']._attach_connection(self.connection) 63 | 64 | self.on('onSceneCreate', self._on_scene_create_or_update) 65 | self.on('onSceneUpdate', self._on_scene_create_or_update) 66 | self.on('onSceneDelete', self._on_scene_delete) 67 | self.on('onControlCreate', self._on_control_update_or_create) 68 | self.on('onControlUpdate', self._on_control_update_or_create) 69 | self.on('onControlDelete', self._on_control_delete) 70 | self.on('giveInput', self._give_input) 71 | 72 | def scene(self, name): 73 | """ 74 | Looks up an existing scene by ID. It returns None if the scene does 75 | not exist. 76 | 77 | :param name: The name of the scene to look up 78 | :type name: str 79 | :return: 80 | :rtype: Scene 81 | """ 82 | return self._scenes.get(name, None) 83 | 84 | def pump_async(self, loop=asyncio.get_event_loop()): 85 | """ 86 | Starts a pump() process working in the background. Events will be 87 | dispatched asynchronously. 88 | 89 | Returns a future that can be used for cancelling the pump, if desired. 90 | Otherwise the pump will automatically stop once 91 | the connection is closed. 92 | 93 | :rtype: asyncio.Future 94 | """ 95 | self._enable_event_queue = False 96 | 97 | async def run(): 98 | try: 99 | while await self.connection.has_packet(): 100 | self.pump() 101 | except asyncio.CancelledError: 102 | self._enable_event_queue = True 103 | except Exception as e: 104 | self.emit('error', e) 105 | 106 | return asyncio.ensure_future(run(), loop=loop) 107 | 108 | def pump(self): 109 | """ 110 | pump causes the state to read any updates it has queued up. This 111 | should usually be called at the start of any game loop where you're 112 | going to be doing processing of Interactive events. 113 | 114 | Any events that have not been read when pump() is called are discarded. 115 | 116 | Alternately, you can call pump_async() to have delivery handled for you 117 | without manual input. 118 | 119 | :rtype: Iterator of Calls 120 | """ 121 | self._event_queue.clear() 122 | while True: 123 | call = self.connection.get_packet() 124 | if call is None: 125 | return 126 | 127 | self.emit(call.name, call) 128 | 129 | if self._enable_event_queue: 130 | self._event_queue.append(call) 131 | 132 | return self._event_queue 133 | 134 | async def create_scenes(self, *scenes): 135 | """ 136 | Can be called with one or more Scenes to add them to Interactive. 137 | :param scenes: list of scenes to create 138 | :type scenes: Scene 139 | """ 140 | for scene in scenes: 141 | self._scenes[scene.id] = scene 142 | scene._attach_connection(self) 143 | 144 | return await self.connection.call( 145 | 'createScenes', [s._resolve_all() for s in scenes]) 146 | 147 | async def set_ready(self, is_ready=True): 148 | """ 149 | Marks the interactive integration as being ready-to-go. Must be called 150 | before controls will appear. 151 | 152 | :param is_ready: True or False to allow input 153 | :rtype: Reply 154 | """ 155 | return await self.connection.call('ready', {'isReady': is_ready}) 156 | 157 | def _give_input(self, call): 158 | control_id = call.data['input']['controlID'] 159 | for scene in self._scenes.values(): 160 | if control_id in scene.controls: 161 | scene.controls[control_id]._give_input(call) 162 | break 163 | 164 | def _on_scene_delete(self, call): 165 | if call.data['sceneID'] not in self._scenes: 166 | return 167 | 168 | self.scenes[call.data['sceneID']].delete(call) 169 | del self.scenes[call.data['sceneID']] 170 | 171 | def _on_scene_create_or_update(self, call): 172 | for scene in call.data.scenes: 173 | if scene['sceneID'] not in self._scenes: 174 | self._scenes[scene['sceneID']] = Scene(self, scene['sceneID']) 175 | 176 | self._scenes[scene['sceneID']]._apply_changes(scene, call) 177 | 178 | def _on_control_delete(self, call): 179 | if call.data['sceneID'] in self._scenes: 180 | self._scenes[call.data['sceneID']]._on_control_delete(call) 181 | 182 | def _on_control_update_or_create(self, call): 183 | if call.data['sceneID'] in self._scenes: 184 | self._scenes[call.data['sceneID']].\ 185 | _on_control_update_or_create(call) 186 | 187 | @staticmethod 188 | async def connect(discovery=Discovery(), **kwargs): 189 | """ 190 | Creates a new interactive connection. Most arguments will be passed 191 | through into the Connection constructor. 192 | 193 | :param discovery: 194 | :type discovery: Discovery 195 | :param kwargs: 196 | :rtype: State 197 | """ 198 | 199 | if 'address' not in kwargs: 200 | kwargs['address'] = await discovery.find() 201 | 202 | connection = Connection(**kwargs) 203 | await connection.connect() 204 | return State(connection) 205 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # interactive-python [![Build Status](https://travis-ci.org/WatchBeam/interactive-python.svg)](https://travis-ci.org/WatchBeam/interactive-python) 2 | 3 | # NOTE: This repo is no longer being maintained by Microsoft. 4 | You can find more information here: dev.mixer.com/blog/mixplaysdkchanges 5 | 6 | This is a base implementation of a Beam Interactive 2 game client. Documentation for the protocol and tutorials can be found on our [developer site](https://dev.beam.pro) 7 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = readme.md 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from setuptools import setup, find_packages 3 | 4 | if sys.version_info < (3, 5): 5 | raise Exception("interactive_python makes use of asyncio, async, and" 6 | "await, and therefore requires Python >= 3.5.x") 7 | 8 | setup( 9 | name='interactive_python', 10 | version='0.1.0', 11 | description='Reference API implementation for Mixer Interactive 2', 12 | classifiers=[ 13 | 'Development Status :: 2 - Pre-Alpha', 14 | 'Programming Language :: Python :: 3.5', 15 | 'Programming Language :: Python :: 3.6', 16 | 'License :: OSI Approved :: MIT License', 17 | 'Topic :: Games/Entertainment', 18 | 'Topic :: Software Development', 19 | 'Topic :: Software Development :: Libraries' 20 | ], 21 | author='Connor Peet', 22 | author_email='connor@xbox.com', 23 | url='https://github.com/mixer/interactive-python', 24 | license='MIT', 25 | packages=find_packages(exclude=['tests']), 26 | install_requires=['websockets>=3.3', 'varint>=1.0.2', 'pyee>=3.0.3', 27 | 'aiohttp>=2.0.7'], 28 | include_package_data=True, 29 | ) 30 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mixer/interactive-python/348628aec594bf48cf503647e41ec080751fac09/tests/__init__.py -------------------------------------------------------------------------------- /tests/_util.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import asyncio 3 | import os 4 | import functools 5 | import json 6 | from nose.tools import nottest 7 | 8 | file_path = os.path.dirname(os.path.realpath(__file__)) 9 | 10 | 11 | def fixture(path, mode='r'): 12 | with open(os.path.join(file_path, 'fixture', path), mode) as content_file: 13 | contents = content_file.read() 14 | if isinstance(contents, str): 15 | contents = contents.strip() 16 | 17 | return contents 18 | 19 | 20 | @nottest 21 | def async_test(fn): 22 | coroutine = asyncio.coroutine(fn) 23 | 24 | @functools.wraps(fn) 25 | def run(self, *args, **kwargs): 26 | future = coroutine(self, *args, **kwargs) 27 | self._loop.run_until_complete(future) 28 | 29 | return run 30 | 31 | 32 | def resolve(value): 33 | future = asyncio.Future() 34 | future.set_result(value) 35 | return future 36 | 37 | 38 | class AsyncTestCase(unittest.TestCase): 39 | 40 | def setUp(self): 41 | self._loop = asyncio.new_event_loop() 42 | 43 | def tearDown(self): 44 | self._loop.close() 45 | del self._loop 46 | 47 | def assertJsonEqual(self, a, b): 48 | if isinstance(a, str): 49 | a = json.loads(a) 50 | if isinstance(b, str): 51 | b = json.loads(b) 52 | self.assertEqual(a, b) 53 | -------------------------------------------------------------------------------- /tests/connection_test.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import Mock 2 | import asyncio 3 | import websockets 4 | 5 | from interactive_python import Connection, GzipEncoding 6 | from ._util import AsyncTestCase, async_test, resolve, fixture 7 | 8 | 9 | sample_method = '{"id":0,"type":"method","method":"some_method",' \ 10 | '"params":{"foo": 42}}' 11 | 12 | 13 | class TestInteractiveConnection(AsyncTestCase): 14 | 15 | def setUp(self): 16 | super(TestInteractiveConnection, self).setUp() 17 | self._mock_socket = Mock() 18 | self._mock_socket.close = asyncio.Future(loop=self._loop) 19 | self._mock_socket.close.set_result(None) 20 | send_future = asyncio.Future(loop=self._loop) 21 | send_future.set_result(None) 22 | self._mock_socket.send.return_value = send_future 23 | self._queue = asyncio.Queue(loop=self._loop) 24 | self._connection = Connection(socket=self._mock_socket, loop=self._loop) 25 | self._mock_socket.recv = self._queue.get 26 | self._queue.put_nowait('{"type":"method","method":"hello","seq":1}') 27 | 28 | def tearDown(self): 29 | if self._connection._recv_task is not None: 30 | self._connection._recv_task.cancel() 31 | super(TestInteractiveConnection, self).tearDown() 32 | 33 | async def _upgrade_to_gzip(self): 34 | result = await asyncio.gather( 35 | self._connection.set_compression(GzipEncoding()), 36 | self._queue.put( 37 | '{"id":0,"type":"reply","result":{"scheme":"gzip"},"seq":2}'), 38 | loop=self._loop) 39 | 40 | self.assertTrue(result[0]) 41 | self.assertEqual('gzip', self._connection._encoding.name()) 42 | 43 | @async_test 44 | def test_sends_method_calls(self): 45 | yield from self._connection.connect() 46 | results = yield from asyncio.gather( 47 | self._connection.call('square', 2), 48 | self._queue.put('{"id":0,"type":"reply","result":4,"seq":2}'), 49 | loop=self._loop) 50 | 51 | self.assertEqual(4, results[0]) 52 | self.assertJsonEqual( 53 | self._mock_socket.send.call_args[0][0], 54 | {'type': 'method', 'method': 'square', 55 | 'params': 2, 'id': 0, 'seq': 0} 56 | ) 57 | 58 | @async_test 59 | def test_times_out_calls(self): 60 | yield from self._connection.connect() 61 | with self.assertRaises(asyncio.TimeoutError): 62 | yield from self._connection.call('square', 2, timeout=0.1) 63 | 64 | @async_test 65 | def test_upgrades_compression(self): 66 | yield from self._connection.connect() 67 | yield from self._upgrade_to_gzip() 68 | result = yield from asyncio.gather( 69 | self._connection.call('square', 2), 70 | self._queue.put(fixture('gzipped_square_reply', 'rb')), 71 | loop=self._loop) 72 | 73 | self.assertEquals(4, result[0]) 74 | self.assertIsInstance(self._mock_socket.send.call_args[0][0], bytes) 75 | 76 | @async_test 77 | def test_does_not_upgrade_if_the_server_denies(self): 78 | yield from self._connection.connect() 79 | result = yield from asyncio.gather( 80 | self._connection.set_compression(GzipEncoding()), 81 | self._queue.put( 82 | '{"id":0,"type":"reply","result":{"scheme":"text"},"seq":2}'), 83 | loop=self._loop) 84 | 85 | self.assertFalse(result[0]) 86 | self.assertEqual('text', self._connection._encoding.name()) 87 | 88 | @async_test 89 | def test_falls_back_if_given_unknown_bytes(self): 90 | yield from self._connection.connect() 91 | yield from self._upgrade_to_gzip() 92 | self.assertEqual('gzip', self._connection._encoding.name()) 93 | yield from self._queue.put(fixture('gzipped_square_reply', 'rb')[::-1]) 94 | yield from asyncio.sleep(0, loop=self._loop) 95 | yield from self._queue.put('{"id":0,"type":"reply",' 96 | '"result":{"scheme":"text"},"seq":2}') 97 | yield from asyncio.sleep(0, loop=self._loop) 98 | 99 | self.assertEqual('text', self._connection._encoding.name()) 100 | self.assertJsonEqual( 101 | self._mock_socket.send.call_args[0][0], 102 | {'type': 'method', 'method': 'setCompression', 'params': { 103 | 'scheme': ['text']}, 'id': 1, 'seq': 2}) 104 | 105 | @async_test 106 | def test_queues_packets(self): 107 | yield from self._connection.connect() 108 | self._queue.put_nowait(sample_method) 109 | has_packet = yield from self._connection.has_packet() 110 | self.assertTrue(has_packet) 111 | self.assertJsonEqual(self._connection.get_packet().data, '{"foo":42}') 112 | self.assertIsNone(self._connection.get_packet()) 113 | 114 | @async_test 115 | def test_handles_connection_closed(self): 116 | yield from self._connection.connect() 117 | 118 | def raise_closed(): 119 | raise websockets.ConnectionClosed(4000, "") 120 | 121 | yield from asyncio.sleep(0) 122 | self._mock_socket.recv = raise_closed 123 | self._queue.put_nowait(sample_method) 124 | 125 | has_packet = yield from self._connection.has_packet() 126 | self.assertTrue(has_packet) # reads what we pushed to get unblocked 127 | has_packet = yield from self._connection.has_packet() 128 | self.assertFalse(has_packet) # gets a connection closed 129 | -------------------------------------------------------------------------------- /tests/encoding_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from interactive_python import GzipEncoding 3 | from ._util import fixture 4 | 5 | samples = 3 6 | 7 | 8 | class TestGzipEncoding(unittest.TestCase): 9 | def test_round_trip(self): 10 | encoder = GzipEncoding() 11 | for i in range(samples): 12 | sample = fixture('sample{}_decoded'.format(i)) 13 | encoded = encoder.encode(sample) 14 | decoded = encoder.decode(encoded) 15 | self.assertEqual(sample, decoded) 16 | 17 | def test_against_tetrisd(self): 18 | """ tests against 'real' tetrisd output """ 19 | encoder = GzipEncoding() 20 | for i in range(samples): 21 | go_decoded = fixture('sample{}_decoded'.format(i)) 22 | py_decoded = encoder.decode( 23 | fixture('sample{}_encoded'.format(i), 'rb')) 24 | self.assertEqual(py_decoded, go_decoded) 25 | 26 | -------------------------------------------------------------------------------- /tests/fixture/gzipped_square_reply: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mixer/interactive-python/348628aec594bf48cf503647e41ec080751fac09/tests/fixture/gzipped_square_reply -------------------------------------------------------------------------------- /tests/fixture/sample0_decoded: -------------------------------------------------------------------------------- 1 | {"type":"method","method":"auth","arguments":[1,1,"fc3f865c156f32cac0755cde007654a8"],"id":0} 2 | -------------------------------------------------------------------------------- /tests/fixture/sample0_encoded: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mixer/interactive-python/348628aec594bf48cf503647e41ec080751fac09/tests/fixture/sample0_encoded -------------------------------------------------------------------------------- /tests/fixture/sample1_decoded: -------------------------------------------------------------------------------- 1 | {"type":"reply","error":null,"id":0,"data":{"authenticated":true,"roles":["Owner"]}} 2 | -------------------------------------------------------------------------------- /tests/fixture/sample1_encoded: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mixer/interactive-python/348628aec594bf48cf503647e41ec080751fac09/tests/fixture/sample1_encoded -------------------------------------------------------------------------------- /tests/fixture/sample2_decoded: -------------------------------------------------------------------------------- 1 | {"type":"method","method":"msg","arguments":["Hello world :)"],"id":2} 2 | -------------------------------------------------------------------------------- /tests/fixture/sample2_encoded: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mixer/interactive-python/348628aec594bf48cf503647e41ec080751fac09/tests/fixture/sample2_encoded -------------------------------------------------------------------------------- /tests/keycode_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from interactive_python import keycode 3 | 4 | class TestKeycode(unittest.TestCase): 5 | def test_gives_aliased_codes(self): 6 | self.assertEqual(91, keycode.cmd) 7 | 8 | def test_gives_non_aliawses(self): 9 | self.assertEqual(65, keycode.a) 10 | 11 | def test_raises_on_invalid_codes(self): 12 | with self.assertRaises(AttributeError): 13 | keycode.A 14 | 15 | with self.assertRaises(AttributeError): 16 | keycode.wut 17 | -------------------------------------------------------------------------------- /tests/resource_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from interactive_python._util import Resource 3 | 4 | 5 | def get_fixture(): 6 | resource = Resource( 7 | id='red_team', 8 | id_property='groupID' 9 | ) 10 | 11 | resource.assign( 12 | groupID='red_team', 13 | color='red', 14 | disabled=False, 15 | meta={'spooky': True}, 16 | ) 17 | 18 | resource._mark_synced() 19 | 20 | return resource 21 | 22 | 23 | class TestResources(unittest.TestCase): 24 | 25 | def test_accesses_data(self): 26 | resource = get_fixture() 27 | self.assertEqual('red_team', resource.id) 28 | self.assertEqual('red', resource.color) 29 | self.assertEqual(True, resource.meta.spooky) 30 | with self.assertRaises(AttributeError): 31 | resource.meta.wut 32 | with self.assertRaises(AttributeError): 33 | resource.wut 34 | 35 | def test_diffs_resource(self): 36 | resource = get_fixture() 37 | resource.disabled = True # changing a property 38 | resource.groupID = 'red_team' # setting without notification 39 | self.assertTrue(resource.has_changed()) 40 | self.assertEqual( 41 | {'groupID': 'red_team', 'disabled': True}, 42 | resource._capture_changes() 43 | ) 44 | self.assertFalse(resource.has_changed()) 45 | 46 | def test_includes_changes_to_metadata(self): 47 | resource = get_fixture() 48 | resource.meta.spooky = False 49 | 50 | self.assertTrue(resource.has_changed()) 51 | self.assertEqual( 52 | { 53 | 'groupID': 'red_team', 54 | 'meta': {'spooky': False} 55 | }, 56 | resource._capture_changes() 57 | ) 58 | self.assertFalse(resource.has_changed()) 59 | --------------------------------------------------------------------------------