From 87fb830bd803809a6577332a838285b4bc22dce5 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Sun, 25 Nov 2018 15:18:36 +0000 Subject: [PATCH] improved documentation --- docs/Makefile | 191 +---- docs/_static/README.md | 1 + docs/_static/index.html | 12 - docs/_static/logo.png | Bin 55497 -> 0 bytes docs/_themes/LICENSE | 37 - docs/_themes/README | 31 - docs/_themes/flask/layout.html | 24 - docs/_themes/flask/relations.html | 19 - docs/_themes/flask/static/flasky.css_t | 577 -------------- docs/_themes/flask/theme.conf | 9 - docs/_themes/flask_small/layout.html | 22 - docs/_themes/flask_small/static/flasky.css_t | 291 ------- docs/_themes/flask_small/theme.conf | 10 - docs/_themes/flask_theme_support.py | 86 --- docs/api.rst | 88 +++ docs/conf.py | 285 +++---- docs/deployment.rst | 277 +++++++ docs/guide.rst | 346 +++++++++ docs/index.rst | 765 +------------------ docs/intro.rst | 148 ++++ docs/make.bat | 298 +------- socketio/asgi.py | 2 + 22 files changed, 1013 insertions(+), 2506 deletions(-) create mode 100644 docs/_static/README.md delete mode 100644 docs/_static/index.html delete mode 100644 docs/_static/logo.png delete mode 100644 docs/_themes/LICENSE delete mode 100644 docs/_themes/README delete mode 100644 docs/_themes/flask/layout.html delete mode 100644 docs/_themes/flask/relations.html delete mode 100644 docs/_themes/flask/static/flasky.css_t delete mode 100644 docs/_themes/flask/theme.conf delete mode 100644 docs/_themes/flask_small/layout.html delete mode 100644 docs/_themes/flask_small/static/flasky.css_t delete mode 100644 docs/_themes/flask_small/theme.conf delete mode 100644 docs/_themes/flask_theme_support.py create mode 100644 docs/api.rst create mode 100644 docs/deployment.rst create mode 100644 docs/guide.rst create mode 100644 docs/intro.rst diff --git a/docs/Makefile b/docs/Makefile index 68ceda8..298ea9e 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,192 +1,19 @@ -# Makefile for Sphinx documentation +# Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build -PAPER = +SOURCEDIR = . 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 - +# Put it first so that "make" without argument is like "make help". help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " 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." + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -xml: - $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml - @echo - @echo "Build finished. The XML files are in $(BUILDDIR)/xml." +.PHONY: help Makefile -pseudoxml: - $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml - @echo - @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/docs/_static/README.md b/docs/_static/README.md new file mode 100644 index 0000000..0d94aa7 --- /dev/null +++ b/docs/_static/README.md @@ -0,0 +1 @@ +Place static files used by the documentation here. diff --git a/docs/_static/index.html b/docs/_static/index.html deleted file mode 100644 index d2f10fe..0000000 --- a/docs/_static/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - python-socketio documentation - - - - - The python-socketio documentation is available at Read the Docs. - If your browser does not automatically redirect you, please click here. - - diff --git a/docs/_static/logo.png b/docs/_static/logo.png deleted file mode 100644 index bd27187c0c824245393e5bf2c25f392e8d3ffb23..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 55497 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 GC;tQNV!%HD diff --git a/docs/_themes/LICENSE b/docs/_themes/LICENSE deleted file mode 100644 index 8daab7e..0000000 --- a/docs/_themes/LICENSE +++ /dev/null @@ -1,37 +0,0 @@ -Copyright (c) 2010 by Armin Ronacher. - -Some rights reserved. - -Redistribution and use in source and binary forms of the theme, with or -without modification, are permitted provided that the following conditions -are met: - -* Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - -* The names of the contributors may not be used to endorse or - promote products derived from this software without specific - prior written permission. - -We kindly ask you to only use these themes in an unmodified manner just -for Flask and Flask-related products, not for unrelated projects. If you -like the visual style and want to use it for your own projects, please -consider making some larger changes to the themes (such as changing -font faces, sizes, colors or margins). - -THIS THEME IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -ARISING IN ANY WAY OUT OF THE USE OF THIS THEME, EVEN IF ADVISED OF THE -POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/_themes/README b/docs/_themes/README deleted file mode 100644 index b3292bd..0000000 --- a/docs/_themes/README +++ /dev/null @@ -1,31 +0,0 @@ -Flask Sphinx Styles -=================== - -This repository contains sphinx styles for Flask and Flask related -projects. To use this style in your Sphinx documentation, follow -this guide: - -1. put this folder as _themes into your docs folder. Alternatively - you can also use git submodules to check out the contents there. -2. add this to your conf.py: - - sys.path.append(os.path.abspath('_themes')) - html_theme_path = ['_themes'] - html_theme = 'flask' - -The following themes exist: - -- 'flask' - the standard flask documentation theme for large - projects -- 'flask_small' - small one-page theme. Intended to be used by - very small addon libraries for flask. - -The following options exist for the flask_small theme: - - [options] - index_logo = '' filename of a picture in _static - to be used as replacement for the - h1 in the index.rst file. - index_logo_height = 120px height of the index logo - github_fork = '' repository name on github for the - "fork me" badge diff --git a/docs/_themes/flask/layout.html b/docs/_themes/flask/layout.html deleted file mode 100644 index 19c43fb..0000000 --- a/docs/_themes/flask/layout.html +++ /dev/null @@ -1,24 +0,0 @@ -{%- extends "basic/layout.html" %} -{%- block extrahead %} - {{ super() }} - {% if theme_touch_icon %} - - {% endif %} - -{% endblock %} -{%- block relbar2 %}{% endblock %} -{% block header %} - {{ super() }} - {% if pagename == 'index' %} -
- {% endif %} -{% endblock %} -{%- block footer %} - - {% if pagename == 'index' %} -
- {% endif %} -{%- endblock %} diff --git a/docs/_themes/flask/relations.html b/docs/_themes/flask/relations.html deleted file mode 100644 index 3bbcde8..0000000 --- a/docs/_themes/flask/relations.html +++ /dev/null @@ -1,19 +0,0 @@ -

Related Topics

