├── .gitignore ├── .readthedocs.yaml ├── .travis.yml ├── .travis └── install.sh ├── LICENSE ├── README.rst ├── Vagrantfile ├── appveyor.yml ├── docs ├── Makefile ├── _static │ └── README ├── build.cmd ├── conf.py ├── cookbook.rst ├── cookbook │ ├── advanced-discovery.rst │ ├── advanced-discovery │ │ ├── discover_group_a.py │ │ ├── discover_group_b.py │ │ ├── discover_group_c.py │ │ ├── discover_group_colon.py │ │ ├── dynamically_discover_nodes_a.py │ │ ├── dynamically_discover_nodes_b.py │ │ ├── dynamically_discover_nodes_c.py │ │ └── dynamically_discover_nodes_master.py │ ├── advanced-messaging.rst │ ├── advanced-messaging │ │ ├── news_from_multiple_sources_abcd.py │ │ ├── news_from_multiple_sources_e.py │ │ ├── polling_messages_a.py │ │ ├── polling_messages_b.py │ │ ├── sending_data_a.py │ │ └── sending_data_b.py │ ├── complete-examples.rst │ ├── complete-examples │ │ ├── anagram-solver │ │ │ ├── anagram-client.py │ │ │ ├── anagram-server.py │ │ │ ├── readme.txt │ │ │ └── words.txt │ │ └── pong │ │ │ └── pong.py │ ├── simple-discovery.rst │ ├── simple-discovery │ │ ├── advertise_a_service.py │ │ ├── advertise_a_service_with_a_specific_address.py │ │ ├── advertise_a_service_with_a_specific_port.py │ │ ├── advertise_a_service_with_a_wildcard_address.py │ │ ├── discover_a_service_a.py │ │ ├── discover_a_service_b.py │ │ ├── discover_all_services_a.py │ │ ├── discover_all_services_b.py │ │ └── discover_all_services_c.py │ ├── simple-messaging.rst │ ├── simple-messaging │ │ ├── send_a_message_a.py │ │ ├── send_a_message_b.py │ │ ├── send_a_message_without_reply_a.py │ │ ├── send_a_message_without_reply_b.py │ │ ├── send_news_a.py │ │ ├── send_news_b.py │ │ ├── send_news_on_topic_a.py │ │ ├── send_news_on_topic_b.py │ │ └── send_news_on_topic_c.py │ └── test-recipes.py ├── design-guidelines.rst ├── design-questions.rst ├── discovery.rst ├── for-developers.rst ├── for-teachers.rst ├── index.rst ├── installation.rst ├── make.bat ├── messenger.rst ├── networkzero.rst ├── toc.rst └── usage.rst ├── examples ├── chat │ ├── chat.py │ ├── hub.py │ ├── readme.txt │ ├── run.cmd │ └── status.py ├── halifax │ ├── halifax.py │ ├── run.cmd │ └── words.txt └── reverso │ ├── client.py │ ├── readme.txt │ └── server.py ├── misc └── pyconuk2017 │ ├── 02.jpg │ ├── 03.jpg │ ├── 04.png │ ├── 05.jpg │ ├── 06.png │ ├── 07.jpg │ ├── 07b.jpg │ ├── 08.jpg │ ├── 09.jpg │ ├── 10.jpg │ ├── 11.png │ ├── 12.jpg │ ├── 12a.png │ ├── 13.jpg │ ├── 14.PNG │ ├── 15a.jpg │ ├── 15b.jpg │ ├── 15c.jpg │ ├── 16.png │ ├── 17.png │ ├── 17b.jpg │ ├── adventure.py │ ├── chat.py │ ├── chatter.py │ ├── chattery.py │ ├── image.jpg │ ├── notes.txt │ ├── pyconuk2017.pdf │ ├── pyconuk2017.pptx │ ├── quiz-controller.py │ ├── robotics │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ └── robot │ │ ├── __init__.py │ │ ├── config.py │ │ ├── logging.py │ │ ├── outputs │ │ ├── __init__.py │ │ ├── graphical.py │ │ ├── motor.py │ │ └── text.py │ │ ├── robot.py │ │ └── shell.py │ ├── sad.jpg │ ├── watch-news.py │ └── yellow_happy.jpg ├── networkzero ├── __init__.py ├── config.py ├── core.py ├── discovery.py ├── messenger.py └── sockets.py ├── requirements.txt ├── setup.py ├── shell.cmd ├── shell.py ├── tests ├── test_core.py ├── test_discovery.py ├── test_messenger.py ├── test_networkzero.py ├── test_scenarios.py └── test_threading.py ├── tox.ini └── trials ├── bclient.c ├── bserver.c ├── find-best-address.py ├── json-bytes.py ├── keep-going.py ├── play-adverts.py ├── recv_bcast.py ├── router-dealer.py ├── send_bcast.py ├── show-adverts.py ├── try-dealer-nw0.py └── try-dealer.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | #Ipython Notebook 62 | .ipynb_checkpoints 63 | /.vagrant 64 | /.venv 65 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-22.04 11 | tools: 12 | python: "3.11" 13 | 14 | # Build documentation in the docs/ directory with Sphinx 15 | sphinx: 16 | configuration: docs/conf.py 17 | 18 | # We recommend specifying your dependencies to enable reproducible builds: 19 | # https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 20 | # python: 21 | # install: 22 | # - requirements: docs/requirements.txt -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Based on https://github.com/pyca/cryptography/blob/master/.travis.yml 2 | # which is subject to the following BSD license: 3 | 4 | # Copyright (c) Individual contributors. 5 | # All rights reserved. 6 | 7 | # Redistribution and use in source and binary forms, with or without 8 | # modification, are permitted provided that the following conditions are met: 9 | 10 | # 1. Redistributions of source code must retain the above copyright notice, 11 | # this list of conditions and the following disclaimer. 12 | 13 | # 2. Redistributions in binary form must reproduce the above copyright 14 | # notice, this list of conditions and the following disclaimer in the 15 | # documentation and/or other materials provided with the distribution. 16 | 17 | # 3. Neither the name of PyCA Cryptography nor the names of its contributors 18 | # may be used to endorse or promote products derived from this software 19 | # without specific prior written permission. 20 | 21 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 22 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 23 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 25 | # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 26 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 28 | # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 30 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | 32 | sudo: false 33 | 34 | language: python 35 | 36 | matrix: 37 | include: 38 | - python: 2.7 # these are just to make travis's UI a bit prettier 39 | env: TOXENV=py27 40 | - python: 3.3 41 | env: TOXENV=py33 42 | - python: 3.4 43 | env: TOXENV=py34 44 | - python: 3.5 45 | env: TOXENV=py35 46 | - os: osx 47 | language: generic 48 | python: 2.7 49 | env: TOXENV=py27 50 | - os: osx 51 | language: generic 52 | python: 3.3 53 | env: TOXENV=py33 54 | - os: osx 55 | language: generic 56 | python: 3.4 57 | env: TOXENV=py34 58 | - os: osx 59 | language: generic 60 | python: 3.5 61 | env: TOXENV=py35 62 | 63 | install: ./.travis/install.sh 64 | 65 | script: source ~/.venv/bin/activate && tox 66 | 67 | branches: 68 | only: 69 | - master 70 | -------------------------------------------------------------------------------- /.travis/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Based on https://github.com/pyca/cryptography/blob/master/.travis/install.sh 4 | # which is subject to the following BSD license: 5 | 6 | # Copyright (c) Individual contributors. 7 | # All rights reserved. 8 | 9 | # Redistribution and use in source and binary forms, with or without 10 | # modification, are permitted provided that the following conditions are met: 11 | 12 | # 1. Redistributions of source code must retain the above copyright notice, 13 | # this list of conditions and the following disclaimer. 14 | 15 | # 2. Redistributions in binary form must reproduce the above copyright 16 | # notice, this list of conditions and the following disclaimer in the 17 | # documentation and/or other materials provided with the distribution. 18 | 19 | # 3. Neither the name of PyCA Cryptography nor the names of its contributors 20 | # may be used to endorse or promote products derived from this software 21 | # without specific prior written permission. 22 | 23 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 24 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 25 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 26 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 27 | # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 28 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 29 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 30 | # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 31 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 32 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 33 | 34 | set -e 35 | set -x 36 | 37 | if [[ "$(uname -s)" == 'Darwin' ]]; then 38 | brew update || brew update 39 | 40 | brew outdated zeromq || brew upgrade zeromq 41 | 42 | # install pyenv 43 | git clone https://github.com/yyuu/pyenv.git ~/.pyenv 44 | PYENV_ROOT="$HOME/.pyenv" 45 | PATH="$PYENV_ROOT/bin:$PATH" 46 | eval "$(pyenv init -)" 47 | 48 | case "${TOXENV}" in 49 | py27) 50 | curl -O https://bootstrap.pypa.io/get-pip.py 51 | python get-pip.py --user 52 | ;; 53 | py33) 54 | pyenv install 3.3.6 55 | pyenv global 3.3.6 56 | ;; 57 | py34) 58 | pyenv install 3.4.4 59 | pyenv global 3.4.4 60 | ;; 61 | py35) 62 | pyenv install 3.5.1 63 | pyenv global 3.5.1 64 | ;; 65 | esac 66 | pyenv rehash 67 | python -m pip install --user virtualenv 68 | else 69 | pip install virtualenv 70 | fi 71 | 72 | python -m virtualenv ~/.venv 73 | source ~/.venv/bin/activate 74 | pip install tox 75 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Tim Golden 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | NetworkZero 2 | =========== 3 | 4 | .. image:: https://travis-ci.org/tjguk/networkzero.svg?branch=master 5 | :target: https://travis-ci.org/tjguk/networkzero 6 | 7 | Make it easy for learning groups to use simple networking in Python 8 | 9 | * Docs: http://networkzero.readthedocs.org/en/latest/ 10 | 11 | * Development: https://github.com/tjguk/networkzero 12 | 13 | * Tests: to run the tests, run tox. All tests are run on Travis (Linux & Apple) 14 | and Appveyor (Windows) 15 | 16 | API 17 | --- 18 | 19 | address below refers to an IP:Port string eg "192.0.2.5:4567" 20 | 21 | Discovery 22 | ~~~~~~~~~ 23 | 24 | * address = advertise(name, address=None) 25 | 26 | * address = discover(name, wait_for_s=FOREVER) 27 | 28 | * [(name, address), ...] = discover_all() 29 | 30 | * [(name, address), ...] = discover_group(group_name, separator="/") 31 | 32 | Messaging 33 | ~~~~~~~~~ 34 | 35 | * reply = send_message_to(address, message, wait_for_reply_s=FOREVER) 36 | 37 | * message = wait_for_message_from(address, [wait_for_s=FOREVER]) 38 | 39 | * send_reply_to(address, reply) 40 | 41 | * send_news_to(address, news) 42 | 43 | * wait_for_news_from(address[, pattern=EVERYTHING][, wait_for_s=FOREVER]) 44 | 45 | Typical Usage 46 | ------------- 47 | 48 | On computer (or process) A: 49 | 50 | .. code-block:: python 51 | 52 | import networkzero as nw0 53 | 54 | address = nw0.advertise("hello") 55 | while True: 56 | name = nw0.wait_for_message_from(address) 57 | nw0.send_reply_to(address, "Hello, %s" % name) 58 | 59 | On computer (or process) B and C and D...: 60 | 61 | .. code-block:: python 62 | 63 | import networkzero as nw0 64 | 65 | server = nw0.discover("hello") 66 | reply = nw0.send_message_to(server, "World") 67 | print(reply) 68 | reply = nw0.send_message_to(server, "Tim") 69 | print(reply) 70 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | # All Vagrant configuration is done below. The "2" in Vagrant.configure 5 | # configures the configuration version (we support older styles for 6 | # backwards compatibility). Please don't change it unless you know what 7 | # you're doing. 8 | Vagrant.configure(2) do |config| 9 | # The most common configuration options are documented and commented below. 10 | # For a complete reference, please see the online documentation at 11 | # https://docs.vagrantup.com. 12 | 13 | # Every Vagrant development environment requires a box. You can search for 14 | # boxes at https://atlas.hashicorp.com/search. 15 | config.vm.box = "minimal/jessie64" 16 | 17 | # Enable provisioning with a shell script. Additional provisioners such as 18 | # Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the 19 | # documentation for more information about their specific syntax and use. 20 | config.vm.provision "shell", inline: <<-SHELL 21 | sudo apt-get update 22 | sudo apt-get install -y python3 python3-pip 23 | sudo apt-get install -y python python-pip 24 | SHELL 25 | end 26 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # Based on https://github.com/audreyr/cookiecutter/blob/master/appveyor.yml 2 | # which has this license: 3 | 4 | # Copyright (c) 2013-2015, Audrey Roy 5 | # All rights reserved. 6 | 7 | # Redistribution and use in source and binary forms, with or 8 | # without modification, are permitted provided that the following 9 | # conditions are met: 10 | 11 | # * Redistributions of source code must retain the above copyright 12 | # notice, this list of conditions and the following disclaimer. 13 | 14 | # * Redistributions in binary form must reproduce the above 15 | # copyright notice, this list of conditions and the following 16 | # disclaimer in the documentation and/or other materials provided 17 | # with the distribution. 18 | 19 | # * Neither the name of border nor the names of its contributors 20 | # may be used to endorse or promote products derived from this 21 | # software without specific prior written permission. 22 | 23 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 24 | # CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 25 | # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 26 | # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 27 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 28 | # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 29 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 30 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF 31 | # USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED 32 | # AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 33 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 34 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 35 | # POSSIBILITY OF SUCH DAMAGE. 36 | 37 | # What Python version is installed where: 38 | # http://www.appveyor.com/docs/installed-software#python 39 | 40 | environment: 41 | matrix: 42 | - PYTHON: "C:\\Python27" 43 | TOX_ENV: "py27" 44 | 45 | - PYTHON: "C:\\Python33" 46 | TOX_ENV: "py33" 47 | 48 | - PYTHON: "C:\\Python34" 49 | TOX_ENV: "py34" 50 | 51 | - PYTHON: "C:\\Python35" 52 | TOX_ENV: "py35" 53 | 54 | 55 | init: 56 | - "%PYTHON%/python -V" 57 | - "%PYTHON%/python -c \"import struct;print( 8 * struct.calcsize(\'P\'))\"" 58 | 59 | install: 60 | - "%PYTHON%/Scripts/easy_install -U pip" 61 | - "%PYTHON%/Scripts/pip install tox" 62 | 63 | build: false 64 | 65 | test_script: 66 | - "%PYTHON%/Scripts/tox -e %TOX_ENV%" 67 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don\'t have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help 23 | help: 24 | @echo "Please use \`make ' where is one of" 25 | @echo " html to make standalone HTML files" 26 | @echo " dirhtml to make HTML files named index.html in directories" 27 | @echo " singlehtml to make a single large HTML file" 28 | @echo " pickle to make pickle files" 29 | @echo " json to make JSON files" 30 | @echo " htmlhelp to make HTML files and a HTML help project" 31 | @echo " qthelp to make HTML files and a qthelp project" 32 | @echo " applehelp to make an Apple Help Book" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " epub3 to make an epub3" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | 51 | .PHONY: clean 52 | clean: 53 | rm -rf $(BUILDDIR)/* 54 | 55 | .PHONY: html 56 | html: 57 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 58 | @echo 59 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 60 | 61 | .PHONY: dirhtml 62 | dirhtml: 63 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 64 | @echo 65 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 66 | 67 | .PHONY: singlehtml 68 | singlehtml: 69 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 70 | @echo 71 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 72 | 73 | .PHONY: pickle 74 | pickle: 75 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 76 | @echo 77 | @echo "Build finished; now you can process the pickle files." 78 | 79 | .PHONY: json 80 | json: 81 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 82 | @echo 83 | @echo "Build finished; now you can process the JSON files." 84 | 85 | .PHONY: htmlhelp 86 | htmlhelp: 87 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 88 | @echo 89 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 90 | ".hhp project file in $(BUILDDIR)/htmlhelp." 91 | 92 | .PHONY: qthelp 93 | qthelp: 94 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 95 | @echo 96 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 97 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 98 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/NetworkZero.qhcp" 99 | @echo "To view the help file:" 100 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/NetworkZero.qhc" 101 | 102 | .PHONY: applehelp 103 | applehelp: 104 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 105 | @echo 106 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 107 | @echo "N.B. You won't be able to view it unless you put it in" \ 108 | "~/Library/Documentation/Help or install it in your application" \ 109 | "bundle." 110 | 111 | .PHONY: devhelp 112 | devhelp: 113 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 114 | @echo 115 | @echo "Build finished." 116 | @echo "To view the help file:" 117 | @echo "# mkdir -p $$HOME/.local/share/devhelp/NetworkZero" 118 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/NetworkZero" 119 | @echo "# devhelp" 120 | 121 | .PHONY: epub 122 | epub: 123 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 124 | @echo 125 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 126 | 127 | .PHONY: epub3 128 | epub3: 129 | $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 130 | @echo 131 | @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." 132 | 133 | .PHONY: latex 134 | latex: 135 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 136 | @echo 137 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 138 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 139 | "(use \`make latexpdf' here to do that automatically)." 140 | 141 | .PHONY: latexpdf 142 | latexpdf: 143 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 144 | @echo "Running LaTeX files through pdflatex..." 145 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 146 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 147 | 148 | .PHONY: latexpdfja 149 | latexpdfja: 150 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 151 | @echo "Running LaTeX files through platex and dvipdfmx..." 152 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 153 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 154 | 155 | .PHONY: text 156 | text: 157 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 158 | @echo 159 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 160 | 161 | .PHONY: man 162 | man: 163 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 164 | @echo 165 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 166 | 167 | .PHONY: texinfo 168 | texinfo: 169 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 170 | @echo 171 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 172 | @echo "Run \`make' in that directory to run these through makeinfo" \ 173 | "(use \`make info' here to do that automatically)." 174 | 175 | .PHONY: info 176 | info: 177 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 178 | @echo "Running Texinfo files through makeinfo..." 179 | make -C $(BUILDDIR)/texinfo info 180 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 181 | 182 | .PHONY: gettext 183 | gettext: 184 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 185 | @echo 186 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 187 | 188 | .PHONY: changes 189 | changes: 190 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 191 | @echo 192 | @echo "The overview file is in $(BUILDDIR)/changes." 193 | 194 | .PHONY: linkcheck 195 | linkcheck: 196 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 197 | @echo 198 | @echo "Link check complete; look for any errors in the above output " \ 199 | "or in $(BUILDDIR)/linkcheck/output.txt." 200 | 201 | .PHONY: doctest 202 | doctest: 203 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 204 | @echo "Testing of doctests in the sources finished, look at the " \ 205 | "results in $(BUILDDIR)/doctest/output.txt." 206 | 207 | .PHONY: coverage 208 | coverage: 209 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 210 | @echo "Testing of coverage in the sources finished, look at the " \ 211 | "results in $(BUILDDIR)/coverage/python.txt." 212 | 213 | .PHONY: xml 214 | xml: 215 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 216 | @echo 217 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 218 | 219 | .PHONY: pseudoxml 220 | pseudoxml: 221 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 222 | @echo 223 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 224 | -------------------------------------------------------------------------------- /docs/_static/README: -------------------------------------------------------------------------------- 1 | static folder -------------------------------------------------------------------------------- /docs/build.cmd: -------------------------------------------------------------------------------- 1 | CALL make.bat html 2 | IF ERRORLEVEL 1 GOTO error 3 | start _build\html\index.html 4 | PAUSE 5 | GOTO xit 6 | 7 | :error 8 | PAUSE 9 | GOTO xit 10 | 11 | :xit 12 | 13 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # NetworkZero documentation build configuration file, created by 5 | # sphinx-quickstart on Thu Apr 7 10:02:05 2016. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | import sys 17 | import os 18 | 19 | # If extensions (or modules to document with autodoc) are in another directory, 20 | # add these directories to sys.path here. If the directory is relative to the 21 | # documentation root, use os.path.abspath to make it absolute, like shown here. 22 | sys.path.insert(0, os.path.abspath('..')) 23 | 24 | # -- General configuration ------------------------------------------------ 25 | 26 | # If your documentation needs a minimal Sphinx version, state it here. 27 | #needs_sphinx = '1.0' 28 | 29 | # Add any Sphinx extension module names here, as strings. They can be 30 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 31 | # ones. 32 | extensions = [ 33 | 'sphinx.ext.autodoc', 34 | ] 35 | 36 | # Add any paths that contain templates here, relative to this directory. 37 | templates_path = ['_templates'] 38 | 39 | # The suffix(es) of source filenames. 40 | # You can specify multiple suffix as a list of string: 41 | # source_suffix = ['.rst', '.md'] 42 | source_suffix = '.rst' 43 | 44 | # The encoding of source files. 45 | #source_encoding = 'utf-8-sig' 46 | 47 | # The master toctree document. 48 | master_doc = 'toc' 49 | 50 | # General information about the project. 51 | project = 'NetworkZero' 52 | copyright = '2016, Tim Golden' 53 | author = 'Tim Golden' 54 | 55 | # The version info for the project you're documenting, acts as replacement for 56 | # |version| and |release|, also used in various other places throughout the 57 | # built documents. 58 | # 59 | # The short X.Y version. 60 | version = '1.0' 61 | # The full version, including alpha/beta/rc tags. 62 | release = '1.0beta1' 63 | 64 | # The language for content autogenerated by Sphinx. Refer to documentation 65 | # for a list of supported languages. 66 | # 67 | # This is also used if you do content translation via gettext catalogs. 68 | # Usually you set "language" from the command line for these cases. 69 | language = None 70 | 71 | # There are two options for replacing |today|: either, you set today to some 72 | # non-false value, then it is used: 73 | #today = '' 74 | # Else, today_fmt is used as the format for a strftime call. 75 | #today_fmt = '%B %d, %Y' 76 | 77 | # List of patterns, relative to source directory, that match files and 78 | # directories to ignore when looking for source files. 79 | # This patterns also effect to html_static_path and html_extra_path 80 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 81 | 82 | # The reST default role (used for this markup: `text`) to use for all 83 | # documents. 84 | #default_role = None 85 | 86 | # If true, '()' will be appended to :func: etc. cross-reference text. 87 | #add_function_parentheses = True 88 | 89 | # If true, the current module name will be prepended to all description 90 | # unit titles (such as .. function::). 91 | #add_module_names = True 92 | 93 | # If true, sectionauthor and moduleauthor directives will be shown in the 94 | # output. They are ignored by default. 95 | #show_authors = False 96 | 97 | # The name of the Pygments (syntax highlighting) style to use. 98 | pygments_style = 'sphinx' 99 | 100 | # A list of ignored prefixes for module index sorting. 101 | #modindex_common_prefix = [] 102 | 103 | # If true, keep warnings as "system message" paragraphs in the built documents. 104 | #keep_warnings = False 105 | 106 | # If true, `todo` and `todoList` produce output, else they produce nothing. 107 | todo_include_todos = False 108 | 109 | 110 | # -- Options for HTML output ---------------------------------------------- 111 | 112 | # The theme to use for HTML and HTML Help pages. See the documentation for 113 | # a list of builtin themes. 114 | html_theme = 'bizstyle' 115 | 116 | # Theme options are theme-specific and customize the look and feel of a theme 117 | # further. For a list of options available for each theme, see the 118 | # documentation. 119 | #html_theme_options = {} 120 | 121 | # Add any paths that contain custom themes here, relative to this directory. 122 | #html_theme_path = [] 123 | 124 | # The name for this set of Sphinx documents. 125 | # " v documentation" by default. 126 | #html_title = 'NetworkZero v0.9' 127 | 128 | # A shorter title for the navigation bar. Default is the same as html_title. 129 | #html_short_title = None 130 | 131 | # The name of an image file (relative to this directory) to place at the top 132 | # of the sidebar. 133 | #html_logo = None 134 | 135 | # The name of an image file (relative to this directory) to use as a favicon of 136 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 137 | # pixels large. 138 | #html_favicon = None 139 | 140 | # Add any paths that contain custom static files (such as style sheets) here, 141 | # relative to this directory. They are copied after the builtin static files, 142 | # so a file named "default.css" will overwrite the builtin "default.css". 143 | html_static_path = ['_static'] 144 | 145 | # Add any extra paths that contain custom files (such as robots.txt or 146 | # .htaccess) here, relative to this directory. These files are copied 147 | # directly to the root of the documentation. 148 | #html_extra_path = [] 149 | 150 | # If not None, a 'Last updated on:' timestamp is inserted at every page 151 | # bottom, using the given strftime format. 152 | # The empty string is equivalent to '%b %d, %Y'. 153 | #html_last_updated_fmt = None 154 | 155 | # If true, SmartyPants will be used to convert quotes and dashes to 156 | # typographically correct entities. 157 | #html_use_smartypants = True 158 | 159 | # Custom sidebar templates, maps document names to template names. 160 | #html_sidebars = {} 161 | 162 | # Additional templates that should be rendered to pages, maps page names to 163 | # template names. 164 | #html_additional_pages = {} 165 | 166 | # If false, no module index is generated. 167 | #html_domain_indices = True 168 | 169 | # If false, no index is generated. 170 | #html_use_index = True 171 | 172 | # If true, the index is split into individual pages for each letter. 173 | #html_split_index = False 174 | 175 | # If true, links to the reST sources are added to the pages. 176 | #html_show_sourcelink = True 177 | 178 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 179 | #html_show_sphinx = True 180 | 181 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 182 | #html_show_copyright = True 183 | 184 | # If true, an OpenSearch description file will be output, and all pages will 185 | # contain a tag referring to it. The value of this option must be the 186 | # base URL from which the finished HTML is served. 187 | #html_use_opensearch = '' 188 | 189 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 190 | #html_file_suffix = None 191 | 192 | # Language to be used for generating the HTML full-text search index. 193 | # Sphinx supports the following languages: 194 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' 195 | # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr', 'zh' 196 | #html_search_language = 'en' 197 | 198 | # A dictionary with options for the search language support, empty by default. 199 | # 'ja' uses this config value. 200 | # 'zh' user can custom change `jieba` dictionary path. 201 | #html_search_options = {'type': 'default'} 202 | 203 | # The name of a javascript file (relative to the configuration directory) that 204 | # implements a search results scorer. If empty, the default will be used. 205 | #html_search_scorer = 'scorer.js' 206 | 207 | # Output file base name for HTML help builder. 208 | htmlhelp_basename = 'NetworkZerodoc' 209 | 210 | # -- Options for LaTeX output --------------------------------------------- 211 | 212 | latex_elements = { 213 | # The paper size ('letterpaper' or 'a4paper'). 214 | #'papersize': 'letterpaper', 215 | 216 | # The font size ('10pt', '11pt' or '12pt'). 217 | #'pointsize': '10pt', 218 | 219 | # Additional stuff for the LaTeX preamble. 220 | #'preamble': '', 221 | 222 | # Latex figure (float) alignment 223 | #'figure_align': 'htbp', 224 | } 225 | 226 | # Grouping the document tree into LaTeX files. List of tuples 227 | # (source start file, target name, title, 228 | # author, documentclass [howto, manual, or own class]). 229 | latex_documents = [ 230 | (master_doc, 'NetworkZero.tex', 'NetworkZero Documentation', 231 | 'Tim Golden', 'manual'), 232 | ] 233 | 234 | # The name of an image file (relative to this directory) to place at the top of 235 | # the title page. 236 | #latex_logo = None 237 | 238 | # For "manual" documents, if this is true, then toplevel headings are parts, 239 | # not chapters. 240 | #latex_use_parts = False 241 | 242 | # If true, show page references after internal links. 243 | #latex_show_pagerefs = False 244 | 245 | # If true, show URL addresses after external links. 246 | #latex_show_urls = False 247 | 248 | # Documents to append as an appendix to all manuals. 249 | #latex_appendices = [] 250 | 251 | # If false, no module index is generated. 252 | #latex_domain_indices = True 253 | 254 | 255 | # -- Options for manual page output --------------------------------------- 256 | 257 | # One entry per manual page. List of tuples 258 | # (source start file, name, description, authors, manual section). 259 | man_pages = [ 260 | (master_doc, 'networkzero', 'NetworkZero Documentation', 261 | [author], 1) 262 | ] 263 | 264 | # If true, show URL addresses after external links. 265 | #man_show_urls = False 266 | 267 | 268 | # -- Options for Texinfo output ------------------------------------------- 269 | 270 | # Grouping the document tree into Texinfo files. List of tuples 271 | # (source start file, target name, title, author, 272 | # dir menu entry, description, category) 273 | texinfo_documents = [ 274 | (master_doc, 'NetworkZero', 'NetworkZero Documentation', 275 | author, 'NetworkZero', 'One line description of project.', 276 | 'Miscellaneous'), 277 | ] 278 | 279 | # Documents to append as an appendix to all manuals. 280 | #texinfo_appendices = [] 281 | 282 | # If false, no module index is generated. 283 | #texinfo_domain_indices = True 284 | 285 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 286 | #texinfo_show_urls = 'footnote' 287 | 288 | # If true, do not generate a @detailmenu in the "Top" node's menu. 289 | #texinfo_no_detailmenu = False 290 | -------------------------------------------------------------------------------- /docs/cookbook.rst: -------------------------------------------------------------------------------- 1 | .. module:: networkzero 2 | 3 | Cookbook 4 | ======== 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | :glob: 9 | 10 | cookbook/simple-discovery 11 | cookbook/simple-messaging 12 | cookbook/advanced-discovery 13 | cookbook/advanced-messaging 14 | cookbook/complete-examples 15 | -------------------------------------------------------------------------------- /docs/cookbook/advanced-discovery.rst: -------------------------------------------------------------------------------- 1 | .. currentmodule:: discovery 2 | .. highlight:: python 3 | :linenothreshold: 1 4 | 5 | Advertise & Discover Services: Advanced Usage 6 | ============================================= 7 | 8 | The examples here all refer to the :mod:`networkzero.discovery` module. 9 | 10 | 11 | Discover a group of services 12 | ---------------------------- 13 | 14 | On machine A 15 | ~~~~~~~~~~~~ 16 | .. literalinclude:: advanced-discovery/discover_group_a.py 17 | 18 | On machine B 19 | ~~~~~~~~~~~~ 20 | .. literalinclude:: advanced-discovery/discover_group_b.py 21 | 22 | On machine C 23 | ~~~~~~~~~~~~ 24 | .. literalinclude:: advanced-discovery/discover_group_c.py 25 | 26 | Discussion 27 | ~~~~~~~~~~ 28 | By adopting a common convention, it is possible to group advertised services 29 | together. This could be used, as in this example, to identify all members of 30 | a particular chat session. Or---in a classroom or club situation---to have 31 | teams identify their own services so as not to confuse them with other team's 32 | services of the same name. 33 | 34 | Use a different separator for a group of services 35 | ------------------------------------------------- 36 | 37 | .. literalinclude:: advanced-discovery/discover_group_colon.py 38 | 39 | Discussion 40 | ~~~~~~~~~~ 41 | Perhaps it's not convenient to use the "/" character to separate the group 42 | from the service name. You can use any character to separate the parts of 43 | the name. Then you specify that character when you call `discover_group`. 44 | 45 | 46 | Dynamically discover services 47 | ----------------------------- 48 | 49 | Node A 50 | ~~~~~~ 51 | .. literalinclude:: advanced-discovery/dynamically_discover_nodes_a.py 52 | 53 | Node B 54 | ~~~~~~ 55 | .. literalinclude:: advanced-discovery/dynamically_discover_nodes_b.py 56 | 57 | Node C 58 | ~~~~~~ 59 | .. literalinclude:: advanced-discovery/dynamically_discover_nodes_c.py 60 | 61 | Master 62 | ~~~~~~ 63 | .. literalinclude:: advanced-discovery/dynamically_discover_nodes_master.py 64 | 65 | Discussion 66 | ~~~~~~~~~~ 67 | A cluster is a group of computers working together. Each computer registers 68 | a service called "cluster/X" where "X" is the computer's name. If that 69 | machine stops running, its advert will time out and no longer be available. 70 | 71 | The computer which is co-ordinating the cluster checks every so often to 72 | see which computers have left the cluster and which have joined. 73 | -------------------------------------------------------------------------------- /docs/cookbook/advanced-discovery/discover_group_a.py: -------------------------------------------------------------------------------- 1 | import networkzero as nw0 2 | 3 | alice = nw0.advertise("chat/alice") 4 | -------------------------------------------------------------------------------- /docs/cookbook/advanced-discovery/discover_group_b.py: -------------------------------------------------------------------------------- 1 | import networkzero as nw0 2 | 3 | bob = nw0.advertise("chat/bob") 4 | -------------------------------------------------------------------------------- /docs/cookbook/advanced-discovery/discover_group_c.py: -------------------------------------------------------------------------------- 1 | import networkzero as nw0 2 | 3 | services = dict(nw0.discover_group("chat")) 4 | print(services) -------------------------------------------------------------------------------- /docs/cookbook/advanced-discovery/discover_group_colon.py: -------------------------------------------------------------------------------- 1 | import networkzero as nw0 2 | 3 | alice = nw0.advertise("chat:alice") 4 | bob = nw0.advertise("chat:bob") 5 | doug = nw0.advertise("chat:doug") 6 | services = dict(nw0.discover_group("chat", separator=":")) 7 | 8 | print(services) -------------------------------------------------------------------------------- /docs/cookbook/advanced-discovery/dynamically_discover_nodes_a.py: -------------------------------------------------------------------------------- 1 | import networkzero as nw0 2 | 3 | name = "A" 4 | 5 | nw0.advertise("cluster/%s" % name) 6 | master = nw0.discover("cluster/master") 7 | 8 | while True: 9 | command = nw0.wait_for_message_from(master, autoreply=True) 10 | 11 | # 12 | # ... something goes wrong 13 | # 14 | raise RuntimeError 15 | -------------------------------------------------------------------------------- /docs/cookbook/advanced-discovery/dynamically_discover_nodes_b.py: -------------------------------------------------------------------------------- 1 | import networkzero as nw0 2 | 3 | name = "B" 4 | 5 | nw0.advertise("cluster/%s" % name) 6 | master = nw0.discover("cluster/master") 7 | 8 | while True: 9 | command = nw0.wait_for_message_from(master, autoreply=True) 10 | # 11 | # Do useful things until told to stop 12 | # 13 | if command == "STOP": 14 | break 15 | -------------------------------------------------------------------------------- /docs/cookbook/advanced-discovery/dynamically_discover_nodes_c.py: -------------------------------------------------------------------------------- 1 | import networkzero as nw0 2 | 3 | name = "C" 4 | 5 | # 6 | # This machine only starts up a few seconds after the others 7 | # 8 | time.sleep(3) 9 | nw0.advertise("cluster/%s" % name) 10 | master = nw0.discover("cluster/master") 11 | 12 | while True: 13 | command = nw0.wait_for_message_from(master, autoreply=True) 14 | # 15 | # Do useful things until told to stop 16 | # 17 | if command == "STOP": 18 | break 19 | -------------------------------------------------------------------------------- /docs/cookbook/advanced-discovery/dynamically_discover_nodes_master.py: -------------------------------------------------------------------------------- 1 | import networkzero as nw0 2 | 3 | name = "cluster/master" 4 | address = nw0.advertise(name) 5 | 6 | # 7 | # On the cluster master 8 | # 9 | nodes = set(nw0.discover_group("cluster", exclude=name)) 10 | old_nodes = nodes 11 | print("Nodes:", ", ".join(nodes)) 12 | 13 | # 14 | # Send a command to A .... which will unaccountably fail 15 | # 16 | node_a = nw0.discover("cluster/A") 17 | nw0.send_message_to(node_a, "STOP") 18 | 19 | # 20 | # Wait a few seconds for node C to wake up 21 | # 22 | time.sleep(5) 23 | 24 | # 25 | # On the cluster master 26 | # 27 | nodes = set(nw0.discover_group("cluster")) 28 | for name, address in old_nodes - nodes: 29 | print("%s has left the cluster" % name) 30 | for name, address in nodes - old_nodes: 31 | print("%s has joined the cluster" % name) 32 | 33 | for name, address in nodes: 34 | nw0.send_message_to(address, "STOP") 35 | -------------------------------------------------------------------------------- /docs/cookbook/advanced-messaging.rst: -------------------------------------------------------------------------------- 1 | .. currentmodule:: messenger 2 | .. highlight:: python 3 | :linenothreshold: 1 4 | 5 | Exchanging Messages: Advanced Usage 6 | =================================== 7 | 8 | The examples here all refer to the :mod:`messenger` module. 9 | 10 | News from more than one source 11 | ------------------------------ 12 | 13 | Process A, B, C and D 14 | ~~~~~~~~~~~~~~~~~~~~~ 15 | 16 | .. literalinclude:: advanced-messaging/news_from_multiple_sources_abcd.py 17 | 18 | Process E 19 | ~~~~~~~~~ 20 | 21 | .. literalinclude:: advanced-messaging/news_from_multiple_sources_e.py 22 | 23 | Discussion 24 | ~~~~~~~~~~ 25 | A process can listen for news from more than one source. For example, 26 | if you have four movement sensors, one in each corner of a room, all 27 | ending their updates on the same channel, you can gather updates from 28 | all four at once. 29 | 30 | Sending Data 31 | ------------ 32 | 33 | Process A 34 | ~~~~~~~~~ 35 | 36 | .. literalinclude:: advanced-messaging/sending_data_a.py 37 | 38 | Process B 39 | ~~~~~~~~~ 40 | 41 | .. literalinclude:: advanced-messaging/sending_data_b.py 42 | 43 | Discussion 44 | ~~~~~~~~~~ 45 | 46 | Sometimes the data you want to send isn't text or numbers: it's binary 47 | data---bytes---such as the bytes which make up an image or a sound file. To send that via 48 | networkzero you have to treat it specially: at the sending end, you convert 49 | it to a string; and at the receiving end, you convert it back into bytes. 50 | 51 | Polling 52 | ------- 53 | 54 | Process A 55 | ~~~~~~~~~ 56 | 57 | .. literalinclude:: advanced-messaging/polling_messages_a.py 58 | 59 | Process B 60 | ~~~~~~~~~ 61 | 62 | .. literalinclude:: advanced-messaging/polling_messages_b.py 63 | 64 | Discussion 65 | ~~~~~~~~~~ 66 | 67 | By default, when you wait for a network message, your process blocks: that is, 68 | it won't do anything else until a message arrives. Sometimes that's perfectly 69 | sensible: if your robot can't go anywhere until it knows where to go then 70 | the process must block until it receives an instruction. 71 | 72 | Sometimes, though, you want to be able to get on with other things while 73 | waiting for a message to arrive on the network. There are several approaches 74 | to this in general: threading, asynchronous IO, event loops... and polling. 75 | Polling is the simplest of these and is easy to do with networkzero: you 76 | simply check for a message briefly before carrying on. If a message has 77 | arrived, you can act on it; otherwise, you do the other things you want 78 | to do (for example update your game screen, have your robot check its 79 | sensors, etc.) -------------------------------------------------------------------------------- /docs/cookbook/advanced-messaging/news_from_multiple_sources_abcd.py: -------------------------------------------------------------------------------- 1 | import networkzero as nw0 2 | import random 3 | import time 4 | import uuid 5 | 6 | name = "movement/%s" % uuid.uuid1().hex 7 | address = nw0.advertise(name) 8 | print("Sending from %s -> %s" % (name, address)) 9 | 10 | # 11 | # In lieu of an actual sensor! 12 | # 13 | while True: 14 | is_movement = random.random() > 0.5 15 | nw0.send_news_to(address, "movement", (name, is_movement)) 16 | time.sleep(random.random()) 17 | -------------------------------------------------------------------------------- /docs/cookbook/advanced-messaging/news_from_multiple_sources_e.py: -------------------------------------------------------------------------------- 1 | import networkzero as nw0 2 | 3 | addresses = [address for name, address in nw0.discover_group("movement")] 4 | 5 | while True: 6 | topic, (sensor, is_movement) = nw0.wait_for_news_from(addresses) 7 | if is_movement: 8 | print("Movement from %s!!!" % sensor) 9 | -------------------------------------------------------------------------------- /docs/cookbook/advanced-messaging/polling_messages_a.py: -------------------------------------------------------------------------------- 1 | import time 2 | import networkzero as nw0 3 | 4 | address = nw0.advertise("poller1") 5 | 6 | while True: 7 | message = nw0.wait_for_message_from(address, wait_for_s=0) 8 | if message is not None: 9 | print("Received:", message) 10 | nw0.send_reply_to(address, "Received: %s" % message) 11 | break 12 | else: 13 | print("Doing other useful things ...") 14 | time.sleep(1) 15 | -------------------------------------------------------------------------------- /docs/cookbook/advanced-messaging/polling_messages_b.py: -------------------------------------------------------------------------------- 1 | import time 2 | import networkzero as nw0 3 | 4 | service = nw0.discover("poller1") 5 | 6 | time.sleep(3) 7 | reply = nw0.send_message_to(service, "This is a message") 8 | print("Reply: ", reply) 9 | -------------------------------------------------------------------------------- /docs/cookbook/advanced-messaging/sending_data_a.py: -------------------------------------------------------------------------------- 1 | import os, sys 2 | import tempfile 3 | import networkzero as nw0 4 | 5 | address = nw0.advertise("gallery") 6 | print("Gallery:", address) 7 | while True: 8 | filename, data = nw0.wait_for_message_from(address, autoreply=True) 9 | bytes = nw0.string_to_bytes(data) 10 | 11 | temp_filepath = os.path.join(tempfile.gettempdir(), filename) 12 | with open(temp_filepath, "wb") as f: 13 | f.write(bytes) 14 | print("Wrote", temp_filepath) 15 | -------------------------------------------------------------------------------- /docs/cookbook/advanced-messaging/sending_data_b.py: -------------------------------------------------------------------------------- 1 | import os, sys 2 | import networkzero as nw0 3 | 4 | try: 5 | input = raw_input 6 | except NameError: 7 | pass 8 | 9 | gallery = nw0.discover("gallery") 10 | filepath = input("Filename: ") 11 | filename = os.path.basename(filepath) 12 | with open(filepath, "rb") as f: 13 | data = f.read() 14 | 15 | nw0.send_message_to(gallery, (filename, nw0.bytes_to_string(data))) 16 | -------------------------------------------------------------------------------- /docs/cookbook/complete-examples.rst: -------------------------------------------------------------------------------- 1 | .. currentmodule:: messenger 2 | .. highlight:: python 3 | :linenothreshold: 1 4 | 5 | Complete Examples 6 | ================= 7 | 8 | ... coming soon ... -------------------------------------------------------------------------------- /docs/cookbook/complete-examples/anagram-solver/anagram-client.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import networkzero as nw0 3 | 4 | try: 5 | # Python 2.7 compat 6 | input = raw_input 7 | except NameError: 8 | pass 9 | 10 | service = nw0.discover("anagram") 11 | while True: 12 | anagram = input("Enter anagram: ") 13 | word = nw0.send_message_to(service, anagram) 14 | if word: 15 | print(word) 16 | else: 17 | print("Nothing found") 18 | -------------------------------------------------------------------------------- /docs/cookbook/complete-examples/anagram-solver/anagram-server.py: -------------------------------------------------------------------------------- 1 | import networkzero as nw0 2 | 3 | # 4 | # The anagram-solving element of this will be brute-force: 5 | # maintain a list of words and compare against them until 6 | # you get a match or run out of words. There is obvious 7 | # scope for optimisation here, but this will run fast enough 8 | # and should be clear; anything further is left as an exercise 9 | # for the reader! 10 | # 11 | with open("words.txt") as f: 12 | words = f.read().split() 13 | 14 | # 15 | # Advertise service as 'anagram' 16 | # 17 | address = nw0.advertise("anagram") 18 | 19 | while True: 20 | print("Waiting for a word...") 21 | # 22 | # Receive an anagram for which we have to find a match 23 | # 24 | anagram = nw0.wait_for_message_from(address).lower().strip() 25 | print("Looking for %s" % anagram) 26 | 27 | # 28 | # To compare two words we'll sort each and compare 29 | # the result. We need only sort the word we're searching 30 | # for once. 31 | # 32 | letters = "".join(sorted(anagram)) 33 | 34 | # 35 | # Compare our incoming anagram, sorted, against each word 36 | # in turn, sorted. If we get a match, send the matching 37 | # word and stop searching 38 | # 39 | for word in words: 40 | if "".join(sorted(word)) == letters: 41 | print("Found %s" % word) 42 | nw0.send_reply_to(address, word) 43 | break 44 | 45 | # 46 | # If we can't find a match, return None 47 | # 48 | else: 49 | print("No match found") 50 | nw0.send_reply_to(address, None) 51 | -------------------------------------------------------------------------------- /docs/cookbook/complete-examples/anagram-solver/readme.txt: -------------------------------------------------------------------------------- 1 | This very simple question-response server trivially reverses whatever 2 | is sent to it. -------------------------------------------------------------------------------- /docs/cookbook/complete-examples/pong/pong.py: -------------------------------------------------------------------------------- 1 | # Pong is a two-dimensional sports game that simulates table tennis. 2 | # The player controls an in-game paddle by moving it vertically across 3 | # the left side of the screen, and can compete against either a 4 | # computer-controlled opponent or another player controlling a second 5 | # paddle on the opposing side. Players use the paddles to hit a ball 6 | # back and forth. The aim is for each player to reach eleven points 7 | # before the opponent; points are earned when one fails to return 8 | # the ball to the other. 9 | import random 10 | 11 | WIDTH = 800 12 | HEIGHT = 600 13 | TITLE = 'pong' 14 | 15 | # a color used to draw things 16 | MAIN_COLOR = 'yellow' 17 | 18 | # width and height of a player paddle 19 | PADDLE_WIDTH = 15 20 | PADDLE_HEIGHT = 100 21 | 22 | # radius of the tennis ball 23 | TENNIS_BALL_RADIUS = 10 24 | 25 | LEFT_PLAYER = "left" 26 | RIGHT_PLAYER = "right" 27 | 28 | 29 | class Paddle(Rect): 30 | """ 31 | Paddle represents one player on the screen. 32 | 33 | It is drawn like a long rectangle and positioned either left or 34 | right on the screen. 35 | 36 | Two helper methods move the paddle up or down. 37 | """ 38 | 39 | def __init__(self, start_x, start_y): 40 | super().__init__(start_x, start_y, PADDLE_WIDTH, PADDLE_HEIGHT) 41 | 42 | def up(self): 43 | if self.y - 5 > 40: 44 | self.y -= 5 45 | 46 | def down(self): 47 | if self.y + self.height + 5 < HEIGHT - 40: 48 | self.y += 5 49 | 50 | def draw(self): 51 | screen.draw.filled_rect(self, MAIN_COLOR) 52 | 53 | 54 | class TennisBall(): 55 | """ 56 | Represents a tennis ball on the screen 57 | """ 58 | 59 | def __init__(self, start_pos, dt): 60 | """ 61 | Initialize the tennis ball position and set the movement rate 62 | """ 63 | self.x, self.y = start_pos 64 | self.dx = self.dy = dt 65 | 66 | @property 67 | def pos(self): 68 | return (self.x, self.y) 69 | 70 | def move(self): 71 | self.x += self.dx 72 | self.y += self.dy 73 | 74 | def draw(self): 75 | screen.draw.filled_circle(self.pos, TENNIS_BALL_RADIUS, MAIN_COLOR) 76 | 77 | 78 | class Game(): 79 | 80 | def __init__(self, player): 81 | self.active_player = player 82 | self.score_left = 0 83 | self.score_right = 0 84 | 85 | self.in_progress = False 86 | self.computer_acting = False 87 | 88 | # position paddles in the middle of the screen 89 | middle = HEIGHT/2 - PADDLE_HEIGHT/2 90 | self.left_paddle = Paddle(20, middle) 91 | self.right_paddle = Paddle(WIDTH-40, middle) 92 | 93 | self.set_ball(self.ball_pos) 94 | 95 | @property 96 | def ball_pos(self): 97 | if self.active_player == LEFT_PLAYER: 98 | return (20 + PADDLE_WIDTH + 10, self.left_paddle.centery) 99 | else: 100 | return (WIDTH - 35 - PADDLE_WIDTH, self.right_paddle.centery) 101 | 102 | def set_ball(self, pos): 103 | # a ball is set on the paddle of last player that got a point 104 | dt = 5 if self.active_player == LEFT_PLAYER else -5 105 | self.tennis_ball = TennisBall(pos, dt) 106 | 107 | def position_ball(self): 108 | # used when the player moves tha paddle and 109 | # game is not in progress 110 | self.tennis_ball.x, self.tennis_ball.y = self.ball_pos 111 | 112 | def score_for_left(self): 113 | self.in_progress = False 114 | self.active_player = LEFT_PLAYER 115 | self.score_left += 1 116 | self.set_ball(self.ball_pos) 117 | 118 | def score_for_right(self): 119 | self.in_progress = False 120 | self.active_player = RIGHT_PLAYER 121 | self.score_right += 1 122 | self.set_ball(self.ball_pos) 123 | 124 | def proceed(self): 125 | self.tennis_ball.move() 126 | 127 | # bounce from the walls 128 | if self.tennis_ball.y <= 40: 129 | self.tennis_ball.dy = -self.tennis_ball.dy 130 | 131 | if self.tennis_ball.y >= HEIGHT - 40: 132 | self.tennis_ball.dy = -self.tennis_ball.dy 133 | 134 | # bounce from the paddles 135 | if self.left_paddle.collidepoint(self.tennis_ball.pos): 136 | self.tennis_ball.dx = -self.tennis_ball.dx 137 | 138 | if self.right_paddle.collidepoint(self.tennis_ball.pos): 139 | self.tennis_ball.dx = -self.tennis_ball.dx 140 | 141 | # if we didn't bounce, then that is a score 142 | if self.tennis_ball.x <= 0: 143 | self.score_for_right() 144 | 145 | if self.tennis_ball.x >= WIDTH: 146 | self.score_for_left() 147 | 148 | if self.score_left == 11 or self.score_right == 11: 149 | self.in_progress = False 150 | 151 | # computer movement 152 | def computer_launch(self): 153 | self.in_progress = True 154 | self.computer_acting = False 155 | 156 | def computer_stop_acting(self): 157 | self.computer_acting = False 158 | 159 | def computer_move(self): 160 | # move towards the center of the screen when the ball is 161 | # travelling toward the enemy 162 | if self.tennis_ball.dx > 0: 163 | target_y = HEIGHT / 2 164 | else: 165 | # when the ball is on other side of screen, just move 166 | # in general direction 167 | if self.tennis_ball.x > WIDTH / 2: 168 | delta = int(WIDTH * 0.25) 169 | if self.tennis_ball.dy < 0: 170 | target_y = self.tennis_ball.y - delta 171 | else: 172 | target_y = self.tennis_ball.y + delta 173 | else: 174 | # the ball is on our side, move with it 175 | rnd = random.randint(40, 200) 176 | if self.tennis_ball.dy < 0: 177 | target_y = self.tennis_ball.y - rnd 178 | else: 179 | target_y = self.tennis_ball.y + rnd 180 | 181 | target_y = max(40, min(target_y, HEIGHT - 80)) 182 | 183 | distance = abs(self.left_paddle.y - target_y) 184 | duration = min(1.0, distance / 600.0) 185 | 186 | animate(self.left_paddle, y=target_y, duration=.50, 187 | on_finished=self.computer_stop_acting) 188 | 189 | def computer_move_randomly(self): 190 | # move the paddle randomly during one second before launching the ball 191 | target_y = random.randint(40, HEIGHT - PADDLE_HEIGHT - 80) 192 | distance = abs(self.left_paddle.y - target_y) 193 | duration = max(0.1, distance / 200.0) 194 | self.computer_total_duration += duration 195 | 196 | if self.computer_total_duration + duration < 1.0: 197 | on_finished = self.computer_move_randomly 198 | else: 199 | on_finished = self.computer_launch 200 | 201 | animate(self.left_paddle, y=target_y, duration=duration, 202 | on_finished=on_finished) 203 | 204 | def computer_act(self): 205 | if self.in_progress: 206 | # predict where the ball will move and move towards it 207 | self.computer_move() 208 | elif self.active_player == LEFT_PLAYER: 209 | # move randomly for a bit, then shoot the ball out 210 | if not self.computer_acting: 211 | self.computer_acting = True 212 | 213 | self.computer_total_duration = 0.0 214 | self.computer_move_randomly() 215 | 216 | def draw(self): 217 | # slightly gray background 218 | screen.fill((64, 64, 64)) 219 | 220 | # show the score for the left player 221 | screen.draw.text( 222 | 'Computer: {}'.format(self.score_left), 223 | color=MAIN_COLOR, 224 | center=(WIDTH/4 - 20, 20), 225 | fontsize=48 226 | ) 227 | 228 | # show the score for the right player 229 | screen.draw.text( 230 | 'Player: {}'.format(self.score_right), 231 | color=MAIN_COLOR, 232 | center=(WIDTH/2 + WIDTH/4 - 20, 20), 233 | fontsize=48 234 | ) 235 | 236 | # a dividing line 237 | screen.draw.line( 238 | (WIDTH/2, 40), 239 | (WIDTH/2, HEIGHT-40), 240 | color=MAIN_COLOR) 241 | 242 | if self.score_left == 11: 243 | screen.draw.text( 244 | 'COMPUTER WINS!!!', 245 | color=MAIN_COLOR, 246 | center=(WIDTH/2, HEIGHT/2), 247 | fontsize=96 248 | ) 249 | elif self.score_right == 11: 250 | screen.draw.text( 251 | 'PLAYER WINS!!!', 252 | color=MAIN_COLOR, 253 | center=(WIDTH/2, HEIGHT/2), 254 | fontsize=96 255 | ) 256 | else: 257 | self.left_paddle.draw() 258 | self.right_paddle.draw() 259 | self.tennis_ball.draw() 260 | 261 | 262 | # first player is chosen randomly 263 | # player = LEFT_PLAYER if random.randint(0, 1000) % 2 == 0 else RIGHT_PLAYER 264 | player = LEFT_PLAYER 265 | game = Game(player) 266 | 267 | 268 | def draw(): 269 | game.draw() 270 | 271 | 272 | def update(): 273 | if keyboard.up: 274 | game.right_paddle.up() 275 | elif keyboard.down: 276 | game.right_paddle.down() 277 | 278 | # move the computer controlled paddle 279 | game.computer_act() 280 | 281 | # set the position of the ball to be in the middle of the paddle 282 | if not game.in_progress: 283 | game.position_ball() 284 | 285 | if game.in_progress: 286 | game.proceed() 287 | 288 | 289 | 290 | def on_key_down(key): 291 | # pressing SPACE launches the ball 292 | if key == keys.SPACE: 293 | if game.score_left == 11 or game.score_right == 11: 294 | game.score_left = game.score_right = 0 295 | 296 | if not game.in_progress and game.active_player == RIGHT_PLAYER: 297 | game.in_progress = True 298 | -------------------------------------------------------------------------------- /docs/cookbook/simple-discovery.rst: -------------------------------------------------------------------------------- 1 | .. currentmodule:: discovery 2 | .. highlight:: python 3 | :linenothreshold: 1 4 | 5 | Advertise & Discover Services 6 | ============================= 7 | 8 | The examples here all refer to the :mod:`networkzero.discovery` module. 9 | 10 | In the context of networkzero, an address is a string combining an 11 | IP address and a port, eg "192.0.2.1:12345". You can specify just 12 | the IP address, just the port, both or neither, depending on how 13 | much control you want. 14 | 15 | Advertise a service 16 | ------------------- 17 | 18 | .. literalinclude:: simple-discovery/advertise_a_service.py 19 | 20 | Discussion 21 | ~~~~~~~~~~ 22 | If you are running a service in one process, advertising it will enable 23 | other processes and machines to know what address to connect to. This 24 | example shows the minimum you need supply---just a name---to advertise 25 | a service. If you haven't chosen an address, one will be returned which 26 | you can then use to send and receive messages. 27 | 28 | Advertise a service with a specific address 29 | ------------------------------------------- 30 | 31 | .. literalinclude:: simple-discovery/advertise_a_service_with_a_specific_address.py 32 | 33 | Discussion 34 | ~~~~~~~~~~ 35 | If you know exactly what address you want to use for your service, you can 36 | specify that when advertising. In this example, no port is given so one 37 | will be selected at random from the range of ports designated as dynamic. 38 | 39 | Advertise a service with a specific port 40 | ---------------------------------------- 41 | 42 | .. literalinclude:: simple-discovery/advertise_a_service_with_a_specific_port.py 43 | 44 | Discussion 45 | ~~~~~~~~~~ 46 | It may be that you've agreed a particular port to work with, regardless 47 | of the network you're on. In that case, you can supply just a port number 48 | and a suitable IP address will be supplied by networkzero. 49 | 50 | Advertise a service with a wildcard address 51 | ------------------------------------------- 52 | 53 | .. literalinclude:: simple-discovery/advertise_a_service_with_a_wildcard_address.py 54 | 55 | Discussion 56 | ~~~~~~~~~~ 57 | Sometimes the machine you're on has several IP addresses, for example when 58 | it is connected to to a wireless and to a wired network, or when it supports 59 | a virtual machine. Most of the time, networkzero will select the best address 60 | but if it doesn't you can specify which of the networks you want to use. 61 | 62 | Note that this doesn't mean you have to specify the entire IP address, merely 63 | enough of it to distinguish one network from another. 64 | 65 | Discover a service 66 | ------------------ 67 | 68 | On machine A 69 | ~~~~~~~~~~~~ 70 | .. literalinclude:: simple-discovery/discover_a_service_a.py 71 | 72 | On machine B 73 | ~~~~~~~~~~~~ 74 | .. literalinclude:: simple-discovery/discover_a_service_b.py 75 | 76 | Discussion 77 | ~~~~~~~~~~ 78 | Once a service has been advertised by name, that name can be discovered by any 79 | other networkzero program on the same network. You only need to know the name 80 | used. Discovery will wait for up to 60 seconds for a service to be advertised. 81 | If one is discovered, its address string is returned; if not, `None` is 82 | returned. 83 | 84 | Discover all services 85 | --------------------- 86 | 87 | On machine A 88 | ~~~~~~~~~~~~ 89 | .. literalinclude:: simple-discovery/discover_all_services_a.py 90 | 91 | On machine B 92 | ~~~~~~~~~~~~ 93 | .. literalinclude:: simple-discovery/discover_all_services_b.py 94 | 95 | On machine C 96 | ~~~~~~~~~~~~ 97 | .. literalinclude:: simple-discovery/discover_all_services_c.py 98 | 99 | Discussion 100 | ~~~~~~~~~~ 101 | It can be useful to see what services are advertised across the whole 102 | network. This can be used, for example, in a chat room to discover new 103 | members joining the room. Pairs of names and addresses are returned, suitable 104 | for converting into a Python dictionary. 105 | 106 | -------------------------------------------------------------------------------- /docs/cookbook/simple-discovery/advertise_a_service.py: -------------------------------------------------------------------------------- 1 | import networkzero as nw0 2 | 3 | address = nw0.advertise("myservice1") 4 | print("Service is at", address) -------------------------------------------------------------------------------- /docs/cookbook/simple-discovery/advertise_a_service_with_a_specific_address.py: -------------------------------------------------------------------------------- 1 | import networkzero as nw0 2 | 3 | ip_address = "192.0.2.1" 4 | address = nw0.advertise("myservice2", ip_address) 5 | print("Service is at", address) -------------------------------------------------------------------------------- /docs/cookbook/simple-discovery/advertise_a_service_with_a_specific_port.py: -------------------------------------------------------------------------------- 1 | import networkzero as nw0 2 | 3 | address = nw0.advertise("myservice3", 1234) 4 | print("Service is at address", address) -------------------------------------------------------------------------------- /docs/cookbook/simple-discovery/advertise_a_service_with_a_wildcard_address.py: -------------------------------------------------------------------------------- 1 | import networkzero as nw0 2 | 3 | ip_address = "127.*" 4 | address = nw0.advertise("myservice4", ip_address) 5 | print("Service is at", address) 6 | -------------------------------------------------------------------------------- /docs/cookbook/simple-discovery/discover_a_service_a.py: -------------------------------------------------------------------------------- 1 | import networkzero as nw0 2 | 3 | nw0.advertise("myservice5") 4 | -------------------------------------------------------------------------------- /docs/cookbook/simple-discovery/discover_a_service_b.py: -------------------------------------------------------------------------------- 1 | import networkzero as nw0 2 | 3 | myservice = nw0.discover("myservice5") 4 | print("myservice is at", myservice) -------------------------------------------------------------------------------- /docs/cookbook/simple-discovery/discover_all_services_a.py: -------------------------------------------------------------------------------- 1 | import networkzero as nw0 2 | 3 | nw0.advertise("myservice7") 4 | -------------------------------------------------------------------------------- /docs/cookbook/simple-discovery/discover_all_services_b.py: -------------------------------------------------------------------------------- 1 | import networkzero as nw0 2 | 3 | nw0.advertise("myservice8") 4 | -------------------------------------------------------------------------------- /docs/cookbook/simple-discovery/discover_all_services_c.py: -------------------------------------------------------------------------------- 1 | import networkzero as nw0 2 | 3 | services = nw0.discover_all() 4 | print("Services:", services) 5 | -------------------------------------------------------------------------------- /docs/cookbook/simple-messaging.rst: -------------------------------------------------------------------------------- 1 | .. currentmodule:: messenger 2 | .. highlight:: python 3 | :linenothreshold: 1 4 | 5 | Exchanging Messages 6 | =================== 7 | 8 | The examples here all refer to the :mod:`messenger` module. 9 | 10 | Send & receive a message 11 | ------------------------ 12 | 13 | Process A 14 | ~~~~~~~~~ 15 | 16 | .. literalinclude:: simple-messaging/send_a_message_a.py 17 | 18 | Process B 19 | ~~~~~~~~~ 20 | 21 | .. literalinclude:: simple-messaging/send_a_message_b.py 22 | 23 | Discussion 24 | ~~~~~~~~~~ 25 | The message exchange in networkzero works on a very simple message-reply 26 | model: one process waits for a message from another and sends a reply. 27 | The process sending the original message waits for the reply before carrying 28 | on. This is less flexible that network communications usually are (the 29 | sending process must wait for a reply before carrying on) but cuts out 30 | the complications which arise when several processes are sending messages 31 | to the same listening process. 32 | 33 | Send a message without expecting a reply 34 | ---------------------------------------- 35 | 36 | Process A 37 | ~~~~~~~~~ 38 | 39 | .. literalinclude:: simple-messaging/send_a_message_without_reply_a.py 40 | 41 | Process B 42 | ~~~~~~~~~ 43 | 44 | .. literalinclude:: simple-messaging/send_a_message_without_reply_b.py 45 | 46 | Discussion 47 | ~~~~~~~~~~ 48 | The message exchange in networkzero always requires a reply to any 49 | message. But sometimes, no reply is needed, for example when sending 50 | commands to a robot. It would be possible for the process which receives 51 | a message to send a dummy reply and for the process which sent the 52 | message to ignore any reply. 53 | 54 | This situation is so common that there is a special parameter `autoreply` 55 | to the `wait_for_message_from` function. Behind the scenes, networkzero 56 | completes the message-reply contract for you by sending an empty reply. 57 | The process which sent the message can simply ignore any reply. 58 | 59 | Send news 60 | --------- 61 | 62 | Process A 63 | ~~~~~~~~~ 64 | 65 | .. literalinclude:: simple-messaging/send_news_a.py 66 | 67 | Process B and C and D... 68 | ~~~~~~~~~~~~~~~~~~~~~~~~ 69 | 70 | .. literalinclude:: simple-messaging/send_news_b.py 71 | 72 | Discussion 73 | ~~~~~~~~~~ 74 | One process can send news to any process which is listening. It doesn't 75 | matter if no-one is listening or if 100 other machines are listening. This 76 | could be used, for example, to have one machine linked directly to a 77 | temperature sensor outside a classroom which broadcasts the temperature once 78 | a minute. 79 | 80 | Send news on a specific topic 81 | ----------------------------- 82 | 83 | Process A 84 | ~~~~~~~~~ 85 | 86 | .. literalinclude:: simple-messaging/send_news_on_topic_a.py 87 | 88 | Process B 89 | ~~~~~~~~~ 90 | 91 | .. literalinclude:: simple-messaging/send_news_on_topic_b.py 92 | 93 | Process C 94 | ~~~~~~~~~ 95 | 96 | .. literalinclude:: simple-messaging/send_news_on_topic_c.py 97 | 98 | Discussion 99 | ~~~~~~~~~~ 100 | One process can send news on a variety of topics. Other processes can 101 | choose to listen to a specific topic or to all topics. In this example, 102 | Process A is sending news on temperature and humidity. Process B is 103 | listening to the temperature feed while process C is listening to the 104 | data about humidity. -------------------------------------------------------------------------------- /docs/cookbook/simple-messaging/send_a_message_a.py: -------------------------------------------------------------------------------- 1 | import networkzero as nw0 2 | 3 | address = nw0.advertise("messenger1") 4 | 5 | message = nw0.wait_for_message_from(address) 6 | print("Message received: ", message) 7 | nw0.send_reply_to(address, "Received: %s" % message) 8 | -------------------------------------------------------------------------------- /docs/cookbook/simple-messaging/send_a_message_b.py: -------------------------------------------------------------------------------- 1 | import networkzero as nw0 2 | 3 | service = nw0.discover("messenger1") 4 | 5 | reply = nw0.send_message_to(service, "This is a message") 6 | print("Reply: ", reply) 7 | -------------------------------------------------------------------------------- /docs/cookbook/simple-messaging/send_a_message_without_reply_a.py: -------------------------------------------------------------------------------- 1 | import networkzero as nw0 2 | 3 | address = nw0.advertise("messenger2") 4 | 5 | message = nw0.wait_for_message_from(address, autoreply=True) 6 | print("Message received: ", message) 7 | -------------------------------------------------------------------------------- /docs/cookbook/simple-messaging/send_a_message_without_reply_b.py: -------------------------------------------------------------------------------- 1 | import networkzero as nw0 2 | 3 | service = nw0.discover("messenger2") 4 | 5 | nw0.send_message_to(service, "This is a command") 6 | -------------------------------------------------------------------------------- /docs/cookbook/simple-messaging/send_news_a.py: -------------------------------------------------------------------------------- 1 | import networkzero as nw0 2 | import random 3 | import time 4 | 5 | address = nw0.advertise("news1") 6 | 7 | # 8 | # In lieu of an actual temperature sensor! 9 | # 10 | temperatures = range(15, 20) 11 | while True: 12 | temperature = random.choice(temperatures) + random.random() 13 | nw0.send_news_to(address, "temperature", temperature) 14 | time.sleep(1.0) 15 | -------------------------------------------------------------------------------- /docs/cookbook/simple-messaging/send_news_b.py: -------------------------------------------------------------------------------- 1 | import networkzero as nw0 2 | 3 | address = nw0.discover("news1") 4 | 5 | while True: 6 | topic, temperature = nw0.wait_for_news_from(address) 7 | print("Temperature is:", temperature) 8 | -------------------------------------------------------------------------------- /docs/cookbook/simple-messaging/send_news_on_topic_a.py: -------------------------------------------------------------------------------- 1 | import networkzero as nw0 2 | import random 3 | import time 4 | 5 | address = nw0.advertise("news2") 6 | 7 | # 8 | # In lieu of an actual temperature sensor! 9 | # 10 | temperatures = range(15, 20) 11 | humidities = range(0, 100) 12 | while True: 13 | temperature = random.choice(temperatures) + random.random() 14 | humidity = random.choice(humidities) 15 | nw0.send_news_to(address, "temperature", temperature) 16 | time.sleep(random.random()) 17 | nw0.send_news_to(address, "humidity", humidity) 18 | time.sleep(random.random()) 19 | -------------------------------------------------------------------------------- /docs/cookbook/simple-messaging/send_news_on_topic_b.py: -------------------------------------------------------------------------------- 1 | import networkzero as nw0 2 | 3 | address = nw0.discover("news2") 4 | 5 | while True: 6 | topic, temperature = nw0.wait_for_news_from(address, "temperature") 7 | print("Temperature is:", temperature) 8 | -------------------------------------------------------------------------------- /docs/cookbook/simple-messaging/send_news_on_topic_c.py: -------------------------------------------------------------------------------- 1 | import networkzero as nw0 2 | 3 | address = nw0.discover("news2") 4 | 5 | while True: 6 | topic, humidity = nw0.wait_for_news_from(address, "humidity") 7 | print("Humidity is:", humidity) 8 | -------------------------------------------------------------------------------- /docs/cookbook/test-recipes.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import os, sys 3 | import fnmatch 4 | import subprocess 5 | import time 6 | 7 | my_filepath = os.path.abspath(sys.argv[0]) 8 | my_dirpath = os.path.dirname(my_filepath) 9 | 10 | def process_one_folder(dirpath, filenames, filename_pattern): 11 | # 12 | # Run all the processes in one folder in parallel as 13 | # some of them will depend on others (eg a request/response) 14 | # 15 | processes = {} 16 | for filename in sorted(filenames): 17 | if not filename.endswith(".py"): 18 | continue 19 | if not fnmatch.fnmatch(filename, filename_pattern): 20 | #~ print("%s does not match %s; skipping" % (filename, filename_pattern)) 21 | continue 22 | filepath = os.path.join(dirpath, filename) 23 | if filepath == my_filepath: 24 | continue 25 | print("About to run", filepath) 26 | 27 | processes[filename] = subprocess.Popen([sys.executable, filepath]) 28 | 29 | t0 = time.time() 30 | while True: 31 | results = [(name, p.poll()) for (name, p) in processes.items()] 32 | if all(r is not None for (name, r) in results): 33 | break 34 | time.sleep(0.5) 35 | if time.time() - t0 > 15: 36 | print("Recipes taking longer than 30s; timing out...") 37 | for p in processes.values(): 38 | if p.poll() is None: 39 | p.kill() 40 | break 41 | print("Results:") 42 | for name, p in processes.items(): 43 | print(" ", name, "=>", p.poll()) 44 | 45 | def main(filename_pattern="*"): 46 | # 47 | # Fire up a beacon and wait for it to start 48 | # 49 | beacon = subprocess.Popen([sys.executable, "-m", "networkzero.discovery"]) 50 | time.sleep(2) 51 | try: 52 | for dirpath, dirnames, filenames in os.walk(my_dirpath): 53 | print(dirpath) 54 | process_one_folder(dirpath, filenames, filename_pattern) 55 | print("\n\n") 56 | finally: 57 | beacon.kill() 58 | 59 | if __name__ == '__main__': 60 | main(*sys.argv[1:]) 61 | -------------------------------------------------------------------------------- /docs/design-guidelines.rst: -------------------------------------------------------------------------------- 1 | Design Guidelines 2 | ================= 3 | 4 | *(Not hard and fast but intended to act as a tiebreaker if needed)* 5 | 6 | * networkzero is aimed at beginners and particularly at those in an 7 | educational setting (classroom, Raspberry Jam etc.) 8 | 9 | * If you need more than this, you'll want to drop down to ZeroMQ itself, 10 | or some other library, and implement your own. Or at least use the 11 | internals of networkzero directly. 12 | 13 | * The preference is for module-level functions rather than objects. 14 | (Behind the scenes, global object caches are used) 15 | 16 | * As much as possible, code should work in the interactive interpreter 17 | 18 | * Thread-safety should be considered especially since libraries such as GPIOZero 19 | implement callbacks via threads, so NetworkZero code might unwittingly 20 | be invoked from within a thread. 21 | 22 | * Code will work unchanged on one box and on several. 23 | 24 | * Code will work unchanged on Linux, OS X & Windows, assuming 25 | that the prerequisites are met (basically: recent Python & zmqlib). 26 | 27 | * Code will work unchanged on Python 2.7 and Python 3.2+ 28 | 29 | * The discovery & messenger modules are uncoupled: neither relies on 30 | or knows about the internals of the other. 31 | 32 | * All useful functions & constants are exported from the root of the package 33 | so either "import networkzero as nw0" or "from networkzero import \*" 34 | will provide the whole of the public API. 35 | 36 | * Underscore prefixes will be used to ensure that only the public API 37 | be visible via `help()`. This reduces visual clutter. 38 | -------------------------------------------------------------------------------- /docs/design-questions.rst: -------------------------------------------------------------------------------- 1 | Questions to be answered 2 | ======================== 3 | 4 | * Do you have to use ZeroMQ? Why not avoid dependencies? 5 | 6 | There's nothing in the design which requires ZeroMQ. The API contract 7 | hides the implementation. However ZeroMQ does what is needed and is 8 | available cross-platform and cross-language and with Python bindings. 9 | 10 | (Just came across http://nanomsg.org/index.html which is a---possibly 11 | unmaintained---fork of ZeroMQ with a particular eye to Posix-compliance & 12 | open licensing). 13 | 14 | A couple more factors have been pointed out which tell against using 15 | ZeroMQ as a dependency: 16 | 17 | * The build process, at least for some \*nix, might be daunting 18 | for teachers unfamiliar with the concepts. 19 | * It moves the student one step further away from the underlying 20 | sockets. 21 | 22 | * Why not use [some zeroconf implementation]? 23 | 24 | This is somewhat the converse of the ZeroMQ question above. And the answer 25 | is similar, *mutatis mutandis*: there's nothing which precludes the use of using a zeroconf 26 | solution on a given platform. But cross-platform support is spotty, and 27 | it's more geared towards, eg, printer discovery and machine discovery. 28 | What we're after is a little more ad hoc and transient. 29 | 30 | The discovery API is simply advertise / discover. If it turns out that 31 | those can be implemented more simply and/or robustly on top of a zeroconf 32 | service---or some other existing library---then we can switch to that 33 | under the covers. 34 | 35 | * Do we want to allow multiple services to register under the same name? 36 | 37 | This sounds sort of neat, allowing for load-balancing etc. But it raises 38 | all sorts of complications in the code especially when one of them is removed. 39 | Although the implementation as I write allows for this, I think on mature 40 | reflection that it is best left out of a simple package like this. 41 | [**UPDATE**: multiple registration has been removed] 42 | 43 | If it were needed, eg in a many-to-many chat situation, it could be implemented 44 | fairly easily on top of networkzero by defining a "service:" naming 45 | convention to distinguish related by distinct services. 46 | 47 | * What happens if the process hosting the Beacon shuts down before the others do? 48 | 49 | This is actually less of a problem than it sounds. There are three situations I 50 | can think of: 51 | 52 | 1) A new service starts up and want to find an existing service -- this will fail 53 | because the existing adverts are lost. 54 | 55 | 2) An existing service wants to use another existing service whose address it has 56 | previously discovered. This will succeed as long as it no needs to discover 57 | the address of a named service. 58 | 59 | 3) An existing service attempts to unadvertise itself, typically on shutdown. This 60 | will fail, but that failure can be mitigated by having the unadvertise code run 61 | with a timeout and simply warn if there's no response. 62 | 63 | [**UPDATE** Somewhat relevant: time-to-live (TTL) functionality has been added to adverts. 64 | This means that, if a beacon shuts down, its adverts will expire fairly soon and can be 65 | replaced by later adverts for the same service. Also, an attempt is made to unadvertise 66 | when a process shuts down.] 67 | 68 | * We have commands as well as messages. Do we need both? 69 | 70 | Perhaps not: under the covers, command is implemented as a message 71 | which swallows its reply. (Possibly warning if none arrives within a 72 | short space of time). But it's likely to be such a common usage pattern 73 | that people will usually re-implement it anyway. 74 | 75 | [**UPDATE**: I've removed the command functionality, having decided that 76 | the value that it brings is not worth the ambiguity it adds to the API. 77 | To help in the common case, I've added an autoreply option to 78 | :func:`networkzero.wait_for_message_from` and a helper function 79 | :func:`networkzero.action_and_params` which will take a text commandline 80 | and return a command and its params.] 81 | 82 | * What about multi IP addresses? 83 | 84 | My dev machine has a VM setup with its host-only network. More plausibly 85 | for the classroom it's quite possible to have, eg, an RPi connected both 86 | to wired & wireless networks at the same time. At present, we're only 87 | choosing one IP address. Our options seem to be: 88 | 89 | i) Let the user deal with it: deactivate IP addresses which are not 90 | wanted for the purposes (eg host-only addresses). 91 | 92 | ii) Have some sort of config.ini which allows users to disregard or prefer 93 | certain addresses or networks 94 | 95 | iii) Allow the "address" object to be more than one address in a list. 96 | These multiple addresses will then be advertised and messages sent 97 | across them. 98 | 99 | Of course, a combination of these could be used. Just for now, we can 100 | defer deciding as most machines, at least in the classroom, will have 101 | only one IP address at a time. My slight preference is for (iii) as I see 102 | it being fairly easy to implement and fairly transparent. 103 | 104 | [**UPDATE**: We're now using the cross-platform netifaces module as an external 105 | dependency to determine all local IP4 addresses. We're also allowing 106 | the :func:`networkzero.address` function to take a wildcard IP so that, for 107 | example, you could specify that you want a command in the 192.0.2.* 108 | network without knowing exactly which one you're currently bound to.] 109 | 110 | [**UPDATE 2**: accepted a PR from Sandy Wilson to choose the default gateway if 111 | possible. In combination, we seem to have solved the most common problems around 112 | selecting the best address.] 113 | 114 | * Exceptions or returning None/sentinel? 115 | 116 | Where we have a "soft" error, eg a wait times out, the function called 117 | will return a None or some other useful sentinel. 118 | 119 | However if the error is such that no recovery is meaningful, we should raise 120 | an exception as usual. In particular, because of the statefulness of ZeroMQ 121 | sockets, if we don't receive a reply, that socket becomes unusable. [TODO: 122 | there is the possibility of tracking the state of the underlying socket 123 | and reacting more helpfully when, a wait is started before a reply is 124 | sent]. 125 | 126 | NB This is a pragmatic choice. We're really just dodging the issue knowing 127 | that, in a classroom situation, we can always bomb out and restart the process. 128 | In reality, we'd be looking at a zombie socket of some sort, stuck somewhere 129 | inside its own state machine. 130 | 131 | * We currently used marshal to serialise messages. Is this a good idea? 132 | 133 | Possibly not: the advantage is that it handles simple objects in a 134 | consistent way [although not necessarily across Python versions, it 135 | occurs to me]. The obvious alternatives are: 136 | 137 | * Bytes: let the user encode 138 | * JSON/YAML 139 | * pickle 140 | * A.N.Other serialisation protocol 141 | 142 | The actual serialisation is transparent to users; however, the current 143 | implementation allows simple Python structures without any extra effort. 144 | So someone can pass a tuple of values or a dictionary. Or a unicode 145 | string / byte string. 146 | 147 | The downside to this is that code written for ZeroMQ but in another 148 | language will struggle to match this. (Obviously it would be possible, but 149 | far more trouble than it was worth). JSON would be an obvious x-platform 150 | alternative but, when I tried it, gave some difficulties over encoding. 151 | (Waves hands; I can't remember exactly what the issue was...) 152 | 153 | [**UPDATE**: we're now using JSON to avoid the issue with marshalled 154 | data not working across different Python versions. This does mean 155 | that bytestrings cannot be used directly (nor any other type which 156 | doesn't support JSON serialisation). For now we've provided a pair 157 | of bytes<->string converters; later, probably detect this automatically]. 158 | 159 | pickle has well-known security implications. There are pickle-alikes 160 | (dill, serpent) in the Python space which do a better job, but they're 161 | still Python specific. One possibility is to attempt to unserialise 162 | with marshal and to fall back to raw bytes if that fails, letting the 163 | user decide how to cope with the data. 164 | 165 | NB The pubsub stuff has to use bytes because that's how the prefix-matching 166 | works. [**UPDATE**: pubsub now uses ZeroMQ multipart messages to separate 167 | out the topic which has to be bytes from the message which can be any 168 | simple Python object]. -------------------------------------------------------------------------------- /docs/discovery.rst: -------------------------------------------------------------------------------- 1 | Advertising & Discovering Network Services 2 | ========================================== 3 | 4 | .. automodule:: networkzero.discovery 5 | :synopsis: Network discovery 6 | :show-inheritance: 7 | .. moduleauthor:: Tim Golden 8 | 9 | 10 | Introduction 11 | ------------ 12 | 13 | The discovery module implements a UDP beacon in a daemon thread to advertise 14 | a directory of name-to-address mappings. The names are added to the directory 15 | via the :func:`advertise` function, and discovered via :func:`discover`. 16 | A list of all currently-advertised names can be obtained by calling 17 | :func:`discover_all`. 18 | 19 | When a name is advertised, if no address is supplied, one 20 | is generated by combining the first of the host's IP addresses with a port 21 | randomly selected from the list of valid dynamic ports [0xC000 - 0xFFFF]. 22 | While the selected port is removed from the list for the purposes of the 23 | networkzero package, there's a slim chance that it might be in use by some 24 | other process. 25 | 26 | Functions 27 | --------- 28 | 29 | .. autofunction:: advertise 30 | .. autofunction:: discover 31 | .. autofunction:: discover_all 32 | .. autofunction:: discover_group 33 | -------------------------------------------------------------------------------- /docs/for-developers.rst: -------------------------------------------------------------------------------- 1 | For Developers 2 | ============== 3 | 4 | Background 5 | ---------- 6 | 7 | At a club I help at, we've just done a small two-week project 8 | involving three Raspberry Pi boxes: one controlling a motor to drive a 9 | turntable; one detecting hits against targets mounted on the turntable; 10 | and one stopping and starting the turntable and adding up the scores. 11 | They talked to each other over WiFi. 12 | 13 | Previously, we entered a robot for PiWars and used WiFi to control 14 | the robot from a laptop with a SNES-like controller attached. 15 | 16 | In both cases the use of network was very simple: send a command (START, 17 | STOP, LEFT etc.) possibly with one or more parameters (eg LEFT 0.5). 18 | Under the covers we used ZeroMQ which offers a robust layer on 19 | top of standard sockets. Although ZeroMQ offers a simple and consistent 20 | API, even that's a little too much for newcomers both to networks and 21 | programming. 22 | 23 | In addition, a constant challenge was discovering the other "nodes" 24 | on the network in the face of DHCP changes and switching from development 25 | to live networks. 26 | 27 | So in the spirit of PyGame Zero and GPIO Zero, I'm offering 28 | Network Zero which depends on ZeroMQ and allows you to: 29 | 30 | * Discover services 31 | 32 | * Send / Reply 33 | 34 | * Publish / Subscribe 35 | 36 | * Switch painlessly between one and more than one machine 37 | 38 | Services 39 | -------- 40 | 41 | One of the challenges which is particular to the classroom is that you may not 42 | know how to address the box which a service is running on. Even if you 43 | give your boxes static IPs (and you probably don't) or well-known hostnames 44 | then you need some way of knowing easily what IP or name it has. 45 | If you're using DHCP (and you probably are) then you can't even write the IP 46 | on a sticky label. All this is exacerbated as kids switch machine between 47 | session and can be as true for a classroom laptop set as much as 48 | it is for a Raspberry Pi lab. 49 | 50 | In addition, the services may be run on the same or a different box which 51 | may be running the same or a different Operating System. 52 | 53 | In the robot example above we have two "services": the robot RPi and 54 | the controller laptop. They need to find and talk to each other without knowing 55 | their respective IPs in advance. This is especially true if we are developing 56 | using a local WiFi or even Ethernet setup but testing or running live with 57 | the robot itself becoming an AP. 58 | 59 | For the turntable project, we have three RPis: the Turntable listens 60 | for commands; the Detector publishes hits; and the Controller sends commands 61 | and subscribes to hits. For development, we want to run two or more of those 62 | services on the *same* box, using different processes. 63 | 64 | NB this is a different problem to what DNS solves: we don't want to discover 65 | hosts, we want to discover services, and in a fairly transparent manner. It's 66 | the same kind of problem which zeroconf is solving. [http://www.zeroconf.org/] 67 | 68 | Basic Offering 69 | -------------- 70 | 71 | While the networkzero package will hopefully be of wider use, its 72 | target audience is teachers or organisers of group sessions where 73 | a -- possibly heterogenous -- collection of machines will want to 74 | pass messages simply across a network. 75 | 76 | The package offers two things which will be of use to people 77 | running an educational project in a lab situation where machines 78 | and addresses change: 79 | 80 | * Advertising and discovery of other programs 81 | 82 | * Simple but robust message sending 83 | 84 | The first is achieved by having each program fire up a "beacon" which 85 | advertises services by name, indicating the port they are listening on. 86 | Other programs listen for those service adverts until they find the one 87 | they are looking for. 88 | 89 | The second uses ZeroMQ socket abstractions to reduce obstacles arising from 90 | the order in which processes start up; and to ensure that messages arrive 91 | complete regardless of network latency &c. 92 | 93 | These two are offered independently of each other: the discovery aspect 94 | will leave you with an IP address and a port number. The message aspect 95 | needs an IP address and a port number. But you don't need one to use 96 | the other: it just makes it simpler. 97 | 98 | 99 | More Information 100 | ---------------- 101 | 102 | .. toctree:: 103 | :maxdepth: 2 104 | 105 | design-guidelines 106 | design-questions 107 | networkzero 108 | discovery 109 | messenger 110 | 111 | -------------------------------------------------------------------------------- /docs/for-teachers.rst: -------------------------------------------------------------------------------- 1 | For Teachers 2 | ============ 3 | 4 | Introduction 5 | ------------ 6 | 7 | Have you ever wanted to get one computer to talk to another with the 8 | minimum of fuss? Perhaps to have a laptop talk to a robot? Or just to 9 | get the "wow!" factor of a chat or a photo go from one machine to another. 10 | Or to extend a simple game into two-player mode with each player on a 11 | different computer? Or to have a RPi with a temperature sensor send its 12 | readings to a workstation showing graphs? 13 | 14 | To do that, you need two key things: to know the names or addresses of 15 | each computer in the scheme; and to have each one listen and respond 16 | to messages from the other. 17 | 18 | NetworkZero makes that possible and even easy from within Python, running 19 | on any supported platform, including Windows, Mac and Linux boxes such as 20 | the Raspberry Pi. 21 | 22 | Benefits 23 | -------- 24 | 25 | The benefits of NetworkZero fall into two categories: the advantages which 26 | communicating over a network bring; and, given that you are using a network, 27 | the benefits of using NetworkZero. 28 | 29 | Using a Network 30 | ~~~~~~~~~~~~~~~ 31 | 32 | * Some kinds of programs, such as instant chat, are inherently network-bound. 33 | Other programs can grow an extra dimension by adding network capabilities. 34 | 35 | * Network-based programs are more loosely coupled. This is generally considered 36 | A Good Thing. 37 | 38 | * A set of programs communicating across a network are usually agnostic as to 39 | platform and language. You could have a headless RaspberryPi with a sensor 40 | attached sending data to a Windows PC with a graphical output. 41 | 42 | Using NetworkZero 43 | ~~~~~~~~~~~~~~~~~ 44 | 45 | * NetworkZero makes it easier to advertise and discover services running on the 46 | network on the same or different machines. However, the discovery is 47 | merely a shortcut: it's always possible to pass a literal ip:port address. 48 | 49 | * Two simple means of sending and receiving data, each with its own 50 | semantics and uses. 51 | 52 | * Simple Python data can be sent transparently. Except when sending/receiving 53 | commands, the messages passed can be any built-in Python object. This obviously 54 | includes strings and numbers, but also includes tuples, lists & dictionaries. 55 | 56 | 57 | Difficulties 58 | ------------ 59 | 60 | Even with the advantages which NetworkZero brings, there are still some difficulties: 61 | 62 | Messy Network Setups 63 | ~~~~~~~~~~~~~~~~~~~~ 64 | 65 | NetworkZero assumes a fairly simple setup on each of the machines and the network 66 | in general. For a classroom of networked RPis or laptops, this is likely to be the 67 | case. But, if you have an inventive network manager, you may find that the laptops 68 | and the RPi boxes are on two separate subnets or VLANs and that the broadcast mechanism 69 | which allows for discovery doesn't work. 70 | 71 | Or it might be that one or more machines have multiple IP addresses. This could be the 72 | case, for example, if you have a PiNet setup running over wired ethernet while the boxes 73 | also have WiFi adapters. This might mean that the address which one advertises is on its 74 | ethernet address while the other attempts to reach it over WiFi. You can work 75 | around this partly by passing a wildcard IP address which will attempt to 76 | use an address on *that* network as opposed to any of the others which 77 | the machine may have. 78 | 79 | Keyboard Input blocks the network 80 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 81 | 82 | Imagine you're writing a simple chat program where you want the students 83 | to see updates come in from the others while typing their own messages. 84 | The trouble is that the normal means of waiting for keyboard input (the 85 | `input` function) blocks all other activity: the incoming messages will 86 | simply queue up until the user presses `Enter`. 87 | 88 | The chat program in the examples/ folder avoids this by having the keyboard 89 | input in one program with the updates showing in another. Both windows are 90 | running on the student's machine and coordinating via the network. 91 | 92 | There *are* techniques for looking for keyboard input without blocking. But 93 | none is cleanly cross-platform and really needs a separate package (KeyboardZero?). 94 | Of course, using a different interface such as PyGame or of the GUI packages 95 | available for Python would avoid this. But each brings its own complexity. 96 | 97 | Thinking in Network 98 | ~~~~~~~~~~~~~~~~~~~ 99 | 100 | Perhaps the most important roadblock is that you have, in some cases, to change 101 | your way of thinking about a program when you're coordinating across a network. 102 | NetworkZero smoothes away some of the complexity, but it's ultimately a good 103 | idea for students to understand how network-based programs differ from local ones. 104 | 105 | Some of the factors are: 106 | 107 | * Message passing vs shared variables 108 | * Event-driven activity, reacting to incoming data 109 | * Possible loss of remote nodes 110 | * Dealing with blocking/non-blocking I/O 111 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | NetworkZero 2 | =========== 3 | 4 | NetworkZero makes it easier to use Python to connect things together 5 | across the internet. It's especially focused on the classroom 6 | or club situation where short-lived programs need to discover 7 | each other on the same or a different computer without having to 8 | know IP addresses or hostnames. 9 | 10 | It runs on Python 2.7 and Python 3.3+ and should run anywhere that 11 | Python runs. In particular, it's tested automatically on Windows, 12 | Mac and Linux, and is used on Raspberry Pi. 13 | 14 | NetworkZero offers two main services: 15 | 16 | * Discovering short-lived and ad hoc programs 17 | * Sending simple Python objects between programs 18 | 19 | What can I use it for? 20 | ---------------------- 21 | 22 | Anything which can make use of one machine or process to talk to 23 | another across a network. For example: 24 | 25 | * Sending commands to a robot 26 | * Sending data from an RPi with a sensor to a graphing PC and a 27 | long-term storage log 28 | * Showing the state of a shared game in one window while sending 29 | commands from another. 30 | 31 | Can you give me an example? 32 | --------------------------- 33 | 34 | [Machine or Process A]:: 35 | 36 | import networkzero as nw0 37 | 38 | address = nw0.advertise("hello") 39 | while True: 40 | name = nw0.wait_for_message_from(address) 41 | nw0.send_reply_to(address, "Hello " + name) 42 | 43 | [Machine or Process B and C and D ...]:: 44 | 45 | import networkzero as nw0 46 | 47 | hello = nw0.discover("hello") 48 | reply = nw0.send_message_to(hello, "World!") 49 | print(reply) 50 | reply = nw0.send_message_to(hello, "Tim") 51 | print(reply) 52 | 53 | This runs a service, advertised under the name "hello" 54 | which will send back "Hello " whenever is sent to it. 55 | Other machines or processes discover the service and send 56 | names to it, receiving a greeting in return. 57 | 58 | Read More 59 | --------- 60 | 61 | .. toctree:: 62 | :maxdepth: 2 63 | 64 | installation 65 | usage 66 | for-teachers 67 | for-developers 68 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | .. module:: networkzero 2 | 3 | Installation 4 | ============ 5 | 6 | Introduction 7 | ------------ 8 | 9 | NetworkZero is designed to be used in an educational context, so it targets the Raspberry Pi, 10 | but you should be able to install it anywhere that runs Python. This can be a good choice for 11 | experimenting with what it can do on your own laptop (see whether it's right for your next 12 | workshop), for example. 13 | 14 | Dependencies 15 | ------------ 16 | 17 | NetworkZero has two external dependencies: 18 | 19 | * PyZMQ 20 | * netifaces 21 | 22 | Getting these dependencies is a little bit different on each platform, so see whether you can 23 | find yours below. Once you've done the bit for your platform, go on down to `Get NetworkZero`_. 24 | 25 | Raspberry Pi 26 | ~~~~~~~~~~~~ 27 | 28 | Todo: 29 | 30 | * Verify the below. 31 | 32 | :: 33 | 34 | sudo apt-get install zeromq 35 | 36 | You should be good to go - head on down to `Get NetworkZero`_. 37 | 38 | Windows 39 | ~~~~~~~ 40 | 41 | Todo: 42 | 43 | * Write instructions for getting libzeromq installed. 44 | * PyZMQ and netifaces have wheels available. 45 | 46 | Once you've got these set up, head on down to `Get NetworkZero`_. 47 | 48 | MacOS 49 | ~~~~~ 50 | 51 | Todo: 52 | 53 | * Write instructions for getting libzeromq installed. 54 | * PyZMQ has wheels available 55 | * netifaces will need source installation as there's no wheel - so macos's equivalent of build-essential is probably required. 56 | 57 | Linux 58 | ~~~~~ 59 | 60 | On Linux, there are a few dependencies that you'll need to have installed so that you can run the setup. 61 | 62 | Fedora: :: 63 | 64 | # Common tools for building and installing python packages on Linux 65 | sudo dnf install gcc gcc-c++ python-devel redhat-rpm-config 66 | 67 | # ZeroMQ's native library 68 | sudo dnf install zeromq 69 | 70 | That shoud be everything - head on down to `Get NetworkZero`_. 71 | 72 | Get NetworkZero 73 | --------------- 74 | 75 | :: 76 | 77 | pip install networkzero 78 | 79 | Now, check that it's installed correctly. You should get something like the following: :: 80 | 81 | python -c 'import networkzero' 82 | 83 | If that returns without an error you're good to go! You can move on. If that doesn't work, 84 | check that there were no errors or warnings when running the setup commands in previous 85 | sections. If there were - and you can figure out what went wrong - try fixing those up and 86 | trying again. If not, please let us know - it's probably easiest to 87 | `file an issue`. 88 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. epub3 to make an epub3 31 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 32 | echo. text to make text files 33 | echo. man to make manual pages 34 | echo. texinfo to make Texinfo files 35 | echo. gettext to make PO message catalogs 36 | echo. changes to make an overview over all changed/added/deprecated items 37 | echo. xml to make Docutils-native XML files 38 | echo. pseudoxml to make pseudoxml-XML files for display purposes 39 | echo. linkcheck to check all external links for integrity 40 | echo. doctest to run all doctests embedded in the documentation if enabled 41 | echo. coverage to run coverage check of the documentation if enabled 42 | goto end 43 | ) 44 | 45 | if "%1" == "clean" ( 46 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 47 | del /q /s %BUILDDIR%\* 48 | goto end 49 | ) 50 | 51 | 52 | REM Check if sphinx-build is available and fallback to Python version if any 53 | %SPHINXBUILD% 1>NUL 2>NUL 54 | if errorlevel 9009 goto sphinx_python 55 | goto sphinx_ok 56 | 57 | :sphinx_python 58 | 59 | set SPHINXBUILD=python -m sphinx.__init__ 60 | %SPHINXBUILD% 2> nul 61 | if errorlevel 9009 ( 62 | echo. 63 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 64 | echo.installed, then set the SPHINXBUILD environment variable to point 65 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 66 | echo.may add the Sphinx directory to PATH. 67 | echo. 68 | echo.If you don't have Sphinx installed, grab it from 69 | echo.http://sphinx-doc.org/ 70 | exit /b 1 71 | ) 72 | 73 | :sphinx_ok 74 | 75 | 76 | if "%1" == "html" ( 77 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 78 | if errorlevel 1 exit /b 1 79 | echo. 80 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 81 | goto end 82 | ) 83 | 84 | if "%1" == "dirhtml" ( 85 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 86 | if errorlevel 1 exit /b 1 87 | echo. 88 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 89 | goto end 90 | ) 91 | 92 | if "%1" == "singlehtml" ( 93 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 94 | if errorlevel 1 exit /b 1 95 | echo. 96 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 97 | goto end 98 | ) 99 | 100 | if "%1" == "pickle" ( 101 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 102 | if errorlevel 1 exit /b 1 103 | echo. 104 | echo.Build finished; now you can process the pickle files. 105 | goto end 106 | ) 107 | 108 | if "%1" == "json" ( 109 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 110 | if errorlevel 1 exit /b 1 111 | echo. 112 | echo.Build finished; now you can process the JSON files. 113 | goto end 114 | ) 115 | 116 | if "%1" == "htmlhelp" ( 117 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 118 | if errorlevel 1 exit /b 1 119 | echo. 120 | echo.Build finished; now you can run HTML Help Workshop with the ^ 121 | .hhp project file in %BUILDDIR%/htmlhelp. 122 | goto end 123 | ) 124 | 125 | if "%1" == "qthelp" ( 126 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 127 | if errorlevel 1 exit /b 1 128 | echo. 129 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 130 | .qhcp project file in %BUILDDIR%/qthelp, like this: 131 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\NetworkZero.qhcp 132 | echo.To view the help file: 133 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\NetworkZero.ghc 134 | goto end 135 | ) 136 | 137 | if "%1" == "devhelp" ( 138 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 139 | if errorlevel 1 exit /b 1 140 | echo. 141 | echo.Build finished. 142 | goto end 143 | ) 144 | 145 | if "%1" == "epub" ( 146 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 147 | if errorlevel 1 exit /b 1 148 | echo. 149 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 150 | goto end 151 | ) 152 | 153 | if "%1" == "epub3" ( 154 | %SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3 155 | if errorlevel 1 exit /b 1 156 | echo. 157 | echo.Build finished. The epub3 file is in %BUILDDIR%/epub3. 158 | goto end 159 | ) 160 | 161 | if "%1" == "latex" ( 162 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 163 | if errorlevel 1 exit /b 1 164 | echo. 165 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 166 | goto end 167 | ) 168 | 169 | if "%1" == "latexpdf" ( 170 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 171 | cd %BUILDDIR%/latex 172 | make all-pdf 173 | cd %~dp0 174 | echo. 175 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 176 | goto end 177 | ) 178 | 179 | if "%1" == "latexpdfja" ( 180 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 181 | cd %BUILDDIR%/latex 182 | make all-pdf-ja 183 | cd %~dp0 184 | echo. 185 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 186 | goto end 187 | ) 188 | 189 | if "%1" == "text" ( 190 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 191 | if errorlevel 1 exit /b 1 192 | echo. 193 | echo.Build finished. The text files are in %BUILDDIR%/text. 194 | goto end 195 | ) 196 | 197 | if "%1" == "man" ( 198 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 199 | if errorlevel 1 exit /b 1 200 | echo. 201 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 202 | goto end 203 | ) 204 | 205 | if "%1" == "texinfo" ( 206 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 207 | if errorlevel 1 exit /b 1 208 | echo. 209 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 210 | goto end 211 | ) 212 | 213 | if "%1" == "gettext" ( 214 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 215 | if errorlevel 1 exit /b 1 216 | echo. 217 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 218 | goto end 219 | ) 220 | 221 | if "%1" == "changes" ( 222 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 223 | if errorlevel 1 exit /b 1 224 | echo. 225 | echo.The overview file is in %BUILDDIR%/changes. 226 | goto end 227 | ) 228 | 229 | if "%1" == "linkcheck" ( 230 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 231 | if errorlevel 1 exit /b 1 232 | echo. 233 | echo.Link check complete; look for any errors in the above output ^ 234 | or in %BUILDDIR%/linkcheck/output.txt. 235 | goto end 236 | ) 237 | 238 | if "%1" == "doctest" ( 239 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 240 | if errorlevel 1 exit /b 1 241 | echo. 242 | echo.Testing of doctests in the sources finished, look at the ^ 243 | results in %BUILDDIR%/doctest/output.txt. 244 | goto end 245 | ) 246 | 247 | if "%1" == "coverage" ( 248 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 249 | if errorlevel 1 exit /b 1 250 | echo. 251 | echo.Testing of coverage in the sources finished, look at the ^ 252 | results in %BUILDDIR%/coverage/python.txt. 253 | goto end 254 | ) 255 | 256 | if "%1" == "xml" ( 257 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 258 | if errorlevel 1 exit /b 1 259 | echo. 260 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 261 | goto end 262 | ) 263 | 264 | if "%1" == "pseudoxml" ( 265 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 266 | if errorlevel 1 exit /b 1 267 | echo. 268 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 269 | goto end 270 | ) 271 | 272 | :end 273 | -------------------------------------------------------------------------------- /docs/messenger.rst: -------------------------------------------------------------------------------- 1 | Sending & Receiving Messages 2 | ============================ 3 | 4 | .. automodule:: networkzero.messenger 5 | :synopsis: Sending & receiving messages across the network 6 | :show-inheritance: 7 | .. moduleauthor:: Tim Golden 8 | 9 | 10 | Introduction 11 | ------------ 12 | 13 | The messenger module offers two ways to send and/or receive messages across 14 | the network: 15 | 16 | * Messages: :func:`send_message_to` / :func:`wait_for_message_from` / :func:`send_reply_to` 17 | * News: :func:`send_news_to` / :func:`wait_for_news_from` 18 | 19 | Functions 20 | --------- 21 | 22 | Sending Messages 23 | ~~~~~~~~~~~~~~~~ 24 | 25 | Messages are exchanged in a strict request / reply sequence. This restricts 26 | flexibility but makes the semantics obvious. A message is sent to an 27 | address and the process waits until a reply is received. Likewise, a message 28 | is awaited from an address and a reply must be sent before another message 29 | can be received. 30 | 31 | The data sent as part of the message will be serialised as JSON so any 32 | object supported by the Python JSON library is a candidate for message payload. 33 | 34 | Any number of processes can send a message to one process which is listening. 35 | The messages are sent in a strict request-reply sequence so no ambiguity 36 | should occur. 37 | 38 | .. autofunction:: send_message_to 39 | .. autofunction:: wait_for_message_from 40 | .. autofunction:: send_reply_to 41 | 42 | Sending News 43 | ~~~~~~~~~~~~ 44 | 45 | News (also known as publications & subscriptions or "pubsub") allows 46 | multiple processes to wait for messages from one (or more) processes. This 47 | is not possible in the message-sending functionality above. There is a notion 48 | of "topics" which allow a publisher to produce a broader range of news 49 | to which a subscriber need only listen for some. 50 | 51 | Since a wildcard filter can be used for the topic, the topic used is returned 52 | along with the data when news is received. 53 | 54 | .. autofunction:: send_news_to 55 | .. autofunction:: wait_for_news_from 56 | -------------------------------------------------------------------------------- /docs/networkzero.rst: -------------------------------------------------------------------------------- 1 | NetworkZero 2 | =========== 3 | 4 | .. automodule:: networkzero 5 | :synopsis: Discover & exchanging messages across the network 6 | :show-inheritance: 7 | .. moduleauthor:: Tim Golden 8 | 9 | 10 | Functions 11 | --------- 12 | 13 | General 14 | ~~~~~~~ 15 | 16 | .. autofunction:: address 17 | .. autofunction:: action_and_params 18 | 19 | Discovery 20 | ~~~~~~~~~ 21 | .. autofunction:: advertise 22 | .. autofunction:: discover 23 | .. autofunction:: discover_all 24 | 25 | Message-Sending 26 | ~~~~~~~~~~~~~~~ 27 | .. autofunction:: send_message_to 28 | .. autofunction:: wait_for_message_from 29 | .. autofunction:: send_reply_to 30 | .. autofunction:: send_news_to 31 | .. autofunction:: wait_for_news_from 32 | -------------------------------------------------------------------------------- /docs/toc.rst: -------------------------------------------------------------------------------- 1 | Contents 2 | -------- 3 | 4 | .. toctree:: 5 | :maxdepth: 3 6 | 7 | index 8 | usage 9 | cookbook 10 | for-teachers 11 | for-developers 12 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | .. module:: networkzero 2 | 3 | Usage 4 | ===== 5 | 6 | Introduction 7 | ------------ 8 | 9 | At its core, NetworkZero offers two discovery functions and 5 message-sending functions: 10 | 11 | Discovery 12 | ~~~~~~~~~ 13 | 14 | * :func:`advertise` -- advertise a named service running at an address 15 | * :func:`discover` -- discover a service by name 16 | 17 | Message-Sending 18 | ~~~~~~~~~~~~~~~ 19 | 20 | To send a message and receive a reply: 21 | 22 | * :func:`send_message_to` -- send a message to an address 23 | * :func:`wait_for_message_from` -- wait for a message to arrive at an address 24 | * :func:`send_reply_to` -- send a reply from an address to a message 25 | 26 | To have several machines subscribe to topics from a publisher: 27 | 28 | * :func:`send_news_to` -- send a message to everyone subscribed to a topic at an address 29 | * :func:`wait_for_news_from` -- subscribe to and wait for a topic-specific message from a publisher 30 | 31 | General 32 | ~~~~~~~ 33 | 34 | A few convenience functions are available to help some common cases: 35 | 36 | * :func:`address` -- return a networkzero address valid for this machine 37 | * :func:`action_and_params` -- split a command line out into an action and a list of parameters 38 | 39 | Concepts 40 | -------- 41 | 42 | NetworkZero is built around the idea of `addresses` and `messages`. 43 | 44 | Address 45 | ~~~~~~~ 46 | 47 | An `address` is a string containing an IP address and a port separated by a colon, 48 | exactly as you would type in to a web browser (except that you have to specify the port). 49 | 50 | Examples: 51 | 52 | * `pi:1234` 53 | * `127.0.0.1:45678` 54 | * `192.0.2.5:9876` 55 | 56 | If you know what you're about, you can simply pass one of these around directly, eg:: 57 | 58 | import networkzero as nw0 59 | 60 | address = "127.0.0.1:1234" 61 | nw0.advertise("myservice", address) 62 | message = nw0.wait_for_message(address) 63 | 64 | But to save you knowing what IP addresses and what ports are in use, NetworkZero 65 | makes it easy to generate an address automatically. If you pass no address to 66 | :func:`advertise`, an address will be returned, constructed from your machine's 67 | IP address and a pool of spare ports. 68 | 69 | The IP address can be a wildcard, eg "192.168.*". This will nudge the 70 | automatic address onto a valid address which matches that network. This 71 | is useful where your machine has multiple valid addresses, each on a 72 | different network. 73 | 74 | .. note:: 75 | 76 | You don't *need* to ask NetworkZero to generate an address: any valid address 77 | can be used. The automatic address is merely a convenience. 78 | 79 | Message 80 | ~~~~~~~ 81 | 82 | A message, for the message-passing and news functionality, can be any built-in 83 | Python object. This will often be just some text, but can be a number or a list, a tuple 84 | or a dictionary. Note that the restriction to built-in objects is recursive: any lists 85 | or dictionaries you send can themselves only contain built-in objects. 86 | 87 | Examples 88 | -------- 89 | 90 | .. toctree:: 91 | :maxdepth: 2 92 | :glob: 93 | 94 | cookbook/simple-discovery 95 | cookbook/simple-messaging 96 | cookbook/advanced-discovery 97 | cookbook/advanced-messaging 98 | cookbook/complete-examples 99 | -------------------------------------------------------------------------------- /examples/chat/chat.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import networkzero as nw0 3 | 4 | try: 5 | # Python 2.7 compat 6 | input = raw_input 7 | except NameError: 8 | pass 9 | 10 | print("Looking for chat hub") 11 | hub = nw0.discover("chat-hub") 12 | if not hub: 13 | print("Unable to find chat hub after 60s") 14 | raise SystemExit 15 | 16 | print("Chat hub found at", hub) 17 | 18 | def main(name=None): 19 | name = name or input("Name: ") 20 | nw0.send_message_to(hub, ["JOIN", name]) 21 | try: 22 | while True: 23 | try: 24 | message = input("Message: ") 25 | except KeyboardInterrupt: 26 | message = None 27 | if not message: 28 | break 29 | nw0.send_message_to(hub, ["SPEAK", (name, message)]) 30 | finally: 31 | nw0.send_message_to(hub, ["LEAVE", name]) 32 | 33 | if __name__ == '__main__': 34 | main(*sys.argv[1:]) 35 | -------------------------------------------------------------------------------- /examples/chat/hub.py: -------------------------------------------------------------------------------- 1 | import networkzero as nw0 2 | 3 | # 4 | # If the hub is already running in another process, drop out 5 | # 6 | hub = nw0.discover("chat-hub", 3) 7 | if hub is not None: 8 | raise SystemExit("Hub is already running on %s" % hub) 9 | 10 | hub = nw0.advertise("chat-hub") 11 | print("Hub on", hub) 12 | updates = nw0.advertise("chat-updates") 13 | print("Updates on", updates) 14 | while True: 15 | action, params = nw0.wait_for_message_from(hub, autoreply=True) 16 | print("Action: %s, Params: %s" % (action, params)) 17 | nw0.send_news_to(updates, action, params) 18 | -------------------------------------------------------------------------------- /examples/chat/readme.txt: -------------------------------------------------------------------------------- 1 | This example implements a very simple hub-based (ie not peer-to-peer) 2 | chat system. One machine needs to start the hub. (If there's one already 3 | running, it will just shut down again). 4 | 5 | Each other machine will need to run two processes: 6 | 7 | * status.py shows who's joined and left and who's saying what 8 | * chat.py enters people into the chat and publishes the messages the type 9 | 10 | The hub (advertised as "chat-hub") receives messages via send_message/wait_for_message 11 | from the chat processes and sends out notifications (advertised as "chat-updates") which 12 | are picked up via send_notification/wait_for_notification by the status processes. 13 | 14 | Obviously a more sophisticated version of this could use pygame or 15 | tkinter windows to display the updates along side the input. -------------------------------------------------------------------------------- /examples/chat/run.cmd: -------------------------------------------------------------------------------- 1 | start hub.py 2 | start status.py 3 | start chat.py Alice 4 | start chat.py Bob 5 | -------------------------------------------------------------------------------- /examples/chat/status.py: -------------------------------------------------------------------------------- 1 | import networkzero as nw0 2 | 3 | print("Looking for chat updates channel") 4 | updates = nw0.discover("chat-updates") 5 | if not updates: 6 | print("Unable to find chat updates channel after 60s") 7 | raise SystemExit 8 | 9 | print("Chat updates found at", updates) 10 | while True: 11 | action, message = nw0.wait_for_news_from(updates) 12 | if action is None: 13 | break 14 | elif action == "JOIN": 15 | print("%s has joined" % message) 16 | elif action == "LEAVE": 17 | print("%s has left" % message) 18 | elif action == "SPEAK": 19 | [person, words] = message 20 | print("%s says: %s" % (person, words)) 21 | else: 22 | print("!! Unexpected message: %s" % message) 23 | -------------------------------------------------------------------------------- /examples/halifax/halifax.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import random 3 | import time 4 | import traceback 5 | import uuid 6 | 7 | import networkzero as nw0 8 | 9 | words = {} 10 | with open("words.txt") as f: 11 | for word in f: 12 | words.setdefault(word[0], []).append(word.strip()) 13 | 14 | my_name = "halifax/" + uuid.uuid4().hex 15 | my_address = nw0.advertise(my_name) 16 | if len(sys.argv) == 2: 17 | first_word = sys.argv[1].lower().strip() 18 | else: 19 | first_word = '' 20 | 21 | N_SECONDS_WAIT_FOR_NEIGHBOURS = 5 22 | # 23 | # Wait 30 seconds to discover all neighbours 24 | # 25 | print("Waiting %d seconds for neighbours to show up..." % N_SECONDS_WAIT_FOR_NEIGHBOURS) 26 | time.sleep(N_SECONDS_WAIT_FOR_NEIGHBOURS) 27 | 28 | print("Looking for neighbours") 29 | addresses = [address for (name, address) in nw0.discover_group("halifax", exclude=[my_name])] 30 | print(addresses) 31 | 32 | while True: 33 | 34 | if first_word: 35 | word = first_word 36 | first_word = None 37 | else: 38 | print("Waiting for next word...") 39 | word = nw0.wait_for_message_from(my_address, autoreply=True) 40 | 41 | print("Got word", word) 42 | candidate_words = words[word[-1]] 43 | random.shuffle(candidate_words) 44 | next_word = candidate_words.pop() 45 | 46 | print("Sending word", next_word) 47 | nw0.send_message_to(random.choice(addresses), next_word) 48 | -------------------------------------------------------------------------------- /examples/halifax/run.cmd: -------------------------------------------------------------------------------- 1 | start python -mnetworkzero.discovery 2 | start python halifax.py banana 3 | start python halifax.py 4 | start python halifax.py 5 | -------------------------------------------------------------------------------- /examples/reverso/client.py: -------------------------------------------------------------------------------- 1 | import networkzero as nw0 2 | 3 | try: 4 | # Python 2.7 compat 5 | input = raw_input 6 | except NameError: 7 | pass 8 | 9 | reverso = nw0.discover("reverso") 10 | while True: 11 | word = input("Enter word: ") 12 | reversed_word = nw0.send_message_to(reverso, word) 13 | print("Reversed:", reversed_word) 14 | -------------------------------------------------------------------------------- /examples/reverso/readme.txt: -------------------------------------------------------------------------------- 1 | This very simple question-response server trivially reverses whatever 2 | is sent to it. -------------------------------------------------------------------------------- /examples/reverso/server.py: -------------------------------------------------------------------------------- 1 | import networkzero as nw0 2 | 3 | address = nw0.advertise("reverso") 4 | while True: 5 | message = nw0.wait_for_message_from(address) 6 | nw0.send_reply_to(address, message[::-1]) 7 | -------------------------------------------------------------------------------- /misc/pyconuk2017/02.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjguk/networkzero/d22faf619d2fbd349d9b091fc832f8be4896eec6/misc/pyconuk2017/02.jpg -------------------------------------------------------------------------------- /misc/pyconuk2017/03.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjguk/networkzero/d22faf619d2fbd349d9b091fc832f8be4896eec6/misc/pyconuk2017/03.jpg -------------------------------------------------------------------------------- /misc/pyconuk2017/04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjguk/networkzero/d22faf619d2fbd349d9b091fc832f8be4896eec6/misc/pyconuk2017/04.png -------------------------------------------------------------------------------- /misc/pyconuk2017/05.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjguk/networkzero/d22faf619d2fbd349d9b091fc832f8be4896eec6/misc/pyconuk2017/05.jpg -------------------------------------------------------------------------------- /misc/pyconuk2017/06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjguk/networkzero/d22faf619d2fbd349d9b091fc832f8be4896eec6/misc/pyconuk2017/06.png -------------------------------------------------------------------------------- /misc/pyconuk2017/07.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjguk/networkzero/d22faf619d2fbd349d9b091fc832f8be4896eec6/misc/pyconuk2017/07.jpg -------------------------------------------------------------------------------- /misc/pyconuk2017/07b.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjguk/networkzero/d22faf619d2fbd349d9b091fc832f8be4896eec6/misc/pyconuk2017/07b.jpg -------------------------------------------------------------------------------- /misc/pyconuk2017/08.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjguk/networkzero/d22faf619d2fbd349d9b091fc832f8be4896eec6/misc/pyconuk2017/08.jpg -------------------------------------------------------------------------------- /misc/pyconuk2017/09.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjguk/networkzero/d22faf619d2fbd349d9b091fc832f8be4896eec6/misc/pyconuk2017/09.jpg -------------------------------------------------------------------------------- /misc/pyconuk2017/10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjguk/networkzero/d22faf619d2fbd349d9b091fc832f8be4896eec6/misc/pyconuk2017/10.jpg -------------------------------------------------------------------------------- /misc/pyconuk2017/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjguk/networkzero/d22faf619d2fbd349d9b091fc832f8be4896eec6/misc/pyconuk2017/11.png -------------------------------------------------------------------------------- /misc/pyconuk2017/12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjguk/networkzero/d22faf619d2fbd349d9b091fc832f8be4896eec6/misc/pyconuk2017/12.jpg -------------------------------------------------------------------------------- /misc/pyconuk2017/12a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjguk/networkzero/d22faf619d2fbd349d9b091fc832f8be4896eec6/misc/pyconuk2017/12a.png -------------------------------------------------------------------------------- /misc/pyconuk2017/13.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjguk/networkzero/d22faf619d2fbd349d9b091fc832f8be4896eec6/misc/pyconuk2017/13.jpg -------------------------------------------------------------------------------- /misc/pyconuk2017/14.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjguk/networkzero/d22faf619d2fbd349d9b091fc832f8be4896eec6/misc/pyconuk2017/14.PNG -------------------------------------------------------------------------------- /misc/pyconuk2017/15a.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjguk/networkzero/d22faf619d2fbd349d9b091fc832f8be4896eec6/misc/pyconuk2017/15a.jpg -------------------------------------------------------------------------------- /misc/pyconuk2017/15b.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjguk/networkzero/d22faf619d2fbd349d9b091fc832f8be4896eec6/misc/pyconuk2017/15b.jpg -------------------------------------------------------------------------------- /misc/pyconuk2017/15c.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjguk/networkzero/d22faf619d2fbd349d9b091fc832f8be4896eec6/misc/pyconuk2017/15c.jpg -------------------------------------------------------------------------------- /misc/pyconuk2017/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjguk/networkzero/d22faf619d2fbd349d9b091fc832f8be4896eec6/misc/pyconuk2017/16.png -------------------------------------------------------------------------------- /misc/pyconuk2017/17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjguk/networkzero/d22faf619d2fbd349d9b091fc832f8be4896eec6/misc/pyconuk2017/17.png -------------------------------------------------------------------------------- /misc/pyconuk2017/17b.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjguk/networkzero/d22faf619d2fbd349d9b091fc832f8be4896eec6/misc/pyconuk2017/17b.jpg -------------------------------------------------------------------------------- /misc/pyconuk2017/adventure.py: -------------------------------------------------------------------------------- 1 | import os, sys 2 | 3 | from PyQt5 import QtCore, QtGui, QtWidgets 4 | 5 | """Screen display containing a panel for an accompanying image; a large 6 | box for descriptive text; a smaller box for inventory, strength & other 7 | attributes; and a line for entering commands 8 | """ 9 | 10 | class Adventure(QtWidgets.QWidget): 11 | 12 | def __init__(self, parent=None): 13 | super(Adventure, self).__init__(parent) 14 | 15 | # 16 | # Top-half of the 17 | # 18 | self.image_panel = QtWidgets.QLabel() 19 | self.image_panel.setAlignment(QtCore.Qt.AlignCenter) 20 | self.image = QtGui.QPixmap("image.jpg") 21 | self.image_panel.setPixmap(self.image) 22 | 23 | self.text_panel = QtWidgets.QTextEdit() 24 | self.text_panel.setReadOnly(True) 25 | self.text_panel.setTextBackgroundColor(QtGui.QColor("blue")) 26 | self.text_panel.setHtml("""

