commit aa2e146a60aa9b2257ce748690104c796a09b551 Author: Miguel Grinberg Date: Wed Jul 15 00:04:53 2015 -0700 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d619cd7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,44 @@ +*.py[cod] + +# C extensions +*.so + +# Packages +*.egg +*.egg-info +dist +build +eggs +parts +bin +var +sdist +develop-eggs +.installed.cfg +lib +lib64 +__pycache__ + +# Installer logs +pip-log.txt + +# Unit test / coverage reports +.coverage +.tox +nosetests.xml + +# Translations +*.mo + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +docs/_build +venv* +.eggs +.ropeproject +.idea +htmlcov +*.swp diff --git a/LICENSE b/LICENSE new file mode 100755 index 0000000..c22109a --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2015 Miguel Grinberg + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.rst b/README.rst new file mode 100755 index 0000000..6ee835d --- /dev/null +++ b/README.rst @@ -0,0 +1,77 @@ +python-socketio +=============== + +.. image:: https://travis-ci.org/miguelgrinberg/python-socketio.svg?branch=master + :target: https://travis-ci.org/miguelgrinberg/python-socketio + +Python implementation of the `Socket.IO`_ realtime server. + +Features +-------- + +- Fully compatible with the Javascript `socket.io-client`_ library. +- Compatible with Python 2.7 and Python 3.3+. +- Based on `Eventlet`_, enabling large number of clients even on modest + hardware. +- Includes a WSGI middleware that integrates Socket.IO traffic with + standard WSGI applications. +- Uses an event-based architecture implemented with decorators that + hides the details of the protocol. +- Implements HTTP long-polling and WebSocket transports. +- Supports XHR2 and XHR browsers as clients. +- Supports text and binary messages. +- Supports gzip and deflate HTTP compression. +- Configurable CORS responses to avoid cross-origin problems with + browsers. + +Example +------- + +The following application uses Flask to serve the HTML/Javascript to the +client: + +:: + + import socketio + import eventlet + from flask import Flask, render_template + + sio = socketio.Server() + app = Flask(__name__) + + @app.route('/') + def index(): + """Serve the client-side application.""" + return render_template('index.html') + + @sio.on('connect', namespace='/chat') + def connect(sid, environ): + print("connect ", sid) + + @sio.on('chat message', namespace='/chat') + def message(sid, data): + print("message ", data) + sio.emit(sid, 'reply') + + @sio.on('disconnect', namespace='/chat') + def disconnect(sid): + print('disconnect ', sid) + + if __name__ == '__main__': + # wrap Flask application with engineio's middleware + app = socketio.Middleware(eio, app) + + # deploy as an eventlet WSGI server + eventlet.wsgi.server(eventlet.listen(('', 8000)), app) + +Resources +--------- + +- `Documentation`_ +- `PyPI`_ + +.. _Socket.IO: https://github.com/Automattic/socket.io +.. _socket.io-client: https://github.com/Automattic/socket.io-client +.. _Eventlet: http://eventlet.net/ +.. _Documentation: http://pythonhosted.org/python-socketio +.. _PyPI: https://pypi.python.org/pypi/python-socketio \ No newline at end of file diff --git a/docs/Makefile b/docs/Makefile new file mode 100755 index 0000000..68ceda8 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,192 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# User-friendly check for sphinx-build +ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) +$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) +endif + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " applehelp to make an Apple Help Book" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + @echo " coverage to run coverage check of the documentation (if enabled)" + +clean: + rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/socketio.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/socketio.qhc" + +applehelp: + $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp + @echo + @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." + @echo "N.B. You won't be able to view it unless you put it in" \ + "~/Library/Documentation/Help or install it in your application" \ + "bundle." + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/socketio" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/socketio" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +coverage: + $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage + @echo "Testing of coverage in the sources finished, look at the " \ + "results in $(BUILDDIR)/coverage/python.txt." + +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/docs/_static/index.html b/docs/_static/index.html new file mode 100755 index 0000000..d2f10fe --- /dev/null +++ b/docs/_static/index.html @@ -0,0 +1,12 @@ + + + + python-socketio documentation + + + + + The python-socketio documentation is available at Read the Docs. + If your browser does not automatically redirect you, please click here. + + diff --git a/docs/_static/logo.png b/docs/_static/logo.png new file mode 100755 index 0000000..92a9ef8 Binary files /dev/null and b/docs/_static/logo.png differ diff --git a/docs/_themes/LICENSE b/docs/_themes/LICENSE new file mode 100755 index 0000000..8daab7e --- /dev/null +++ b/docs/_themes/LICENSE @@ -0,0 +1,37 @@ +Copyright (c) 2010 by Armin Ronacher. + +Some rights reserved. + +Redistribution and use in source and binary forms of the theme, with or +without modification, are permitted provided that the following conditions +are met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + +* The names of the contributors may not be used to endorse or + promote products derived from this software without specific + prior written permission. + +We kindly ask you to only use these themes in an unmodified manner just +for Flask and Flask-related products, not for unrelated projects. If you +like the visual style and want to use it for your own projects, please +consider making some larger changes to the themes (such as changing +font faces, sizes, colors or margins). + +THIS THEME IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS THEME, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/_themes/README b/docs/_themes/README new file mode 100755 index 0000000..b3292bd --- /dev/null +++ b/docs/_themes/README @@ -0,0 +1,31 @@ +Flask Sphinx Styles +=================== + +This repository contains sphinx styles for Flask and Flask related +projects. To use this style in your Sphinx documentation, follow +this guide: + +1. put this folder as _themes into your docs folder. Alternatively + you can also use git submodules to check out the contents there. +2. add this to your conf.py: + + sys.path.append(os.path.abspath('_themes')) + html_theme_path = ['_themes'] + html_theme = 'flask' + +The following themes exist: + +- 'flask' - the standard flask documentation theme for large + projects +- 'flask_small' - small one-page theme. Intended to be used by + very small addon libraries for flask. + +The following options exist for the flask_small theme: + + [options] + index_logo = '' filename of a picture in _static + to be used as replacement for the + h1 in the index.rst file. + index_logo_height = 120px height of the index logo + github_fork = '' repository name on github for the + "fork me" badge diff --git a/docs/_themes/flask/layout.html b/docs/_themes/flask/layout.html new file mode 100755 index 0000000..19c43fb --- /dev/null +++ b/docs/_themes/flask/layout.html @@ -0,0 +1,24 @@ +{%- extends "basic/layout.html" %} +{%- block extrahead %} + {{ super() }} + {% if theme_touch_icon %} + + {% endif %} + +{% endblock %} +{%- block relbar2 %}{% endblock %} +{% block header %} + {{ super() }} + {% if pagename == 'index' %} +
+ {% endif %} +{% endblock %} +{%- block footer %} + + {% if pagename == 'index' %} +
+ {% endif %} +{%- endblock %} diff --git a/docs/_themes/flask/relations.html b/docs/_themes/flask/relations.html new file mode 100755 index 0000000..3bbcde8 --- /dev/null +++ b/docs/_themes/flask/relations.html @@ -0,0 +1,19 @@ +

Related Topics