- diff --git a/docs/_themes/flask/static/flasky.css_t b/docs/_themes/flask/static/flasky.css_t deleted file mode 100644 index 5906e75..0000000 --- a/docs/_themes/flask/static/flasky.css_t +++ /dev/null @@ -1,577 +0,0 @@ -/* - * flasky.css_t - * ~~~~~~~~~~~~ - * - * :copyright: Copyright 2010 by Armin Ronacher. - * :license: Flask Design License, see LICENSE for details. - */ - -{% set page_width = '940px' %} -{% set sidebar_width = '220px' %} - -@import url("basic.css"); - -/* -- page layout ----------------------------------------------------------- */ - -body { - font-family: 'Georgia', serif; - font-size: 17px; - background-color: white; - color: #000; - margin: 0; - padding: 0; -} - -div.document { - width: {{ page_width }}; - margin: 30px auto 0 auto; -} - -div.documentwrapper { - float: left; - width: 100%; -} - -div.bodywrapper { - margin: 0 0 0 {{ sidebar_width }}; -} - -div.sphinxsidebar { - width: {{ sidebar_width }}; -} - -hr { - border: 1px solid #B1B4B6; -} - -div.body { - background-color: #ffffff; - color: #3E4349; - padding: 0 30px 0 30px; -} - -img.floatingflask { - padding: 0 0 10px 10px; - float: right; -} - -div.footer { - width: {{ page_width }}; - margin: 20px auto 30px auto; - font-size: 14px; - color: #888; - text-align: right; -} - -div.footer a { - color: #888; -} - -div.related { - display: none; -} - -div.sphinxsidebar a { - color: #444; - text-decoration: none; - border-bottom: 1px dotted #999; -} - -div.sphinxsidebar a:hover { - border-bottom: 1px solid #999; -} - -div.sphinxsidebar { - font-size: 14px; - line-height: 1.5; -} - -div.sphinxsidebarwrapper { - padding: 18px 10px; -} - -div.sphinxsidebarwrapper p.logo { - padding: 0 0 20px 0; - margin: 0; - text-align: center; -} - -div.sphinxsidebar h3, -div.sphinxsidebar h4 { - font-family: 'Garamond', 'Georgia', serif; - color: #444; - font-size: 24px; - font-weight: normal; - margin: 0 0 5px 0; - padding: 0; -} - -div.sphinxsidebar h4 { - font-size: 20px; -} - -div.sphinxsidebar h3 a { - color: #444; -} - -div.sphinxsidebar p.logo a, -div.sphinxsidebar h3 a, -div.sphinxsidebar p.logo a:hover, -div.sphinxsidebar h3 a:hover { - border: none; -} - -div.sphinxsidebar p { - color: #555; - margin: 10px 0; -} - -div.sphinxsidebar ul { - margin: 10px 0; - padding: 0; - color: #000; -} - -div.sphinxsidebar input { - border: 1px solid #ccc; - font-family: 'Georgia', serif; - font-size: 1em; -} - -/* -- body styles ----------------------------------------------------------- */ - -a { - color: #004B6B; - text-decoration: underline; -} - -a:hover { - color: #6D4100; - text-decoration: underline; -} - -div.body h1, -div.body h2, -div.body h3, -div.body h4, -div.body h5, -div.body h6 { - font-family: 'Garamond', 'Georgia', serif; - font-weight: normal; - margin: 30px 0px 10px 0px; - padding: 0; -} - -{% if theme_index_logo %} -div.indexwrapper h1 { - text-indent: -999999px; - background: url({{ theme_index_logo }}) no-repeat center center; - height: {{ theme_index_logo_height }}; -} -{% endif %} -div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; } -div.body h2 { font-size: 180%; } -div.body h3 { font-size: 150%; } -div.body h4 { font-size: 130%; } -div.body h5 { font-size: 100%; } -div.body h6 { font-size: 100%; } - -a.headerlink { - color: #ddd; - padding: 0 4px; - text-decoration: none; -} - -a.headerlink:hover { - color: #444; - background: #eaeaea; -} - -div.body p, div.body dd, div.body li { - line-height: 1.4em; -} - -div.admonition { - background: #fafafa; - margin: 20px -30px; - padding: 10px 30px; - border-top: 1px solid #ccc; - border-bottom: 1px solid #ccc; -} - -div.admonition tt.xref, div.admonition a tt { - border-bottom: 1px solid #fafafa; -} - -dd div.admonition { - margin-left: -60px; - padding-left: 60px; -} - -div.admonition p.admonition-title { - font-family: 'Garamond', 'Georgia', serif; - font-weight: normal; - font-size: 24px; - margin: 0 0 10px 0; - padding: 0; - line-height: 1; -} - -div.admonition p.last { - margin-bottom: 0; -} - -div.highlight { - background-color: white; -} - -dt:target, .highlight { - background: #FAF3E8; -} - -div.note { - background-color: #eee; - border: 1px solid #ccc; -} - -div.seealso { - background-color: #ffc; - border: 1px solid #ff6; -} - -div.topic { - background-color: #eee; -} - -p.admonition-title { - display: inline; -} - -p.admonition-title:after { - content: ":"; -} - -pre, tt { - font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; - font-size: 0.9em; -} - -img.screenshot { -} - -tt.descname, tt.descclassname { - font-size: 0.95em; -} - -tt.descname { - padding-right: 0.08em; -} - -img.screenshot { - -moz-box-shadow: 2px 2px 4px #eee; - -webkit-box-shadow: 2px 2px 4px #eee; - box-shadow: 2px 2px 4px #eee; -} - -table.docutils { - border: 1px solid #888; - -moz-box-shadow: 2px 2px 4px #eee; - -webkit-box-shadow: 2px 2px 4px #eee; - box-shadow: 2px 2px 4px #eee; -} - -table.docutils td, table.docutils th { - border: 1px solid #888; - padding: 0.25em 0.7em; -} - -table.field-list, table.footnote { - border: none; - -moz-box-shadow: none; - -webkit-box-shadow: none; - box-shadow: none; -} - -table.footnote { - margin: 15px 0; - width: 100%; - border: 1px solid #eee; - background: #fdfdfd; - font-size: 0.9em; -} - -table.footnote + table.footnote { - margin-top: -15px; - border-top: none; -} - -table.field-list th { - padding: 0 0.8em 0 0; -} - -table.field-list td { - padding: 0; -} - -table.footnote td.label { - width: 0px; - padding: 0.3em 0 0.3em 0.5em; -} - -table.footnote td { - padding: 0.3em 0.5em; -} - -dl { - margin: 0; - padding: 0; -} - -dl dd { - margin-left: 30px; -} - -blockquote { - margin: 0 0 0 30px; - padding: 0; -} - -ul, ol { - margin: 10px 0 10px 30px; - padding: 0; -} - -pre { - background: #eee; - padding: 7px 30px; - margin: 15px -30px; - line-height: 1.3em; -} - -dl pre, blockquote pre, li pre { - margin-left: -60px; - padding-left: 60px; -} - -dl dl pre { - margin-left: -90px; - padding-left: 90px; -} - -tt { - background-color: #ecf0f3; - color: #222; - /* padding: 1px 2px; */ -} - -tt.xref, a tt { - background-color: #FBFBFB; - border-bottom: 1px solid white; -} - -a.reference { - text-decoration: none; - border-bottom: 1px dotted #004B6B; -} - -a.reference:hover { - border-bottom: 1px solid #6D4100; -} - -a.footnote-reference { - text-decoration: none; - font-size: 0.7em; - vertical-align: top; - border-bottom: 1px dotted #004B6B; -} - -a.footnote-reference:hover { - border-bottom: 1px solid #6D4100; -} - -a:hover tt { - background: #EEE; -} - - -@media screen and (max-width: 870px) { - - div.sphinxsidebar { - display: none; - } - - div.document { - width: 100%; - - } - - div.documentwrapper { - margin-left: 0; - margin-top: 0; - margin-right: 0; - margin-bottom: 0; - } - - div.bodywrapper { - margin-top: 0; - margin-right: 0; - margin-bottom: 0; - margin-left: 0; - } - - ul { - margin-left: 0; - } - - .document { - width: auto; - } - - .footer { - width: auto; - } - - .bodywrapper { - margin: 0; - } - - .footer { - width: auto; - } - - .github { - display: none; - } - - - -} - - - -@media screen and (max-width: 875px) { - - body { - margin: 0; - padding: 20px 30px; - } - - div.documentwrapper { - float: none; - background: white; - } - - div.sphinxsidebar { - display: block; - float: none; - width: 102.5%; - margin: 50px -30px -20px -30px; - padding: 10px 20px; - background: #333; - color: white; - } - - div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p, - div.sphinxsidebar h3 a { - color: white; - } - - div.sphinxsidebar a { - color: #aaa; - } - - div.sphinxsidebar p.logo { - display: none; - } - - div.document { - width: 100%; - margin: 0; - } - - div.related { - display: block; - margin: 0; - padding: 10px 0 20px 0; - } - - div.related ul, - div.related ul li { - margin: 0; - padding: 0; - } - - div.footer { - display: none; - } - - div.bodywrapper { - margin: 0; - } - - div.body { - min-height: 0; - padding: 0; - } - - .rtd_doc_footer { - display: none; - } - - .document { - width: auto; - } - - .footer { - width: auto; - } - - .footer { - width: auto; - } - - .github { - display: none; - } -} - - -/* scrollbars */ - -::-webkit-scrollbar { - width: 6px; - height: 6px; -} - -::-webkit-scrollbar-button:start:decrement, -::-webkit-scrollbar-button:end:increment { - display: block; - height: 10px; -} - -::-webkit-scrollbar-button:vertical:increment { - background-color: #fff; -} - -::-webkit-scrollbar-track-piece { - background-color: #eee; - -webkit-border-radius: 3px; -} - -::-webkit-scrollbar-thumb:vertical { - height: 50px; - background-color: #ccc; - -webkit-border-radius: 3px; -} - -::-webkit-scrollbar-thumb:horizontal { - width: 50px; - background-color: #ccc; - -webkit-border-radius: 3px; -} - -/* misc. */ - -.revsys-inline { - display: none!important; -} \ No newline at end of file diff --git a/docs/_themes/flask/theme.conf b/docs/_themes/flask/theme.conf deleted file mode 100644 index 18c720f..0000000 --- a/docs/_themes/flask/theme.conf +++ /dev/null @@ -1,9 +0,0 @@ -[theme] -inherit = basic -stylesheet = flasky.css -pygments_style = flask_theme_support.FlaskyStyle - -[options] -index_logo = '' -index_logo_height = 120px -touch_icon = diff --git a/docs/_themes/flask_small/layout.html b/docs/_themes/flask_small/layout.html deleted file mode 100644 index aa1716a..0000000 --- a/docs/_themes/flask_small/layout.html +++ /dev/null @@ -1,22 +0,0 @@ -{% extends "basic/layout.html" %} -{% block header %} - {{ super() }} - {% if pagename == 'index' %} -
- {% endif %} -{% endblock %} -{% block footer %} - {% if pagename == 'index' %} -
- {% endif %} -{% endblock %} -{# do not display relbars #} -{% block relbar1 %}{% endblock %} -{% block relbar2 %} - {% if theme_github_fork %} - Fork me on GitHub - {% endif %} -{% endblock %} -{% block sidebar1 %}{% endblock %} -{% block sidebar2 %}{% endblock %} diff --git a/docs/_themes/flask_small/static/flasky.css_t b/docs/_themes/flask_small/static/flasky.css_t deleted file mode 100644 index 802ebaf..0000000 --- a/docs/_themes/flask_small/static/flasky.css_t +++ /dev/null @@ -1,291 +0,0 @@ -/* - * flasky.css_t - * ~~~~~~~~~~~~ - * - * Sphinx stylesheet -- flasky theme based on nature theme. - * - * :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. - * :license: BSD, see LICENSE for details. - * - */ - -@import url("basic.css"); - -/* -- page layout ----------------------------------------------------------- */ - -body { - font-family: 'Georgia', serif; - font-size: 17px; - color: #000; - background: white; - margin: 0; - padding: 0; -} - -div.documentwrapper { - float: left; - width: 100%; -} - -div.bodywrapper { - margin: 40px auto 0 auto; - width: 700px; -} - -hr { - border: 1px solid #B1B4B6; -} - -div.body { - background-color: #ffffff; - color: #3E4349; - padding: 0 30px 30px 30px; -} - -img.floatingflask { - padding: 0 0 10px 10px; - float: right; -} - -div.footer { - text-align: right; - color: #888; - padding: 10px; - font-size: 14px; - width: 650px; - margin: 0 auto 40px auto; -} - -div.footer a { - color: #888; - text-decoration: underline; -} - -div.related { - line-height: 32px; - color: #888; -} - -div.related ul { - padding: 0 0 0 10px; -} - -div.related a { - color: #444; -} - -/* -- body styles ----------------------------------------------------------- */ - -a { - color: #004B6B; - text-decoration: underline; -} - -a:hover { - color: #6D4100; - text-decoration: underline; -} - -div.body { - padding-bottom: 40px; /* saved for footer */ -} - -div.body h1, -div.body h2, -div.body h3, -div.body h4, -div.body h5, -div.body h6 { - font-family: 'Garamond', 'Georgia', serif; - font-weight: normal; - margin: 30px 0px 10px 0px; - padding: 0; -} - -{% if theme_index_logo %} -div.indexwrapper h1 { - text-indent: -999999px; - background: url({{ theme_index_logo }}) no-repeat center center; - height: {{ theme_index_logo_height }}; -} -{% endif %} - -div.body h2 { font-size: 180%; } -div.body h3 { font-size: 150%; } -div.body h4 { font-size: 130%; } -div.body h5 { font-size: 100%; } -div.body h6 { font-size: 100%; } - -a.headerlink { - color: white; - padding: 0 4px; - text-decoration: none; -} - -a.headerlink:hover { - color: #444; - background: #eaeaea; -} - -div.body p, div.body dd, div.body li { - line-height: 1.4em; -} - -div.admonition { - background: #fafafa; - margin: 20px -30px; - padding: 10px 30px; - border-top: 1px solid #ccc; - border-bottom: 1px solid #ccc; -} - -div.admonition p.admonition-title { - font-family: 'Garamond', 'Georgia', serif; - font-weight: normal; - font-size: 24px; - margin: 0 0 10px 0; - padding: 0; - line-height: 1; -} - -div.admonition p.last { - margin-bottom: 0; -} - -div.highlight{ - background-color: white; -} - -dt:target, .highlight { - background: #FAF3E8; -} - -div.note { - background-color: #eee; - border: 1px solid #ccc; -} - -div.seealso { - background-color: #ffc; - border: 1px solid #ff6; -} - -div.topic { - background-color: #eee; -} - -div.warning { - background-color: #ffe4e4; - border: 1px solid #f66; -} - -p.admonition-title { - display: inline; -} - -p.admonition-title:after { - content: ":"; -} - -pre, tt { - font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; - font-size: 0.85em; -} - -img.screenshot { -} - -tt.descname, tt.descclassname { - font-size: 0.95em; -} - -tt.descname { - padding-right: 0.08em; -} - -img.screenshot { - -moz-box-shadow: 2px 2px 4px #eee; - -webkit-box-shadow: 2px 2px 4px #eee; - box-shadow: 2px 2px 4px #eee; -} - -table.docutils { - border: 1px solid #888; - -moz-box-shadow: 2px 2px 4px #eee; - -webkit-box-shadow: 2px 2px 4px #eee; - box-shadow: 2px 2px 4px #eee; -} - -table.docutils td, table.docutils th { - border: 1px solid #888; - padding: 0.25em 0.7em; -} - -table.field-list, table.footnote { - border: none; - -moz-box-shadow: none; - -webkit-box-shadow: none; - box-shadow: none; -} - -table.footnote { - margin: 15px 0; - width: 100%; - border: 1px solid #eee; -} - -table.field-list th { - padding: 0 0.8em 0 0; -} - -table.field-list td { - padding: 0; -} - -table.footnote td { - padding: 0.5em; -} - -dl { - margin: 0; - padding: 0; -} - -dl dd { - margin-left: 30px; -} - -pre { - padding: 0; - margin: 15px -30px; - padding: 8px; - line-height: 1.3em; - padding: 7px 30px; - background: #eee; - border-radius: 2px; - -moz-border-radius: 2px; - -webkit-border-radius: 2px; -} - -dl pre { - margin-left: -60px; - padding-left: 60px; -} - -dl.class { - margin-bottom: 50px; -} - -tt { - background-color: #ecf0f3; - color: #222; - /* padding: 1px 2px; */ -} - -tt.xref, a tt { - background-color: #FBFBFB; -} - -a:hover tt { - background: #EEE; -} diff --git a/docs/_themes/flask_small/theme.conf b/docs/_themes/flask_small/theme.conf deleted file mode 100644 index 542b462..0000000 --- a/docs/_themes/flask_small/theme.conf +++ /dev/null @@ -1,10 +0,0 @@ -[theme] -inherit = basic -stylesheet = flasky.css -nosidebar = true -pygments_style = flask_theme_support.FlaskyStyle - -[options] -index_logo = '' -index_logo_height = 120px -github_fork = '' diff --git a/docs/_themes/flask_theme_support.py b/docs/_themes/flask_theme_support.py deleted file mode 100644 index 33f4744..0000000 --- a/docs/_themes/flask_theme_support.py +++ /dev/null @@ -1,86 +0,0 @@ -# flasky extensions. flasky pygments style based on tango style -from pygments.style import Style -from pygments.token import Keyword, Name, Comment, String, Error, \ - Number, Operator, Generic, Whitespace, Punctuation, Other, Literal - - -class FlaskyStyle(Style): - background_color = "#f8f8f8" - default_style = "" - - styles = { - # No corresponding class for the following: - #Text: "", # class: '' - Whitespace: "underline #f8f8f8", # class: 'w' - Error: "#a40000 border:#ef2929", # class: 'err' - Other: "#000000", # class 'x' - - Comment: "italic #8f5902", # class: 'c' - Comment.Preproc: "noitalic", # class: 'cp' - - Keyword: "bold #004461", # class: 'k' - Keyword.Constant: "bold #004461", # class: 'kc' - Keyword.Declaration: "bold #004461", # class: 'kd' - Keyword.Namespace: "bold #004461", # class: 'kn' - Keyword.Pseudo: "bold #004461", # class: 'kp' - Keyword.Reserved: "bold #004461", # class: 'kr' - Keyword.Type: "bold #004461", # class: 'kt' - - Operator: "#582800", # class: 'o' - Operator.Word: "bold #004461", # class: 'ow' - like keywords - - Punctuation: "bold #000000", # class: 'p' - - # because special names such as Name.Class, Name.Function, etc. - # are not recognized as such later in the parsing, we choose them - # to look the same as ordinary variables. - Name: "#000000", # class: 'n' - Name.Attribute: "#c4a000", # class: 'na' - to be revised - Name.Builtin: "#004461", # class: 'nb' - Name.Builtin.Pseudo: "#3465a4", # class: 'bp' - Name.Class: "#000000", # class: 'nc' - to be revised - Name.Constant: "#000000", # class: 'no' - to be revised - Name.Decorator: "#888", # class: 'nd' - to be revised - Name.Entity: "#ce5c00", # class: 'ni' - Name.Exception: "bold #cc0000", # class: 'ne' - Name.Function: "#000000", # class: 'nf' - Name.Property: "#000000", # class: 'py' - Name.Label: "#f57900", # class: 'nl' - Name.Namespace: "#000000", # class: 'nn' - to be revised - Name.Other: "#000000", # class: 'nx' - Name.Tag: "bold #004461", # class: 'nt' - like a keyword - Name.Variable: "#000000", # class: 'nv' - to be revised - Name.Variable.Class: "#000000", # class: 'vc' - to be revised - Name.Variable.Global: "#000000", # class: 'vg' - to be revised - Name.Variable.Instance: "#000000", # class: 'vi' - to be revised - - Number: "#990000", # class: 'm' - - Literal: "#000000", # class: 'l' - Literal.Date: "#000000", # class: 'ld' - - String: "#4e9a06", # class: 's' - String.Backtick: "#4e9a06", # class: 'sb' - String.Char: "#4e9a06", # class: 'sc' - String.Doc: "italic #8f5902", # class: 'sd' - like a comment - String.Double: "#4e9a06", # class: 's2' - String.Escape: "#4e9a06", # class: 'se' - String.Heredoc: "#4e9a06", # class: 'sh' - String.Interpol: "#4e9a06", # class: 'si' - String.Other: "#4e9a06", # class: 'sx' - String.Regex: "#4e9a06", # class: 'sr' - String.Single: "#4e9a06", # class: 's1' - String.Symbol: "#4e9a06", # class: 'ss' - - Generic: "#000000", # class: 'g' - Generic.Deleted: "#a40000", # class: 'gd' - Generic.Emph: "italic #000000", # class: 'ge' - Generic.Error: "#ef2929", # class: 'gr' - Generic.Heading: "bold #000080", # class: 'gh' - Generic.Inserted: "#00A000", # class: 'gi' - Generic.Output: "#888", # class: 'go' - Generic.Prompt: "#745334", # class: 'gp' - Generic.Strong: "bold #000000", # class: 'gs' - Generic.Subheading: "bold #800080", # class: 'gu' - Generic.Traceback: "bold #a40000", # class: 'gt' - } diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 0000000..5b4f1bb --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,88 @@ +API Reference +============= + +.. toctree:: + :maxdepth: 3 + +.. module:: socketio + +``Server`` class +---------------- + +.. autoclass:: Server + :members: + +``AsyncServer`` class +--------------------- + +.. autoclass:: AsyncServer + :members: + :inherited-members: + +``WSGIApp`` class +----------------- + +.. autoclass:: WSGIApp + :members: + +``ASGIApp`` class +----------------- + +.. autoclass:: ASGIApp + :members: + +``Middleware`` class (deprecated) +--------------------------------- + +.. autoclass:: Middleware + :members: + +``Namespace`` class +------------------- + +.. autoclass:: Namespace + :members: + +``AsyncNamespace`` class +------------------------ + +.. autoclass:: AsyncNamespace + :members: + :inherited-members: + +``BaseManager`` class +--------------------- + +.. autoclass:: BaseManager + :members: + +``PubSubManager`` class +----------------------- + +.. autoclass:: PubSubManager + :members: + +``KombuManager`` class +---------------------- + +.. autoclass:: KombuManager + :members: + +``RedisManager`` class +---------------------- + +.. autoclass:: RedisManager + :members: + +``AsyncManager`` class +---------------------- + +.. autoclass:: AsyncManager + :members: + :inherited-members: + +``AsyncRedisManager`` class +--------------------------- + +.. autoclass:: AsyncRedisManager + :members: diff --git a/docs/conf.py b/docs/conf.py index a1f6a13..e8398c3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,31 +1,39 @@ # -*- coding: utf-8 -*- # -# socketio documentation build configuration file, created by -# sphinx-quickstart on Sat Jun 13 23:41:23 2015. +# Configuration file for the Sphinx documentation builder. # -# 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. +# This file does only contain a selection of the most common options. For a +# full list see the documentation: +# http://www.sphinx-doc.org/en/master/config -import sys -import os -import shlex +# -- Path setup -------------------------------------------------------------- # 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')) +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- Project information ----------------------------------------------------- + +project = 'python-socketio' +copyright = '2018, Miguel Grinberg' +author = 'Miguel Grinberg' + +# The short X.Y version +version = '' +# The full version, including alpha/beta/rc tags +release = '' + -# -- General configuration ------------------------------------------------ +# -- General configuration --------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# +# 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 @@ -39,29 +47,13 @@ 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. # @@ -69,222 +61,125 @@ release = '0.1' # 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 +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = None -# 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 ---------------------------------------------- +# -- 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' +# +html_theme = 'alabaster' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. +# html_theme_options = { - 'index_logo': 'logo.png', - 'github_fork': 'miguelgrinberg/python-socketio' -} - -# Add any paths that contain custom themes here, relative to this directory. -html_theme_path = ['_themes'] - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -#html_title = None + 'github_user': 'miguelgrinberg', + 'github_repo': 'python-socketio', + 'github_banner': True, + 'github_button': True, + 'github_type': 'star', + 'fixed_sidebar': True, -# A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -#html_logo = None - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -#html_favicon = None +} # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] -# Add any extra paths that contain custom files (such as robots.txt or -# .htaccess) here, relative to this directory. These files are copied -# directly to the root of the documentation. -#html_extra_path = [] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -#html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -#html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} - -# If false, no module index is generated. -#html_domain_indices = True - -# If false, no index is generated. -#html_use_index = True - -# If true, the index is split into individual pages for each letter. -#html_split_index = False - -# If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -#html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None - -# Language to be used for generating the HTML full-text search index. -# Sphinx supports the following languages: -# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' -# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' -#html_search_language = 'en' +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# The default sidebars (for documents that don't match any pattern) are +# defined by theme itself. Builtin themes are using these templates by +# default: ``['localtoc.html', 'relations.html', 'sourcelink.html', +# 'searchbox.html']``. +# +# html_sidebars = {} -# 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' +# -- Options for HTMLHelp output --------------------------------------------- # Output file base name for HTML help builder. -htmlhelp_basename = 'socketiodoc' +htmlhelp_basename = 'python-socketiodoc' -# -- Options for LaTeX output --------------------------------------------- + +# -- Options for LaTeX output ------------------------------------------------ latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', -# Latex figure (float) alignment -#'figure_align': 'htbp', + # 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'), + (master_doc, 'python-socketio.tex', 'python-socketio Documentation', + '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 --------------------------------------- +# -- 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', + (master_doc, 'python-socketio', 'python-socketio Documentation', [author], 1) ] -# If true, show URL addresses after external links. -#man_show_urls = False - -# -- Options for Texinfo output ------------------------------------------- +# -- 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'), + (master_doc, 'python-socketio', 'python-socketio Documentation', + author, 'python-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 +# -- Options for Epub output ------------------------------------------------- + +# Bibliographic Dublin Core info. +epub_title = project + +# The unique identifier of the text. This can be a ISBN number +# or the project homepage. +# +# epub_identifier = '' + +# A unique identification for the text. +# +# epub_uid = '' + +# A list of files that should not be packed into the epub file. +epub_exclude_files = ['search.html'] -# 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 +# -- Extension configuration ------------------------------------------------- diff --git a/docs/deployment.rst b/docs/deployment.rst new file mode 100644 index 0000000..7de1644 --- /dev/null +++ b/docs/deployment.rst @@ -0,0 +1,277 @@ +Deployment +========== + +The following sections describe a variety of deployment strategies for +Socket.IO servers. + +aiohttp +------- + +`Aiohttp `_ is a framework with support for HTTP +and WebSocket, based on asyncio. Support for this framework is limited to Python +3.5 and newer. + +Instances of class ``socketio.AsyncServer`` will automatically use aiohttp +for asynchronous operations if the library is installed. To request its use +explicitly, the ``async_mode`` option can be given in the constructor:: + + sio = socketio.AsyncServer(async_mode='aiohttp') + +A server configured for aiohttp must be attached to an existing application:: + + app = web.Application() + sio.attach(app) + +The aiohttp application can define regular routes that will coexist with the +Socket.IO server. A typical pattern is to add routes that serve a client +application and any associated static files. + +The aiohttp application is then executed in the usual manner:: + + if __name__ == '__main__': + web.run_app(app) + +Tornado +------- + +`Tornado `_ is a web framework with support +for HTTP and WebSocket. Support for this framework requires Python 3.5 and +newer. Only Tornado version 5 and newer are supported, thanks to its tight +integration with asyncio. + +Instances of class ``socketio.AsyncServer`` will automatically use tornado +for asynchronous operations if the library is installed. To request its use +explicitly, the ``async_mode`` option can be given in the constructor:: + + sio = socketio.AsyncServer(async_mode='tornado') + +A server configured for tornado must include a request handler for +Engine.IO:: + + app = tornado.web.Application( + [ + (r"/socketio.io/", socketio.get_tornado_handler(sio)), + ], + # ... other application options + ) + +The tornado application can define other routes that will coexist with the +Socket.IO server. A typical pattern is to add routes that serve a client +application and any associated static files. + +The tornado application is then executed in the usual manner:: + + app.listen(port) + tornado.ioloop.IOLoop.current().start() + +Sanic +----- + +`Sanic `_ is a very efficient asynchronous web +server for Python 3.5 and newer. + +Instances of class ``socketio.AsyncServer`` will automatically use Sanic for +asynchronous operations if the framework is installed. To request its use +explicitly, the ``async_mode`` option can be given in the constructor:: + + sio = socketio.AsyncServer(async_mode='sanic') + +A server configured for aiohttp must be attached to an existing application:: + + app = web.Application() + sio.attach(app) + +The Sanic application can define regular routes that will coexist with the +Socket.IO server. A typical pattern is to add routes that serve a client +application and any associated static files. + +The Sanic application is then executed in the usual manner:: + + if __name__ == '__main__': + app.run() + +Uvicorn, Daphne, and other ASGI servers +--------------------------------------- + +The ``socketio.ASGIApp`` class is an ASGI compatible application that can +forward Socket.IO traffic to an ``socketio.AsyncServer`` instance:: + + sio = socketio.AsyncServer(async_mode='asgi') + app = socketio.ASGIApp(sio) + +The application can then be deployed with any ASGI compatible web server. + +Eventlet +-------- + +`Eventlet `_ is a high performance concurrent networking +library for Python 2 and 3 that uses coroutines, enabling code to be written in +the same style used with the blocking standard library functions. An Socket.IO +server deployed with eventlet has access to the long-polling and WebSocket +transports. + +Instances of class ``socketio.Server`` will automatically use eventlet for +asynchronous operations if the library is installed. To request its use +explicitly, the ``async_mode`` option can be given in the constructor:: + + sio = socketio.Server(async_mode='eventlet') + +A server configured for eventlet is deployed as a regular WSGI application, +using the provided ``socketio.Middleware``:: + + app = socketio.Middleware(sio) + import eventlet + eventlet.wsgi.server(eventlet.listen(('', 8000)), app) + +Using Gunicorn with Eventlet +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +An alternative to running the eventlet WSGI server as above is to use +`gunicorn `_, a fully featured pure Python web server. The +command to launch the application under gunicorn is shown below:: + + $ gunicorn -k eventlet -w 1 module:app + +Due to limitations in its load balancing algorithm, gunicorn can only be used +with one worker process, so the ``-w`` option cannot be set to a value higher +than 1. A single eventlet worker can handle a large number of concurrent +clients, each handled by a greenlet. + +Eventlet provides a ``monkey_patch()`` function that replaces all the blocking +functions in the standard library with equivalent asynchronous versions. While +python-socketio does not require monkey patching, other libraries such as +database drivers are likely to require it. + +Gevent +------ + +`Gevent `_ is another asynchronous framework based on +coroutines, very similar to eventlet. An Socket.IO server deployed with +gevent has access to the long-polling transport. If project +`gevent-websocket `_ is +installed, the WebSocket transport is also available. + +Instances of class ``socketio.Server`` will automatically use gevent for +asynchronous operations if the library is installed and eventlet is not +installed. To request gevent to be selected explicitly, the ``async_mode`` +option can be given in the constructor:: + + sio = socketio.Server(async_mode='gevent') + +A server configured for gevent is deployed as a regular WSGI application, +using the provided ``socketio.Middleware``:: + + app = socketio.Middleware(sio) + from gevent import pywsgi + pywsgi.WSGIServer(('', 8000), app).serve_forever() + +If the WebSocket transport is installed, then the server must be started as +follows:: + + from gevent import pywsgi + from geventwebsocket.handler import WebSocketHandler + app = socketio.Middleware(sio) + pywsgi.WSGIServer(('', 8000), app, + handler_class=WebSocketHandler).serve_forever() + +Using Gunicorn with Gevent +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +An alternative to running the gevent WSGI server as above is to use +`gunicorn `_, a fully featured pure Python web server. The +command to launch the application under gunicorn is shown below:: + + $ gunicorn -k gevent -w 1 module:app + +Or to include WebSocket:: + + $ gunicorn -k geventwebsocket.gunicorn.workers.GeventWebSocketWorker -w 1 module: app + +Same as with eventlet, due to limitations in its load balancing algorithm, +gunicorn can only be used with one worker process, so the ``-w`` option cannot +be higher than 1. A single gevent worker can handle a large number of +concurrent clients through the use of greenlets. + +Gevent provides a ``monkey_patch()`` function that replaces all the blocking +functions in the standard library with equivalent asynchronous versions. While +python-socketio does not require monkey patching, other libraries such as +database drivers are likely to require it. + +uWSGI +----- + +When using the uWSGI server in combination with gevent, the Socket.IO server +can take advantage of uWSGI's native WebSocket support. + +Instances of class ``socketio.Server`` will automatically use this option for +asynchronous operations if both gevent and uWSGI are installed and eventlet is +not installed. To request this asynchronous mode explicitly, the +``async_mode`` option can be given in the constructor:: + + # gevent with uWSGI + sio = socketio.Server(async_mode='gevent_uwsgi') + +A complete explanation of the configuration and usage of the uWSGI server is +beyond the scope of this documentation. The uWSGI server is a fairly complex +package that provides a large and comprehensive set of options. It must be +compiled with WebSocket and SSL support for the WebSocket transport to be +available. As way of an introduction, the following command starts a uWSGI +server for the ``latency.py`` example on port 5000:: + + $ uwsgi --http :5000 --gevent 1000 --http-websockets --master --wsgi-file latency.py --callable app + +Standard Threads +---------------- + +While not comparable to eventlet and gevent in terms of performance, +the Socket.IO server can also be configured to work with multi-threaded web +servers that use standard Python threads. This is an ideal setup to use with +development servers such as `Werkzeug `_. Only the +long-polling transport is currently available when using standard threads. + +Instances of class ``socketio.Server`` will automatically use the threading +mode if neither eventlet nor gevent are not installed. To request the +threading mode explicitly, the ``async_mode`` option can be given in the +constructor:: + + sio = socketio.Server(async_mode='threading') + +A server configured for threading is deployed as a regular web application, +using any WSGI complaint multi-threaded server. The example below deploys an +Socket.IO application combined with a Flask web application, using Flask's +development web server based on Werkzeug:: + + sio = socketio.Server(async_mode='threading') + app = Flask(__name__) + app.wsgi_app = socketio.Middleware(sio, app.wsgi_app) + + # ... Socket.IO and Flask handler functions ... + + if __name__ == '__main__': + app.run(threaded=True) + +When using the threading mode, it is important to ensure that the WSGI server +can handle multiple concurrent requests using threads, since a client can have +up to two outstanding requests at any given time. The Werkzeug server is +single-threaded by default, so the ``threaded=True`` option is required. + +Note that servers that use worker processes instead of threads, such as +gunicorn, do not support a Socket.IO server configured in threading mode. + +Scalability Notes +----------------- + +Socket.IO is a stateful protocol, which makes horizontal scaling more +difficult. To deploy a cluster of Socket.IO processes hosted on one or +multiple servers, the following conditions must be met: + +- Each Socket.IO process must be able to handle multiple requests + concurrently. This is required because long-polling clients send two + requests in parallel. Worker processes that can only handle one request at a + time are not supported. +- The load balancer must be configured to always forward requests from a + client to the same worker process. Load balancers call this *sticky + sessions*, or *session affinity*. +- The worker processes need to communicate with each other to coordinate + complex operations such as broadcasts. This is done through a configured + message queue. See the section on using message queues for details. diff --git a/docs/guide.rst b/docs/guide.rst new file mode 100644 index 0000000..27f5d4e --- /dev/null +++ b/docs/guide.rst @@ -0,0 +1,346 @@ +User's Guide +============ + +The ``Server`` and ``AsyncServer`` classes +------------------------------------------ + +A Socket.IO server is an instance of class :class:`socketio.Server`. This +instance can be transformed into a standard WSGI application by wrapping it +with the :class:`socketio.WSGIApp` class:: + + import socketio + + # create a Socket.IO server + sio = socketio.Server() + + # wrap with a WSGI application + app = socketio.WSGIApp(sio) + +For asyncio based servers, the :class:`socketio.AsyncServer` class provides +the same functionality, but in a coroutine friendly format. If desired, The +:class:`socketio.ASGIApp` class can transform the server into a standard +ASGI application:: + + # create a Socket.IO server + sio = socketio.AsyncServer() + + # wrap with ASGI application + app = socketio.ASGIApp(sio) + +The WSGI and ASGI application wrappers support serving static files, which is +a convenient way to deliver JavaScript based Socket.IO clients to the web +browser:: + + app = socketio.ASGIApp(sio, static_files={ + '/': {'content_type': 'text/html', 'filename': 'latency.html'}, + '/static/style.css': {'content_type': 'text/css', + 'filename': 'static/style.css'}, + }) + +The dictionary provided with the ``static_files`` argument has static file +endpoints as keys. For each of these endpoints, a dictionary with the file's +content type and local filename is given. + +These wrappers can also act as middlewares, forwarding any traffic that is not +intended to Socket.IO server to another application. This allows Socket.IO +servers to integrate easily into existing WSGI or ASGI applications:: + + from wsgi import app # a Flask, Django, etc. application + + app = socketio.WSGIApp(sio, app) + +Receiving Events +---------------- + +The Socket.IO protocol is event based. When a client wants to communicate with +the server it *emits* an event. Each event has a name, and a list of +arguments. The server registers event handler functions with the +:func:`socketio.Server.on` decorator:: + + @sio.on('my custom event') + def my_custom_event(sid, data): + pass + +For asyncio servers, event handlers can optionally be given as coroutines:: + + @sio.on('my custom event') + async def my_custom_event(sid, data): + pass + +The ``sid`` argument is the Socket.IO session id, a unique identifier of each +client connection. All the events sent by a given client will have the same +``sid`` value. + +The ``connect`` and ``disconnect`` are special; they are invoked automatically +when a client connects or disconnects from the server:: + + @sio.on('connect') + def connect(sid, environ): + print('connect ', sid) + + @sio.on('disconnect') + def disconnect(sid): + print('disconnect ', sid) + +The ``connect`` event is an ideal place to perform user authentication, and +any necessary mapping between user entities in the application and the ``sid`` +that was assigned to the client. The ``environ`` argument is a dictionary in +standard WSGI format containing the request information, including HTTP +headers. After inspecting the request, the connect event handler can return +``False`` to reject the connection with the client. + +Sending Events +-------------- + +Socket.IO is a bidirectional protocol, so at any time the server can send an +event to its connected clients. The :func:`socketio.Server.emit` method is +used for this task:: + + sio.emit('my event', {'data': 'foobar'}) + +Sometimes the server may want to send an event just to a particular client. +This can be achieved by adding a ``room`` argument to the emit call:: + + sio.emit('my event', {'data': 'foobar'}, room=user_sid) + +The :func:`socketio.Server.emit` method takes an event name, a message payload +of type ``str``, ``bytes``, ``list``, ``dict`` or ``tuple``, and the recipient +room. When sending a ``tuple``, the elements in it need to be of any of the +other four allowed types. The elements of the tuple will be passed as multiple +arguments to the client-side event handler function. The ``room`` argument is +used to identify the client that should receive the event, and is set to the +``sid`` value assigned to that client's connection with the server. When +omitted, the event is broadcasted to all connected clients. + +Rooms +----- + +To make it easy for the server to emit events to groups of related clients, +the application can put its clients into "rooms", and then address messages to +these rooms. + +In the previous section the ``room`` argument of the +:func:`socketio.SocketIO.emit` method was used to designate a specific +client as the recipient of the event. This is because upon connection, a +personal room for each client is created and named with the ``sid`` assigned +to the connection. The application is then 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. + +:: + + @sio.on('chat') + def begin_chat(sid): + sio.enter_room(sid, 'chat_users') + + @sio.on('exit_chat') + def exit_chat(sid): + sio.leave_room(sid, 'chat_users') + +In chat applications it is often desired that an event is broadcasted to all +the members of the room except one, which is the originator of the event such +as a chat message. The :func:`socketio.Server.emit` method provides an +optional ``skip_sid`` argument to indicate a client that should be skipped +during the broadcast. + +:: + + @sio.on('my message') + def message(sid, data): + sio.emit('my reply', data, room='chat_users', skip_sid=sid) + +Event Callbacks +--------------- + +When a client sends an event to the server, it can optionally provide a +callback function, to be invoked as a way of acknowledgment that the server +has processed the event. While this is entirely managed by the client, the +server can provide a list of values that are to be passed on to the callback +function, simply by returning them from the handler function:: + + @sio.on('my event', namespace='/chat') + def my_event_handler(sid, data): + # handle the message + return "OK", 123 + +Likewise, the server can request a callback function to be invoked after a +client has processed an event. The :func:`socketio.Server.emit` method has an +optional ``callback`` argument that can be set to a callable. If this +argument is given, the callable will be invoked after the client has processed +the event, and any values returned by the client will be passed as arguments +to this function. 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*. + +Each namespace is handled independently from the others, with separate session +IDs (``sid``\ s), event handlers and rooms. It is important that applications +that use multiple namespaces specify the correct namespace when setting up +their event handlers and rooms, using the optional ``namespace`` argument +available in all the methods in the :class:`socketio.Server` class:: + + @sio.on('my custom event', namespace='/chat') + def my_custom_event(sid, data): + pass + +When emitting an event, the ``namespace`` optional argument is used to specify +which namespace to send it on. When the ``namespace`` argument is omitted, the +default Socket.IO namespace, which is named ``/``, is used. + +Class-Based Namespaces +---------------------- + +As an alternative to the decorator-based event handlers, the event handlers +that belong to a namespace can be created as methods of a subclass of +:class:`socketio.Namespace`:: + + class MyCustomNamespace(socketio.Namespace): + def on_connect(self, sid, environ): + pass + + def on_disconnect(self, sid): + pass + + def on_my_event(self, sid, data): + self.emit('my_response', data) + + sio.register_namespace(MyCustomNamespace('/test')) + +For asyncio based severs, namespaces must inherit from +:class:`socketio.AsyncNamespace`, and can define event handlers as coroutines +if desired:: + + class MyCustomNamespace(socketio.AsyncNamespace): + def on_connect(self, sid, environ): + pass + + def on_disconnect(self, sid): + pass + + async def on_my_event(self, sid, data): + await self.emit('my_response', data) + + sio.register_namespace(MyCustomNamespace('/test')) + +When class-based namespaces are used, any events received by the server are +dispatched to a method named as the event name with the ``on_`` prefix. For +example, event ``my_event`` will be handled by a method named ``on_my_event``. +If an event is received for which there is no corresponding method defined in +the namespace class, then the event is ignored. All event names used in +class-based namespaces must use characters that are legal in method names. + +As a convenience to methods defined in a class-based namespace, the namespace +instance includes versions of several of the methods in the +:class:`socketio.Server` and :class:`socketio.AsyncServer` classes that default +to the proper namespace when the ``namespace`` argument is not given. + +In the case that an event has a handler in a class-based namespace, and also a +decorator-based function handler, only the standalone function handler is +invoked. + +It is important to note that class-based namespaces are singletons. This means +that a single instance of a namespace class is used for all clients, and +consequently, a namespace instance cannot be used to store client specific +information. + +Using a Message Queue +--------------------- + +When working with distributed applications, it is often necessary to access +the functionality of the Socket.IO from multiple processes. There are two +specific use cases: + +- Applications that use a work queues such as + `Celery `_ may need to emit an event to a + client once a background job completes. The most convenient place to carry + out this task is the worker process that handled this job. + +- Highly available applications may want to use horizontal scaling of the + Socket.IO server to be able to handle very large number of concurrent + clients. + +As a solution to the above problems, the Socket.IO server can be configured +to connect to a message queue such as `Redis `_ or +`RabbitMQ `_, to communicate with other related +Socket.IO servers or auxiliary workers. + +Redis +~~~~~ + +To use a Redis message queue, a Python Redis client must be installed:: + + # socketio.Server class + pip install redis + + # socketio.AsyncServer class + pip install aioredis + +The Redis queue is configured through the :class:`socketio.RedisManager` and +:class:`socketio.AsyncRedisManager` classes. These classes connect directly to +the Redis store and use the queue's pub/sub functionality:: + + # socketio.Server class + mgr = socketio.RedisManager('redis://') + sio = socketio.Server(client_manager=mgr) + + # socketio.AsyncServer class + mgr = socketio.AsyncRedisManager('redis://') + sio = socketio.AsyncServer(client_manager=mgr) + +The ``client_manager`` argument instructs the server to connect to the given +message queue, and to coordinate with other processes connected to the queue. + +Kombu +~~~~~ + +`Kombu `_ is a Python package that +provides access to RabbitMQ and many other message queues. It can be installed +with pip:: + + pip install kombu + +To use RabbitMQ or other AMQP protocol compatible queues, that is the only +required dependency. But for other message queues, Kombu may require +additional packages. For example, to use a Redis queue via Kombu, the Python +package for Redis needs to be installed as well:: + + pip install redis + +The queue is configured through the :class:`socketio.KombuManager`:: + + mgr = socketio.KombuManager('amqp://') + sio = socketio.Server(client_manager=mgr) + +The connection URL passed to the :class:`KombuManager` constructor is passed +directly to Kombu's `Connection object +`_, so +the Kombu documentation should be consulted for information on how to build +the correct URL for a given message queue. + +Note that Kombu currently does not support asyncio, so it cannot be used with +the :class:`socketio.AsyncServer` class. + +Emitting from external processes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To have a process other than a server connect to the queue to emit a message, +the same client manager classes can be used as standalone objects. In this +case, the ``write_only`` argument should be set to ``True`` to disable the +creation of a listening thread, which only makes sense in a server. For +example:: + + # connect to the redis queue as an external process + external_sio = socketio.RedisManager('redis://', write_only=True) + + # emit an event + external_sio.emit('my event', data={'foo': 'bar'}, room='my room') diff --git a/docs/index.rst b/docs/index.rst index 1a26d7a..b267c5c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,756 +1,25 @@ -.. socketio documentation master file, created by - sphinx-quickstart on Sat Jun 13 23:41:23 2015. +.. python-socketio documentation master file, created by + sphinx-quickstart on Sun Nov 25 11:52:38 2018. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. -socketio documentation -====================== +python-socketio +=============== -This project implements a Python Socket.IO server that can run standalone or -integrated with a web application. The following are some of its -features: +This projects implements a Socket.IO server that can run standalone or +integrated with a variety of Python web frameworks. -- Fully compatible with the - `Javascript `_, - `Swift `_, - `C++ `_ and - `Java `_ official - Socket.IO clients, plus any third party clients that comply with the - Socket.IO specification. -- Compatible with Python 2.7 and Python 3.3+. -- Supports large number of clients even on modest hardware when used with an - asynchronous server based on `asyncio `_ - (`sanic `_, `aiohttp `_ or - `tornado `_), - `eventlet `_ or `gevent `_. For - development and testing, any WSGI compliant multi-threaded server can also be - used. -- Includes a WSGI middleware that integrates Socket.IO traffic with standard - WSGI applications. -- Broadcasting of messages to all connected clients, or to subsets of them - assigned to "rooms". -- Optional support for multiple servers, connected through a messaging queue - such as Redis or RabbitMQ. -- Send messages to clients from external processes, such as Celery workers or - auxiliary scripts. -- 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. +.. toctree:: + :maxdepth: 2 -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 original implementations of the client and server components are -written in JavaScript. - -Getting Started ---------------- - -The Socket.IO server can be installed with pip:: - - pip install python-socketio - -The following is a basic example of a Socket.IO server that uses the -`aiohttp `_ framework for asyncio (Python 3.5+ -only): - -.. code:: python - - from aiohttp import web - import socketio - - sio = socketio.AsyncServer() - app = web.Application() - sio.attach(app) - - async def index(request): - """Serve the client-side application.""" - with open('index.html') as f: - return web.Response(text=f.read(), content_type='text/html') - - @sio.on('connect', namespace='/chat') - def connect(sid, environ): - print("connect ", sid) - - @sio.on('chat message', namespace='/chat') - async def message(sid, data): - print("message ", data) - await sio.emit('reply', room=sid) - - @sio.on('disconnect', namespace='/chat') - def disconnect(sid): - print('disconnect ', sid) - - app.router.add_static('/static', 'static') - app.router.add_get('/', index) - - if __name__ == '__main__': - web.run_app(app) - -And below is a similar example, but using Flask and Eventlet. This example is -compatible with Python 2.7 and 3.3+:: - - import socketio - import eventlet - from flask import Flask, render_template - - sio = socketio.Server() - app = Flask(__name__) - - @app.route('/') - def index(): - """Serve the client-side application.""" - return render_template('index.html') - - @sio.on('connect') - def connect(sid, environ): - print('connect ', sid) - - @sio.on('my message') - def message(sid, data): - print('message ', data) - - @sio.on('disconnect') - def disconnect(sid): - print('disconnect ', sid) - - if __name__ == '__main__': - # wrap Flask application with socketio's middleware - app = socketio.Middleware(sio, app) - - # deploy as an eventlet WSGI server - eventlet.wsgi.server(eventlet.listen(('', 8000)), app) - -The client-side application must include the -`socket.io-client `_ library -(versions 1.3.5 or newer recommended). - -Each time a client connects to the server the ``connect`` event handler is -invoked with the ``sid`` (session ID) assigned to the connection and the WSGI -environment dictionary. The server can inspect authentication or other headers -to decide if the client is allowed to connect. To reject a client the handler -must return ``False``. - -When the client sends an event to the server, the appropriate event handler is -invoked with the ``sid`` and the message, which can be a single or multiple -arguments. 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. - -Server ------- - -Socket.IO servers are instances of class :class:`socketio.Server`, which can be -combined with a WSGI compliant application using :class:`socketio.Middleware`:: - - # create a Socket.IO server - sio = socketio.Server() - - # wrap WSGI application with socketio's middleware - app = socketio.Middleware(sio, app) - - -For asyncio based servers, the :class:`socketio.AsyncServer` class provides a -coroutine friendly server:: - - # create a Socket.IO server - sio = socketio.AsyncServer() - - # attach server to application - sio.attach(app) - -Event handlers for servers are registered using the :func:`socketio.Server.on` -method:: - - @sio.on('my custom event') - def my_custom_event(): - pass - -For asyncio servers, event handlers can be regular functions or coroutines:: - - @sio.on('my custom event') - async def my_custom_event(): - await sio.emit('my reply') - -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 groups of clients, -the application can put clients into rooms, and then address messages to the -entire 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('leave 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``, ``dict`` or ``tuple``, and the recipient -room. When sending a ``tuple``, the elements in it need to be of any of the -other four allowed types. The elements of the tuple will be passed as multiple -arguments to the client-side callback function. 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*. - -Each namespace is handled independently from the others, with separate session -IDs (``sid``\ s), event handlers and rooms. It is important that applications -that use multiple namespaces specify the correct namespace when setting up -their event handlers and rooms, using the optional ``namespace`` argument -available in all the methods in the :class:`socketio.Server` class. - -When the ``namespace`` argument is omitted, set to ``None`` or to ``'/'``, a -default namespace is used. - -Class-Based Namespaces ----------------------- - -As an alternative to the decorator-based event handlers, the event handlers -that belong to a namespace can be created as methods of a subclass of -:class:`socketio.Namespace`:: - - class MyCustomNamespace(socketio.Namespace): - def on_connect(self, sid, environ): - pass - - def on_disconnect(self, sid): - pass - - def on_my_event(self, sid, data): - self.emit('my_response', data) - - sio.register_namespace(MyCustomNamespace('/test')) - -For asyncio based severs, namespaces must inherit from -:class:`socketio.AsyncNamespace`, and can define event handlers as regular -methods or coroutines:: - - class MyCustomNamespace(socketio.AsyncNamespace): - def on_connect(self, sid, environ): - pass - - def on_disconnect(self, sid): - pass - - async def on_my_event(self, sid, data): - await self.emit('my_response', data) - - sio.register_namespace(MyCustomNamespace('/test')) - -When class-based namespaces are used, any events received by the server are -dispatched to a method named as the event name with the ``on_`` prefix. For -example, event ``my_event`` will be handled by a method named ``on_my_event``. -If an event is received for which there is no corresponding method defined in -the namespace class, then the event is ignored. All event names used in -class-based namespaces must used characters that are legal in method names. - -As a convenience to methods defined in a class-based namespace, the namespace -instance includes versions of several of the methods in the -:class:`socketio.Server` and :class:`socketio.AsyncServer` classes that default -to the proper namespace when the ``namespace`` argument is not given. - -In the case that an event has a handler in a class-based namespace, and also a -decorator-based function handler, only the standalone function handler is -invoked. - -It is important to note that class-based namespaces are singletons. This means -that a single instance of a namespace class is used for all clients, and -consequently, a namespace instance cannot be used to store client specific -information. - -Using a Message Queue ---------------------- - -The Socket.IO server owns the socket connections to all the clients, so it is -the only process that can emit events to them. Unfortunately this becomes a -limitation for many applications that use more than one process. A common need -is to emit events to clients from a process other than the server, for example -a `Celery `_ worker. - -To enable these auxiliary processes to emit events, the server can be -configured to listen for externally issued events on a message queue such as -`Redis `_ or `RabbitMQ `_. -Processes that need to emit events to client then post these events to the -queue. - -Another situation in which the use of a message queue is necessary is with -high traffic applications that work with large number of clients. To support -these clients, it may be necessary to horizontally scale the Socket.IO -server by splitting the client list among multiple server processes. In this -type of installation, each server processes owns the connections to a subset -of the clients. To make broadcasting work in this environment, the servers -communicate with each other through the message queue. - -Kombu -~~~~~ - -One of the messaging options offered by this package to access the message -queue is `Kombu `_ , which means that -any message queue supported by this package can be used. Kombu can be installed -with pip:: - - pip install kombu - -To use RabbitMQ or other AMQP protocol compatible queues, that is the only -required dependency. But for other message queues, Kombu may require -additional packages. For example, to use a Redis queue, Kombu needs the Python -package for Redis installed as well:: - - pip install redis - -The appropriate message queue service, such as RabbitMQ or Redis, must also be -installed. To configure a Socket.IO server to connect to a Kombu queue, the -``client_manager`` argument must be passed in the server creation. The -following example instructs the server to connect to a Redis service running -on the same host and on the default port:: - - mgr = socketio.KombuManager('redis://') - sio = socketio.Server(client_manager=mgr) - -For a RabbitMQ queue also running on the local server with default -credentials, the configuration is as follows:: - - mgr = socketio.KombuManager('amqp://') - sio = socketio.Server(client_manager=mgr) - -The URL passed to the :class:`KombuManager` constructor is passed directly to -Kombu's `Connection object -`_, so -the Kombu documentation should be consulted for information on how to -connect to the message queue appropriately. - -Note that Kombu currently does not support asyncio, so it cannot be used with -the :class:`socketio.AsyncServer` class. - -Redis -~~~~~ + intro + guide + deployment + api -To use a Redis message queue, the Python package for Redis must also be -installed:: - - # WSGI server - pip install redis - - # asyncio server - pip install aioredis - -Native Redis support is accessed through the :class:`socketio.RedisManager` and -:class:`socketio.AsyncRedisManager` classes. These classes connect directly to -the Redis store and use the queue's pub/sub functionality:: - - # WSGI server - mgr = socketio.RedisManager('redis://') - sio = socketio.Server(client_manager=mgr) - - # asyncio server - mgr = socketio.AsyncRedisManager('redis://') - sio = socketio.AsyncServer(client_manager=mgr) - -Horizontal scaling -~~~~~~~~~~~~~~~~~~ - -If multiple Socket.IO servers are connected to the same message queue, they -automatically communicate with each other and manage a combined client list, -without any need for additional configuration. When a load balancer such as -nginx is used, this provides virtually unlimited scaling capabilities for the -server. - -Emitting from external processes -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -To have a process other than a server connect to the queue to emit a message, -the same client manager classes can be used as standalone objects. In this -case, the ``write_only`` argument should be set to ``True`` to disable the -creation of a listening thread, which only makes sense in a server. For -example:: - - # connect to the redis queue through Kombu - external_sio = socketio.KombuManager('redis://', write_only=True) - - # emit an event - external_sio.emit('my event', data={'foo': 'bar'}, room='my room') - -Deployment ----------- - -The following sections describe a variety of deployment strategies for -Socket.IO servers. - -Sanic -~~~~~ - -`Sanic `_ is a very efficient asynchronous web -server for Python 3.5 and newer. - -Instances of class ``socketio.AsyncServer`` will automatically use Sanic for -asynchronous operations if the framework is installed. To request its use -explicitly, the ``async_mode`` option can be given in the constructor:: - - sio = socketio.AsyncServer(async_mode='sanic') - -A server configured for aiohttp must be attached to an existing application:: - - app = web.Application() - sio.attach(app) - -The Sanic application can define regular routes that will coexist with the -Socket.IO server. A typical pattern is to add routes that serve a client -application and any associated static files. - -The Sanic application is then executed in the usual manner:: - - if __name__ == '__main__': - app.run() - -aiohttp -~~~~~~~ - -`Aiohttp `_ is a framework with support for HTTP -and WebSocket, based on asyncio. Support for this framework is limited to Python -3.5 and newer. - -Instances of class ``socketio.AsyncServer`` will automatically use aiohttp -for asynchronous operations if the library is installed. To request its use -explicitly, the ``async_mode`` option can be given in the constructor:: - - sio = socketio.AsyncServer(async_mode='aiohttp') - -A server configured for aiohttp must be attached to an existing application:: - - app = web.Application() - sio.attach(app) - -The aiohttp application can define regular routes that will coexist with the -Socket.IO server. A typical pattern is to add routes that serve a client -application and any associated static files. - -The aiohttp application is then executed in the usual manner:: - - if __name__ == '__main__': - web.run_app(app) - -Tornado -~~~~~~~ - -`Tornado `_ is a web framework with support -for HTTP and WebSocket. Support for this framework requires Python 3.5 and -newer. Only Tornado version 5 and newer are supported, thanks to its tight -integration with asyncio. - -Instances of class ``socketio.AsyncServer`` will automatically use tornado -for asynchronous operations if the library is installed. To request its use -explicitly, the ``async_mode`` option can be given in the constructor:: - - sio = socketio.AsyncServer(async_mode='tornado') - -A server configured for tornado must include a request handler for -Engine.IO:: - - app = tornado.web.Application( - [ - (r"/socketio.io/", socketio.get_tornado_handler(sio)), - ], - # ... other application options - ) - -The tornado application can define other routes that will coexist with the -Socket.IO server. A typical pattern is to add routes that serve a client -application and any associated static files. - -The tornado application is then executed in the usual manner:: - - app.listen(port) - tornado.ioloop.IOLoop.current().start() - -Eventlet -~~~~~~~~ - -`Eventlet `_ is a high performance concurrent networking -library for Python 2 and 3 that uses coroutines, enabling code to be written in -the same style used with the blocking standard library functions. An Socket.IO -server deployed with eventlet has access to the long-polling and WebSocket -transports. - -Instances of class ``socketio.Server`` will automatically use eventlet for -asynchronous operations if the library is installed. To request its use -explicitly, the ``async_mode`` option can be given in the constructor:: - - sio = socketio.Server(async_mode='eventlet') - -A server configured for eventlet is deployed as a regular WSGI application, -using the provided ``socketio.Middleware``:: - - app = socketio.Middleware(sio) - import eventlet - eventlet.wsgi.server(eventlet.listen(('', 8000)), app) - -An alternative to running the eventlet WSGI server as above is to use -`gunicorn `_, a fully featured pure Python web server. The -command to launch the application under gunicorn is shown below:: - - $ gunicorn -k eventlet -w 1 module:app - -Due to limitations in its load balancing algorithm, gunicorn can only be used -with one worker process, so the ``-w`` option cannot be set to a value higher -than 1. A single eventlet worker can handle a large number of concurrent -clients, each handled by a greenlet. - -Eventlet provides a ``monkey_patch()`` function that replaces all the blocking -functions in the standard library with equivalent asynchronous versions. While -python-socketio does not require monkey patching, other libraries such as -database drivers are likely to require it. - -Gevent -~~~~~~ - -`Gevent `_ is another asynchronous framework based on -coroutines, very similar to eventlet. An Socket.IO server deployed with -gevent has access to the long-polling transport. If project -`gevent-websocket `_ is -installed, the WebSocket transport is also available. - -Instances of class ``socketio.Server`` will automatically use gevent for -asynchronous operations if the library is installed and eventlet is not -installed. To request gevent to be selected explicitly, the ``async_mode`` -option can be given in the constructor:: - - sio = socketio.Server(async_mode='gevent') - -A server configured for gevent is deployed as a regular WSGI application, -using the provided ``socketio.Middleware``:: - - app = socketio.Middleware(sio) - from gevent import pywsgi - pywsgi.WSGIServer(('', 8000), app).serve_forever() - -If the WebSocket transport is installed, then the server must be started as -follows:: - - from gevent import pywsgi - from geventwebsocket.handler import WebSocketHandler - app = socketio.Middleware(sio) - pywsgi.WSGIServer(('', 8000), app, - handler_class=WebSocketHandler).serve_forever() - -An alternative to running the gevent WSGI server as above is to use -`gunicorn `_, a fully featured pure Python web server. The -command to launch the application under gunicorn is shown below:: - - $ gunicorn -k gevent -w 1 module:app - -Or to include WebSocket:: - - $ gunicorn -k geventwebsocket.gunicorn.workers.GeventWebSocketWorker -w 1 module: app - -Same as with eventlet, due to limitations in its load balancing algorithm, -gunicorn can only be used with one worker process, so the ``-w`` option cannot -be higher than 1. A single gevent worker can handle a large number of -concurrent clients through the use of greenlets. - -Gevent provides a ``monkey_patch()`` function that replaces all the blocking -functions in the standard library with equivalent asynchronous versions. While -python-socketio does not require monkey patching, other libraries such as -database drivers are likely to require it. - -Gevent with uWSGI -~~~~~~~~~~~~~~~~~ - -When using the uWSGI server in combination with gevent, the Socket.IO server -can take advantage of uWSGI's native WebSocket support. - -Instances of class ``socketio.Server`` will automatically use this option for -asynchronous operations if both gevent and uWSGI are installed and eventlet is -not installed. To request this asynchronous mode explicitly, the -``async_mode`` option can be given in the constructor:: - - # gevent with uWSGI - sio = socketio.Server(async_mode='gevent_uwsgi') - -A complete explanation of the configuration and usage of the uWSGI server is -beyond the scope of this documentation. The uWSGI server is a fairly complex -package that provides a large and comprehensive set of options. It must be -compiled with WebSocket and SSL support for the WebSocket transport to be -available. As way of an introduction, the following command starts a uWSGI -server for the ``latency.py`` example on port 5000:: - - $ uwsgi --http :5000 --gevent 1000 --http-websockets --master --wsgi-file latency.py --callable app - -Standard Threading Library -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -While not comparable to eventlet and gevent in terms of performance, -the Socket.IO server can also be configured to work with multi-threaded web -servers that use standard Python threads. This is an ideal setup to use with -development servers such as `Werkzeug `_. Only the -long-polling transport is currently available when using standard threads. - -Instances of class ``socketio.Server`` will automatically use the threading -mode if neither eventlet nor gevent are not installed. To request the -threading mode explicitly, the ``async_mode`` option can be given in the -constructor:: - - sio = socketio.Server(async_mode='threading') - -A server configured for threading is deployed as a regular web application, -using any WSGI complaint multi-threaded server. The example below deploys an -Socket.IO application combined with a Flask web application, using Flask's -development web server based on Werkzeug:: - - sio = socketio.Server(async_mode='threading') - app = Flask(__name__) - app.wsgi_app = socketio.Middleware(sio, app.wsgi_app) - - # ... Socket.IO and Flask handler functions ... - - if __name__ == '__main__': - app.run(threaded=True) - -When using the threading mode, it is important to ensure that the WSGI server -can handle multiple concurrent requests using threads, since a client can have -up to two outstanding requests at any given time. The Werkzeug server is -single-threaded by default, so the ``threaded=True`` option is required. - -Note that servers that use worker processes instead of threads, such as -gunicorn, do not support a Socket.IO server configured in threading mode. - -Multi-process deployments -~~~~~~~~~~~~~~~~~~~~~~~~~ - -Socket.IO is a stateful protocol, which makes horizontal scaling more -difficult. To deploy a cluster of Socket.IO processes (hosted on one or -multiple servers), the following conditions must be met: - -- Each Socket.IO process must be able to handle multiple requests, either by - using asyncio, eventlet, gevent, or standard threads. Worker processes that - only handle one request at a time are not supported. -- The load balancer must be configured to always forward requests from a - client to the same worker process. Load balancers call this *sticky - sessions*, or *session affinity*. -- The worker processes communicate with each other through a message queue, - which must be installed and configured. See the section on using message - queues above for instructions. - -API Reference -------------- - -.. module:: socketio - -.. autoclass:: Middleware - :members: - -.. autoclass:: Server - :members: - -.. autoclass:: AsyncServer - :members: - :inherited-members: - -.. autoclass:: Namespace - :members: - -.. autoclass:: AsyncNamespace - :members: - :inherited-members: - -.. autoclass:: BaseManager - :members: - -.. autoclass:: PubSubManager - :members: - -.. autoclass:: KombuManager - :members: - -.. autoclass:: RedisManager - :members: - -.. autoclass:: AsyncManager - :members: - :inherited-members: +Indices and tables +------------------ -.. autoclass:: AsyncRedisManager - :members: +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/intro.rst b/docs/intro.rst new file mode 100644 index 0000000..844f2a6 --- /dev/null +++ b/docs/intro.rst @@ -0,0 +1,148 @@ +.. 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. + +Getting Started +=============== + +What is Socket.IO? +------------------ + +Socket.IO is a transport protocol that enables real-time bidirectional +event-based communication between clients (typically web browsers or +smartphones) and a server. There are Socket.IO clients and servers implemented +in a variety of languages, including JavaScript, Python, C++, Swift, C# and +PHP. + +Features +-------- + +- Fully compatible with the + `Javascript `_, + `Swift `_, + `C++ `_ and + `Java `_ official + Socket.IO clients, plus any third party clients that comply with the + Socket.IO specification. +- Compatible with Python 2.7 and Python 3.3+. +- Supports large number of clients even on modest hardware due to being + asynchronous, even when asyncio is not used. +- Compatible with `aiohttp `_, + `sanic `_, + `tornado `_, + `eventlet `_, + `gevent `_, + or any `WSGI `_ or + `ASGI `_ compatible server. +- Includes WSGI and ASGI middlewares that integrate Socket.IO traffic with + other web applications. +- Broadcasting of messages to all connected clients, or to subsets of them + assigned to "rooms". +- Optional support for multiple servers, connected through a messaging queue + such as Redis or RabbitMQ. +- Send messages to clients from external processes, such as Celery workers or + auxiliary scripts. +- 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. + +Examples +-------- + +The Socket.IO server can be installed with pip:: + + pip install python-socketio + +The following is a basic example of a Socket.IO server that uses the +`aiohttp `_ framework for asyncio (Python 3.5+ +only): + +.. code:: python + + from aiohttp import web + import socketio + + sio = socketio.AsyncServer() + app = web.Application() + sio.attach(app) + + async def index(request): + """Serve the client-side application.""" + with open('index.html') as f: + return web.Response(text=f.read(), content_type='text/html') + + @sio.on('connect', namespace='/chat') + def connect(sid, environ): + print("connect ", sid) + + @sio.on('chat message', namespace='/chat') + async def message(sid, data): + print("message ", data) + await sio.emit('reply', room=sid) + + @sio.on('disconnect', namespace='/chat') + def disconnect(sid): + print('disconnect ', sid) + + app.router.add_static('/static', 'static') + app.router.add_get('/', index) + + if __name__ == '__main__': + web.run_app(app) + +And below is a similar example, but using Flask and Eventlet. This example is +compatible with Python 2.7 and 3.3+:: + + 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.WSGIApp(sio, app) + + # deploy as an eventlet WSGI server + eventlet.wsgi.server(eventlet.listen(('', 8000)), app) + +The client-side application must include the +`socket.io-client `_ library +(versions 1.3.5 or newer recommended). + +Each time a client connects to the server the ``connect`` event handler is +invoked with the ``sid`` (session ID) assigned to the connection and the WSGI +environment dictionary. The server can inspect authentication or other headers +to decide if the client is allowed to connect. To reject a client the handler +must return ``False``. + +When the client sends an event to the server, the appropriate event handler is +invoked with the ``sid`` and the message, which can be a single or multiple +arguments. 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. diff --git a/docs/make.bat b/docs/make.bat index ada489d..27f573b 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -1,263 +1,35 @@ -@ECHO OFF - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set BUILDDIR=_build -set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . -set I18NSPHINXOPTS=%SPHINXOPTS% . -if NOT "%PAPER%" == "" ( - set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% - set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% -) - -if "%1" == "" goto help - -if "%1" == "help" ( - :help - echo.Please use `make ^` where ^ is one of - echo. html to make standalone HTML files - echo. dirhtml to make HTML files named index.html in directories - echo. singlehtml to make a single large HTML file - echo. pickle to make pickle files - echo. json to make JSON files - echo. htmlhelp to make HTML files and a HTML help project - echo. qthelp to make HTML files and a qthelp project - echo. devhelp to make HTML files and a Devhelp project - echo. epub to make an epub - echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter - echo. text to make text files - echo. man to make manual pages - echo. texinfo to make Texinfo files - echo. gettext to make PO message catalogs - echo. changes to make an overview over all changed/added/deprecated items - echo. xml to make Docutils-native XML files - echo. pseudoxml to make pseudoxml-XML files for display purposes - echo. linkcheck to check all external links for integrity - echo. doctest to run all doctests embedded in the documentation if enabled - echo. coverage to run coverage check of the documentation if enabled - goto end -) - -if "%1" == "clean" ( - for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i - del /q /s %BUILDDIR%\* - goto end -) - - -REM Check if sphinx-build is available and fallback to Python version if any -%SPHINXBUILD% 2> nul -if errorlevel 9009 goto sphinx_python -goto sphinx_ok - -:sphinx_python - -set SPHINXBUILD=python -m sphinx.__init__ -%SPHINXBUILD% 2> nul -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -:sphinx_ok - - -if "%1" == "html" ( - %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/html. - goto end -) - -if "%1" == "dirhtml" ( - %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. - goto end -) - -if "%1" == "singlehtml" ( - %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. - goto end -) - -if "%1" == "pickle" ( - %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the pickle files. - goto end -) - -if "%1" == "json" ( - %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the JSON files. - goto end -) - -if "%1" == "htmlhelp" ( - %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run HTML Help Workshop with the ^ -.hhp project file in %BUILDDIR%/htmlhelp. - goto end -) - -if "%1" == "qthelp" ( - %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run "qcollectiongenerator" with the ^ -.qhcp project file in %BUILDDIR%/qthelp, like this: - echo.^> qcollectiongenerator %BUILDDIR%\qthelp\socketio.qhcp - echo.To view the help file: - echo.^> assistant -collectionFile %BUILDDIR%\qthelp\socketio.ghc - goto end -) - -if "%1" == "devhelp" ( - %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. - goto end -) - -if "%1" == "epub" ( - %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The epub file is in %BUILDDIR%/epub. - goto end -) - -if "%1" == "latex" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "latexpdf" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - cd %BUILDDIR%/latex - make all-pdf - cd %~dp0 - echo. - echo.Build finished; the PDF files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "latexpdfja" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - cd %BUILDDIR%/latex - make all-pdf-ja - cd %~dp0 - echo. - echo.Build finished; the PDF files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "text" ( - %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The text files are in %BUILDDIR%/text. - goto end -) - -if "%1" == "man" ( - %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The manual pages are in %BUILDDIR%/man. - goto end -) - -if "%1" == "texinfo" ( - %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. - goto end -) - -if "%1" == "gettext" ( - %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The message catalogs are in %BUILDDIR%/locale. - goto end -) - -if "%1" == "changes" ( - %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes - if errorlevel 1 exit /b 1 - echo. - echo.The overview file is in %BUILDDIR%/changes. - goto end -) - -if "%1" == "linkcheck" ( - %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck - if errorlevel 1 exit /b 1 - echo. - echo.Link check complete; look for any errors in the above output ^ -or in %BUILDDIR%/linkcheck/output.txt. - goto end -) - -if "%1" == "doctest" ( - %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest - if errorlevel 1 exit /b 1 - echo. - echo.Testing of doctests in the sources finished, look at the ^ -results in %BUILDDIR%/doctest/output.txt. - goto end -) - -if "%1" == "coverage" ( - %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage - if errorlevel 1 exit /b 1 - echo. - echo.Testing of coverage in the sources finished, look at the ^ -results in %BUILDDIR%/coverage/python.txt. - goto end -) - -if "%1" == "xml" ( - %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The XML files are in %BUILDDIR%/xml. - goto end -) - -if "%1" == "pseudoxml" ( - %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. - goto end -) - -:end +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 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 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/socketio/asgi.py b/socketio/asgi.py index 6596dbb..2339462 100644 --- a/socketio/asgi.py +++ b/socketio/asgi.py @@ -19,7 +19,9 @@ class ASGIApp(engineio.ASGIApp): :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 uvicorn