Hello, World!

27 | 28 |

You are in a spacious ballroom with the sound of music playing all around you.

29 | """) 30 | 31 | self.data_panel = QtWidgets.QTextEdit() 32 | self.data_panel.setReadOnly(True) 33 | 34 | self.input = QtWidgets.QLineEdit() 35 | 36 | layout = QtWidgets.QVBoxLayout() 37 | layout.addWidget(self.image_panel, 1) 38 | hlayout = QtWidgets.QHBoxLayout() 39 | hlayout.addWidget(self.text_panel, 3) 40 | hlayout.addWidget(self.data_panel, 1) 41 | layout.addLayout(hlayout, 1) 42 | layout.addWidget(self.input) 43 | 44 | self.setLayout(layout) 45 | self.setWindowTitle("Westpark Adventure") 46 | 47 | if __name__ == '__main__': 48 | app = QtWidgets.QApplication(sys.argv) 49 | Adventure = Adventure() 50 | Adventure.show() 51 | sys.exit(app.exec_()) 52 | -------------------------------------------------------------------------------- /misc/pyconuk2017/chat.py: -------------------------------------------------------------------------------- 1 | import os, sys 2 | 3 | import networkzero as nw0 4 | from guizero import App, Box, Text, TextBox 5 | 6 | app = App(width=400) 7 | box = Box(app) 8 | chatter = Text(box, align="top") 9 | speaking = TextBox(box, align="bottom", width=400) 10 | 11 | chatter.append("Hello\n") 12 | chatter.append("World\n") 13 | chatter.color("blue") 14 | chatter.append("!") 15 | 16 | app.display() 17 | -------------------------------------------------------------------------------- /misc/pyconuk2017/chatter.py: -------------------------------------------------------------------------------- 1 | import os, sys 2 | 3 | from PyQt5 import QtCore, QtGui, QtWidgets 4 | 5 | import networkzero as nw0 6 | 7 | """Screen display containing a panel for an accompanying image; a large 8 | box for descriptive text; a smaller box for inventory, strength & other 9 | attributes; and a line for entering commands 10 | """ 11 | 12 | class FeedbackReader(QtCore.QThread): 13 | 14 | message_received = QtCore.pyqtSignal(str, str) 15 | 16 | def __init__(self, chattery): 17 | super(FeedbackReader, self).__init__() 18 | self.chattery = chattery 19 | 20 | def run(self): 21 | while True: 22 | topic, message = nw0.wait_for_news_from(self.chattery) 23 | if topic and message: 24 | self.message_received.emit(topic, message) 25 | 26 | class Chatter(QtWidgets.QWidget): 27 | 28 | def __init__(self, name, parent=None): 29 | super(Chatter, self).__init__(parent) 30 | self.name = name 31 | 32 | self.text_panel = QtWidgets.QTextEdit() 33 | self.text_panel.setReadOnly(True) 34 | self.input = QtWidgets.QLineEdit() 35 | 36 | layout = QtWidgets.QVBoxLayout() 37 | layout.addWidget(self.text_panel, 3) 38 | layout.addWidget(self.input, 1) 39 | 40 | self.setLayout(layout) 41 | self.setWindowTitle("Chatter") 42 | 43 | self.input.editingFinished.connect(self.input_changed) 44 | self.input.setFocus() 45 | 46 | self.chattery = nw0.discover("chattery/news") 47 | self.responder = FeedbackReader(self.chattery) 48 | self.responder.message_received.connect(self.handle_response) 49 | self.responder.start() 50 | 51 | def handle_response(self, topic, message): 52 | text = self.text_panel.toPlainText() 53 | if text: 54 | text += "\n" 55 | if topic == self.name: 56 | topic = "ME" 57 | text += "%s: %s" % (topic, message) 58 | self.text_panel.setPlainText(text) 59 | 60 | def input_changed(self): 61 | text = self.input.text().strip() 62 | if text: 63 | nw0.send_news_to(self.chattery, self.name, text) 64 | self.input.clear() 65 | 66 | if __name__ == '__main__': 67 | app = QtWidgets.QApplication(sys.argv) 68 | Chatter = Chatter(sys.argv[1]) 69 | Chatter.show() 70 | sys.exit(app.exec_()) 71 | -------------------------------------------------------------------------------- /misc/pyconuk2017/chattery.py: -------------------------------------------------------------------------------- 1 | import os, sys 2 | 3 | import networkzero as nw0 4 | 5 | if __name__ == '__main__': 6 | people = [] 7 | 8 | chattery = nw0.advertise("chattery") 9 | 10 | while True: 11 | message = nw0.wait_for_message_from(chattery) 12 | action, params = nw0.action_and_params(message) 13 | action = action.upper() 14 | if action == "JOIN": 15 | name = params[0] 16 | code = "-".join(name.lower().split()) 17 | people.append("chattery/%s" % code) 18 | 19 | elif action == "LEAVE": 20 | elif action == "SAY": 21 | else: 22 | print("Unknown command: %s" % action) 23 | -------------------------------------------------------------------------------- /misc/pyconuk2017/image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjguk/networkzero/d22faf619d2fbd349d9b091fc832f8be4896eec6/misc/pyconuk2017/image.jpg -------------------------------------------------------------------------------- /misc/pyconuk2017/notes.txt: -------------------------------------------------------------------------------- 1 | 1) Intro 2 | 3 | 2) Two years the club I help to run entered PiWars [trojan horse robot] 4 | 5 | 3) We decided to remote control the robot via a controller connected to a laptop over WiFi [SNES controller connected to laptop] 6 | 7 | 4) We had to faff around setting the Pi inside the robot as an AP with a fixed IP [image of https://learn.adafruit.com/setting-up-a-raspberry-pi-as-a-wifi-access-point?view=all] 8 | 9 | 5) But it worked... although when developing the controller code talking to localhost on my laptop I needed to mess around with the addresses / ports [two processes on one laptop / .ini file] 10 | 11 | 6) I'd always wanted an excuse to use ZeroMQ... so we did that, with a very thin wrapper of my own to make it easier for the lads at the club [C:\Users\tim\work-in-progress\westpark.github\robotics\robot\robot.py on RVW] 12 | 13 | 7) Then, last year, we decided to do a simple activity involving three Raspberry Pis talking across the network: one controlling to a disc spinning on a wheel and sensing the Nerf dart hits on two sensor flaps; one starting and stopping the turntable; and the third receiving the scores and showing the scoreboard [turntable] 14 | 15 | 8) We had three teams running over three weeks, with a one-hour session each week. I repurposed the zmq wrapper from the robot and gave the RPis static addresses. But each time the lads came in, we ended up with different addresses and so on, and there was a lot of "type *these* numbers in instead of *those* numbers" going on. [picture of confused network setup] 16 | 17 | 9) So I thought: there are two things we need to do here which are particular to this kind of club situation -- and which would be similar in a CoderDojo or a classroom. [picture of club/classroom IT] 18 | 19 | 10) a) have a simple way of discovering the other machine you're talking to; [beacon] 20 | 21 | 11) and b) have a straightforward pattern of sending messages and perhaps receiving answers. [messaging] 22 | 23 | 12) and, ideally, have these work in a robust way cross-platform with at least Windows, Mac OS/X, Linux/RPi [3 x OS logos] 24 | 25 | 13) Neither of these is a blindingly new problem; and the second already has some reasonable solutions -- including ZeroMQ itself, or even native sockets -- depending on how much pain you're prepared to put up with especially as you go cross-platform. But I'm focusing on the particular constraints of a tight timeframe and relatively inexperienced -- and probably young -- learners. [ 26 | 27 | 14) So I created NetworkZero [networkzero readthedocs page] 28 | 29 | 15) The "Zero" suffix is in relation to the already existing PyGame Zero and GPIOZero (and there have been some more since including GUIZero). The idea is to make Python packages which are built on powerful libraries but which provide an easier entry point for beginners, typically by avoiding some noisy boilerplate which clutters what would otherwise be simple code. [logos/headings for PyGame Zero, GPIO Zero] 30 | 31 | 16) How do you use networkzero [nw0]? In its simplest form, you obtain an address by advertising the name of a service and then you listen on that address for requests which you then act on and reply. Someone wanting to talk to your program will obtain an address by searching for the name of the service and will send a request to that address and receive a reply. [example code] 32 | 33 | 17) And that's it. It actually does more than this, but this is the heart of networkzero: simple network code round which you can create a standalone program, or which you can use to sprinkle some network glitter on top of an existing project. [image of magic wand / network icons] 34 | 35 | 18) Live Demo! 36 | 37 | 19) What are its strengths? 38 | 39 | a) Very straightforward to use 40 | b) Works unchanged on Python 2.7 & 3.3+ and on Windows, Mac, Linux / RPi 41 | c) Send & receive any built-in type, not just strings 42 | d) Request / Reply + Publish / Subscribe 43 | e) Designed to be fairly robust in the face of odd network setups, frequent stop/start etc. 44 | f) Threadsafe, which means you can integrate with GPIOZero callbacks 45 | g) Fully tested with new changes tested across all platforms (h/t Tom Viner) 46 | h) Can use either component (discovery / messaging) independently but work well together 47 | 48 | 20) What are its weaknesses? 49 | 50 | a) Hard dependencies on ZeroMQ & netifaces (both good & bad) 51 | b) It's a little further away from basic socket programming so takes away from the learning experience 52 | c) Still some corner cases where it hangs etc. 53 | 54 | 21) Particular challenges while creating the library 55 | 56 | a) BT DNS redirect! [Trying to resolve a hostname, BT internet succeeds but goes its own page!] 57 | b) Sockets on different platforms (lingering, timeouts, broadcast addresses etc.) 58 | c) in-out-in-out on the messaging functionality (did you hear what happened when the chap died who invented the hokey-cokey? They were putting him into the coffin and they got the left leg in and it all went horribly wrong...) 59 | d) Marshal vs JSON vs whatever... 60 | 61 | 62 | * What are the benefits? [smiley face] 63 | - It's deliberately straightforward to use 64 | - It works unchanged across all major platforms: Windows, Mac OS/X, Linux/RPi [logos of platforms] 65 | - It works on Python 2.7 and 3.3+ 66 | - You can send and receive any builtin Python type, not just strings 67 | - As well as request-reply there's also a publish-subscribe mode (eg for data capture) 68 | - The discovery mechanism works very hard to cope with restarted processes, odd network configurations, platform differences etc. 69 | - It is threadsafe, which essentially means you can integrate it with GPIOZero callbacks [GPIOZero logo] 70 | - It's thoroughly tested, with new changes tested across all supported platforms via Appveyor and Travis (thanks, Tom Viner!) 71 | 72 | * What are the drawbacks? [sad face] 73 | - It currently has hard dependencies on zeromq and netifaces 74 | - It's a little further away from basic socket programming so takes away from the learning experience 75 | - It's still possible to meet corner cases where it hangs etc. 76 | 77 | * Why should I use networkzero? (I'm assuming you're an educator of some sort because that's who the package is aimed at). Two different questions, not unrelated: why should I use networking at all? if I do, why should I use networkzero as opposed to rolling my own socket code? 78 | 79 | * Why networking at all? Because networking is an integral part of how computers are used nowadays [internet of things] 80 | 81 | * -- and it can add an extra layer of interest to an otherwise simple package. [kid looking wowed] 82 | 83 | * Why networkzero? Because it should make it easy for you to add networking without wasting a lot of time on some of the underlying issues: what address the machine has; whether you're blocking on a receive or a send; socket timeouts and orphaned sockets when processes crash out. [some networkzero code] 84 | 85 | * What challenges did you face / are you still facing in its development? [picture of climbing a rockface] 86 | 87 | - BT DNS redirect! 88 | - socket lingering on different platforms 89 | - 90 | - Using marshal/json/strings -------------------------------------------------------------------------------- /misc/pyconuk2017/pyconuk2017.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjguk/networkzero/d22faf619d2fbd349d9b091fc832f8be4896eec6/misc/pyconuk2017/pyconuk2017.pdf -------------------------------------------------------------------------------- /misc/pyconuk2017/pyconuk2017.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjguk/networkzero/d22faf619d2fbd349d9b091fc832f8be4896eec6/misc/pyconuk2017/pyconuk2017.pptx -------------------------------------------------------------------------------- /misc/pyconuk2017/quiz-controller.py: -------------------------------------------------------------------------------- 1 | #!python2 2 | import os, sys 3 | import socket 4 | import subprocess 5 | 6 | import Pyro4 7 | from PyQt4 import QtCore, QtGui 8 | 9 | import core 10 | import screen 11 | import screens 12 | 13 | class FeedbackReader(QtCore.QThread): 14 | 15 | message_received = QtCore.pyqtSignal(unicode, tuple) 16 | 17 | def __init__(self, proxy): 18 | super(FeedbackReader, self).__init__() 19 | self.feedback = proxy 20 | 21 | def run(self): 22 | while True: 23 | feedback = self.feedback.get() 24 | core.log.debug("feedback: %r", feedback) 25 | if feedback: 26 | message, args = feedback 27 | self.message_received.emit(message, args) 28 | 29 | class Panel(QtGui.QGroupBox): 30 | 31 | def __init__(self, controller, position, *args, **kwargs): 32 | super(Panel, self).__init__(position.title(), *args, **kwargs) 33 | self.instructions = controller 34 | self.position = position.lower() 35 | 36 | layout = QtGui.QVBoxLayout() 37 | self.selector = QtGui.QComboBox() 38 | self.selector.currentIndexChanged.connect(self.on_selector) 39 | 40 | layout.addWidget(self.selector) 41 | self.stack = QtGui.QStackedWidget() 42 | layout.addWidget(self.stack) 43 | self.setLayout(layout) 44 | 45 | for cls in screen.ScreenWidget.__subclasses__(): 46 | self.selector.addItem(cls.name) 47 | self.stack.addWidget(cls(controller, position)) 48 | 49 | def on_selector(self, index): 50 | core.log.debug("on_selector: %d, item %r", index, self.selector.itemText(index)) 51 | self.stack.setCurrentIndex(index) 52 | screen_name = unicode(self.selector.itemText(index)) 53 | self.instructions.send_command("SWITCH", self.position, screen_name) 54 | 55 | class QuizController(QtGui.QWidget): 56 | 57 | COMMAND_MAILSLOT_NAME = "quiz" 58 | RESPONSE_MAILSLOT_NAME = "sub" 59 | 60 | def __init__(self, *args, **kwargs): 61 | super(QuizController, self).__init__(*args, **kwargs) 62 | self.setWindowTitle("Quiz Controller") 63 | 64 | self.instructions = Pyro4.Proxy("PYRO:quiz.instructions@localhost:1234") 65 | self.responder = FeedbackReader(Pyro4.Proxy("PYRO:quiz.feedback@localhost:1234")) 66 | self.responder.message_received.connect(self.handle_response) 67 | self.responder.start() 68 | 69 | overall_layout = QtGui.QVBoxLayout() 70 | self.add_teams(overall_layout) 71 | 72 | self.panel_layout = QtGui.QHBoxLayout() 73 | self.panels = {} 74 | for position in "left", "right": 75 | panel = self.panels[position.lower()] = Panel(self, position) 76 | self.panel_layout.addWidget(panel) 77 | overall_layout.addLayout(self.panel_layout) 78 | self.add_controller(overall_layout) 79 | self.setLayout(overall_layout) 80 | 81 | # 82 | # The first response is always lost. Not sure why. 83 | # 84 | self.send_command("COLOURS?") 85 | self.send_command("COLOURS?") 86 | self.send_command("SCORES?") 87 | self.send_command("TEAMS?") 88 | 89 | def add_teams(self, overall_layout): 90 | self.teams = [] 91 | for i in range(4): 92 | team = ( 93 | team_name, 94 | team_score, 95 | team_plus, 96 | team_minus 97 | ) = ( 98 | QtGui.QLineEdit(), 99 | QtGui.QLineEdit(""), 100 | QtGui.QPushButton("+"), 101 | QtGui.QPushButton("-") 102 | ) 103 | self.teams.append(team) 104 | layout = QtGui.QHBoxLayout() 105 | for widget in team: 106 | layout.addWidget(widget) 107 | overall_layout.addLayout(layout) 108 | 109 | def set_team_name(new_name, n_team=i, team_name=team_name, team_score=team_score): 110 | self.send_command("name", n_team, unicode(team_name.text())) 111 | if not team_name.styleSheet(): 112 | self.send_command("COLOURS?") 113 | def set_team_score(new_score, n_team=i): 114 | self.send_command("SCORE", str(n_team), str(new_score)) 115 | def set_team_plus(n_team=i, team_score=team_score): 116 | score = 1 + int(team_score.text() or 0) 117 | team_score.setText(str(score)) 118 | def set_team_minus(n_team=i, team_score=team_score): 119 | score = int(team_score.text() or 0) - 1 120 | team_score.setText(str(score)) 121 | 122 | team_name.textEdited.connect(set_team_name) 123 | team_score.textChanged.connect(set_team_score) 124 | team_plus.pressed.connect(set_team_plus) 125 | team_minus.pressed.connect(set_team_minus) 126 | 127 | def add_controller(self, overall_layout): 128 | command_label = QtGui.QLabel("Command") 129 | self.command = QtGui.QLineEdit() 130 | self.send = QtGui.QPushButton("&Send") 131 | controller_layout = QtGui.QHBoxLayout() 132 | controller_layout.addWidget(command_label) 133 | controller_layout.addWidget(self.command) 134 | controller_layout.addWidget(self.send) 135 | overall_layout.addLayout(controller_layout) 136 | self.send.clicked.connect(self.send_command) 137 | 138 | self.responses = QtGui.QLabel() 139 | responses_layout = QtGui.QHBoxLayout() 140 | responses_layout.addWidget(self.responses) 141 | overall_layout.addLayout(responses_layout) 142 | 143 | def send_command(self, message=None, *args): 144 | if not message: 145 | commands = unicode(self.command.text()).encode("iso-8859-1").split() 146 | if not commands: 147 | core.log.warn("No command") 148 | return 149 | else: 150 | message, args = commands[0], commands[1:] 151 | 152 | args = [(unicode(arg) if isinstance(arg, QtCore.QString) else arg) for arg in args] 153 | command = "%s %s" % (message, " ".join(str(arg) for arg in args)) 154 | core.log.debug("send_command: %s", command) 155 | if hasattr(self, "command"): 156 | self.command.setText(command) 157 | self.instructions.put(message, *args) 158 | 159 | def position_widget(self, position): 160 | return self.groups.get(position.lower()) 161 | 162 | def handle_default(self, *args, **kwargs): 163 | core.log.debug("handle_default: %s, %s", str(args), str(kwargs)) 164 | 165 | #~ def add_positions(self): 166 | #~ for position in "left", "right": 167 | #~ panel = self.panels[position.lower()] = Panel(self, position) 168 | #~ self.panel_layout.addWidget(panel) 169 | 170 | #~ def handle_position(self, position, screen_name): 171 | #~ """Handle the POSITION event by selecting the corresponding 172 | #~ screen from the stacked widget. 173 | #~ """ 174 | #~ panel = self.panels[position.lower()] 175 | #~ if panel.selector.currentText() != screen_name: 176 | #~ panel.selector.setCurrentIndex(panel.selector.findText(screen_name)) 177 | #~ # 178 | #~ # Changing the selector will cause a STATE? query to fire 179 | #~ # 180 | 181 | #~ def _handle_position(self, position, cls_name, state): 182 | #~ core.log.debug("handle_position: %s, %s", position, rest) 183 | 184 | #~ group = self.groups[position] 185 | #~ group.selector.setCurrentIndex(group.selector.findText(cls_name)) 186 | #~ screen_widget = group.stack.currentWidget() 187 | 188 | #~ styles_combo = screen_widget.styles 189 | #~ if "styles" in state: 190 | #~ styles_combo.clear() 191 | #~ styles_combo.addItems([item.strip() for item in state.pop("styles")]) 192 | #~ if "style" in state: 193 | #~ screen_widget.styles.setCurrentIndex(screen_widget.styles.findText(state.pop("style"))) 194 | #~ for k, v in state.items(): 195 | #~ subwidget = getattr(screen_widget, k.lower(), None) 196 | #~ if subwidget: 197 | #~ subwidget.setText(v) 198 | 199 | #~ def handle_left(self, *args, **kwargs): 200 | #~ self._handle_position("left", *args, **kwargs) 201 | 202 | #~ def handle_right(self, *args, **kwargs): 203 | #~ self._handle_position("right", *args, **kwargs) 204 | 205 | def handle_teams(self, teams): 206 | for n_team, new_name in enumerate(teams): 207 | name, _, _, _ = self.teams[n_team] 208 | name.setText(new_name) 209 | 210 | def handle_colours(self, colours): 211 | for n_team, new_colour in enumerate(colours): 212 | name, _, _, _ = self.teams[n_team] 213 | name.setStyleSheet("* { background-color : %s; }" % new_colour) 214 | 215 | def handle_scores(self, scores): 216 | for n_team, new_score in enumerate(scores): 217 | _, score, _, _ = self.teams[n_team] 218 | score.setText(unicode(new_score)) 219 | 220 | def handle_quit(self): 221 | self.close() 222 | 223 | def handle_response(self, message, args): 224 | core.log.debug("Response received: %s, %s", message, args) 225 | message = unicode(message) 226 | response = "%s %s" % (message, " ".join("%r" % arg for arg in args)) 227 | self.responses.setText(response) 228 | handler = getattr(self, "handle_" + message.lower(), self.handle_default) 229 | return handler(*args) 230 | 231 | def main(): 232 | app = QtGui.QApplication([]) 233 | quiz_controller = QuizController() 234 | quiz_controller.show() 235 | return app.exec_() 236 | 237 | if __name__ == '__main__': 238 | try: 239 | socket.socket().connect(("localhost", 1234)) 240 | except socket.error: 241 | subprocess.Popen([sys.executable, "quiz.py"]) 242 | sys.exit(main(*sys.argv[1:])) 243 | -------------------------------------------------------------------------------- /misc/pyconuk2017/robotics/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | -------------------------------------------------------------------------------- /misc/pyconuk2017/robotics/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 westpark 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /misc/pyconuk2017/robotics/README.md: -------------------------------------------------------------------------------- 1 | # robotics 2 | Westpark Robotics activity for piwars 3 | -------------------------------------------------------------------------------- /misc/pyconuk2017/robotics/robot/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjguk/networkzero/d22faf619d2fbd349d9b091fc832f8be4896eec6/misc/pyconuk2017/robotics/robot/__init__.py -------------------------------------------------------------------------------- /misc/pyconuk2017/robotics/robot/config.py: -------------------------------------------------------------------------------- 1 | #!python3 2 | LISTEN_ON_IP = "127.0.0.1" 3 | LISTEN_ON_PORT = 5555 4 | CODEC = "utf-8" -------------------------------------------------------------------------------- /misc/pyconuk2017/robotics/robot/logging.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | #!python3 3 | import os, sys 4 | import logging 5 | import logging.handlers 6 | 7 | LOGGING_NAME = __package__ 8 | LOGGING_FILENAME = "%s.log" % LOGGING_NAME 9 | LOGGING_FILEPATH = LOGGING_FILENAME 10 | 11 | level = logging.DEBUG 12 | formatter = logging.Formatter("%(asctime)s - %(name)s - %(funcName)s - %(levelname)s - %(message)s") 13 | 14 | handler = logging.FileHandler( 15 | LOGGING_FILEPATH, 16 | mode="a", 17 | encoding="utf-8" 18 | ) 19 | handler.setLevel(level) 20 | handler.setFormatter(formatter) 21 | 22 | stderr_handler = logging.StreamHandler() 23 | stderr_handler.setLevel(level) 24 | stderr_handler.setFormatter(formatter) 25 | 26 | def logger(name): 27 | _logger = logging.getLogger("%s.%s" % (LOGGING_NAME, name)) 28 | _logger.setLevel(level) 29 | _logger.addHandler(handler) 30 | _logger.addHandler(stderr_handler) 31 | return _logger 32 | -------------------------------------------------------------------------------- /misc/pyconuk2017/robotics/robot/outputs/__init__.py: -------------------------------------------------------------------------------- 1 | from . import graphical, motor, text -------------------------------------------------------------------------------- /misc/pyconuk2017/robotics/robot/outputs/graphical.py: -------------------------------------------------------------------------------- 1 | def _init(): 2 | pass 3 | -------------------------------------------------------------------------------- /misc/pyconuk2017/robotics/robot/outputs/motor.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjguk/networkzero/d22faf619d2fbd349d9b091fc832f8be4896eec6/misc/pyconuk2017/robotics/robot/outputs/motor.py -------------------------------------------------------------------------------- /misc/pyconuk2017/robotics/robot/outputs/text.py: -------------------------------------------------------------------------------- 1 | def _init(): 2 | pass 3 | 4 | def do_move(direction): 5 | print("About to move %s" % direction) 6 | 7 | def do_stop(): 8 | print("Stopping...") -------------------------------------------------------------------------------- /misc/pyconuk2017/robotics/robot/robot.py: -------------------------------------------------------------------------------- 1 | #!python3 2 | import os, sys 3 | import queue 4 | import shlex 5 | import threading 6 | import time 7 | 8 | import zmq 9 | 10 | from . import config 11 | from . import logging 12 | log = logging.logger(__package__) 13 | from . import outputs 14 | 15 | class RobotError(BaseException): pass 16 | 17 | class NoSuchActionError(RobotError): pass 18 | 19 | class Robot(object): 20 | 21 | def __init__( 22 | self, 23 | output, 24 | stop_event=None, 25 | listen_on_ip=config.LISTEN_ON_IP, listen_on_port=config.LISTEN_ON_PORT 26 | ): 27 | log.info("Setting up Robot on %s:%s", listen_on_ip, listen_on_port) 28 | log.info("Outputting to %s", output) 29 | self.stop_event = stop_event or threading.Event() 30 | self._init_socket(listen_on_ip, listen_on_port) 31 | self.output = output 32 | self.output._init() 33 | 34 | def _init_socket(self, listen_on_ip, listen_on_port): 35 | context = zmq.Context() 36 | self.socket = context.socket(zmq.REP) 37 | self.socket.bind("tcp://%s:%s" % (listen_on_ip, listen_on_port)) 38 | 39 | def get_command(self): 40 | """Attempt to return a unicode object from the command socket 41 | 42 | If no message is available without blocking (as opposed to a blank 43 | message), return None 44 | """ 45 | try: 46 | message_bytes = self.socket.recv(zmq.NOBLOCK) 47 | log.debug("Received message: %r", message_bytes) 48 | except zmq.ZMQError as exc: 49 | if exc.errno == zmq.EAGAIN: 50 | return None 51 | else: 52 | raise 53 | else: 54 | return message_bytes.decode(config.CODEC) 55 | 56 | def send_response(self, response): 57 | """Send a unicode object as reply to the most recently-issued command 58 | """ 59 | response_bytes = response.encode(config.CODEC) 60 | log.debug("About to send reponse: %r", response_bytes) 61 | self.socket.send(response_bytes) 62 | 63 | def parse_command(self, command): 64 | """Break a multi word command up into an action and its parameters 65 | """ 66 | words = shlex.split(command.lower()) 67 | return words[0], words[1:] 68 | 69 | def dispatch(self, command): 70 | """Pass a command along with its params to a suitable handler 71 | 72 | If the command is blank, succeed silently 73 | If the command has no handler, succeed silently 74 | If the handler raises an exception, fail with the exception message 75 | """ 76 | log.info("Dispatch on %s", command) 77 | if not command: 78 | return "OK" 79 | 80 | action, params = self.parse_command(command) 81 | log.debug("Action = %s, Params = %s", action, params) 82 | try: 83 | function = getattr(self, "do_" + action, None) 84 | if function: 85 | function(*params) 86 | return "OK" 87 | except KeyboardInterrupt: 88 | raise 89 | except Exception as exc: 90 | log.exception("Problem executing action %s", action) 91 | return "ERROR: %s" % exc 92 | 93 | def do_output(self, *args): 94 | """Pass a command directly to the current output processor 95 | """ 96 | if args: 97 | action, params = args[0], args[1:] 98 | log.debug("Pass %s directly to output with %s", action, params) 99 | function = getattr(self.output, "do_" + action, None) 100 | if function: 101 | function(*params) 102 | 103 | def do_finish(self): 104 | self.stop_event.set() 105 | 106 | # 107 | # Main loop 108 | # 109 | def start(self): 110 | while not self.stop_event.is_set(): 111 | try: 112 | command = self.get_command() 113 | if command is not None: 114 | response = self.dispatch(command.strip()) 115 | self.send_response(response) 116 | except KeyboardInterrupt: 117 | log.warn("Closing gracefully...") 118 | self.stop_event.set() 119 | break 120 | except: 121 | log.exception("Problem in main loop") 122 | self.stop_event.set() 123 | raise 124 | 125 | def main(args): 126 | output = args.output 127 | if not hasattr(outputs, args.output): 128 | raise RuntimeError("Invalid output: %s" % args.output) 129 | else: 130 | output = getattr(outputs, args.output) 131 | robot = Robot(output=output) 132 | robot.start() 133 | 134 | if __name__ == '__main__': 135 | import argparse 136 | parser = argparse.ArgumentParser() 137 | parser.add_argument("--output", default="text") 138 | args = parser.parse_args() 139 | sys.exit(main(args)) 140 | -------------------------------------------------------------------------------- /misc/pyconuk2017/robotics/robot/shell.py: -------------------------------------------------------------------------------- 1 | #!python3 2 | import os, sys 3 | import shlex 4 | 5 | import zmq 6 | 7 | from . import config 8 | 9 | def main(): 10 | context = zmq.Context() 11 | socket = context.socket(zmq.REQ) 12 | socket.connect("tcp://%s:%s" % (config.LISTEN_ON_IP, config.LISTEN_ON_PORT)) 13 | 14 | while True: 15 | command = input("Command: ") 16 | socket.send(command.encode(config.CODEC)) 17 | response = socket.recv().decode(config.CODEC) 18 | print(" ... %s" % response) 19 | words = shlex.split(response.lower()) 20 | status = words[0] 21 | if len(words) > 1: 22 | info = words[1:] 23 | if status == "finished": 24 | print("Finished status received from robot") 25 | break 26 | 27 | if __name__ == '__main__': 28 | main(*sys.argv[1:]) 29 | -------------------------------------------------------------------------------- /misc/pyconuk2017/sad.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjguk/networkzero/d22faf619d2fbd349d9b091fc832f8be4896eec6/misc/pyconuk2017/sad.jpg -------------------------------------------------------------------------------- /misc/pyconuk2017/watch-news.py: -------------------------------------------------------------------------------- 1 | import networkzero as nw0 2 | 3 | news = nw0.discover("blink_news") 4 | while True: 5 | topic, message = nw0.wait_for_news_from(news, "BLINK") 6 | print(message) 7 | -------------------------------------------------------------------------------- /misc/pyconuk2017/yellow_happy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjguk/networkzero/d22faf619d2fbd349d9b091fc832f8be4896eec6/misc/pyconuk2017/yellow_happy.jpg -------------------------------------------------------------------------------- /networkzero/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Easy network discovery & messaging 3 | 4 | Aimed at a classrom or club situation, networkzero makes it simpler to 5 | have several machines or several processes on one machine discovering 6 | each other and talking across a network. Typical examples would include: 7 | 8 | * Sending commands to a robot 9 | * Sending scores to a scoreboard 10 | * Having a remote sensor ping a central controller 11 | * A peer-to-peer chat / instant messenger 12 | 13 | To send a message and wait for a reply:: 14 | 15 | [Computer 1] 16 | import networkzero as nw0 17 | 18 | echo_address = nw0.advertise("echo") 19 | while True: 20 | name = nw0.wait_for_message_from(echo_address) 21 | nw0.send_reply_to(echo_address, "Hello " + name) 22 | 23 | :: 24 | 25 | [Computer 2] 26 | import networkzero as nw0 27 | 28 | echo_address = nw0.discover("echo") 29 | 30 | reply = nw0.send_message_to(echo_address, "Alice") 31 | print(reply) 32 | reply = nw0.send_message_to(echo_address, "Bob") 33 | print(reply) 34 | 35 | To send news:: 36 | 37 | [Computer 1] 38 | import networkzero as nw0 39 | 40 | address = nw0.advertise("data-logger") 41 | while True: 42 | # 43 | # ... do stuff 44 | # 45 | nw0.send_news_to(address, "data", ...) 46 | 47 | :: 48 | 49 | [Computer 2, 3, 4...] 50 | import networkzero as nw0 51 | 52 | logger = nw0.discover("data-logger") 53 | while True: 54 | topic, data = nw0.wait_for_news_from(logger, "data") 55 | # 56 | # ... write the data to a database etc. 57 | # 58 | 59 | """ 60 | from .core import ( 61 | NetworkZeroError, SocketAlreadyExistsError, 62 | SocketTimedOutError, InvalidAddressError, 63 | SocketInterruptedError, DifferentThreadError, 64 | 65 | address, action_and_params, 66 | string_to_bytes, bytes_to_string 67 | ) 68 | from .discovery import advertise, discover, discover_all, discover_group 69 | from .messenger import ( 70 | send_message_to, wait_for_message_from, send_reply_to, 71 | send_news_to, wait_for_news_from 72 | ) 73 | -------------------------------------------------------------------------------- /networkzero/config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Common configuration elements for networkzero 3 | """ 4 | ENCODING = "UTF-8" 5 | class _Forever(object): 6 | def __repr__(self): return "" 7 | FOREVER = _Forever() 8 | SHORT_WAIT = 1 # 1 second 9 | EVERYTHING = "" 10 | COMMAND_ACK = "ack" 11 | # 12 | # Beacons will broadcast adverts at this frequency 13 | # 14 | BEACON_ADVERT_FREQUENCY_S = 2 15 | # 16 | # Adverts will expire after this many seconds unless 17 | # a fresh broadcast is received. Default it above the 18 | # broadcast frequency so adverts are not forever expiring 19 | # and being recreated by the next received broadcast. 20 | # 21 | # NB since adverts are broadcast round-robin (ie only one advert 22 | # is broadcast every BEACON_ADVERT_FREQUENCY_S seconds) we need 23 | # to allow for the possibility that any given name might only 24 | # be advertised, say, once every 5 times. 25 | # 26 | ADVERT_TTL_S = 10 * BEACON_ADVERT_FREQUENCY_S 27 | 28 | VALID_PORTS = range(0x10000) 29 | DYNAMIC_PORTS = range(0xC000, 0x10000) 30 | -------------------------------------------------------------------------------- /networkzero/messenger.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import uuid 3 | 4 | from . import config 5 | from . import core 6 | from . import sockets 7 | 8 | _logger = core.get_logger(__name__) 9 | EMPTY = None 10 | 11 | def send_message_to(address, message=EMPTY, wait_for_reply_s=config.FOREVER): 12 | """Send a message and return the reply 13 | 14 | :param address: a nw0 address (eg from `nw0.discover`) 15 | :param message: any simple Python object, including text & tuples 16 | :param wait_for_reply_s: how many seconds to wait for a reply [default: forever] 17 | 18 | :returns: the reply returned from the address or None if out of time 19 | """ 20 | _logger.info("Sending message %s to %s", message, address) 21 | if isinstance(address, list): 22 | raise core.InvalidAddressError("Multiple addresses are not allowed") 23 | return sockets._sockets.send_message_to(address, message, wait_for_reply_s) 24 | 25 | def wait_for_message_from(address, wait_for_s=config.FOREVER, autoreply=False): 26 | """Wait for a message 27 | 28 | :param address: a nw0 address (eg from `nw0.advertise`) 29 | :param wait_for_s: how many seconds to wait for a message before giving up [default: forever] 30 | :param autoreply: whether to send an empty reply [default: No] 31 | 32 | :returns: the message received from another address or None if out of time 33 | """ 34 | _logger.info("Waiting for message on %s for %s secs", address, wait_for_s) 35 | message = sockets._sockets.wait_for_message_from(address, wait_for_s) 36 | if message is not None and autoreply: 37 | sockets._sockets.send_reply_to(address, EMPTY) 38 | return message 39 | 40 | def send_reply_to(address, reply=EMPTY): 41 | """Reply to a message previously received 42 | 43 | :param address: a nw0 address (eg from `nw0.advertise`) 44 | :param reply: any simple Python object, including text & tuples 45 | """ 46 | _logger.debug("Sending reply %s to %s", reply, address) 47 | return sockets._sockets.send_reply_to(address, reply) 48 | 49 | def send_news_to(address, topic, data=None): 50 | """Publish news to all subscribers 51 | 52 | :param address: a nw0 address, eg from `nw0.advertise` 53 | :param topic: any text object 54 | :param data: any simple Python object including test & tuples [default: empty] 55 | """ 56 | _logger.info("Publish topic %s with data %s to %s", topic, data, address) 57 | return sockets._sockets.send_news_to(address, topic, data) 58 | 59 | def wait_for_news_from(address, prefix=config.EVERYTHING, wait_for_s=config.FOREVER, is_raw=False): 60 | """Wait for news whose topic starts with `prefix`. 61 | 62 | :param address: a nw0 address, eg from `nw0.discover` 63 | :param prefix: any text object [default: all messages] 64 | :param wait_for_s: how many seconds to wait before giving up [default: forever] 65 | 66 | :returns: a 2-tuple of (topic, data) or (None, None) if out of time 67 | """ 68 | _logger.info("Listen on %s for news matching %s waiting for %s secs", address, prefix, wait_for_s) 69 | return sockets._sockets.wait_for_news_from(address, prefix, wait_for_s, is_raw) 70 | 71 | -------------------------------------------------------------------------------- /networkzero/sockets.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import json 3 | import threading 4 | import time 5 | try: 6 | string = unicode 7 | except NameError: 8 | string = str 9 | 10 | import zmq 11 | 12 | from . import config 13 | from . import core 14 | 15 | _logger = core.get_logger(__name__) 16 | 17 | def _serialise(message): 18 | return json.dumps(message).encode(config.ENCODING) 19 | 20 | def _unserialise(message_bytes): 21 | return json.loads(message_bytes.decode(config.ENCODING)) 22 | 23 | def _serialise_for_pubsub(topic, data): 24 | topic_bytes = topic.encode(config.ENCODING) 25 | if isinstance(data, bytes): 26 | data_bytes = data 27 | else: 28 | data_bytes = _serialise(data) 29 | return [topic_bytes, data_bytes] 30 | 31 | def _unserialise_for_pubsub(message_bytes, is_raw=False): 32 | topic_bytes, data_bytes = message_bytes 33 | topic = topic_bytes.decode(config.ENCODING) 34 | if is_raw: 35 | data = data_bytes 36 | else: 37 | data = _unserialise(data_bytes) 38 | return topic, data 39 | 40 | class Socket(zmq.Socket): 41 | 42 | binding_roles = {"listener", "publisher"} 43 | 44 | def __init__(self, *args, **kwargs): 45 | zmq.Socket.__init__(self, *args, **kwargs) 46 | # 47 | # Keep track of which thread this socket was created in 48 | # 49 | self.__dict__['_thread'] = threading.current_thread() 50 | 51 | def __repr__(self): 52 | return "<%s socket %x on %s>" % (self.role, id(self), getattr(self, "address", "")) 53 | 54 | def _get_address(self): 55 | return self._address 56 | def _set_address(self, address): 57 | _logger.debug("About to set address: %s", address) 58 | if self.role in self.binding_roles: 59 | if isinstance(address, (list, tuple)): 60 | raise core.NetworkZeroError("A listening socket can be bound to only one address, not: %r" % address) 61 | else: 62 | self.bind("tcp://%s" % address) 63 | else: 64 | if isinstance(address, (list, tuple)): 65 | addresses = address 66 | else: 67 | addresses = [address] 68 | for a in addresses: 69 | _logger.debug("About to connect to %s", a) 70 | self.connect("tcp://%s" % a) 71 | 72 | self.__dict__['_address'] = address 73 | # 74 | # ZeroMQ has a well-documented feature whereby the 75 | # newly-added subscriber will always miss the first 76 | # few posts by a publisher. Just to avoid the surprise, 77 | # we hackily avoid this here by having each socket 78 | # wait a short while once it's bound/connected. 79 | # 80 | if self.role in ("publisher", "subscriber"): 81 | time.sleep(0.5) 82 | address = property(_get_address, _set_address) 83 | 84 | def _get_role(self): 85 | return self._role 86 | def _set_role(self, role): 87 | self.__dict__['_role'] = role 88 | role = property(_get_role, _set_role) 89 | 90 | class Context(zmq.Context): 91 | 92 | _socket_class = Socket 93 | 94 | context = Context() 95 | 96 | # 97 | # Global mapping from address to socket. When a socket 98 | # is needed, its address (ip:port) is looked up here. If 99 | # a mapping exists, that socket is returned. If not, a new 100 | # one is created of the right type (REQ / SUB etc.) and 101 | # returned 102 | # 103 | class Sockets: 104 | 105 | try_length_ms = 500 # wait for .5 second at a time 106 | roles = { 107 | "listener" : zmq.REP, 108 | "speaker" : zmq.REQ, 109 | "publisher" : zmq.PUB, 110 | "subscriber" : zmq.SUB 111 | } 112 | 113 | def __init__(self): 114 | self._tls = threading.local() 115 | self._lock = threading.Lock() 116 | with self._lock: 117 | self._sockets = set() 118 | 119 | def get_socket(self, address, role): 120 | """Create or retrieve a socket of the right type, already connected 121 | to the address. Address (ip:port) must be fully specified at this 122 | point. core.address can be used to generate an address. 123 | """ 124 | # 125 | # If this thread doesn't yet have a sockets cache 126 | # in its local storage, create one here. 127 | # 128 | try: 129 | self._tls.sockets 130 | except AttributeError: 131 | self._tls.sockets = {} 132 | 133 | # Convert the address to a single canonical string. 134 | # 135 | # If a list of addresses is passed, turn it into a tuple 136 | # of canonical addresses for use as a dictionary key. 137 | # Otherwise convert it to a single canonical string. 138 | # 139 | if isinstance(address, list): 140 | caddress = tuple(core.address(a) for a in address) 141 | else: 142 | caddress = core.address(address) 143 | 144 | # 145 | # Each socket is identified for this thread by its address(es) 146 | # and the role the socket is playing (listener, publisher, etc.) 147 | # That is, within one thread, we are cacheing a read or a write 148 | # socket to the same address(es). 149 | # 150 | # The slight corner case from this is that if you attempt to 151 | # send to [addressA, addressB] and then to addressA and then 152 | # to [addressB, addressA], three separate sockets will be 153 | # created and used. 154 | # 155 | identifier = caddress 156 | 157 | if identifier not in self._tls.sockets: 158 | _logger.debug("%s does not exist in local sockets", identifier) 159 | # 160 | # If this is a listening / subscribing socket, it can only 161 | # be bound once, regardless of thread. Therefore keep a 162 | # threads-global list of addresses used and make sure this 163 | # one hasn't been used elsewhere. 164 | # 165 | if role in Socket.binding_roles: 166 | with self._lock: 167 | if identifier in self._sockets: 168 | raise core.SocketAlreadyExistsError("You cannot create a listening socket in more than one thread") 169 | else: 170 | self._sockets.add(identifier) 171 | 172 | type = self.roles[role] 173 | socket = context.socket(type) 174 | socket.role = role 175 | socket.address = caddress 176 | # 177 | # Do this last so that an exception earlier will result 178 | # in the socket not being cached 179 | # 180 | self._tls.sockets[identifier] = socket 181 | else: 182 | _logger.debug("%s already not exist in local sockets", identifier) 183 | # 184 | # Only return sockets created in this thread 185 | # 186 | socket = self._tls.sockets[identifier] 187 | 188 | return socket 189 | 190 | def intervals_ms(self, timeout_ms): 191 | """Generate a series of interval lengths, in ms, which 192 | will add up to the number of ms in timeout_ms. If timeout_ms 193 | is None, keep returning intervals forever. 194 | """ 195 | if timeout_ms is config.FOREVER: 196 | while True: 197 | yield self.try_length_ms 198 | else: 199 | whole_intervals, part_interval = divmod(timeout_ms, self.try_length_ms) 200 | for _ in range(whole_intervals): 201 | yield self.try_length_ms 202 | yield part_interval 203 | 204 | def _receive_with_timeout(self, socket, timeout_s, use_multipart=False): 205 | """Check for socket activity and either return what's 206 | received on the socket or time out if timeout_s expires 207 | without anything on the socket. 208 | 209 | This is implemented in loops of self.try_length_ms milliseconds 210 | to allow Ctrl-C handling to take place. 211 | """ 212 | if timeout_s is config.FOREVER: 213 | timeout_ms = config.FOREVER 214 | else: 215 | timeout_ms = int(1000 * timeout_s) 216 | 217 | poller = zmq.Poller() 218 | poller.register(socket, zmq.POLLIN) 219 | ms_so_far = 0 220 | try: 221 | for interval_ms in self.intervals_ms(timeout_ms): 222 | sockets = dict(poller.poll(interval_ms)) 223 | ms_so_far += interval_ms 224 | if socket in sockets: 225 | if use_multipart: 226 | return socket.recv_multipart() 227 | else: 228 | return socket.recv() 229 | else: 230 | raise core.SocketTimedOutError(timeout_s) 231 | except KeyboardInterrupt: 232 | raise core.SocketInterruptedError(ms_so_far / 1000.0) 233 | 234 | def wait_for_message_from(self, address, wait_for_s): 235 | socket = self.get_socket(address, "listener") 236 | try: 237 | message = self._receive_with_timeout(socket, wait_for_s) 238 | except (core.SocketTimedOutError): 239 | return None 240 | else: 241 | return _unserialise(message) 242 | 243 | def send_message_to(self, address, message, wait_for_reply_s): 244 | socket = self.get_socket(address, "speaker") 245 | serialised_message = _serialise(message) 246 | socket.send(serialised_message) 247 | return _unserialise(self._receive_with_timeout(socket, wait_for_reply_s)) 248 | 249 | def send_reply_to(self, address, reply): 250 | socket = self.get_socket(address, "listener") 251 | reply = _serialise(reply) 252 | return socket.send(reply) 253 | 254 | def send_news_to(self, address, topic, data): 255 | socket = self.get_socket(address, "publisher") 256 | return socket.send_multipart(_serialise_for_pubsub(topic, data)) 257 | 258 | def wait_for_news_from(self, address, topic, wait_for_s, is_raw=False): 259 | if isinstance(address, list): 260 | addresses = address 261 | else: 262 | addresses = [address] 263 | socket = self.get_socket(addresses, "subscriber") 264 | if isinstance(topic, str): 265 | topics = [topic] 266 | else: 267 | topics = topic 268 | for t in topics: 269 | socket.set(zmq.SUBSCRIBE, t.encode(config.ENCODING)) 270 | try: 271 | result = self._receive_with_timeout(socket, wait_for_s, use_multipart=True) 272 | unserialised_result = _unserialise_for_pubsub(result, is_raw) 273 | return unserialised_result 274 | except (core.SocketTimedOutError, core.SocketInterruptedError): 275 | return None, None 276 | 277 | _sockets = Sockets() 278 | 279 | def get_socket(address, role): 280 | return _sockets.get_socket(address, role) 281 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyzmq 2 | netifaces>=0.10.6 3 | sphinx 4 | tox 5 | pytest 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # 2 | # Initially copied from: 3 | # https://raw.githubusercontent.com/pypa/sampleproject/master/setup.py 4 | # 5 | 6 | from setuptools import setup, find_packages 7 | import os 8 | import codecs 9 | 10 | here = os.path.abspath(os.path.dirname(__file__)) 11 | 12 | with codecs.open(os.path.join(here, 'README.rst'), encoding='utf-8') as f: 13 | long_description = f.read() 14 | 15 | setup( 16 | name='networkzero', 17 | 18 | version='1.0b1', 19 | 20 | description='Making networking simple for teachers', 21 | long_description=long_description, 22 | 23 | url='https://github.com/tjguk/networkzero', 24 | 25 | author='Tim Golden', 26 | author_email='mail@timgolden.me.uk', 27 | 28 | license='MIT', 29 | 30 | classifiers=[ 31 | 'Development Status :: 4 - Beta', 32 | 33 | 'Intended Audience :: Developers', 34 | 'Topic :: Software Development :: Build Tools', 35 | 36 | 'License :: OSI Approved :: MIT License', 37 | 38 | 'Programming Language :: Python :: 2', 39 | 'Programming Language :: Python :: 2.7', 40 | 'Programming Language :: Python :: 3', 41 | 'Programming Language :: Python :: 3.3', 42 | 'Programming Language :: Python :: 3.4', 43 | 'Programming Language :: Python :: 3.5', 44 | ], 45 | 46 | keywords='networking education', 47 | 48 | packages=find_packages(exclude=['contrib', 'docs', 'tests']), 49 | 50 | install_requires=[ 51 | 'pyzmq', 52 | 'netifaces', 53 | ], 54 | ) 55 | -------------------------------------------------------------------------------- /shell.cmd: -------------------------------------------------------------------------------- 1 | pushd %VENVS% && CALL nw0\scripts\activate.bat && cls && popd 2 | python -i shell.py 3 | -------------------------------------------------------------------------------- /shell.py: -------------------------------------------------------------------------------- 1 | #!python3 2 | import networkzero as nw0 3 | 4 | import logging 5 | logger = logging.getLogger("networkzero") 6 | handler = logging.FileHandler("network.log", encoding="utf-8") 7 | handler.setFormatter(logging.Formatter("%(asctime)s %(name)s %(levelname)s %(message)s")) 8 | handler.setLevel(logging.DEBUG) 9 | logger.addHandler(handler) 10 | handler = logging.StreamHandler() 11 | handler.setFormatter(logging.Formatter("%(levelname)s %(message)s")) 12 | handler.setLevel(logging.INFO) 13 | logger.addHandler(handler) 14 | del handler, logging 15 | -------------------------------------------------------------------------------- /tests/test_core.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import re 3 | import uuid 4 | 5 | try: 6 | unicode 7 | except NameError: 8 | unicode = str 9 | 10 | import pytest 11 | 12 | import networkzero as nw0 13 | nw0.core._enable_debug_logging() 14 | 15 | is_valid_port = nw0.core.is_valid_port 16 | is_valid_address = nw0.core.is_valid_address 17 | 18 | class TestAddress(object): 19 | 20 | def test_none_supplied(self): 21 | canonical_address = nw0.core.address() 22 | assert is_valid_address(canonical_address, port_range=nw0.config.DYNAMIC_PORTS) 23 | 24 | def test_valid_ip_supplied(self): 25 | address = "127.0.0.1" 26 | canonical_address = nw0.core.address(address) 27 | assert is_valid_address(canonical_address) 28 | 29 | def test_valid_port_string_is_supplied(self): 30 | address = "1234" 31 | canonical_address = nw0.core.address(address) 32 | assert is_valid_address(canonical_address) 33 | 34 | def test_valid_port_int_is_supplied(self): 35 | address = 1234 36 | canonical_address = nw0.core.address(address) 37 | assert is_valid_address(canonical_address) 38 | 39 | def test_valid_port_zero_is_supplied(self): 40 | address = 0 41 | canonical_address = nw0.core.address(address) 42 | assert is_valid_address(canonical_address, port_range=nw0.config.DYNAMIC_PORTS) 43 | 44 | def test_valid_both_are_supplied(self): 45 | address = "127.0.0.1:1234" 46 | canonical_address = nw0.core.address(address) 47 | assert is_valid_address(canonical_address) 48 | 49 | def test_invalid_ip_supplied(self): 50 | address = "!!!" 51 | with pytest.raises(nw0.core.InvalidAddressError): 52 | nw0.core.address(address) 53 | 54 | def test_invalid_port_supplied(self): 55 | address = 123456 56 | with pytest.raises(nw0.core.AddressError): 57 | nw0.core.address(address) 58 | 59 | def test_invalid_both_supplied(self): 60 | address = "!!!:INVALID" 61 | with pytest.raises(nw0.core.AddressError): 62 | nw0.core.address(address) 63 | 64 | def test_valid_ip_pattern_supplied(self): 65 | address = "127.*" 66 | canonical_address = nw0.core.address(address) 67 | assert is_valid_address(canonical_address) 68 | 69 | def test_invalid_ip_pattern_supplied(self): 70 | address = "255.*" 71 | with pytest.raises(nw0.core.AddressError): 72 | nw0.core.address(address) 73 | 74 | class TestCommand(object): 75 | 76 | def test_action_only(self): 77 | commandline = "ACTION" 78 | assert nw0.core.action_and_params(commandline) == ("ACTION", []) 79 | 80 | def test_action_and_one_param(self): 81 | commandline = "ACTION PARAM1" 82 | assert nw0.core.action_and_params(commandline) == ("ACTION", ["PARAM1"]) 83 | 84 | def test_action_and_several_params(self): 85 | commandline = "ACTION PARAM1 PARAM2 PARAM3" 86 | assert nw0.core.action_and_params(commandline) == ("ACTION", ["PARAM1", "PARAM2", "PARAM3"]) 87 | 88 | def test_param_with_space(self): 89 | commandline = "ACTION 'PARAM 1'" 90 | assert nw0.core.action_and_params(commandline) == ("ACTION", ["PARAM 1"]) 91 | 92 | class TestBytesToString(object): 93 | 94 | bytes = uuid.uuid1().hex.encode("ascii") 95 | string = base64.b64encode(bytes).decode("ascii") 96 | 97 | def test_string_to_bytes(self): 98 | b = nw0.core.string_to_bytes(self.string) 99 | assert isinstance(b, bytes) 100 | assert b == self.bytes 101 | 102 | def test_bytes_to_string(self): 103 | s = nw0.core.bytes_to_string(self.bytes) 104 | assert isinstance(s, unicode) 105 | assert s == self.string 106 | 107 | def test_reflection_from_bytes(self): 108 | assert nw0.core.string_to_bytes(nw0.core.bytes_to_string(self.bytes)) == self.bytes 109 | 110 | def test_reflection_from_string(self): 111 | assert nw0.core.bytes_to_string(nw0.core.string_to_bytes(self.string)) == self.string -------------------------------------------------------------------------------- /tests/test_discovery.py: -------------------------------------------------------------------------------- 1 | try: 2 | import queue 3 | except ImportError: 4 | import Queue as queue 5 | import random 6 | import re 7 | import socket 8 | import threading 9 | import time 10 | import uuid 11 | 12 | import pytest 13 | 14 | import networkzero as nw0 15 | _logger = nw0.core.get_logger("networkzero.tests") 16 | nw0.core._enable_debug_logging() 17 | 18 | is_valid_port = nw0.core.is_valid_port 19 | is_valid_address = nw0.core.is_valid_address 20 | 21 | class SupportThread(threading.Thread): 22 | """Fake the other end of the message/command/news chain 23 | 24 | NB we use as little as possible of the nw0 machinery here, 25 | mostly to avoid the possibility of complicated cross-thread 26 | interference but also to test our own code. 27 | """ 28 | 29 | def __init__(self, context): 30 | threading.Thread.__init__(self) 31 | self.context = context 32 | self.queue = queue.Queue() 33 | self.setDaemon(True) 34 | 35 | def run(self): 36 | try: 37 | while True: 38 | test_name, args = self.queue.get() 39 | if test_name is None: 40 | break 41 | function = getattr(self, "support_test_" + test_name) 42 | function(*args) 43 | except: 44 | _logger.exception("Problem in thread") 45 | 46 | def support_test_discover_before_advertise(self, service): 47 | time.sleep(1) 48 | nw0.advertise(service) 49 | 50 | @pytest.fixture 51 | def support(request): 52 | thread = SupportThread(nw0.sockets.context) 53 | def finalise(): 54 | thread.queue.put((None, None)) 55 | thread.join() 56 | thread.start() 57 | return thread 58 | 59 | @pytest.fixture 60 | def beacon(request): 61 | port = random.choice(nw0.config.DYNAMIC_PORTS) 62 | nw0.discovery._start_beacon(port=port) 63 | request.addfinalizer(nw0.discovery.reset_beacon) 64 | 65 | def test_beacon_already_running(): 66 | # 67 | # NB this one has to run without the beacon fixture 68 | # 69 | # Bind a socket on a random port before attempting 70 | # to start a beacon on that same port. 71 | # 72 | port = random.choice(nw0.config.DYNAMIC_PORTS) 73 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) 74 | s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) 75 | s.bind(("", port)) 76 | try: 77 | assert nw0.discovery._beacon is None 78 | nw0.discovery._start_beacon(port=port) 79 | assert nw0.discovery._beacon is nw0.discovery._remote_beacon 80 | finally: 81 | s.close() 82 | # 83 | # Make sure any future beacon use assumes it's not 84 | # already running. 85 | # 86 | nw0.discovery._stop_beacon() 87 | 88 | def test_advertise_no_address(beacon): 89 | service = uuid.uuid4().hex 90 | address = nw0.advertise(service) 91 | assert is_valid_address(address) 92 | assert [service, address] in nw0.discover_all() 93 | 94 | def test_advertise_no_port(beacon): 95 | service = uuid.uuid4().hex 96 | address = nw0.advertise(service) 97 | assert is_valid_address(address, port_range=nw0.config.DYNAMIC_PORTS) 98 | assert [service, address] in nw0.discover_all() 99 | 100 | def test_advertise_full_address(beacon): 101 | service = uuid.uuid4().hex 102 | service_address = "192.168.1.1:1234" 103 | address = nw0.advertise(service, service_address) 104 | assert address == service_address 105 | assert [service, address] in nw0.discover_all() 106 | 107 | def test_advertise_ttl(beacon): 108 | service = uuid.uuid4().hex 109 | ttl_s = 5 110 | address = nw0.advertise(service, ttl_s=ttl_s) 111 | assert [service, address] in nw0.discover_all() 112 | # 113 | # Stop advert broadcast for long enough that any 114 | # stale ones will be expired 115 | # 116 | nw0.discovery._pause() 117 | try: 118 | time.sleep(1 + ttl_s) 119 | assert [service, address] not in nw0.discover_all() 120 | finally: 121 | nw0.discovery._resume() 122 | 123 | def test_unadvertise(beacon): 124 | ttl_s = 2 125 | service = uuid.uuid4().hex 126 | address = nw0.advertise(service, ttl_s=ttl_s) 127 | assert [service, address] in nw0.discover_all() 128 | nw0.discovery._unadvertise(service) 129 | time.sleep(1 + ttl_s) 130 | assert [service, address] not in nw0.discover_all() 131 | 132 | def test_discover(beacon): 133 | service = uuid.uuid4().hex 134 | address = nw0.advertise(service) 135 | assert address == nw0.discover(service) 136 | 137 | def test_discover_not_exists_with_timeout(beacon): 138 | service = uuid.uuid4().hex 139 | address = nw0.advertise(service) 140 | assert None is nw0.discover(uuid.uuid4().hex, wait_for_s=2) 141 | 142 | def test_discover_exists_with_timeout(beacon): 143 | service = uuid.uuid4().hex 144 | address = nw0.advertise(service) 145 | assert address == nw0.discover(service, wait_for_s=2) 146 | 147 | def test_discover_all(beacon): 148 | service1 = uuid.uuid4().hex 149 | address1 = nw0.advertise(service1) 150 | service2 = uuid.uuid4().hex 151 | address2 = nw0.advertise(service2) 152 | services = dict(nw0.discover_all()) 153 | assert services == {service1:address1, service2:address2} 154 | 155 | def test_discover_before_advertise(beacon, support): 156 | service1 = uuid.uuid4().hex 157 | support.queue.put(("discover_before_advertise", [service1])) 158 | address1 = nw0.discover(service1, wait_for_s=5) 159 | assert address1 is not None 160 | 161 | def test_discover_group(beacon): 162 | group = uuid.uuid4().hex 163 | service1 = "%s/%s" % (group, uuid.uuid4().hex) 164 | service2 = "%s/%s" % (group, uuid.uuid4().hex) 165 | service3 = "%s/%s" % (uuid.uuid4().hex, uuid.uuid4().hex) 166 | address1 = nw0.advertise(service1) 167 | address2 = nw0.advertise(service2) 168 | address3 = nw0.advertise(service3) 169 | discovered_group = nw0.discover_group(group) 170 | assert set(discovered_group) == set([(service1, address1), (service2, address2)]) 171 | 172 | def test_discover_group_with_different_separator(beacon): 173 | group = uuid.uuid4().hex 174 | service1 = "%s:%s" % (group, uuid.uuid4().hex) 175 | service2 = "%s:%s" % (group, uuid.uuid4().hex) 176 | service3 = "%s:%s" % (uuid.uuid4().hex, uuid.uuid4().hex) 177 | address1 = nw0.advertise(service1) 178 | address2 = nw0.advertise(service2) 179 | address3 = nw0.advertise(service3) 180 | discovered_group = nw0.discover_group(group, separator=":") 181 | assert set(discovered_group) == set([(service1, address1), (service2, address2)]) 182 | 183 | def test_rpc_with_timeout(): 184 | """Access to the local beacon is via an RPC mechanism using 185 | ZeroMQ REQ-REP sockets. If the beacon has stopped, this will block. 186 | This is especially problematic when an attempt is being made to 187 | unadvertise services via the atexit.register mechanism. 188 | """ 189 | # 190 | # Make sure the beacon is not running and attempt an RPC 191 | # connection with a timeout 192 | # 193 | nw0.discovery._stop_beacon() 194 | with pytest.raises(nw0.core.SocketTimedOutError): 195 | nw0.discovery._rpc(uuid.uuid4().hex, wait_for_s=2) 196 | 197 | def _test_unadvertise_with_timeout(): 198 | """Adverts are unadvertised when a process exits (via atexit.register). 199 | However, if the local beacon has already stopped, any attempt to send 200 | it a message will block. 201 | """ 202 | # 203 | # Start a beacon on a random port and register an arbitrary 204 | # service. We need to know neither the port nor the service; 205 | # all we do is check unadvertising will succeed. 206 | # 207 | nw0.discovery._start_beacon(port=random.choice(nw0.config.DYNAMIC_PORTS)) 208 | nw0.advertise(uuid.uuid4().hex) 209 | nw0.discovery._stop_beacon() 210 | -------------------------------------------------------------------------------- /tests/test_messenger.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import contextlib 3 | import io 4 | import logging 5 | try: 6 | import queue 7 | except ImportError: 8 | import Queue as queue 9 | import re 10 | import threading 11 | import time 12 | import uuid 13 | 14 | import pytest 15 | import zmq 16 | 17 | import networkzero as nw0 18 | _logger = nw0.core.get_logger("networkzero.tests") 19 | nw0.core._enable_debug_logging() 20 | 21 | roles = nw0.sockets.Sockets.roles 22 | 23 | class SupportThread(threading.Thread): 24 | """Fake the other end of the message/command/news chain 25 | 26 | NB we use as little as possible of the nw0 machinery here, 27 | mostly to avoid the possibility of complicated cross-thread 28 | interference but also to test our own code. 29 | """ 30 | 31 | def __init__(self, context): 32 | threading.Thread.__init__(self) 33 | _logger.debug("Init thread") 34 | self.context = context 35 | self.queue = queue.Queue() 36 | self.setDaemon(True) 37 | 38 | def run(self): 39 | try: 40 | while True: 41 | test_name, args = self.queue.get() 42 | if test_name is None: 43 | break 44 | _logger.info("test_support: %s, %s", test_name, args) 45 | function = getattr(self, "support_test_" + test_name) 46 | function(*args) 47 | except: 48 | _logger.exception("Problem in thread") 49 | 50 | def support_test_send_message_to(self, address): 51 | with self.context.socket(roles['listener']) as socket: 52 | socket.bind("tcp://%s" % address) 53 | message = nw0.sockets._unserialise(socket.recv()) 54 | socket.send(nw0.sockets._serialise(message)) 55 | 56 | def support_test_wait_for_message_from(self, address, message): 57 | with self.context.socket(roles['speaker']) as socket: 58 | socket.connect("tcp://%s" % address) 59 | _logger.debug("About to send %s", message) 60 | socket.send(nw0.sockets._serialise(message)) 61 | socket.recv() 62 | 63 | def support_test_wait_for_message_from_with_autoreply(self, address, q): 64 | with self.context.socket(roles['speaker']) as socket: 65 | socket.connect("tcp://%s" % address) 66 | message = uuid.uuid4().hex 67 | socket.send(nw0.sockets._serialise(message)) 68 | reply = nw0.sockets._unserialise(socket.recv()) 69 | q.put(reply) 70 | 71 | def support_test_send_reply_to(self, address, queue): 72 | message = uuid.uuid4().hex 73 | with self.context.socket(roles['speaker']) as socket: 74 | socket.connect("tcp://%s" % address) 75 | socket.send(nw0.sockets._serialise(message)) 76 | reply = nw0.sockets._unserialise(socket.recv()) 77 | queue.put(reply) 78 | 79 | def support_test_send_news_to(self, address, topic, queue): 80 | with self.context.socket(roles['subscriber']) as socket: 81 | socket.connect("tcp://%s" % address) 82 | socket.subscribe = topic.encode("utf-8") 83 | while True: 84 | topic, data = nw0.sockets._unserialise_for_pubsub(socket.recv_multipart()) 85 | queue.put((topic, data)) 86 | if data is not None: 87 | break 88 | 89 | def support_test_wait_for_news_from(self, address, topic, data, sync_queue): 90 | with self.context.socket(roles['publisher']) as socket: 91 | socket.bind("tcp://%s" % address) 92 | while True: 93 | socket.send_multipart(nw0.sockets._serialise_for_pubsub(topic, None)) 94 | try: 95 | sync = sync_queue.get_nowait() 96 | except queue.Empty: 97 | time.sleep(0.1) 98 | else: 99 | break 100 | 101 | socket.send_multipart(nw0.sockets._serialise_for_pubsub(topic, data)) 102 | 103 | def support_test_send_to_multiple_addresses(self, address1, address2): 104 | poller = zmq.Poller() 105 | 106 | socket1 = self.context.socket(roles['listener']) 107 | socket2 = self.context.socket(roles['listener']) 108 | try: 109 | socket1.bind("tcp://%s" % address1) 110 | socket2.bind("tcp://%s" % address2) 111 | poller.register(socket1, zmq.POLLIN) 112 | poller.register(socket2, zmq.POLLIN) 113 | polled = dict(poller.poll(2000)) 114 | if socket1 in polled: 115 | socket1.recv() 116 | socket1.send(nw0.sockets._serialise(address1)) 117 | elif socket2 in polled: 118 | socket2.recv() 119 | socket2.send(nw0.sockets._serialise(address2)) 120 | else: 121 | raise RuntimeError("Nothing found") 122 | finally: 123 | socket1.close() 124 | socket2.close() 125 | 126 | def support_test_wait_for_news_from_multiple_addresses(self, address1, address2, topic, data, sync_queue): 127 | socket1 = self.context.socket(roles['publisher']) 128 | socket2 = self.context.socket(roles['publisher']) 129 | try: 130 | socket1.bind("tcp://%s" % address1) 131 | socket2.bind("tcp://%s" % address2) 132 | while True: 133 | socket1.send_multipart(nw0.sockets._serialise_for_pubsub(topic, None)) 134 | try: 135 | sync = sync_queue.get_nowait() 136 | except queue.Empty: 137 | time.sleep(0.1) 138 | else: 139 | break 140 | socket1.send_multipart(nw0.sockets._serialise_for_pubsub(topic, data)) 141 | socket2.send_multipart(nw0.sockets._serialise_for_pubsub(topic, data)) 142 | finally: 143 | socket1.close() 144 | socket2.close() 145 | 146 | @pytest.fixture 147 | def support(request): 148 | thread = SupportThread(nw0.sockets.context) 149 | def finalise(): 150 | thread.queue.put((None, None)) 151 | thread.join() 152 | thread.start() 153 | return thread 154 | 155 | @contextlib.contextmanager 156 | def capture_logging(logger, stream): 157 | handler = logging.StreamHandler(stream) 158 | handler.setFormatter(logging.Formatter("%(levelname)s %(message)s")) 159 | handler.setLevel(logging.WARN) 160 | logger.addHandler(handler) 161 | yield 162 | logger.removeHandler(handler) 163 | 164 | def check_log(logger, pattern): 165 | return bool(re.search(pattern, logger.getvalue())) 166 | 167 | # 168 | # send_message_to 169 | # 170 | def test_send_message(support): 171 | address = nw0.core.address() 172 | message = uuid.uuid4().hex 173 | support.queue.put(("send_message_to", [address])) 174 | reply = nw0.send_message_to(address, message) 175 | assert reply == message 176 | 177 | def test_send_message_with_timeout(support): 178 | address = nw0.core.address() 179 | with pytest.raises(nw0.core.SocketTimedOutError): 180 | nw0.send_message_to(address, wait_for_reply_s=1.0) 181 | 182 | def test_send_message_empty(support): 183 | address = nw0.core.address() 184 | support.queue.put(("send_message_to", [address])) 185 | reply = nw0.send_message_to(address) 186 | assert reply == nw0.messenger.EMPTY 187 | 188 | # 189 | # wait_for_message_from 190 | # 191 | 192 | def test_wait_for_message(support): 193 | address = nw0.core.address() 194 | message_sent = uuid.uuid4().hex 195 | support.queue.put(("wait_for_message_from", [address, message_sent])) 196 | message_received = nw0.wait_for_message_from(address, wait_for_s=5) 197 | assert message_received == message_sent 198 | nw0.send_reply_to(address, message_received) 199 | 200 | def test_wait_for_message_with_timeout(): 201 | address = nw0.core.address() 202 | message = nw0.wait_for_message_from(address, wait_for_s=0.1) 203 | assert message is None 204 | 205 | def test_wait_for_message_with_autoreply(support): 206 | address = nw0.core.address() 207 | reply_queue = queue.Queue() 208 | support.queue.put(("wait_for_message_from_with_autoreply", [address, reply_queue])) 209 | nw0.wait_for_message_from(address, autoreply=True) 210 | assert reply_queue.get() == nw0.messenger.EMPTY 211 | 212 | # 213 | # send_reply_to 214 | # 215 | def test_send_reply(support): 216 | address = nw0.core.address() 217 | reply_queue = queue.Queue() 218 | 219 | support.queue.put(("send_reply_to", [address, reply_queue])) 220 | message_received = nw0.wait_for_message_from(address, wait_for_s=5) 221 | nw0.send_reply_to(address, message_received) 222 | reply = reply_queue.get() 223 | assert reply == message_received 224 | 225 | # 226 | # send_news_to 227 | # 228 | def test_send_news(support): 229 | address = nw0.core.address() 230 | topic = uuid.uuid4().hex 231 | data = uuid.uuid4().hex 232 | reply_queue = queue.Queue() 233 | 234 | support.queue.put(("send_news_to", [address, topic, reply_queue])) 235 | while True: 236 | nw0.send_news_to(address, topic, None) 237 | try: 238 | in_topic, in_data = reply_queue.get_nowait() 239 | except queue.Empty: 240 | time.sleep(0.1) 241 | else: 242 | break 243 | 244 | nw0.send_news_to(address, topic, data) 245 | while in_data is None: 246 | in_topic, in_data = reply_queue.get() 247 | 248 | assert in_topic, in_data == (topic, data) 249 | 250 | # 251 | # wait_for_news_from 252 | # 253 | def test_wait_for_news(support): 254 | address = nw0.core.address() 255 | topic = uuid.uuid4().hex 256 | data = uuid.uuid4().hex 257 | sync_queue = queue.Queue() 258 | 259 | support.queue.put(("wait_for_news_from", [address, topic, data, sync_queue])) 260 | in_topic, in_data = nw0.wait_for_news_from(address, topic, wait_for_s=5) 261 | sync_queue.put(True) 262 | while in_data is None: 263 | in_topic, in_data = nw0.wait_for_news_from(address, topic, wait_for_s=5) 264 | assert (topic, data) == (in_topic, in_data) 265 | 266 | # 267 | # send to multiple addresses 268 | # For now, this is disallowed as the semantics aren't 269 | # intuitive. (It does a round-robin selection which is useful 270 | # for things like load-scheduling but not for broadcasting). 271 | # 272 | def test_send_to_multiple_addresses(support): 273 | address1 = nw0.core.address() 274 | address2 = nw0.core.address() 275 | message = uuid.uuid4().hex 276 | with pytest.raises(nw0.core.InvalidAddressError): 277 | nw0.send_message_to([address1, address2], message) 278 | 279 | # 280 | # Wait for newss from multiple addresses 281 | # 282 | def test_wait_for_news_from_multiple_addresses(support): 283 | address1 = nw0.core.address() 284 | address2 = nw0.core.address() 285 | topic = uuid.uuid4().hex 286 | data = uuid.uuid4().hex 287 | sync_queue = queue.Queue() 288 | 289 | support.queue.put(("wait_for_news_from_multiple_addresses", [address1, address2, topic, data, sync_queue])) 290 | 291 | in_topic, in_data = nw0.wait_for_news_from([address1, address2], topic, wait_for_s=5) 292 | sync_queue.put(True) 293 | while in_data is None: 294 | in_topic, in_data = nw0.wait_for_news_from([address1, address2], topic, wait_for_s=5) 295 | assert (topic, data) == (in_topic, in_data) 296 | in_topic, in_data = nw0.wait_for_news_from([address1, address2], topic, wait_for_s=5) 297 | assert (topic, data) == (in_topic, in_data) 298 | -------------------------------------------------------------------------------- /tests/test_networkzero.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import pytest 4 | 5 | import networkzero as nw0 6 | nw0.core._enable_debug_logging() 7 | 8 | def test_import_all_relevant_names(): 9 | all_names = { 10 | "advertise", "discover", "discover_all", "discover_group", 11 | "send_message_to", "wait_for_message_from", "send_reply_to", 12 | "send_news_to", "wait_for_news_from", 13 | "action_and_params", "address", 14 | "bytes_to_string", "string_to_bytes", 15 | "NetworkZeroError", "SocketAlreadyExistsError", 16 | "SocketTimedOutError", "InvalidAddressError", 17 | "SocketInterruptedError", "DifferentThreadError", 18 | } 19 | # 20 | # Find all the names imported into the nw0 package except 21 | # the submodules which are implictly imported. 22 | # 23 | nw0_names = set( 24 | name 25 | for name in dir(nw0) 26 | if not name.startswith("_") and 27 | type(getattr(nw0, name)) != type(nw0) 28 | ) 29 | assert not all_names - nw0_names, "Mismatch: %s" % (all_names - nw0_names) 30 | assert not nw0_names - all_names, "Mismatch: %s" % (nw0_names - all_names) 31 | 32 | -------------------------------------------------------------------------------- /tests/test_scenarios.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import contextlib 3 | import io 4 | import logging 5 | try: 6 | import queue 7 | except ImportError: 8 | import Queue as queue 9 | import re 10 | import threading 11 | import time 12 | import uuid 13 | 14 | import pytest 15 | import zmq 16 | 17 | import networkzero as nw0 18 | _logger = nw0.core.get_logger("networkzero.tests") 19 | nw0.core._enable_debug_logging() 20 | 21 | -------------------------------------------------------------------------------- /tests/test_threading.py: -------------------------------------------------------------------------------- 1 | try: 2 | import queue 3 | except ImportError: 4 | import Queue as queue 5 | import threading 6 | 7 | import pytest 8 | 9 | import networkzero as nw0 10 | _logger = nw0.core.get_logger("networkzero.tests") 11 | nw0.core._enable_debug_logging() 12 | 13 | def support_test_bound_in_other_thread(address, event1): 14 | # 15 | # Create a socket in a thread and signal to the test 16 | # which will try -- and fail -- to create a counterpart 17 | # listening socket. 18 | # 19 | nw0.sockets.get_socket(address, "listener") 20 | event1.set() 21 | 22 | def test_bound_in_other_thread(): 23 | """If a socket is bound in one thread it cannot be 24 | created and bound in another. 25 | """ 26 | event1 = threading.Event() 27 | address = nw0.address() 28 | t = threading.Thread(target=support_test_bound_in_other_thread, args=(address, event1)) 29 | t.setDaemon(True) 30 | t.start() 31 | # 32 | # Wait until the thread has created its socket otherwise 33 | # we risk a race condition where the main thread creates 34 | # first and the exception occurs in the support thread 35 | # 36 | event1.wait() 37 | with pytest.raises(nw0.SocketAlreadyExistsError): 38 | socket_from_this_thread = nw0.sockets.get_socket(address, "listener") 39 | t.join() 40 | 41 | def support_test_connected_in_other_thread(address, q): 42 | # 43 | # Create a socket in a thread which will be a different 44 | # socket from its counterpart created in the test 45 | # 46 | q.put(nw0.sockets.get_socket(address, "speaker")) 47 | 48 | def test_connected_in_other_thread(): 49 | """If a sending socket is connected in a different thread, the socket returned 50 | should be different socket from the equivalent one created in this thread. 51 | """ 52 | event = threading.Event() 53 | q = queue.Queue() 54 | address = nw0.address() 55 | t = threading.Thread(target=support_test_connected_in_other_thread, args=(address, q)) 56 | t.setDaemon(True) 57 | t.start() 58 | socket_from_other_thread = q.get() 59 | socket_from_this_thread = nw0.sockets.get_socket(address, "speaker") 60 | assert socket_from_other_thread is not socket_from_this_thread 61 | t.join() 62 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27, py34, py35, py36 3 | 4 | [testenv] 5 | commands = py.test {posargs:tests/} 6 | deps = 7 | pytest 8 | -------------------------------------------------------------------------------- /trials/bclient.c: -------------------------------------------------------------------------------- 1 | /* from http://www.tack.ch/multicast/broadcast.shtml */ 2 | /* $Id$ */ 3 | 4 | /* 5 | * build instructions 6 | * 7 | * gcc -o bclient bclient.c 8 | * 9 | * Usage: 10 | * ./bclient 11 | */ 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | #define MAXBUF 65536 21 | 22 | int main(int argc, char*argv[]) 23 | { 24 | int sock, status, buflen, sinlen; 25 | char buffer[MAXBUF]; 26 | struct sockaddr_in sock_in; 27 | int yes = 1; 28 | 29 | sinlen = sizeof(struct sockaddr_in); 30 | memset(&sock_in, 0, sinlen); 31 | buflen = MAXBUF; 32 | 33 | sock = socket (PF_INET, SOCK_DGRAM, IPPROTO_UDP); 34 | 35 | sock_in.sin_addr.s_addr = htonl(INADDR_ANY); 36 | sock_in.sin_port = htons(0); 37 | sock_in.sin_family = PF_INET; 38 | 39 | status = bind(sock, (struct sockaddr *)&sock_in, sinlen); 40 | printf("Bind Status = %d\n", status); 41 | 42 | status = setsockopt(sock, SOL_SOCKET, SO_BROADCAST, &yes, sizeof(int) ); 43 | printf("Setsockopt Status = %d\n", status); 44 | 45 | /* -1 = 255.255.255.255 this is a BROADCAST address, 46 | a local broadcast address could also be used. 47 | you can comput the local broadcat using NIC address and its NETMASK 48 | */ 49 | 50 | sock_in.sin_addr.s_addr=htonl(-1); /* send message to 255.255.255.255 */ 51 | sock_in.sin_port = htons(atoi(argv[1])); /* port number */ 52 | sock_in.sin_family = PF_INET; 53 | 54 | sprintf(buffer, "Ciao"); 55 | buflen = strlen(buffer); 56 | status = sendto(sock, buffer, buflen, 0, (struct sockaddr *)&sock_in, sinlen); 57 | printf("sendto Status = %d\n", status); 58 | 59 | shutdown(sock, 2); 60 | close(sock); 61 | } 62 | -------------------------------------------------------------------------------- /trials/bserver.c: -------------------------------------------------------------------------------- 1 | /* from http://www.tack.ch/multicast/broadcast.shtml */ 2 | /* $Id$ */ 3 | 4 | /* 5 | * build instructions: 6 | * 7 | * gcc -o bserver bserver.c 8 | * 9 | * Usage: 10 | * ./bserver 11 | */ 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | #define MAXBUF 65536 21 | 22 | int main() 23 | { 24 | int sock, status, buflen; 25 | unsigned sinlen; 26 | char buffer[MAXBUF]; 27 | struct sockaddr_in sock_in; 28 | int yes = 1; 29 | 30 | sinlen = sizeof(struct sockaddr_in); 31 | memset(&sock_in, 0, sinlen); 32 | 33 | sock = socket (PF_INET, SOCK_DGRAM, IPPROTO_UDP); 34 | 35 | sock_in.sin_addr.s_addr = htonl(INADDR_ANY); 36 | sock_in.sin_port = htons(0); 37 | sock_in.sin_family = PF_INET; 38 | 39 | status = bind(sock, (struct sockaddr *)&sock_in, sinlen); 40 | printf("Bind Status = %d\n", status); 41 | 42 | status = getsockname(sock, (struct sockaddr *)&sock_in, &sinlen); 43 | printf("Sock port %d\n",htons(sock_in.sin_port)); 44 | 45 | buflen = MAXBUF; 46 | memset(buffer, 0, buflen); 47 | status = recvfrom(sock, buffer, buflen, 0, (struct sockaddr *)&sock_in, &sinlen); 48 | printf("sendto Status = %d\n", status); 49 | 50 | shutdown(sock, 2); 51 | close(sock); 52 | } 53 | -------------------------------------------------------------------------------- /trials/find-best-address.py: -------------------------------------------------------------------------------- 1 | import os, sys 2 | import netifaces 3 | 4 | def get_ip4_broadcast_addresses(): 5 | broadcast_addresses = [] 6 | for interface in netifaces.interfaces(): 7 | ifaddresses = netifaces.ifaddresses(interface) 8 | for family in ifaddresses: 9 | if family == netifaces.AF_INET: 10 | address_info = ifaddresses[family] 11 | for info in address_info: 12 | if "broadcast" in info: 13 | broadcast_addresses.append(info['broadcast']) 14 | 15 | return broadcast_addresses 16 | 17 | def dump_addresses(): 18 | for interface in netifaces.interfaces(): 19 | print(interface) 20 | ifaddresses = netifaces.ifaddresses(interface) 21 | for family in ifaddresses: 22 | print(" ", netifaces.address_families[family]) 23 | address_info = ifaddresses[family] 24 | for info in address_info: 25 | print(" ", info) 26 | 27 | if __name__ == '__main__': 28 | print(dump_addresses()) 29 | -------------------------------------------------------------------------------- /trials/json-bytes.py: -------------------------------------------------------------------------------- 1 | #!python3 2 | import base64 3 | import json 4 | 5 | class NetworkZeroEncoder(json.JSONEncoder): 6 | 7 | def default(self, obj): 8 | if isinstance(obj, bytes): 9 | return (b"\x00" + base64.b64encode(obj)).decode("ascii") 10 | else: 11 | return json.JSONEncoder.default(self, obj) 12 | 13 | def json_loads_hook(obj): 14 | print("hook called for", obj) 15 | 16 | source = {"data": b"123"} 17 | print("Source: %r" % source) 18 | dumped = json.dumps(source, cls=NetworkZeroEncoder) 19 | print("Dumped: %r" % dumped) 20 | loaded = json.loads(dumped, object_hook=json_loads_hook) 21 | print("Loaded: %r" % loaded) 22 | -------------------------------------------------------------------------------- /trials/keep-going.py: -------------------------------------------------------------------------------- 1 | try: 2 | import queue 3 | except ImportError: 4 | import Queue as queue 5 | import threading 6 | import time 7 | import uuid 8 | 9 | import networkzero as nw0 10 | 11 | lock = threading.Lock() 12 | def log(text, *args): 13 | with lock: 14 | print(threading.current_thread(), text % args) 15 | 16 | def do(finish_at, number=None): 17 | collector = nw0.discover("collector") 18 | group = "keep-going" 19 | name = "%s/%s" % (group, uuid.uuid4().hex) 20 | address = nw0.advertise(name) 21 | time.sleep(3) 22 | 23 | neighbours = [address for name, address in nw0.discover_group(group, exclude=[name])] 24 | 25 | while True: 26 | if number is not None: 27 | log("About to send %s-%s to collector", name, number) 28 | nw0.send_message(collector, (name, number)) 29 | if number < finish_at: 30 | log("About to send %s to %s", number+1, neighbours) 31 | nw0.send_message(neighbours, number + 1) 32 | # 33 | # Wait up to three seconds for a number and then give up 34 | # 35 | log("Waiting for message from %s", address) 36 | number = nw0.wait_for_message(address, wait_for_s=3) 37 | if number is None: 38 | break 39 | 40 | def main(n_threads=4, finish_at=1000): 41 | collector = nw0.advertise("collector") 42 | threads = [] 43 | for n in range(n_threads): 44 | threads.append(threading.Thread(target=do, args=(finish_at, 1 if n == 0 else None))) 45 | for thread in threads: 46 | thread.setDaemon(True) 47 | thread.start() 48 | 49 | collected = {} 50 | while True: 51 | name, number = nw0.wait_for_message(collector) 52 | collected.setdefault(name, set()).add(number) 53 | print(name, number) 54 | if number >= finish_at: 55 | break 56 | 57 | for name, numbers in collected.items(): 58 | print(name, "=>", len(numbers)) 59 | 60 | if __name__ == '__main__': 61 | main() 62 | -------------------------------------------------------------------------------- /trials/play-adverts.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os, sys 3 | print(sys.version_info) 4 | import json 5 | import select 6 | import socket 7 | import time 8 | 9 | def _unpack(message): 10 | return json.loads(message.decode("utf-8")) 11 | 12 | def _pack(message): 13 | return json.dumps(message).encode("utf-8") 14 | 15 | IP_ADDRESS = "192.168.1.255" 16 | PORT = 9999 17 | MESSAGE_SIZE = 256 18 | message = _pack("Hello") 19 | 20 | # 21 | # Set the socket up to broadcast datagrams over UDP 22 | # 23 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) 24 | s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) 25 | s.bind(("", PORT)) 26 | 27 | while True: 28 | print("Sending", message) 29 | s.sendto(message, 0, (IP_ADDRESS, PORT)) 30 | time.sleep(3) 31 | -------------------------------------------------------------------------------- /trials/recv_bcast.py: -------------------------------------------------------------------------------- 1 | import os, sys 2 | import select 3 | import socket 4 | 5 | port = 53005 # where do you expect to get a msg? 6 | bufferSize = 1024 # whatever you need 7 | 8 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 9 | s.bind(('', port)) 10 | s.setblocking(0) 11 | 12 | while True: 13 | result = select.select([s],[],[], 2.0) 14 | if result[0]: 15 | msg = result[0][0].recv(bufferSize) 16 | print(msg) -------------------------------------------------------------------------------- /trials/router-dealer.py: -------------------------------------------------------------------------------- 1 | import zmq 2 | 3 | frontend_address = "tcp://127.0.0.1:50001" 4 | backend_address = "tcp://127.0.0.1:50002" 5 | 6 | c = zmq.Context() 7 | 8 | router = c.socket(zmq.ROUTER) 9 | router.bind(frontend_address) 10 | dealer = c.socket(zmq.DEALER) 11 | dealer.bind(backend_address) 12 | 13 | req = c.socket(zmq.REQ) 14 | rep = c.socket(zmq.REP) 15 | 16 | rep.bind(address) 17 | -------------------------------------------------------------------------------- /trials/send_bcast.py: -------------------------------------------------------------------------------- 1 | #!python3 2 | import os, sys 3 | import socket 4 | import time 5 | 6 | def run(message=""): 7 | if not message: 8 | message = input("Message: ") 9 | bmessage = message.encode("utf-8") 10 | 11 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 12 | s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 13 | s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) 14 | 15 | while True: 16 | s.sendto(bmessage, ('192.168.1.255', 53005)) 17 | time.sleep(1) 18 | 19 | if __name__ == '__main__': 20 | run(*sys.argv[1:]) 21 | -------------------------------------------------------------------------------- /trials/show-adverts.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os, sys 3 | print(sys.version_info) 4 | import json 5 | import select 6 | import socket 7 | import time 8 | 9 | def _unpack(message): 10 | return json.loads(message.decode("utf-8")) 11 | 12 | def _pack(message): 13 | return json.dumps(message).encode("utf-8") 14 | 15 | PORT = 9999 16 | MESSAGE_SIZE = 256 17 | 18 | # 19 | # Set the socket up to broadcast datagrams over UDP 20 | # 21 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) 22 | s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) 23 | s.bind(("192.168.31.2", PORT)) 24 | 25 | print("Listening...") 26 | while True: 27 | rlist, wlist, xlist = select.select([s], [], [], 1) 28 | if s in rlist: 29 | message, source = s.recvfrom(MESSAGE_SIZE) 30 | service_name, service_address = _unpack(message) 31 | print("%s: Found %s at %s" % (time.asctime(), service_name, service_address)) 32 | -------------------------------------------------------------------------------- /trials/try-dealer-nw0.py: -------------------------------------------------------------------------------- 1 | import networkzero as nw0 2 | 3 | import uuid 4 | 5 | a1 = "127.0.0.1:5000" 6 | a2 = "127.0.0.1:5001" 7 | 8 | d1 = uuid.uuid4().hex 9 | d2 = uuid.uuid4().hex 10 | 11 | print("About to send %s to %s" % (d1, a1)) 12 | nw0.send_message(a1, d1) 13 | print("About to receive on s2") 14 | data1 = nw0.wait_for_message(a1) 15 | print(data1) 16 | assert data1 == d1 17 | nw0.send_reply(a1, d2) 18 | data2 = nw0.wait_for_message(a1) 19 | print(data2) 20 | assert data2 == d2 -------------------------------------------------------------------------------- /trials/try-dealer.py: -------------------------------------------------------------------------------- 1 | import zmq 2 | import uuid 3 | 4 | address = "tcp://127.0.0.1:5001" 5 | 6 | c = zmq.Context() 7 | s1 = c.socket(zmq.DEALER) 8 | s2 = c.socket(zmq.DEALER) 9 | s3 = c.socket(zmq.DEALER) 10 | 11 | d1 = uuid.uuid4().hex.encode("utf-8") 12 | d2 = uuid.uuid4().hex.encode("utf-8") 13 | d3 = uuid.uuid4().hex.encode("utf-8") 14 | 15 | #~ p = zmq.Poller() 16 | #~ p.register(s1, zmq.POLLIN) 17 | #~ p.register(s2, zmq.POLLIN) 18 | 19 | print("About to bind to", address) 20 | s2.bind(address) 21 | print("About to connect to", address) 22 | s1.connect(address) 23 | s3.connect(address) 24 | 25 | print("About to send %s from s1" % d1) 26 | s1.send(d1) 27 | print("About to send %s from s3" % d3) 28 | s3.send(d3) 29 | 30 | print("About to receive on s2") 31 | data1 = s2.recv() 32 | print(data1) 33 | assert data1 == d1 34 | data3 = s2.recv() 35 | print(data3) 36 | assert data3 == d3 37 | 38 | 39 | print("About to send %s from s1" % d2) 40 | s1.send(d2) 41 | print("About to receive on s2") 42 | data2 = s2.recv() 43 | print(data2) 44 | assert data2 == d2 45 | 46 | print("About to reply %s from s2" % data1) 47 | s2.send(data1) 48 | print("Checking for reply on s1") 49 | print(s1.recv()) 50 | 51 | print("About to reply %s from s2" % data2) 52 | s2.send(data2) 53 | print("Checking for reply on s1") 54 | print(s1.recv()) 55 | --------------------------------------------------------------------------------