+ diff --git a/docs/_themes/flask/static/flasky.css_t b/docs/_themes/flask/static/flasky.css_t new file mode 100755 index 0000000..5906e75 --- /dev/null +++ b/docs/_themes/flask/static/flasky.css_t @@ -0,0 +1,577 @@ +/* + * flasky.css_t + * ~~~~~~~~~~~~ + * + * :copyright: Copyright 2010 by Armin Ronacher. + * :license: Flask Design License, see LICENSE for details. + */ + +{% set page_width = '940px' %} +{% set sidebar_width = '220px' %} + +@import url("basic.css"); + +/* -- page layout ----------------------------------------------------------- */ + +body { + font-family: 'Georgia', serif; + font-size: 17px; + background-color: white; + color: #000; + margin: 0; + padding: 0; +} + +div.document { + width: {{ page_width }}; + margin: 30px auto 0 auto; +} + +div.documentwrapper { + float: left; + width: 100%; +} + +div.bodywrapper { + margin: 0 0 0 {{ sidebar_width }}; +} + +div.sphinxsidebar { + width: {{ sidebar_width }}; +} + +hr { + border: 1px solid #B1B4B6; +} + +div.body { + background-color: #ffffff; + color: #3E4349; + padding: 0 30px 0 30px; +} + +img.floatingflask { + padding: 0 0 10px 10px; + float: right; +} + +div.footer { + width: {{ page_width }}; + margin: 20px auto 30px auto; + font-size: 14px; + color: #888; + text-align: right; +} + +div.footer a { + color: #888; +} + +div.related { + display: none; +} + +div.sphinxsidebar a { + color: #444; + text-decoration: none; + border-bottom: 1px dotted #999; +} + +div.sphinxsidebar a:hover { + border-bottom: 1px solid #999; +} + +div.sphinxsidebar { + font-size: 14px; + line-height: 1.5; +} + +div.sphinxsidebarwrapper { + padding: 18px 10px; +} + +div.sphinxsidebarwrapper p.logo { + padding: 0 0 20px 0; + margin: 0; + text-align: center; +} + +div.sphinxsidebar h3, +div.sphinxsidebar h4 { + font-family: 'Garamond', 'Georgia', serif; + color: #444; + font-size: 24px; + font-weight: normal; + margin: 0 0 5px 0; + padding: 0; +} + +div.sphinxsidebar h4 { + font-size: 20px; +} + +div.sphinxsidebar h3 a { + color: #444; +} + +div.sphinxsidebar p.logo a, +div.sphinxsidebar h3 a, +div.sphinxsidebar p.logo a:hover, +div.sphinxsidebar h3 a:hover { + border: none; +} + +div.sphinxsidebar p { + color: #555; + margin: 10px 0; +} + +div.sphinxsidebar ul { + margin: 10px 0; + padding: 0; + color: #000; +} + +div.sphinxsidebar input { + border: 1px solid #ccc; + font-family: 'Georgia', serif; + font-size: 1em; +} + +/* -- body styles ----------------------------------------------------------- */ + +a { + color: #004B6B; + text-decoration: underline; +} + +a:hover { + color: #6D4100; + text-decoration: underline; +} + +div.body h1, +div.body h2, +div.body h3, +div.body h4, +div.body h5, +div.body h6 { + font-family: 'Garamond', 'Georgia', serif; + font-weight: normal; + margin: 30px 0px 10px 0px; + padding: 0; +} + +{% if theme_index_logo %} +div.indexwrapper h1 { + text-indent: -999999px; + background: url({{ theme_index_logo }}) no-repeat center center; + height: {{ theme_index_logo_height }}; +} +{% endif %} +div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; } +div.body h2 { font-size: 180%; } +div.body h3 { font-size: 150%; } +div.body h4 { font-size: 130%; } +div.body h5 { font-size: 100%; } +div.body h6 { font-size: 100%; } + +a.headerlink { + color: #ddd; + padding: 0 4px; + text-decoration: none; +} + +a.headerlink:hover { + color: #444; + background: #eaeaea; +} + +div.body p, div.body dd, div.body li { + line-height: 1.4em; +} + +div.admonition { + background: #fafafa; + margin: 20px -30px; + padding: 10px 30px; + border-top: 1px solid #ccc; + border-bottom: 1px solid #ccc; +} + +div.admonition tt.xref, div.admonition a tt { + border-bottom: 1px solid #fafafa; +} + +dd div.admonition { + margin-left: -60px; + padding-left: 60px; +} + +div.admonition p.admonition-title { + font-family: 'Garamond', 'Georgia', serif; + font-weight: normal; + font-size: 24px; + margin: 0 0 10px 0; + padding: 0; + line-height: 1; +} + +div.admonition p.last { + margin-bottom: 0; +} + +div.highlight { + background-color: white; +} + +dt:target, .highlight { + background: #FAF3E8; +} + +div.note { + background-color: #eee; + border: 1px solid #ccc; +} + +div.seealso { + background-color: #ffc; + border: 1px solid #ff6; +} + +div.topic { + background-color: #eee; +} + +p.admonition-title { + display: inline; +} + +p.admonition-title:after { + content: ":"; +} + +pre, tt { + font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; + font-size: 0.9em; +} + +img.screenshot { +} + +tt.descname, tt.descclassname { + font-size: 0.95em; +} + +tt.descname { + padding-right: 0.08em; +} + +img.screenshot { + -moz-box-shadow: 2px 2px 4px #eee; + -webkit-box-shadow: 2px 2px 4px #eee; + box-shadow: 2px 2px 4px #eee; +} + +table.docutils { + border: 1px solid #888; + -moz-box-shadow: 2px 2px 4px #eee; + -webkit-box-shadow: 2px 2px 4px #eee; + box-shadow: 2px 2px 4px #eee; +} + +table.docutils td, table.docutils th { + border: 1px solid #888; + padding: 0.25em 0.7em; +} + +table.field-list, table.footnote { + border: none; + -moz-box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; +} + +table.footnote { + margin: 15px 0; + width: 100%; + border: 1px solid #eee; + background: #fdfdfd; + font-size: 0.9em; +} + +table.footnote + table.footnote { + margin-top: -15px; + border-top: none; +} + +table.field-list th { + padding: 0 0.8em 0 0; +} + +table.field-list td { + padding: 0; +} + +table.footnote td.label { + width: 0px; + padding: 0.3em 0 0.3em 0.5em; +} + +table.footnote td { + padding: 0.3em 0.5em; +} + +dl { + margin: 0; + padding: 0; +} + +dl dd { + margin-left: 30px; +} + +blockquote { + margin: 0 0 0 30px; + padding: 0; +} + +ul, ol { + margin: 10px 0 10px 30px; + padding: 0; +} + +pre { + background: #eee; + padding: 7px 30px; + margin: 15px -30px; + line-height: 1.3em; +} + +dl pre, blockquote pre, li pre { + margin-left: -60px; + padding-left: 60px; +} + +dl dl pre { + margin-left: -90px; + padding-left: 90px; +} + +tt { + background-color: #ecf0f3; + color: #222; + /* padding: 1px 2px; */ +} + +tt.xref, a tt { + background-color: #FBFBFB; + border-bottom: 1px solid white; +} + +a.reference { + text-decoration: none; + border-bottom: 1px dotted #004B6B; +} + +a.reference:hover { + border-bottom: 1px solid #6D4100; +} + +a.footnote-reference { + text-decoration: none; + font-size: 0.7em; + vertical-align: top; + border-bottom: 1px dotted #004B6B; +} + +a.footnote-reference:hover { + border-bottom: 1px solid #6D4100; +} + +a:hover tt { + background: #EEE; +} + + +@media screen and (max-width: 870px) { + + div.sphinxsidebar { + display: none; + } + + div.document { + width: 100%; + + } + + div.documentwrapper { + margin-left: 0; + margin-top: 0; + margin-right: 0; + margin-bottom: 0; + } + + div.bodywrapper { + margin-top: 0; + margin-right: 0; + margin-bottom: 0; + margin-left: 0; + } + + ul { + margin-left: 0; + } + + .document { + width: auto; + } + + .footer { + width: auto; + } + + .bodywrapper { + margin: 0; + } + + .footer { + width: auto; + } + + .github { + display: none; + } + + + +} + + + +@media screen and (max-width: 875px) { + + body { + margin: 0; + padding: 20px 30px; + } + + div.documentwrapper { + float: none; + background: white; + } + + div.sphinxsidebar { + display: block; + float: none; + width: 102.5%; + margin: 50px -30px -20px -30px; + padding: 10px 20px; + background: #333; + color: white; + } + + div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p, + div.sphinxsidebar h3 a { + color: white; + } + + div.sphinxsidebar a { + color: #aaa; + } + + div.sphinxsidebar p.logo { + display: none; + } + + div.document { + width: 100%; + margin: 0; + } + + div.related { + display: block; + margin: 0; + padding: 10px 0 20px 0; + } + + div.related ul, + div.related ul li { + margin: 0; + padding: 0; + } + + div.footer { + display: none; + } + + div.bodywrapper { + margin: 0; + } + + div.body { + min-height: 0; + padding: 0; + } + + .rtd_doc_footer { + display: none; + } + + .document { + width: auto; + } + + .footer { + width: auto; + } + + .footer { + width: auto; + } + + .github { + display: none; + } +} + + +/* scrollbars */ + +::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +::-webkit-scrollbar-button:start:decrement, +::-webkit-scrollbar-button:end:increment { + display: block; + height: 10px; +} + +::-webkit-scrollbar-button:vertical:increment { + background-color: #fff; +} + +::-webkit-scrollbar-track-piece { + background-color: #eee; + -webkit-border-radius: 3px; +} + +::-webkit-scrollbar-thumb:vertical { + height: 50px; + background-color: #ccc; + -webkit-border-radius: 3px; +} + +::-webkit-scrollbar-thumb:horizontal { + width: 50px; + background-color: #ccc; + -webkit-border-radius: 3px; +} + +/* misc. */ + +.revsys-inline { + display: none!important; +} \ No newline at end of file diff --git a/docs/_themes/flask/theme.conf b/docs/_themes/flask/theme.conf new file mode 100755 index 0000000..18c720f --- /dev/null +++ b/docs/_themes/flask/theme.conf @@ -0,0 +1,9 @@ +[theme] +inherit = basic +stylesheet = flasky.css +pygments_style = flask_theme_support.FlaskyStyle + +[options] +index_logo = '' +index_logo_height = 120px +touch_icon = diff --git a/docs/_themes/flask_small/layout.html b/docs/_themes/flask_small/layout.html new file mode 100755 index 0000000..aa1716a --- /dev/null +++ b/docs/_themes/flask_small/layout.html @@ -0,0 +1,22 @@ +{% extends "basic/layout.html" %} +{% block header %} + {{ super() }} + {% if pagename == 'index' %} +
+ {% endif %} +{% endblock %} +{% block footer %} + {% if pagename == 'index' %} +
+ {% endif %} +{% endblock %} +{# do not display relbars #} +{% block relbar1 %}{% endblock %} +{% block relbar2 %} + {% if theme_github_fork %} + Fork me on GitHub + {% endif %} +{% endblock %} +{% block sidebar1 %}{% endblock %} +{% block sidebar2 %}{% endblock %} diff --git a/docs/_themes/flask_small/static/flasky.css_t b/docs/_themes/flask_small/static/flasky.css_t new file mode 100755 index 0000000..802ebaf --- /dev/null +++ b/docs/_themes/flask_small/static/flasky.css_t @@ -0,0 +1,291 @@ +/* + * flasky.css_t + * ~~~~~~~~~~~~ + * + * Sphinx stylesheet -- flasky theme based on nature theme. + * + * :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +@import url("basic.css"); + +/* -- page layout ----------------------------------------------------------- */ + +body { + font-family: 'Georgia', serif; + font-size: 17px; + color: #000; + background: white; + margin: 0; + padding: 0; +} + +div.documentwrapper { + float: left; + width: 100%; +} + +div.bodywrapper { + margin: 40px auto 0 auto; + width: 700px; +} + +hr { + border: 1px solid #B1B4B6; +} + +div.body { + background-color: #ffffff; + color: #3E4349; + padding: 0 30px 30px 30px; +} + +img.floatingflask { + padding: 0 0 10px 10px; + float: right; +} + +div.footer { + text-align: right; + color: #888; + padding: 10px; + font-size: 14px; + width: 650px; + margin: 0 auto 40px auto; +} + +div.footer a { + color: #888; + text-decoration: underline; +} + +div.related { + line-height: 32px; + color: #888; +} + +div.related ul { + padding: 0 0 0 10px; +} + +div.related a { + color: #444; +} + +/* -- body styles ----------------------------------------------------------- */ + +a { + color: #004B6B; + text-decoration: underline; +} + +a:hover { + color: #6D4100; + text-decoration: underline; +} + +div.body { + padding-bottom: 40px; /* saved for footer */ +} + +div.body h1, +div.body h2, +div.body h3, +div.body h4, +div.body h5, +div.body h6 { + font-family: 'Garamond', 'Georgia', serif; + font-weight: normal; + margin: 30px 0px 10px 0px; + padding: 0; +} + +{% if theme_index_logo %} +div.indexwrapper h1 { + text-indent: -999999px; + background: url({{ theme_index_logo }}) no-repeat center center; + height: {{ theme_index_logo_height }}; +} +{% endif %} + +div.body h2 { font-size: 180%; } +div.body h3 { font-size: 150%; } +div.body h4 { font-size: 130%; } +div.body h5 { font-size: 100%; } +div.body h6 { font-size: 100%; } + +a.headerlink { + color: white; + padding: 0 4px; + text-decoration: none; +} + +a.headerlink:hover { + color: #444; + background: #eaeaea; +} + +div.body p, div.body dd, div.body li { + line-height: 1.4em; +} + +div.admonition { + background: #fafafa; + margin: 20px -30px; + padding: 10px 30px; + border-top: 1px solid #ccc; + border-bottom: 1px solid #ccc; +} + +div.admonition p.admonition-title { + font-family: 'Garamond', 'Georgia', serif; + font-weight: normal; + font-size: 24px; + margin: 0 0 10px 0; + padding: 0; + line-height: 1; +} + +div.admonition p.last { + margin-bottom: 0; +} + +div.highlight{ + background-color: white; +} + +dt:target, .highlight { + background: #FAF3E8; +} + +div.note { + background-color: #eee; + border: 1px solid #ccc; +} + +div.seealso { + background-color: #ffc; + border: 1px solid #ff6; +} + +div.topic { + background-color: #eee; +} + +div.warning { + background-color: #ffe4e4; + border: 1px solid #f66; +} + +p.admonition-title { + display: inline; +} + +p.admonition-title:after { + content: ":"; +} + +pre, tt { + font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; + font-size: 0.85em; +} + +img.screenshot { +} + +tt.descname, tt.descclassname { + font-size: 0.95em; +} + +tt.descname { + padding-right: 0.08em; +} + +img.screenshot { + -moz-box-shadow: 2px 2px 4px #eee; + -webkit-box-shadow: 2px 2px 4px #eee; + box-shadow: 2px 2px 4px #eee; +} + +table.docutils { + border: 1px solid #888; + -moz-box-shadow: 2px 2px 4px #eee; + -webkit-box-shadow: 2px 2px 4px #eee; + box-shadow: 2px 2px 4px #eee; +} + +table.docutils td, table.docutils th { + border: 1px solid #888; + padding: 0.25em 0.7em; +} + +table.field-list, table.footnote { + border: none; + -moz-box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; +} + +table.footnote { + margin: 15px 0; + width: 100%; + border: 1px solid #eee; +} + +table.field-list th { + padding: 0 0.8em 0 0; +} + +table.field-list td { + padding: 0; +} + +table.footnote td { + padding: 0.5em; +} + +dl { + margin: 0; + padding: 0; +} + +dl dd { + margin-left: 30px; +} + +pre { + padding: 0; + margin: 15px -30px; + padding: 8px; + line-height: 1.3em; + padding: 7px 30px; + background: #eee; + border-radius: 2px; + -moz-border-radius: 2px; + -webkit-border-radius: 2px; +} + +dl pre { + margin-left: -60px; + padding-left: 60px; +} + +dl.class { + margin-bottom: 50px; +} + +tt { + background-color: #ecf0f3; + color: #222; + /* padding: 1px 2px; */ +} + +tt.xref, a tt { + background-color: #FBFBFB; +} + +a:hover tt { + background: #EEE; +} diff --git a/docs/_themes/flask_small/theme.conf b/docs/_themes/flask_small/theme.conf new file mode 100755 index 0000000..542b462 --- /dev/null +++ b/docs/_themes/flask_small/theme.conf @@ -0,0 +1,10 @@ +[theme] +inherit = basic +stylesheet = flasky.css +nosidebar = true +pygments_style = flask_theme_support.FlaskyStyle + +[options] +index_logo = '' +index_logo_height = 120px +github_fork = '' diff --git a/docs/_themes/flask_theme_support.py b/docs/_themes/flask_theme_support.py new file mode 100755 index 0000000..33f4744 --- /dev/null +++ b/docs/_themes/flask_theme_support.py @@ -0,0 +1,86 @@ +# flasky extensions. flasky pygments style based on tango style +from pygments.style import Style +from pygments.token import Keyword, Name, Comment, String, Error, \ + Number, Operator, Generic, Whitespace, Punctuation, Other, Literal + + +class FlaskyStyle(Style): + background_color = "#f8f8f8" + default_style = "" + + styles = { + # No corresponding class for the following: + #Text: "", # class: '' + Whitespace: "underline #f8f8f8", # class: 'w' + Error: "#a40000 border:#ef2929", # class: 'err' + Other: "#000000", # class 'x' + + Comment: "italic #8f5902", # class: 'c' + Comment.Preproc: "noitalic", # class: 'cp' + + Keyword: "bold #004461", # class: 'k' + Keyword.Constant: "bold #004461", # class: 'kc' + Keyword.Declaration: "bold #004461", # class: 'kd' + Keyword.Namespace: "bold #004461", # class: 'kn' + Keyword.Pseudo: "bold #004461", # class: 'kp' + Keyword.Reserved: "bold #004461", # class: 'kr' + Keyword.Type: "bold #004461", # class: 'kt' + + Operator: "#582800", # class: 'o' + Operator.Word: "bold #004461", # class: 'ow' - like keywords + + Punctuation: "bold #000000", # class: 'p' + + # because special names such as Name.Class, Name.Function, etc. + # are not recognized as such later in the parsing, we choose them + # to look the same as ordinary variables. + Name: "#000000", # class: 'n' + Name.Attribute: "#c4a000", # class: 'na' - to be revised + Name.Builtin: "#004461", # class: 'nb' + Name.Builtin.Pseudo: "#3465a4", # class: 'bp' + Name.Class: "#000000", # class: 'nc' - to be revised + Name.Constant: "#000000", # class: 'no' - to be revised + Name.Decorator: "#888", # class: 'nd' - to be revised + Name.Entity: "#ce5c00", # class: 'ni' + Name.Exception: "bold #cc0000", # class: 'ne' + Name.Function: "#000000", # class: 'nf' + Name.Property: "#000000", # class: 'py' + Name.Label: "#f57900", # class: 'nl' + Name.Namespace: "#000000", # class: 'nn' - to be revised + Name.Other: "#000000", # class: 'nx' + Name.Tag: "bold #004461", # class: 'nt' - like a keyword + Name.Variable: "#000000", # class: 'nv' - to be revised + Name.Variable.Class: "#000000", # class: 'vc' - to be revised + Name.Variable.Global: "#000000", # class: 'vg' - to be revised + Name.Variable.Instance: "#000000", # class: 'vi' - to be revised + + Number: "#990000", # class: 'm' + + Literal: "#000000", # class: 'l' + Literal.Date: "#000000", # class: 'ld' + + String: "#4e9a06", # class: 's' + String.Backtick: "#4e9a06", # class: 'sb' + String.Char: "#4e9a06", # class: 'sc' + String.Doc: "italic #8f5902", # class: 'sd' - like a comment + String.Double: "#4e9a06", # class: 's2' + String.Escape: "#4e9a06", # class: 'se' + String.Heredoc: "#4e9a06", # class: 'sh' + String.Interpol: "#4e9a06", # class: 'si' + String.Other: "#4e9a06", # class: 'sx' + String.Regex: "#4e9a06", # class: 'sr' + String.Single: "#4e9a06", # class: 's1' + String.Symbol: "#4e9a06", # class: 'ss' + + Generic: "#000000", # class: 'g' + Generic.Deleted: "#a40000", # class: 'gd' + Generic.Emph: "italic #000000", # class: 'ge' + Generic.Error: "#ef2929", # class: 'gr' + Generic.Heading: "bold #000080", # class: 'gh' + Generic.Inserted: "#00A000", # class: 'gi' + Generic.Output: "#888", # class: 'go' + Generic.Prompt: "#745334", # class: 'gp' + Generic.Strong: "bold #000000", # class: 'gs' + Generic.Subheading: "bold #800080", # class: 'gu' + Generic.Traceback: "bold #a40000", # class: 'gt' + } diff --git a/docs/conf.py b/docs/conf.py new file mode 100755 index 0000000..a1f6a13 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,290 @@ +# -*- coding: utf-8 -*- +# +# socketio documentation build configuration file, created by +# sphinx-quickstart on Sat Jun 13 23:41:23 2015. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys +import os +import shlex + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +sys.path.insert(0, os.path.abspath('..')) +sys.path.append(os.path.abspath('_themes')) + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'socketio' +copyright = u'2015, Miguel Grinberg' +author = u'Miguel Grinberg' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '0.1' +# The full version, including alpha/beta/rc tags. +release = '0.1' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['_build'] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +#keep_warnings = False + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'flask_small' #'alabaster' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +html_theme_options = { + 'index_logo': 'logo.png', + 'github_fork': 'miguelgrinberg/python-socketio' +} + +# Add any paths that contain custom themes here, relative to this directory. +html_theme_path = ['_themes'] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +#html_extra_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Language to be used for generating the HTML full-text search index. +# Sphinx supports the following languages: +# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' +# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' +#html_search_language = 'en' + +# A dictionary with options for the search language support, empty by default. +# Now only 'ja' uses this config value +#html_search_options = {'type': 'default'} + +# The name of a javascript file (relative to the configuration directory) that +# implements a search results scorer. If empty, the default will be used. +#html_search_scorer = 'scorer.js' + +# Output file base name for HTML help builder. +htmlhelp_basename = 'socketiodoc' + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { +# The paper size ('letterpaper' or 'a4paper'). +#'papersize': 'letterpaper', + +# The font size ('10pt', '11pt' or '12pt'). +#'pointsize': '10pt', + +# Additional stuff for the LaTeX preamble. +#'preamble': '', + +# Latex figure (float) alignment +#'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'socketio.tex', u'socketio Documentation', + u'Miguel Grinberg', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'socketio', u'socketio Documentation', + [author], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'socketio', u'socketio Documentation', + author, 'socketio', 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +#texinfo_no_detailmenu = False diff --git a/docs/index.rst b/docs/index.rst new file mode 100755 index 0000000..30120d0 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,208 @@ +.. socketio documentation master file, created by + sphinx-quickstart on Sat Jun 13 23:41:23 2015. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +socketio documentation +====================== + +:ref:`genindex` | :ref:`modindex` | :ref:`search` + +This project implements an Socket.IO server that can run standalone or +integrated with a Python WSGI application. The following are some of its +features: + +- Fully compatible with the Javascript + `socket.io-client `_ library. +- Compatible with Python 2.7 and Python 3.3+. +- Based on `Eventlet `_, enabling large number of + clients even on modest hardware. +- Includes a WSGI middleware that integrates Socket.IO traffic with standard + WSGI applications. +- Broadcasting of messages to all or a subset of the connected clients. +- Event-based architecture implemented with decorators that hides the + details of the protocol. +- Support for HTTP long-polling and WebSocket transports. +- Support for XHR2 and XHR browsers. +- Support for text and binary messages. +- Support for gzip and deflate HTTP compression. +- Configurable CORS responses to avoid cross-origin problems with browsers. + +What is Socket.IO? +------------------ + +Socket.IO is a transport protocol that enables real-time bidirectional +event-based communication between clients (typically web browsers) and a +server. The official implementations of the client and server components are +written in JavaScript. + +Getting Started +--------------- + +The following is a basic example of a Socket.IO server that uses Flask to +deploy the client code to the browser:: + + import socketio + import eventlet + from flask import Flask, render_template + + sio = socketio.Server() + app = Flask(__name__) + + @app.route('/') + def index(): + """Serve the client-side application.""" + return render_template('index.html') + + @sio.on('connect') + def connect(sid, environ): + print('connect ', sid) + + @sio.on('my message') + def message(sid, data): + print('message ', data) + + @sio.on('disconnect') + def disconnect(sid): + print('disconnect ', sid) + + if __name__ == '__main__': + # wrap Flask application with socketio's middleware + app = socketio.Middleware(sio, app) + + # deploy as an eventlet WSGI server + eventlet.wsgi.server(eventlet.listen(('', 8000)), app) + +The client-side application must include the +`socket.io-client `_ library +(versions 1.3.5 or newer recommended). + +Each time a client connects to the server the ``connect`` event handler is +invoked with the ``sid`` (session ID) assigned to the connection and the WSGI +environment dictionary. The server can inspect authentication or other headers +to decide if the client is allowed to connect. To reject a client the handler +must return ``False``. + +When the client sends an event to the server the appropriate event handler is +invoked with the ``sid`` and the message. The application can define as many +events as needed and associate them with event handlers. An event is defined +simply by a name. + +When a connection with a client is broken, the ``disconnect`` event is called, +allowing the application to perform cleanup. + +Rooms +----- + +Because Socket.IO is a bidirectional protocol, the server can send messages to +any connected client at any time. To make it easy to address specific subsets +of clients, the application can put clients into rooms, and then address +messages to all the clients in a room. + +When clients first connect, they are assigned to their own rooms, named with +the session ID (the ``sid`` argument passed to all event handlers). The +application is free to create additional rooms and manage which clients are in +them using the :func:`socketio.Server.enter_room` and +:func:`socketio.Server.leave_room` methods. Clients can be in as many rooms as +needed and can be moved between rooms as often as necessary. The individual +rooms assigned to clients when they connect are not special in any way, the +application is free to add or remove clients from them, though once it does +that it will lose the ability to address individual clients. + +:: + + @sio.on('enter room') + def enter_room(sid, data): + sio.enter_room(sid, data['room']) + + @sio.on('enter room') + def leave_room(sid, data): + sio.leave_room(sid, data['room']) + +The :func:`socketio.Server.emit` method takes an event name, a message payload +of type ``str``, ``bytes``, ``list`` or ``dict``, and the recipient room. To +address an individual client, the ``sid`` of that client should be given as +room (assuming the application did not alter these initial rooms). To address +all connected clients, the ``room`` argument should be omitted. + +:: + + @sio.on('my message') + def message(sid, data): + print('message ', data) + sio.emit('my reply', data, room='my room') + +Often when broadcasting a message to group of users in a room, it is desirable +that the sender does not receive its own message. The +:func:`socketio.Server.emit` method provides an optional ``skip_sid`` argument +to specify a client that should be skipped during the broadcast. + +:: + + @sio.on('my message') + def message(sid, data): + print('message ', data) + sio.emit('my reply', data, room='my room', skip_sid=sid) + +Responses +--------- + +When a client sends an event to the server, it can optionally provide a +callback function, to be invoked with a response provided by the server. The +server can provide a response simply by returning it from the corresponding +event handler. + +:: + + @sio.on('my event', namespace='/chat') + def my_event_handler(sid, data): + # handle the message + return "OK", 123 + +The event handler can return a single value, or a tuple with several values. +The callback function on the client side will be invoked with these returned +values as arguments. + +Callbacks +--------- + +The server can also request a response to an event sent to a client. The +:func:`socketio.Server.emit` method has an optional ``callback`` argument that +can be set to a callable. When this argument is given, the callable will be +invoked with the arguments returned by the client as a response. + +Using callback functions when broadcasting to multiple clients is not +recommended, as the callback function will be invoked once for each client +that received the message. + +Namespaces +---------- + +The Socket.IO protocol supports multiple logical connections, all multiplexed +on the same physical connection. Clients can open multiple connections by +specifying a different *namespace* on each. A namespace is given by the client +as a pathname following the hostname and port. For example, connecting to +*http://example.com:8000/chat* would open a connection to the namespace +*/chat*. + +Event handlers and rooms are maintained separately for each namespace, so it is +important that applications that use multiple namespaces specify the correct +namespace when setting up event handlers and rooms, using the ``namespace`` +argument available in All the methods in the :class:`socketio.Server` class. + +When the ``namespace`` argument is omitted, set to ``None`` or to ``'/'``, the +default namespace is used. + +API Reference +------------- + +.. toctree:: + :maxdepth: 2 + +.. module:: socketio + +.. autoclass:: Middleware + :members: + +.. autoclass:: Server + :members: diff --git a/docs/make.bat b/docs/make.bat new file mode 100755 index 0000000..ada489d --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,263 @@ +@ECHO OFF + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set BUILDDIR=_build +set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . +set I18NSPHINXOPTS=%SPHINXOPTS% . +if NOT "%PAPER%" == "" ( + set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% + set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% +) + +if "%1" == "" goto help + +if "%1" == "help" ( + :help + echo.Please use `make ^` where ^ is one of + echo. html to make standalone HTML files + echo. dirhtml to make HTML files named index.html in directories + echo. singlehtml to make a single large HTML file + echo. pickle to make pickle files + echo. json to make JSON files + echo. htmlhelp to make HTML files and a HTML help project + echo. qthelp to make HTML files and a qthelp project + echo. devhelp to make HTML files and a Devhelp project + echo. epub to make an epub + echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter + echo. text to make text files + echo. man to make manual pages + echo. texinfo to make Texinfo files + echo. gettext to make PO message catalogs + echo. changes to make an overview over all changed/added/deprecated items + echo. xml to make Docutils-native XML files + echo. pseudoxml to make pseudoxml-XML files for display purposes + echo. linkcheck to check all external links for integrity + echo. doctest to run all doctests embedded in the documentation if enabled + echo. coverage to run coverage check of the documentation if enabled + goto end +) + +if "%1" == "clean" ( + for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i + del /q /s %BUILDDIR%\* + goto end +) + + +REM Check if sphinx-build is available and fallback to Python version if any +%SPHINXBUILD% 2> nul +if errorlevel 9009 goto sphinx_python +goto sphinx_ok + +:sphinx_python + +set SPHINXBUILD=python -m sphinx.__init__ +%SPHINXBUILD% 2> nul +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +:sphinx_ok + + +if "%1" == "html" ( + %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/html. + goto end +) + +if "%1" == "dirhtml" ( + %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. + goto end +) + +if "%1" == "singlehtml" ( + %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. + goto end +) + +if "%1" == "pickle" ( + %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the pickle files. + goto end +) + +if "%1" == "json" ( + %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the JSON files. + goto end +) + +if "%1" == "htmlhelp" ( + %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run HTML Help Workshop with the ^ +.hhp project file in %BUILDDIR%/htmlhelp. + goto end +) + +if "%1" == "qthelp" ( + %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run "qcollectiongenerator" with the ^ +.qhcp project file in %BUILDDIR%/qthelp, like this: + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\socketio.qhcp + echo.To view the help file: + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\socketio.ghc + goto end +) + +if "%1" == "devhelp" ( + %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. + goto end +) + +if "%1" == "epub" ( + %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The epub file is in %BUILDDIR%/epub. + goto end +) + +if "%1" == "latex" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdf" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf + cd %~dp0 + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdfja" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf-ja + cd %~dp0 + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "text" ( + %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The text files are in %BUILDDIR%/text. + goto end +) + +if "%1" == "man" ( + %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The manual pages are in %BUILDDIR%/man. + goto end +) + +if "%1" == "texinfo" ( + %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. + goto end +) + +if "%1" == "gettext" ( + %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The message catalogs are in %BUILDDIR%/locale. + goto end +) + +if "%1" == "changes" ( + %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes + if errorlevel 1 exit /b 1 + echo. + echo.The overview file is in %BUILDDIR%/changes. + goto end +) + +if "%1" == "linkcheck" ( + %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck + if errorlevel 1 exit /b 1 + echo. + echo.Link check complete; look for any errors in the above output ^ +or in %BUILDDIR%/linkcheck/output.txt. + goto end +) + +if "%1" == "doctest" ( + %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest + if errorlevel 1 exit /b 1 + echo. + echo.Testing of doctests in the sources finished, look at the ^ +results in %BUILDDIR%/doctest/output.txt. + goto end +) + +if "%1" == "coverage" ( + %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage + if errorlevel 1 exit /b 1 + echo. + echo.Testing of coverage in the sources finished, look at the ^ +results in %BUILDDIR%/coverage/python.txt. + goto end +) + +if "%1" == "xml" ( + %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The XML files are in %BUILDDIR%/xml. + goto end +) + +if "%1" == "pseudoxml" ( + %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. + goto end +) + +:end diff --git a/example/app.py b/example/app.py new file mode 100755 index 0000000..59214ea --- /dev/null +++ b/example/app.py @@ -0,0 +1,92 @@ +import eventlet +eventlet.monkey_patch() + +import time +from threading import Thread +import eventlet +from eventlet import wsgi +from flask import Flask, render_template +from socketio import Server as SocketIOServer +from socketio import Middleware as SocketIOMiddleware + +socketio = SocketIOServer(logger=True) +app = Flask(__name__) +app.debug = True +app.config['SECRET_KEY'] = 'secret!' +thread = None + + +def background_thread(): + """Example of how to send server generated events to clients.""" + count = 0 + while True: + time.sleep(10) + count += 1 + socketio.emit('my response', + {'data': 'Server generated event'}, namespace='/test') + + +@app.route('/') +def index(): + global thread + if thread is None: + thread = Thread(target=background_thread) + thread.start() + return render_template('index.html') + + +@socketio.on('my event', namespace='/test') +def test_message(sid, message): + socketio.emit('my response', {'data': message['data']}, + room=sid, namespace='/test') + + +@socketio.on('my broadcast event', namespace='/test') +def test_broadcast_message(sid, message): + socketio.emit('my response', {'data': message['data']}, + namespace='/test') + + +@socketio.on('join', namespace='/test') +def join(sid, message): + socketio.enter_room(sid, message['room'], namespace='/test') + socketio.emit('my response', + {'data': 'Entered room: ' + message['room']}, + room=sid, namespace='/test') + + +@socketio.on('leave', namespace='/test') +def leave(sid, message): + socketio.leave_room(sid, message['room'], namespace='/test') + socketio.emit('my response', + {'data': 'Left room: ' + message['room']}, + room=sid, namespace='/test') + + +@socketio.on('close room', namespace='/test') +def close(sid, message): + socketio.emit('my response', + {'data': 'Room ' + message['room'] + ' is closing.'}, + room=message['room'], namespace='/test') + socketio.close_room(message['room'], namespace='/test') + + +@socketio.on('my room event', namespace='/test') +def send_room_message(sid, message): + socketio.emit('my response', {'data': message['data']}, + room=message['room'], namespace='/test') + + +@socketio.on('connect', namespace='/test') +def test_connect(sid, environ): + socketio.emit('my response', {'data': 'Connected', 'count': 0}, room=sid) + + +@socketio.on('disconnect', namespace='/test') +def test_disconnect(sid): + print('Client disconnected') + + +if __name__ == '__main__': + app = SocketIOMiddleware(socketio, app) + wsgi.server(eventlet.listen(('', 5000)), app) diff --git a/example/requirements.txt b/example/requirements.txt new file mode 100644 index 0000000..4d098a2 --- /dev/null +++ b/example/requirements.txt @@ -0,0 +1,4 @@ +eventlet==0.17.4 +greenlet==0.4.7 +python-engineio==0.1.0 +six==1.9.0 diff --git a/example/templates/index.html b/example/templates/index.html new file mode 100755 index 0000000..8da7b13 --- /dev/null +++ b/example/templates/index.html @@ -0,0 +1,85 @@ + + + + Flask-SocketIO Test + + + + + +

Flask-SocketIO Test

+

Send:

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + +
+
+ + +
+

Receive:

+

+ + diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..c0fa430 --- /dev/null +++ b/setup.py @@ -0,0 +1,46 @@ +""" +python-socketio +--------------- + +Socket.IO server. +""" +from setuptools import setup + + +with open('README.rst', 'r') as f: + long_description = f.read() + +setup( + name='python-socketio', + version='0.1.0', + url='http://github.com/miguelgrinberg/python-socketio/', + license='MIT', + author='Miguel Grinberg', + author_email='miguelgrinberg50@gmail.com', + description='Socket.IO server', + long_description=long_description, + packages=['socketio'], + zip_safe=False, + include_package_data=True, + platforms='any', + install_requires=[ + 'six>=1.9.0', + 'eventlet>=0.17.4', + 'python-engineio>=0.2.0' + ], + tests_require=[ + 'mock', + ], + test_suite='tests', + classifiers=[ + 'Environment :: Web Environment', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 3', + 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', + 'Topic :: Software Development :: Libraries :: Python Modules' + ] +) diff --git a/socketio/__init__.py b/socketio/__init__.py new file mode 100755 index 0000000..6e95349 --- /dev/null +++ b/socketio/__init__.py @@ -0,0 +1,4 @@ +from .middleware import Middleware +from .server import Server + +__all__ = [Middleware, Server] diff --git a/socketio/base_manager.py b/socketio/base_manager.py new file mode 100755 index 0000000..64adad1 --- /dev/null +++ b/socketio/base_manager.py @@ -0,0 +1,97 @@ +import six + + +class BaseManager(object): + """Manage client connections. + + This class keeps track of all the clients and the rooms they are in, to + support the broadcasting of messages. The data used by this class is + stored in a memory structure, making it appropriate only for single process + services. More sophisticated storage backends can be implemented by + subclasses. + """ + def __init__(self, server): + self.server = server + self.rooms = {} + self.pending_removals = [] + + def get_namespaces(self): + """Return an iterable with the active namespace names.""" + return six.iterkeys(self.rooms) + + def get_participants(self, namespace, room): + """Return an iterable with the active participants in a room.""" + for sid, active in six.iteritems(self.rooms[namespace][room]): + if active: + yield sid + self._clean_rooms() + + def connect(self, sid, namespace): + """Record a client connection to a namespace.""" + self.enter_room(sid, namespace, None) + self.enter_room(sid, namespace, sid) + + def disconnect(self, sid, namespace): + """Record a client disconnect event.""" + if namespace == '/': + namespace_list = list(six.iterkeys(self.rooms)) + else: + namespace_list = [namespace] + for n in namespace_list: + rooms = [] + for room_name, room in six.iteritems(self.rooms[n]): + if sid in room: + rooms.append(room_name) + for room in rooms: + self.leave_room(sid, n, room) + + def enter_room(self, sid, namespace, room): + """Add a client to a room.""" + if namespace not in self.rooms: + self.rooms[namespace] = {} + if room not in self.rooms[namespace]: + self.rooms[namespace][room] = {} + self.rooms[namespace][room][sid] = True + + def leave_room(self, sid, namespace, room): + """Remove a client from a room.""" + try: + # do not delete immediately, just mark the client as inactive + # _clean_rooms() will do the clean up when it is safe to do so + self.rooms[namespace][room][sid] = False + self.pending_removals.append((namespace, room, sid)) + except KeyError: + pass + + def close_room(self, namespace, room): + """Remove all participants from a room.""" + try: + for sid in self.get_participants(namespace, room): + self.leave_room(sid, namespace, room) + except KeyError: + pass + + def emit(self, event, data, namespace, room=None, skip_sid=None, + callback=None): + """Emit a message to a single client, a room, or all the clients + connected to the namespace.""" + if namespace not in self.rooms or room not in self.rooms[namespace]: + return + for sid in self.get_participants(namespace, room): + if sid != skip_sid: + self.server._emit_internal(sid, event, data, namespace, + callback) + + def _clean_rooms(self): + """Remove all the inactive room participants.""" + for namespace, room, sid in self.pending_removals: + try: + del self.rooms[namespace][room][sid] + except KeyError: + # failures here could mean there were duplicates so we ignore + continue + if len(self.rooms[namespace][room]) == 0: + del self.rooms[namespace][room] + if len(self.rooms[namespace]) == 0: + del self.rooms[namespace] + self.pending_removals = [] diff --git a/socketio/middleware.py b/socketio/middleware.py new file mode 100755 index 0000000..f7f042a --- /dev/null +++ b/socketio/middleware.py @@ -0,0 +1,27 @@ +import engineio + + +class Middleware(engineio.Middleware): + """WSGI middleware for Socket.IO. + + This middleware dispatches traffic to a Socket.IO application, and + optionally forwards regular HTTP traffic to a WSGI application. + + :param socketio_app: The Socket.IO server. + :param wsgi_app: The WSGI app that receives all other traffic. + :param socketio_path: The endpoint where the Socket.IO application should + be installed. The default value is appropriate for + most cases. + + Example usage:: + + import socketio + import eventlet + from . import wsgi_app + + sio = socketio.Server() + app = socketio.Middleware(sio, wsgi_app) + eventlet.wsgi.server(eventlet.listen(('', 8000)), app) + """ + def __init__(self, socketio_app, wsgi_app=None, socketio_path='socket.io'): + super(Middleware, self).__init__(socketio_app, wsgi_app, socketio_path) diff --git a/socketio/packet.py b/socketio/packet.py new file mode 100755 index 0000000..99ab6d1 --- /dev/null +++ b/socketio/packet.py @@ -0,0 +1,148 @@ +import functools +import json + +import six + +(CONNECT, DISCONNECT, EVENT, ACK, ERROR, BINARY_EVENT, BINARY_ACK) = \ + (0, 1, 2, 3, 4, 5, 6) +packet_names = ['CONNECT', 'DISCONNECT', 'EVENT', 'ACK', 'ERROR', + 'BINARY_EVENT', 'BINARY_ACK'] + + +class Packet(object): + """Socket.IO packet.""" + def __init__(self, packet_type=EVENT, data=None, namespace=None, id=None, + binary=None, encoded_packet=None): + self.packet_type = packet_type + self.data = data + self.namespace = namespace + self.id = id + if binary or (binary is None and self._data_is_binary(self.data)): + if self.packet_type == EVENT: + self.packet_type = BINARY_EVENT + elif self.packet_type == ACK: + self.packet_type = BINARY_ACK + else: + raise ValueError('Packet does not support binary payload.') + self.attachment_count = 0 + if encoded_packet: + self.attachment_count = self.decode(encoded_packet) + + def encode(self): + """Encode the packet for transmission. + + If the packet contains binary elements, this function returns a list + of packets where the first is the original packet with placeholders for + the binary components and the remaining ones the binary attachments. + """ + encoded_packet = six.text_type(self.packet_type) + if self.packet_type == BINARY_EVENT or self.packet_type == BINARY_ACK: + data, attachments = self._deconstruct_binary(self.data) + encoded_packet += six.text_type(len(attachments)) + '-' + else: + data = self.data + attachments = None + needs_comma = False + if self.namespace is not None and self.namespace != '/': + encoded_packet += self.namespace + needs_comma = True + if self.id is not None: + if needs_comma: + encoded_packet += ',' + needs_comma = False + encoded_packet += six.text_type(self.id) + if data is not None: + if needs_comma: + encoded_packet += ',' + encoded_packet += json.dumps(data, separators=(',', ':')) + if attachments is not None: + encoded_packet = [encoded_packet] + attachments + return encoded_packet + + def decode(self, encoded_packet): + """Decode a transmitted package. + + The return value indicates how many binary attachment packets are + necessary to fully decode the packet. + """ + ep = encoded_packet + self.packet_type = int(ep[0:1]) + self.namespace = None + self.data = None + ep = ep[1:] + dash = (ep + '-').find('-') + comma = (ep + ',').find(',') + attachment_count = 0 + if dash < comma: + attachment_count = int(ep[0:dash]) + ep = ep[dash + 1:] + if ep and ep[0:1] == '/': + sep = ep.find(',') + if sep == -1: + self.namespace = ep + ep = '' + else: + self.namespace = ep[0:sep] + ep = ep[sep + 1:] + if ep and ep[0].isdigit(): + self.id = 0 + while ep[0].isdigit(): + self.id = self.id * 10 + int(ep[0]) + ep = ep[1:] + if ep: + self.data = json.loads(ep) + return attachment_count + + def reconstruct_binary(self, attachments): + """Reconstruct a decoded packet using the given list of binary + attachments. + """ + self.data = self._reconstruct_binary_internal(self.data, attachments) + + def _reconstruct_binary_internal(self, data, attachments): + if isinstance(data, list): + return [self._reconstruct_binary_internal(item, attachments) + for item in data] + elif isinstance(data, dict): + if data.get('_placeholder') and 'num' in data: + return attachments[data['num']] + else: + return {key: self._reconstruct_binary_internal(value, + attachments) + for key, value in six.iteritems(data)} + else: + return data + + def _deconstruct_binary(self, data): + """Extract binary components in the packet.""" + attachments = [] + data = self._deconstruct_binary_internal(data, attachments) + return data, attachments + + def _deconstruct_binary_internal(self, data, attachments): + if isinstance(data, six.binary_type): + attachments.append(data) + return {'_placeholder': True, 'num': len(attachments) - 1} + elif isinstance(data, list): + return [self._deconstruct_binary_internal(item, attachments) + for item in data] + elif isinstance(data, dict): + return {key: self._deconstruct_binary_internal(value, attachments) + for key, value in six.iteritems(data)} + else: + return data + + def _data_is_binary(self, data): + """Check if the data contains binary components.""" + if isinstance(data, six.binary_type): + return True + elif isinstance(data, list): + return functools.reduce( + lambda a, b: a or b, [self._data_is_binary(item) + for item in data]) + elif isinstance(data, dict): + return functools.reduce( + lambda a, b: a or b, [self._data_is_binary(item) + for item in six.itervalues(data)]) + else: + return False diff --git a/socketio/server.py b/socketio/server.py new file mode 100755 index 0000000..8908155 --- /dev/null +++ b/socketio/server.py @@ -0,0 +1,413 @@ +import itertools +import logging + +import engineio +import six + +from . import base_manager +from . import packet + + +class Server(object): + """A Socket.IO server. + + This class implements a fully compliant Socket.IO web server with support + for websocket and long-polling transports. + + :param engineio_options: A ``dict`` with options for the Engine.IO server. + The values are passed directly to the + ``engineio.Server`` constructor. + :param client_manager_class: The class that will manage the client list. + The default value is appropriate for most + cases. + :param logger: To enable logging set to ``True`` or pass a logger object to + use. To disable logging set to ``False``. + :param binary: ``True`` to support binary payloads, ``False`` to treat all + payloads as text. On Python 2, if this is set to ``True``, + ``unicode`` values are treated as text, and ``str`` and + ``bytes`` values are treated as binary. This option has no + effect on Python 3, where text and binary payloads are + always automatically discovered. + + This ``engineio_options`` dictionary can include the following settings: + + :param ping_timeout: The time in seconds that the client waits for the + server to respond before disconnecting. + :param ping_interval: The interval in seconds at which the client pings + the server. + :param max_http_buffer_size: The maximum size of a message when using the + polling transport. + :param allow_upgrades: Whether to allow transport upgrades or not. + :param http_compression: Whether to compress packages when using the + polling transport. + :param compression_threshold: Only compress messages when their byte size + is greater than this value. + :param cookie: Name of the HTTP cookie that contains the client session + id. If set to ``None``, a cookie is not sent to the client. + :param cors_allowed_origins: List of origins that are allowed to connect + to this server. All origins are allowed by + default. + :param cors_credentials: Whether credentials (cookies, authentication) are + allowed in requests to this server. + :param logger: To enable Engine.IO logging set to ``True`` or pass a logger + object to use. To disable logging set to ``False``. + """ + def __init__(self, engineio_options=None, client_manager_class=None, + logger=False, binary=False): + if client_manager_class is None: + client_manager_class = base_manager.BaseManager + self.manager = client_manager_class(self) + if engineio_options is None: + engineio_options = {} + self.eio = engineio.Server(**engineio_options) + self.eio.on('connect', self._handle_eio_connect) + self.eio.on('message', self._handle_eio_message) + self.eio.on('disconnect', self._handle_eio_disconnect) + self.binary = binary + + self.environ = {} + self.handlers = {} + self.callbacks = {} + + self._binary_packet = None + self._attachment_count = 0 + self._attachments = [] + + if not isinstance(logger, bool): + self.logger = logger + else: + logging.basicConfig() + self.logger = logging.getLogger('socketio') + if logger: + self.logger.setLevel(logging.INFO) + else: + self.logger.setLevel(logging.ERROR) + + def on(self, event, handler=None, namespace=None): + """Register an event handler. + + :param event: The event name. It can be any string. The event names + ``'connect'``, ``'message'`` and ``'disconnect'`` are + reserved and should not be used. + :param handler: The function that should be invoked to handle the + event. When this parameter is not given, the method + acts as a decorator for the handler function. + :param namespace: The Socket.IO namespace for the event. If this + argument is omitted the handler is associated with + the default namespace. + + Example usage:: + + # as a decorator: + @socket_io.on('connect', namespace='/chat') + def connect_handler(sid, environ): + print('Connection request') + if environ['REMOTE_ADDR'] in blacklisted: + return False # reject + + # as a method: + def message_handler(sid, msg): + print('Received message: ', msg) + eio.send(sid, 'response') + socket_io.on('message', namespace='/chat', message_handler) + + The handler function receives the ``sid`` (session ID) for the + client as first argument. The ``'connect'`` event handler receives the + WSGI environment as a second argument, and can return ``False`` to + reject the connection. The ``'message'`` handler and handlers for + custom event names receive the message payload as a second argument. + Any values returned from a message handler will be passed to the + client's acknowledgement callback function if it exists. The + ``'disconnect'`` handler does not take a second argument. + """ + namespace = namespace or '/' + + def set_handler(handler): + if namespace not in self.handlers: + self.handlers[namespace] = {} + self.handlers[namespace][event] = handler + return handler + + if handler is None: + return set_handler + set_handler(handler) + + def emit(self, event, data, room=None, skip_sid=None, namespace=None, + callback=None): + """Emit a custom event to one or more connected clients. + + :param event: The event name. It can be any string. The event names + ``'connect'``, ``'message'`` and ``'disconnect'`` are + reserved and should not be used. + :param data: The data to send to the client or clients. Data can be of + type ``str``, ``bytes``, ``list`` or ``dict``. If a + ``list`` or ``dict``, the data will be serialized as JSON. + :param room: The recipient of the message. This can be set to the + session ID of a client to address that client's room, or + to any custom room created by the application, If this + argument is omitted the event is broadcasted to all + connected clients. + :param skip_sid: The session ID of a client to skip when broadcasting + to a room or to all clients. This can be used to + prevent a message from being sent to the sender. + :param namespace: The Socket.IO namespace for the event. If this + argument is omitted the event is emitted to the + default namespace. + :param callback: If given, this function will be called to acknowledge + the the client has received the message. The arguments + that will be passed to the function are those provided + by the client. Callback functions can only be used + when addressing an individual client. + """ + namespace = namespace or '/' + self.logger.info('emitting event "%s" to %s [%s]', event, + room or 'all', namespace) + self.manager.emit(event, data, namespace, room, skip_sid, callback) + + def send(self, data, room=None, skip_sid=None, namespace=None, + callback=None): + """Send a message to one or more connected clients. + + This function emits an event with the name ``'message'``. Use + :func:`emit` to issue custom event names. + + :param data: The data to send to the client or clients. Data can be of + type ``str``, ``bytes``, ``list`` or ``dict``. If a + ``list`` or ``dict``, the data will be serialized as JSON. + :param room: The recipient of the message. This can be set to the + session ID of a client to address that client's room, or + to any custom room created by the application, If this + argument is omitted the event is broadcasted to all + connected clients. + :param skip_sid: The session ID of a client to skip when broadcasting + to a room or to all clients. This can be used to + prevent a message from being sent to the sender. + :param namespace: The Socket.IO namespace for the event. If this + argument is omitted the event is emitted to the + default namespace. + :param callback: If given, this function will be called to acknowledge + the the client has received the message. The arguments + that will be passed to the function are those provided + by the client. Callback functions can only be used + when addressing an individual client. + """ + self.emit('message', data, room, skip_sid, namespace, callback) + + def enter_room(self, sid, room, namespace=None): + """Enter a room. + + This function adds the client to a room. The :func:`emit` and + :func:`send` functions can optionally broadcast events to all the + clients in a room. + + :param sid: Session ID of the client. + :param room: Room name. If the room does not exist it is created. + :param namespace: The Socket.IO namespace for the event. If this + argument is omitted the default namespace is used. + """ + namespace = namespace or '/' + self.logger.info('%s is entering room %s [%s]', sid, room, namespace) + self.manager.enter_room(sid, namespace, room) + + def leave_room(self, sid, room, namespace=None): + """Leave a room. + + This function removes the client from a room. + + :param sid: Session ID of the client. + :param room: Room name. + :param namespace: The Socket.IO namespace for the event. If this + argument is omitted the default namespace is used. + """ + namespace = namespace or '/' + self.logger.info('%s is leaving room %s [%s]', sid, room, namespace) + self.manager.leave_room(sid, namespace, room) + + def close_room(self, room, namespace=None): + """Close a room. + + This function removes all the clients from the given room. + + :param room: Room name. + :param namespace: The Socket.IO namespace for the event. If this + argument is omitted the default namespace is used. + """ + namespace = namespace or '/' + self.logger.info('room %s is closing [%s]', room, namespace) + self.manager.close_room(namespace, room) + + def handle_request(self, environ, start_response): + """Handle an HTTP request from the client. + + This is the entry point of the Socket.IO application, using the same + interface as a WSGI application. For the typical usage, this function + is invoked by the :class:`Middleware` instance, but it can be invoked + directly when the middleware is not used. + + :param environ: The WSGI environment. + :param start_response: The WSGI ``start_response`` function. + + This function returns the HTTP response body to deliver to the client + as a byte sequence. + """ + return self.eio.handle_request(environ, start_response) + + def _emit_internal(self, sid, event, data, namespace=None, callback=None): + """Send a message to a client.""" + if callback is not None: + id = self._generate_ack_id(sid, namespace, callback) + else: + id = None + if six.PY2 and not self.binary: + binary = False # pragma: nocover + else: + binary = None + self._send_packet(sid, packet.Packet(packet.EVENT, namespace=namespace, + data=[event, data], id=id, + binary=binary)) + + def _send_packet(self, sid, pkt): + """Send a Socket.IO packet to a client.""" + encoded_packet = pkt.encode() + if isinstance(encoded_packet, list): + binary = False + for ep in encoded_packet: + self.eio.send(sid, ep, binary=binary) + binary = True + else: + self.eio.send(sid, encoded_packet, binary=False) + + def _handle_connect(self, sid, namespace): + """Handle a client connection request.""" + namespace = namespace or '/' + if self._trigger_event('connect', namespace, sid, + self.environ[sid]) is False: + self._send_packet(sid, packet.Packet(packet.ERROR, + namespace=namespace)) + else: + self.manager.connect(sid, namespace) + self._send_packet(sid, packet.Packet(packet.CONNECT, + namespace=namespace)) + + def _handle_disconnect(self, sid, namespace): + """Handle a client disconnect.""" + namespace = namespace or '/' + if namespace == '/': + namespace_list = list(self.manager.get_namespaces()) + else: + namespace_list = [namespace] + for n in namespace_list: + if n != '/': + self._trigger_event('disconnect', n, sid) + self.manager.disconnect(sid, n) + if sid in self.callbacks and n in self.callbacks[sid]: + del self.callbacks[sid][n] + if namespace == '/': + self._trigger_event('disconnect', '/', sid) + self.manager.disconnect(sid, '/') + if sid in self.callbacks: + del self.callbacks[sid] + if sid in self.environ: + del self.environ[sid] + + def _handle_event(self, sid, namespace, id, data): + """Handle an incoming client event.""" + namespace = namespace or '/' + self.logger.info('received event "%s" from %s [%s]', data[0], sid, + namespace) + r = self._trigger_event(data[0], namespace, sid, *data[1:]) + if id is not None: + # send ACK packet with the response returned by the handler + if isinstance(r, tuple): + data = list(r) + elif isinstance(r, list): + data = r + else: + data = [r] + if six.PY2 and not self.binary: + binary = False # pragma: nocover + else: + binary = None + self._send_packet(sid, packet.Packet(packet.ACK, + namespace=namespace, + id=id, data=data, + binary=binary)) + + def _handle_ack(self, sid, namespace, id, data): + """Handle ACK packets from the client.""" + namespace = namespace or '/' + self.logger.info('received ack from %s [%s]', sid, namespace) + self._trigger_callback(sid, namespace, id, data) + + def _trigger_event(self, event, namespace, *args): + """Invoke an application event handler.""" + if namespace in self.handlers and event in self.handlers[namespace]: + return self.handlers[namespace][event](*args) + + def _generate_ack_id(self, sid, namespace, callback): + """Generate a unique identifier for an ACK packet.""" + namespace = namespace or '/' + if sid not in self.callbacks: + self.callbacks[sid] = {} + if namespace not in self.callbacks[sid]: + self.callbacks[sid][namespace] = {0: itertools.count(1)} + id = six.next(self.callbacks[sid][namespace][0]) + self.callbacks[sid][namespace][id] = callback + return id + + def _trigger_callback(self, sid, namespace, id, data): + """Invoke an application callback.""" + namespace = namespace or '/' + try: + callback = self.callbacks[sid][namespace][id] + except KeyError: + raise ValueError('Unknown callback') + del self.callbacks[sid][namespace][id] + callback(*data) + + def _handle_eio_connect(self, sid, environ): + """Handle the Engine.IO connection event.""" + self.environ[sid] = environ + self._handle_connect(sid, '/') + + def _handle_eio_message(self, sid, data): + """Dispatch Engine.IO messages.""" + if self._attachment_count > 0: + self._attachments.append(data) + self._attachment_count -= 1 + + if self._attachment_count == 0: + self._binary_packet.reconstruct_binary(self._attachments) + if self._binary_packet.packet_type == packet.BINARY_EVENT: + self._handle_event(sid, self._binary_packet.namespace, + self._binary_packet.id, + self._binary_packet.data) + else: + self._handle_ack(sid, self._binary_packet.namespace, + self._binary_packet.id, + self._binary_packet.data) + self._binary_packet = None + self._attachments = [] + else: + pkt = packet.Packet(encoded_packet=data) + if pkt.packet_type == packet.CONNECT: + self._handle_connect(sid, pkt.namespace) + elif pkt.packet_type == packet.DISCONNECT: + self._handle_disconnect(sid, pkt.namespace) + elif pkt.packet_type == packet.EVENT: + self._handle_event(sid, pkt.namespace, pkt.id, pkt.data) + elif pkt.packet_type == packet.ACK: + self._handle_ack(sid, pkt.namespace, pkt.id, pkt.data) + elif pkt.packet_type == packet.BINARY_EVENT or \ + pkt.packet_type == packet.BINARY_ACK: + self._binary_packet = pkt + self._attachments = [] + self._attachment_count = pkt.attachment_count + elif pkt.packet_type == packet.ERROR: + raise ValueError('Unexpected ERROR packet.') + else: + raise ValueError('Unknown packet type.') + + def _handle_eio_disconnect(self, sid): + """Handle Engine.IO disconnect event.""" + self._handle_disconnect(sid, '/') diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_base_manager.py b/tests/test_base_manager.py new file mode 100755 index 0000000..88106f7 --- /dev/null +++ b/tests/test_base_manager.py @@ -0,0 +1,176 @@ +import unittest + +import six +if six.PY3: + from unittest import mock +else: + import mock + +from socketio import base_manager + + +class TestBaseManager(unittest.TestCase): + def setUp(self): + mock_server = mock.MagicMock() + mock_server.rooms = {} + self.bm = base_manager.BaseManager(mock_server) + + def test_connect(self): + self.bm.connect('123', '/foo') + self.assertIn(None, self.bm.rooms['/foo']) + self.assertIn('123', self.bm.rooms['/foo']) + self.assertIn('123', self.bm.rooms['/foo'][None]) + self.assertIn('123', self.bm.rooms['/foo']['123']) + self.assertEqual(self.bm.rooms['/foo'], {None: {'123': True}, + '123': {'123': True}}) + + def test_disconnect(self): + self.bm.connect('123', '/foo') + self.bm.connect('456', '/foo') + self.bm.enter_room('123', '/foo', 'bar') + self.bm.enter_room('456', '/foo', 'baz') + self.bm.disconnect('123', '/foo') + self.bm._clean_rooms() + self.assertEqual(self.bm.rooms['/foo'], {None: {'456': True}, + '456': {'456': True}, + 'baz': {'456': True}}) + + def test_disconnect_default_namespace(self): + self.bm.connect('123', '/') + self.bm.connect('123', '/foo') + self.bm.connect('456', '/') + self.bm.connect('456', '/foo') + self.bm.disconnect('123', '/') + self.bm._clean_rooms() + self.assertEqual(self.bm.rooms['/'], {None: {'456': True}, + '456': {'456': True}}) + self.assertEqual(self.bm.rooms['/foo'], {None: {'456': True}, + '456': {'456': True}}) + + def test_disconnect_twice(self): + self.bm.connect('123', '/') + self.bm.connect('123', '/foo') + self.bm.connect('456', '/') + self.bm.connect('456', '/foo') + self.bm.disconnect('123', '/') + self.bm.disconnect('123', '/') + self.bm._clean_rooms() + self.assertEqual(self.bm.rooms['/'], {None: {'456': True}, + '456': {'456': True}}) + self.assertEqual(self.bm.rooms['/foo'], {None: {'456': True}, + '456': {'456': True}}) + + def test_disconnect_all(self): + self.bm.connect('123', '/foo') + self.bm.connect('456', '/foo') + self.bm.enter_room('123', '/foo', 'bar') + self.bm.enter_room('456', '/foo', 'baz') + self.bm.disconnect('123', '/foo') + self.bm.disconnect('456', '/foo') + self.bm._clean_rooms() + self.assertEqual(self.bm.rooms, {}) + + def test_get_namespaces(self): + self.assertEqual(list(self.bm.get_namespaces()), []) + self.bm.connect('123', '/') + self.bm.connect('123', '/foo') + namespaces = list(self.bm.get_namespaces()) + self.assertEqual(len(namespaces), 2) + self.assertIn('/', namespaces) + self.assertIn('/foo', namespaces) + + def test_get_participants(self): + self.bm.connect('123', '/') + self.bm.connect('456', '/') + self.bm.connect('789', '/') + self.bm.disconnect('789', '/') + self.assertEqual(self.bm.rooms['/'][None]['789'], False) + participants = list(self.bm.get_participants('/', None)) + self.assertEqual(len(participants), 2) + self.assertNotIn('789', participants) + self.assertNotIn('789', self.bm.rooms['/'][None]) + + def test_leave_invalid_room(self): + self.bm.connect('123', '/foo') + self.bm.leave_room('123', '/foo', 'baz') + self.bm.leave_room('123', '/bar', 'baz') + + def test_close_room(self): + self.bm.connect('123', '/foo') + self.bm.connect('456', '/foo') + self.bm.connect('789', '/foo') + self.bm.enter_room('123', '/foo', 'bar') + self.bm.enter_room('123', '/foo', 'bar') + self.bm.close_room('/foo', 'bar') + self.assertNotIn('bar', self.bm.rooms['/foo']) + + def test_close_invalid_room(self): + self.bm.close_room('/foo', 'bar') + + def test_emit_to_sid(self): + self.bm.connect('123', '/foo') + self.bm.connect('456', '/foo') + self.bm.emit('my event', {'foo': 'bar'}, namespace='/foo', + room='123') + self.bm.server._emit_internal.assert_called_once_with('123', + 'my event', + {'foo': 'bar'}, + '/foo', None) + + def test_emit_to_room(self): + self.bm.connect('123', '/foo') + self.bm.enter_room('123', '/foo', 'bar') + self.bm.connect('456', '/foo') + self.bm.enter_room('456', '/foo', 'bar') + self.bm.connect('789', '/foo') + self.bm.emit('my event', {'foo': 'bar'}, namespace='/foo', + room='bar') + self.assertEqual(self.bm.server._emit_internal.call_count, 2) + self.bm.server._emit_internal.assert_any_call('123', 'my event', + {'foo': 'bar'}, '/foo', + None) + self.bm.server._emit_internal.assert_any_call('456', 'my event', + {'foo': 'bar'}, '/foo', + None) + + def test_emit_to_all(self): + self.bm.connect('123', '/foo') + self.bm.enter_room('123', '/foo', 'bar') + self.bm.connect('456', '/foo') + self.bm.enter_room('456', '/foo', 'bar') + self.bm.connect('789', '/foo') + self.bm.connect('abc', '/bar') + self.bm.emit('my event', {'foo': 'bar'}, namespace='/foo') + self.assertEqual(self.bm.server._emit_internal.call_count, 3) + self.bm.server._emit_internal.assert_any_call('123', 'my event', + {'foo': 'bar'}, '/foo', + None) + self.bm.server._emit_internal.assert_any_call('456', 'my event', + {'foo': 'bar'}, '/foo', + None) + self.bm.server._emit_internal.assert_any_call('789', 'my event', + {'foo': 'bar'}, '/foo', + None) + + def test_emit_to_all_skip_one(self): + self.bm.connect('123', '/foo') + self.bm.enter_room('123', '/foo', 'bar') + self.bm.connect('456', '/foo') + self.bm.enter_room('456', '/foo', 'bar') + self.bm.connect('789', '/foo') + self.bm.connect('abc', '/bar') + self.bm.emit('my event', {'foo': 'bar'}, namespace='/foo', + skip_sid='456') + self.assertEqual(self.bm.server._emit_internal.call_count, 2) + self.bm.server._emit_internal.assert_any_call('123', 'my event', + {'foo': 'bar'}, '/foo', + None) + self.bm.server._emit_internal.assert_any_call('789', 'my event', + {'foo': 'bar'}, '/foo', + None) + + def test_emit_to_invalid_room(self): + self.bm.emit('my event', {'foo': 'bar'}, namespace='/', room='123') + + def test_emit_to_invalid_namespace(self): + self.bm.emit('my event', {'foo': 'bar'}, namespace='/foo') diff --git a/tests/test_middleware.py b/tests/test_middleware.py new file mode 100755 index 0000000..3a05a3d --- /dev/null +++ b/tests/test_middleware.py @@ -0,0 +1,42 @@ +import unittest + +import six +if six.PY3: + from unittest import mock +else: + import mock + +from socketio import middleware + + +class TestMiddleware(unittest.TestCase): + def test_wsgi_routing(self): + mock_wsgi_app = mock.MagicMock() + mock_sio_app = 'foo' + m = middleware.Middleware(mock_sio_app, mock_wsgi_app) + environ = {'PATH_INFO': '/foo'} + start_response = "foo" + m(environ, start_response) + mock_wsgi_app.assert_called_once_with(environ, start_response) + + def test_sio_routing(self): + mock_wsgi_app = 'foo' + mock_sio_app = mock.Mock() + mock_sio_app.handle_request = mock.MagicMock() + m = middleware.Middleware(mock_sio_app, mock_wsgi_app) + environ = {'PATH_INFO': '/socket.io/'} + start_response = "foo" + m(environ, start_response) + mock_sio_app.handle_request.assert_called_once_with(environ, + start_response) + + def test_404(self): + mock_wsgi_app = None + mock_sio_app = mock.Mock() + m = middleware.Middleware(mock_sio_app, mock_wsgi_app) + environ = {'PATH_INFO': '/foo/bar'} + start_response = mock.MagicMock() + r = m(environ, start_response) + self.assertEqual(r, ['Not Found']) + start_response.assert_called_once_with( + "404 Not Found", [('Content-type', 'text/plain')]) diff --git a/tests/test_packet.py b/tests/test_packet.py new file mode 100755 index 0000000..511cee1 --- /dev/null +++ b/tests/test_packet.py @@ -0,0 +1,167 @@ +import unittest + +import six + +from socketio import packet + + +class TestPacket(unittest.TestCase): + def test_encode_default_packet(self): + pkt = packet.Packet() + self.assertEqual(pkt.packet_type, packet.EVENT) + self.assertIsNone(pkt.data) + self.assertIsNone(pkt.namespace) + self.assertIsNone(pkt.id) + self.assertEqual(pkt.attachment_count, 0) + self.assertEqual(pkt.encode(), '2') + + def test_decode_default_packet(self): + pkt = packet.Packet(encoded_packet='2') + self.assertTrue(pkt.encode(), '2') + + def test_encode_text_event_packet(self): + pkt = packet.Packet(packet_type=packet.EVENT, + data=[six.text_type('foo')]) + self.assertEqual(pkt.packet_type, packet.EVENT) + self.assertEqual(pkt.data, ['foo']) + self.assertEqual(pkt.encode(), '2["foo"]') + + def test_decode_text_event_packet(self): + pkt = packet.Packet(encoded_packet='2["foo"]') + self.assertEqual(pkt.packet_type, packet.EVENT) + self.assertEqual(pkt.data, ['foo']) + self.assertEqual(pkt.encode(), '2["foo"]') + + def test_encode_binary_event_packet(self): + pkt = packet.Packet(packet_type=packet.EVENT, data=b'1234') + self.assertEqual(pkt.packet_type, packet.BINARY_EVENT) + self.assertEqual(pkt.data, b'1234') + a = ['51-{"_placeholder":true,"num":0}', b'1234'] + b = ['51-{"num":0,"_placeholder":true}', b'1234'] + encoded_packet = pkt.encode() + self.assertTrue(encoded_packet == a or encoded_packet == b) + + def test_decode_binary_event_packet(self): + pkt = packet.Packet(encoded_packet='51-{"_placeholder":true,"num":0}') + pkt.reconstruct_binary([b'1234']) + self.assertEqual(pkt.packet_type, packet.BINARY_EVENT) + self.assertEqual(pkt.data, b'1234') + + def test_encode_text_ack_packet(self): + pkt = packet.Packet(packet_type=packet.ACK, + data=[six.text_type('foo')]) + self.assertEqual(pkt.packet_type, packet.ACK) + self.assertEqual(pkt.data, ['foo']) + self.assertEqual(pkt.encode(), '3["foo"]') + + def test_decode_text_ack_packet(self): + pkt = packet.Packet(encoded_packet='3["foo"]') + self.assertEqual(pkt.packet_type, packet.ACK) + self.assertEqual(pkt.data, ['foo']) + self.assertEqual(pkt.encode(), '3["foo"]') + + def test_encode_binary_ack_packet(self): + pkt = packet.Packet(packet_type=packet.ACK, data=b'1234') + self.assertEqual(pkt.packet_type, packet.BINARY_ACK) + self.assertEqual(pkt.data, b'1234') + a = ['61-{"_placeholder":true,"num":0}', b'1234'] + b = ['61-{"num":0,"_placeholder":true}', b'1234'] + encoded_packet = pkt.encode() + self.assertTrue(encoded_packet == a or encoded_packet == b) + + def test_decode_binary_ack_packet(self): + pkt = packet.Packet(encoded_packet='61-{"_placeholder":true,"num":0}') + pkt.reconstruct_binary([b'1234']) + self.assertEqual(pkt.packet_type, packet.BINARY_ACK) + self.assertEqual(pkt.data, b'1234') + + def test_invalid_binary_packet(self): + self.assertRaises(ValueError, packet.Packet, packet_type=packet.ERROR, + data=b'123') + + def test_encode_namespace(self): + pkt = packet.Packet(packet_type=packet.EVENT, + data=[six.text_type('foo')], namespace='/bar') + self.assertEqual(pkt.namespace, '/bar') + self.assertEqual(pkt.encode(), '2/bar,["foo"]') + + def test_decode_namespace(self): + pkt = packet.Packet(encoded_packet='2/bar,["foo"]') + self.assertEqual(pkt.namespace, '/bar') + self.assertEqual(pkt.encode(), '2/bar,["foo"]') + + def test_encode_namespace_no_data(self): + pkt = packet.Packet(packet_type=packet.EVENT, namespace='/bar') + self.assertEqual(pkt.encode(), '2/bar') + + def test_decode_namespace_no_data(self): + pkt = packet.Packet(encoded_packet='2/bar') + self.assertEqual(pkt.namespace, '/bar') + self.assertEqual(pkt.encode(), '2/bar') + + def test_encode_id(self): + pkt = packet.Packet(packet_type=packet.EVENT, + data=[six.text_type('foo')], id=123) + self.assertEqual(pkt.id, 123) + self.assertEqual(pkt.encode(), '2123["foo"]') + + def test_decode_id(self): + pkt = packet.Packet(encoded_packet='2123["foo"]') + self.assertEqual(pkt.id, 123) + self.assertEqual(pkt.encode(), '2123["foo"]') + + def test_encode_namespace_and_id(self): + pkt = packet.Packet(packet_type=packet.EVENT, + data=[six.text_type('foo')], namespace='/bar', + id=123) + self.assertEqual(pkt.namespace, '/bar') + self.assertEqual(pkt.id, 123) + self.assertEqual(pkt.encode(), '2/bar,123["foo"]') + + def test_decode_namespace_and_id(self): + pkt = packet.Packet(encoded_packet='2/bar,123["foo"]') + self.assertEqual(pkt.namespace, '/bar') + self.assertEqual(pkt.id, 123) + self.assertEqual(pkt.encode(), '2/bar,123["foo"]') + + def test_encode_many_binary(self): + pkt = packet.Packet(packet_type=packet.EVENT, + data={'a': six.text_type('123'), + 'b': b'456', + 'c': [b'789', 123]}) + self.assertEqual(pkt.packet_type, packet.BINARY_EVENT) + ep = pkt.encode() + self.assertEqual(len(ep), 3) + self.assertIn(b'456', ep) + self.assertIn(b'789', ep) + + def test_encode_many_binary_ack(self): + pkt = packet.Packet(packet_type=packet.ACK, + data={'a': six.text_type('123'), + 'b': b'456', + 'c': [b'789', 123]}) + self.assertEqual(pkt.packet_type, packet.BINARY_ACK) + ep = pkt.encode() + self.assertEqual(len(ep), 3) + self.assertIn(b'456', ep) + self.assertIn(b'789', ep) + + def test_decode_many_binary(self): + pkt = packet.Packet(encoded_packet=( + '52-{"a":"123","b":{"_placeholder":true,"num":0},' + '"c":[{"_placeholder":true,"num":1},123]}')) + pkt.reconstruct_binary([b'456', b'789']) + self.assertEqual(pkt.packet_type, packet.BINARY_EVENT) + self.assertEqual(pkt.data['a'], '123') + self.assertEqual(pkt.data['b'], b'456') + self.assertEqual(pkt.data['c'], [b'789', 123]) + + def test_decode_many_binary_ack(self): + pkt = packet.Packet(encoded_packet=( + '62-{"a":"123","b":{"_placeholder":true,"num":0},' + '"c":[{"_placeholder":true,"num":1},123]}')) + pkt.reconstruct_binary([b'456', b'789']) + self.assertEqual(pkt.packet_type, packet.BINARY_ACK) + self.assertEqual(pkt.data['a'], '123') + self.assertEqual(pkt.data['b'], b'456') + self.assertEqual(pkt.data['c'], [b'789', 123]) diff --git a/tests/test_server.py b/tests/test_server.py new file mode 100755 index 0000000..b4fcd5c --- /dev/null +++ b/tests/test_server.py @@ -0,0 +1,340 @@ +import logging +import unittest + +import six +if six.PY3: + from unittest import mock +else: + import mock + +from socketio import server + + +@mock.patch('engineio.Server') +class TestServer(unittest.TestCase): + def test_create(self, eio): + mgr = mock.MagicMock() + s = server.Server({'foo': 'bar'}, mgr, binary=True) + mgr.assert_called_once_with(s) + eio.assert_called_once_with(**{'foo': 'bar'}) + self.assertEqual(s.eio.on.call_count, 3) + self.assertEqual(s.binary, True) + + def test_on_event(self, eio): + s = server.Server() + + @s.on('connect') + def foo(): + pass + + def bar(): + pass + s.on('disconnect', bar) + s.on('disconnect', bar, namespace='/foo') + + self.assertEqual(s.handlers['/']['connect'], foo) + self.assertEqual(s.handlers['/']['disconnect'], bar) + self.assertEqual(s.handlers['/foo']['disconnect'], bar) + + def test_emit(self, eio): + mgr = mock.MagicMock() + s = server.Server(client_manager_class=mgr) + s.emit('my event', {'foo': 'bar'}, 'room', '123', namespace='/foo', + callback='cb') + s.manager.emit.assert_called_once_with('my event', {'foo': 'bar'}, + '/foo', 'room', '123', 'cb') + + def test_emit_default_namespace(self, eio): + mgr = mock.MagicMock() + s = server.Server(client_manager_class=mgr) + s.emit('my event', {'foo': 'bar'}, 'room', '123', callback='cb') + s.manager.emit.assert_called_once_with('my event', {'foo': 'bar'}, '/', + 'room', '123', 'cb') + + def test_send(self, eio): + mgr = mock.MagicMock() + s = server.Server(client_manager_class=mgr) + s.send('foo', 'room', '123', namespace='/foo', callback='cb') + s.manager.emit.assert_called_once_with('message', 'foo', '/foo', + 'room', '123', 'cb') + + def test_enter_room(self, eio): + mgr = mock.MagicMock() + s = server.Server(client_manager_class=mgr) + s.enter_room('123', 'room', namespace='/foo') + s.manager.enter_room.assert_called_once_with('123', '/foo', 'room') + + def test_enter_room_default_namespace(self, eio): + mgr = mock.MagicMock() + s = server.Server(client_manager_class=mgr) + s.enter_room('123', 'room') + s.manager.enter_room.assert_called_once_with('123', '/', 'room') + + def test_leave_room(self, eio): + mgr = mock.MagicMock() + s = server.Server(client_manager_class=mgr) + s.leave_room('123', 'room', namespace='/foo') + s.manager.leave_room.assert_called_once_with('123', '/foo', 'room') + + def test_leave_room_default_namespace(self, eio): + mgr = mock.MagicMock() + s = server.Server(client_manager_class=mgr) + s.leave_room('123', 'room') + s.manager.leave_room.assert_called_once_with('123', '/', 'room') + + def test_close_room(self, eio): + mgr = mock.MagicMock() + s = server.Server(client_manager_class=mgr) + s.close_room('room', namespace='/foo') + s.manager.close_room.assert_called_once_with('/foo', 'room') + + def test_close_room_default_namespace(self, eio): + mgr = mock.MagicMock() + s = server.Server(client_manager_class=mgr) + s.close_room('room') + s.manager.close_room.assert_called_once_with('/', 'room') + + def test_handle_request(self, eio): + s = server.Server() + s.handle_request('environ', 'start_response') + s.eio.handle_request.assert_called_once_with('environ', + 'start_response') + + def test_emit_internal(self, eio): + s = server.Server() + s._emit_internal('123', 'my event', 'my data', namespace='/foo') + s.eio.send.assert_called_once_with('123', + '2/foo,["my event","my data"]', + binary=False) + + def test_emit_internal_with_callback(self, eio): + s = server.Server() + s._emit_internal('123', 'my event', 'my data', namespace='/foo', + callback='cb') + s.eio.send.assert_called_once_with('123', + '2/foo,1["my event","my data"]', + binary=False) + + def test_emit_internal_default_namespace(self, eio): + s = server.Server() + s._emit_internal('123', 'my event', 'my data') + s.eio.send.assert_called_once_with('123', '2["my event","my data"]', + binary=False) + + def test_emit_internal_binary(self, eio): + s = server.Server(binary=True) + s._emit_internal('123', u'my event', b'my binary data') + self.assertEqual(s.eio.send.call_count, 2) + + def test_handle_connect(self, eio): + mgr = mock.MagicMock() + s = server.Server(client_manager_class=mgr) + handler = mock.MagicMock() + s.on('connect', handler) + s._handle_eio_connect('123', 'environ') + handler.assert_called_once_with('123', 'environ') + s.manager.connect.assert_called_once_with('123', '/') + s.eio.send.assert_called_once_with('123', '0', binary=False) + + def test_handle_connect_namespace(self, eio): + mgr = mock.MagicMock() + s = server.Server(client_manager_class=mgr) + handler = mock.MagicMock() + s.on('connect', handler, namespace='/foo') + s._handle_eio_connect('123', 'environ') + s._handle_eio_message('123', '0/foo') + handler.assert_called_once_with('123', 'environ') + s.manager.connect.assert_any_call('123', '/') + s.manager.connect.assert_any_call('123', '/foo') + s.eio.send.assert_any_call('123', '0/foo', binary=False) + + def test_handle_connect_rejected(self, eio): + mgr = mock.MagicMock() + s = server.Server(client_manager_class=mgr) + handler = mock.MagicMock(return_value=False) + s.on('connect', handler) + s._handle_eio_connect('123', 'environ') + handler.assert_called_once_with('123', 'environ') + self.assertEqual(s.manager.connect.call_count, 0) + s.eio.send.assert_called_once_with('123', '4', binary=False) + + def test_handle_connect_namespace_rejected(self, eio): + mgr = mock.MagicMock() + s = server.Server(client_manager_class=mgr) + handler = mock.MagicMock(return_value=False) + s.on('connect', handler, namespace='/foo') + s._handle_eio_connect('123', 'environ') + s._handle_eio_message('123', '0/foo') + self.assertEqual(s.manager.connect.call_count, 1) + s.eio.send.assert_any_call('123', '4/foo', binary=False) + + def test_handle_disconnect(self, eio): + mgr = mock.MagicMock() + s = server.Server(client_manager_class=mgr) + handler = mock.MagicMock() + s.on('disconnect', handler) + s._handle_eio_connect('123', 'environ') + s._handle_eio_disconnect('123') + handler.assert_called_once_with('123') + s.manager.disconnect.assert_called_once_with('123', '/') + self.assertEqual(s.environ, {}) + + def test_handle_disconnect_namespace(self, eio): + mgr = mock.MagicMock() + s = server.Server(client_manager_class=mgr) + s.manager.get_namespaces = mock.MagicMock(return_value=['/', '/foo']) + handler = mock.MagicMock() + s.on('disconnect', handler) + handler_namespace = mock.MagicMock() + s.on('disconnect', handler_namespace, namespace='/foo') + s._handle_eio_connect('123', 'environ') + s._handle_eio_message('123', '0/foo') + s._handle_eio_disconnect('123') + handler.assert_called_once_with('123') + handler_namespace.assert_called_once_with('123') + self.assertEqual(s.environ, {}) + + def test_handle_disconnect_only_namespace(self, eio): + mgr = mock.MagicMock() + s = server.Server(client_manager_class=mgr) + s.manager.get_namespaces = mock.MagicMock(return_value=['/', '/foo']) + handler = mock.MagicMock() + s.on('disconnect', handler) + handler_namespace = mock.MagicMock() + s.on('disconnect', handler_namespace, namespace='/foo') + s._handle_eio_connect('123', 'environ') + s._handle_eio_message('123', '0/foo') + s._handle_eio_message('123', '1/foo') + self.assertEqual(handler.call_count, 0) + handler_namespace.assert_called_once_with('123') + self.assertEqual(s.environ, {'123': 'environ'}) + + def test_handle_disconnect_unknown_client(self, eio): + mgr = mock.MagicMock() + s = server.Server(client_manager_class=mgr) + s._handle_eio_disconnect('123') + + def test_handle_event(self, eio): + s = server.Server() + handler = mock.MagicMock() + s.on('my message', handler) + s._handle_eio_message('123', '2["my message","a","b","c"]') + handler.assert_called_once_with('123', 'a', 'b', 'c') + + def test_handle_event_with_namespace(self, eio): + s = server.Server() + handler = mock.MagicMock() + s.on('my message', handler, namespace='/foo') + s._handle_eio_message('123', '2/foo,["my message","a","b","c"]') + handler.assert_called_once_with('123', 'a', 'b', 'c') + + def test_handle_event_binary(self, eio): + s = server.Server() + handler = mock.MagicMock() + s.on('my message', handler) + s._handle_eio_message('123', '52-["my message","a",' + '{"_placeholder":true,"num":1},' + '{"_placeholder":true,"num":0}]') + self.assertEqual(s._attachment_count, 2) + s._handle_eio_message('123', b'foo') + self.assertEqual(s._attachment_count, 1) + s._handle_eio_message('123', b'bar') + self.assertEqual(s._attachment_count, 0) + handler.assert_called_once_with('123', 'a', b'bar', b'foo') + + def test_handle_event_binary_ack(self, eio): + s = server.Server() + s._handle_eio_message('123', '61-1["my message","a",' + '{"_placeholder":true,"num":0}]') + self.assertEqual(s._attachment_count, 1) + self.assertRaises(ValueError, s._handle_eio_message, '123', b'foo') + + def test_handle_event_with_ack(self, eio): + s = server.Server() + handler = mock.MagicMock(return_value='foo') + s.on('my message', handler) + s._handle_eio_message('123', '21000["my message","foo"]') + handler.assert_called_once_with('123', 'foo') + s.eio.send.assert_called_once_with('123', '31000["foo"]', + binary=False) + + def test_handle_event_with_ack_tuple(self, eio): + mgr = mock.MagicMock() + s = server.Server(client_manager_class=mgr) + handler = mock.MagicMock(return_value=(1, '2', True)) + s.on('my message', handler) + s._handle_eio_message('123', '21000["my message","a","b","c"]') + handler.assert_called_once_with('123', 'a', 'b', 'c') + s.eio.send.assert_called_once_with('123', '31000[1,"2",true]', + binary=False) + + def test_handle_event_with_ack_list(self, eio): + mgr = mock.MagicMock() + s = server.Server(client_manager_class=mgr) + handler = mock.MagicMock(return_value=[1, '2', True]) + s.on('my message', handler) + s._handle_eio_message('123', '21000["my message","a","b","c"]') + handler.assert_called_once_with('123', 'a', 'b', 'c') + s.eio.send.assert_called_once_with('123', '31000[1,"2",true]', + binary=False) + + def test_handle_event_with_ack_binary(self, eio): + mgr = mock.MagicMock() + s = server.Server(client_manager_class=mgr, binary=True) + handler = mock.MagicMock(return_value=b'foo') + s.on('my message', handler) + s._handle_eio_message('123', '21000["my message","foo"]') + handler.assert_any_call('123', 'foo') + + def test_handle_error_packet(self, eio): + s = server.Server() + self.assertRaises(ValueError, s._handle_eio_message, '123', '4') + + def test_handle_invalid_packet(self, eio): + s = server.Server() + self.assertRaises(ValueError, s._handle_eio_message, '123', '9') + + def test_send_with_ack(self, eio): + s = server.Server() + cb = mock.MagicMock() + s._handle_eio_connect('123', 'environ') + s._emit_internal('123', 'my event', ['foo'], callback=cb) + s._emit_internal('123', 'my event', ['bar'], callback=cb) + s._handle_eio_message('123', '31["foo",2]') + cb.assert_called_once_with('foo', 2) + self.assertIn('123', s.callbacks) + s._handle_disconnect('123', '/') + self.assertNotIn('123', s.callbacks) + + def test_send_with_ack_namespace(self, eio): + s = server.Server() + cb = mock.MagicMock() + s._handle_eio_connect('123', 'environ') + s._handle_eio_message('123', '0/foo') + s._emit_internal('123', 'my event', ['foo'], namespace='/foo', + callback=cb) + s._handle_eio_message('123', '3/foo,1["foo",2]') + cb.assert_called_once_with('foo', 2) + self.assertIn('/foo', s.callbacks['123']) + s._handle_eio_disconnect('123') + self.assertNotIn('123', s.callbacks) + + def test_invalid_callback(self, eio): + s = server.Server() + cb = mock.MagicMock() + s._handle_eio_connect('123', 'environ') + s._emit_internal('123', 'my event', ['foo'], callback=cb) + self.assertRaises(ValueError, s._handle_eio_message, '124', + '31["foo",2]') + self.assertRaises(ValueError, s._handle_eio_message, '123', + '3/foo,1["foo",2]') + self.assertRaises(ValueError, s._handle_eio_message, '123', + '32["foo",2]') + + def test_logger(self, eio): + s = server.Server(logger=False) + self.assertEqual(s.logger.getEffectiveLevel(), logging.ERROR) + s = server.Server(logger=True) + self.assertEqual(s.logger.getEffectiveLevel(), logging.INFO) + s = server.Server(logger='foo') + self.assertEqual(s.logger, 'foo')