commit
aa2e146a60
33 changed files with 3853 additions and 0 deletions
@ -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 |
@ -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. |
@ -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 |
@ -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 <target>' where <target> is one of" |
||||
|
@echo " html to make standalone HTML files" |
||||
|
@echo " dirhtml to make HTML files named index.html in directories" |
||||
|
@echo " singlehtml to make a single large HTML file" |
||||
|
@echo " pickle to make pickle files" |
||||
|
@echo " json to make JSON files" |
||||
|
@echo " htmlhelp to make HTML files and a HTML help project" |
||||
|
@echo " qthelp to make HTML files and a qthelp project" |
||||
|
@echo " applehelp to make an Apple Help Book" |
||||
|
@echo " devhelp to make HTML files and a Devhelp project" |
||||
|
@echo " epub to make an epub" |
||||
|
@echo " 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." |
@ -0,0 +1,12 @@ |
|||||
|
<!DOCTYPE html> |
||||
|
<html> |
||||
|
<head> |
||||
|
<title>python-socketio documentation</title> |
||||
|
<meta http-equiv="refresh" content="0; URL=http://python-socketio.readthedocs.org/"> |
||||
|
<meta name="keywords" content="automatic redirection"> |
||||
|
</head> |
||||
|
<body> |
||||
|
The python-socketio documentation is available at <a href="http://python-socketio.readthedocs.org">Read the Docs</a>. |
||||
|
If your browser does not automatically redirect you, please <a href="http://python-socketio.readthedocs.org">click here</a>. |
||||
|
</body> |
||||
|
</html> |
After Width: | Height: | Size: 10 KiB |
@ -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. |
@ -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 |
@ -0,0 +1,24 @@ |
|||||
|
{%- extends "basic/layout.html" %} |
||||
|
{%- block extrahead %} |
||||
|
{{ super() }} |
||||
|
{% if theme_touch_icon %} |
||||
|
<link rel="apple-touch-icon" href="{{ pathto('_static/' ~ theme_touch_icon, 1) }}" /> |
||||
|
{% endif %} |
||||
|
<meta name="viewport" content="width=device-width, initial-scale=0.9, maximum-scale=0.9"> |
||||
|
{% endblock %} |
||||
|
{%- block relbar2 %}{% endblock %} |
||||
|
{% block header %} |
||||
|
{{ super() }} |
||||
|
{% if pagename == 'index' %} |
||||
|
<div class=indexwrapper> |
||||
|
{% endif %} |
||||
|
{% endblock %} |
||||
|
{%- block footer %} |
||||
|
<div class="footer"> |
||||
|
© Copyright {{ copyright }}. |
||||
|
Created using <a href="http://sphinx.pocoo.org/">Sphinx</a>. |
||||
|
</div> |
||||
|
{% if pagename == 'index' %} |
||||
|
</div> |
||||
|
{% endif %} |
||||
|
{%- endblock %} |
@ -0,0 +1,19 @@ |
|||||
|
<h3>Related Topics</h3> |
||||
|
<ul> |
||||
|
<li><a href="{{ pathto(master_doc) }}">Documentation overview</a><ul> |
||||
|
{%- for parent in parents %} |
||||
|
<li><a href="{{ parent.link|e }}">{{ parent.title }}</a><ul> |
||||
|
{%- endfor %} |
||||
|
{%- if prev %} |
||||
|
<li>Previous: <a href="{{ prev.link|e }}" title="{{ _('previous chapter') |
||||
|
}}">{{ prev.title }}</a></li> |
||||
|
{%- endif %} |
||||
|
{%- if next %} |
||||
|
<li>Next: <a href="{{ next.link|e }}" title="{{ _('next chapter') |
||||
|
}}">{{ next.title }}</a></li> |
||||
|
{%- endif %} |
||||
|
{%- for parent in parents %} |
||||
|
</ul></li> |
||||
|
{%- endfor %} |
||||
|
</ul></li> |
||||
|
</ul> |
@ -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; |
||||
|
} |
@ -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 = |
@ -0,0 +1,22 @@ |
|||||
|
{% extends "basic/layout.html" %} |
||||
|
{% block header %} |
||||
|
{{ super() }} |
||||
|
{% if pagename == 'index' %} |
||||
|
<div class=indexwrapper> |
||||
|
{% endif %} |
||||
|
{% endblock %} |
||||
|
{% block footer %} |
||||
|
{% if pagename == 'index' %} |
||||
|
</div> |
||||
|
{% endif %} |
||||
|
{% endblock %} |
||||
|
{# do not display relbars #} |
||||
|
{% block relbar1 %}{% endblock %} |
||||
|
{% block relbar2 %} |
||||
|
{% if theme_github_fork %} |
||||
|
<a href="http://github.com/{{ theme_github_fork }}"><img style="position: fixed; top: 0; right: 0; border: 0;" |
||||
|
src="http://s3.amazonaws.com/github/ribbons/forkme_right_darkblue_121621.png" alt="Fork me on GitHub" /></a> |
||||
|
{% endif %} |
||||
|
{% endblock %} |
||||
|
{% block sidebar1 %}{% endblock %} |
||||
|
{% block sidebar2 %}{% endblock %} |
@ -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; |
||||
|
} |
@ -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 = '' |
@ -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' |
||||
|
} |
@ -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 |
||||
|
# "<project> v<release> documentation". |
||||
|
#html_title = None |
||||
|
|
||||
|
# A shorter title for the navigation bar. Default is the same as html_title. |
||||
|
#html_short_title = None |
||||
|
|
||||
|
# The name of an image file (relative to this directory) to place at the top |
||||
|
# of the sidebar. |
||||
|
#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 <link> tag referring to it. The value of this option must be the |
||||
|
# base URL from which the finished HTML is served. |
||||
|
#html_use_opensearch = '' |
||||
|
|
||||
|
# 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 |
@ -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 <https://github.com/Automattic/socket.io-client>`_ library. |
||||
|
- Compatible with Python 2.7 and Python 3.3+. |
||||
|
- Based on `Eventlet <http://eventlet.net/>`_, 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 <https://github.com/Automattic/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: |
@ -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 ^<target^>` where ^<target^> is one of |
||||
|
echo. html to make standalone HTML files |
||||
|
echo. dirhtml to make HTML files named index.html in directories |
||||
|
echo. singlehtml to make a single large HTML file |
||||
|
echo. pickle to make pickle files |
||||
|
echo. json to make JSON files |
||||
|
echo. htmlhelp to make HTML files and a HTML help project |
||||
|
echo. qthelp to make HTML files and a qthelp project |
||||
|
echo. devhelp to make HTML files and a Devhelp project |
||||
|
echo. epub to make an epub |
||||
|
echo. 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 |
@ -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) |
@ -0,0 +1,4 @@ |
|||||
|
eventlet==0.17.4 |
||||
|
greenlet==0.4.7 |
||||
|
python-engineio==0.1.0 |
||||
|
six==1.9.0 |
@ -0,0 +1,85 @@ |
|||||
|
<!DOCTYPE HTML> |
||||
|
<html> |
||||
|
<head> |
||||
|
<title>Flask-SocketIO Test</title> |
||||
|
<script type="text/javascript" src="//code.jquery.com/jquery-1.4.2.min.js"></script> |
||||
|
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/socket.io/1.3.5/socket.io.min.js"></script> |
||||
|
<script type="text/javascript" charset="utf-8"> |
||||
|
$(document).ready(function(){ |
||||
|
namespace = '/test'; |
||||
|
var socket = io.connect('http://' + document.domain + ':' + location.port + namespace); |
||||
|
|
||||
|
socket.on('my response', function(msg) { |
||||
|
$('#log').append('<br>Received: ' + msg.data); |
||||
|
}); |
||||
|
socket.on('connect', function() { |
||||
|
socket.emit('my event', {data: 'I\'m connected!'}); |
||||
|
}); |
||||
|
|
||||
|
// event handler for server sent data |
||||
|
// the data is displayed in the "Received" section of the page |
||||
|
// handlers for the different forms in the page |
||||
|
// these send data to the server in a variety of ways |
||||
|
$('form#emit').submit(function(event) { |
||||
|
socket.emit('my event', {data: $('#emit_data').val()}); |
||||
|
return false; |
||||
|
}); |
||||
|
$('form#broadcast').submit(function(event) { |
||||
|
socket.emit('my broadcast event', {data: $('#broadcast_data').val()}); |
||||
|
return false; |
||||
|
}); |
||||
|
$('form#join').submit(function(event) { |
||||
|
socket.emit('join', {room: $('#join_room').val()}); |
||||
|
return false; |
||||
|
}); |
||||
|
$('form#leave').submit(function(event) { |
||||
|
socket.emit('leave', {room: $('#leave_room').val()}); |
||||
|
return false; |
||||
|
}); |
||||
|
$('form#send_room').submit(function(event) { |
||||
|
socket.emit('my room event', {room: $('#room_name').val(), data: $('#room_data').val()}); |
||||
|
return false; |
||||
|
}); |
||||
|
$('form#close').submit(function(event) { |
||||
|
socket.emit('close room', {room: $('#close_room').val()}); |
||||
|
return false; |
||||
|
}); |
||||
|
$('form#disconnect').submit(function(event) { |
||||
|
socket.emit('disconnect request'); |
||||
|
return false; |
||||
|
}); |
||||
|
}); |
||||
|
</script> |
||||
|
</head> |
||||
|
<body> |
||||
|
<h1>Flask-SocketIO Test</h1> |
||||
|
<h2>Send:</h2> |
||||
|
<form id="emit" method="POST" action='#'> |
||||
|
<input type="text" name="emit_data" id="emit_data" placeholder="Message"> |
||||
|
<input type="submit" value="Echo"> |
||||
|
</form> |
||||
|
<form id="broadcast" method="POST" action='#'> |
||||
|
<input type="text" name="broadcast_data" id="broadcast_data" placeholder="Message"> |
||||
|
<input type="submit" value="Broadcast"> |
||||
|
</form> |
||||
|
<form id="join" method="POST" action='#'> |
||||
|
<input type="text" name="join_room" id="join_room" placeholder="Room Name"> |
||||
|
<input type="submit" value="Join Room"> |
||||
|
</form> |
||||
|
<form id="leave" method="POST" action='#'> |
||||
|
<input type="text" name="leave_room" id="leave_room" placeholder="Room Name"> |
||||
|
<input type="submit" value="Leave Room"> |
||||
|
</form> |
||||
|
<form id="send_room" method="POST" action='#'> |
||||
|
<input type="text" name="room_name" id="room_name" placeholder="Room Name"> |
||||
|
<input type="text" name="room_data" id="room_data" placeholder="Message"> |
||||
|
<input type="submit" value="Send to Room"> |
||||
|
</form> |
||||
|
<form id="close" method="POST" action="#"> |
||||
|
<input type="text" name="close_room" id="close_room" placeholder="Room Name"> |
||||
|
<input type="submit" value="Close Room"> |
||||
|
</form> |
||||
|
<h2>Receive:</h2> |
||||
|
<div><p id="log"></p></div> |
||||
|
</body> |
||||
|
</html> |
@ -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='[email protected]', |
||||
|
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' |
||||
|
] |
||||
|
) |
@ -0,0 +1,4 @@ |
|||||
|
from .middleware import Middleware |
||||
|
from .server import Server |
||||
|
|
||||
|
__all__ = [Middleware, Server] |
@ -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 = [] |
@ -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) |
@ -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 |
@ -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, '/') |
@ -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') |
@ -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')]) |
@ -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]) |
@ -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') |
Loading…
Reference in new issue