├── .gitignore ├── .travis.yml ├── AUTHORS ├── CHANGELOG ├── LICENSE ├── MANIFEST.in ├── README.rst ├── TODO ├── dev-requirements.txt ├── docs ├── Makefile ├── address.rst ├── authors.rst ├── changelog.rst ├── conf.py ├── dbus.rst ├── example1.rst ├── example2.rst ├── example3.rst ├── example4.rst ├── example5.rst ├── examples.rst ├── fibers.rst ├── front.rst ├── futures.rst ├── http.rst ├── included.rst ├── index.rst ├── install.rst ├── jsonrpc.rst ├── lowlevel.rst ├── process.rst ├── rationale.rst ├── readme.rst ├── reference.rst ├── serve.py ├── streams.rst └── sync.rst ├── examples ├── curl.py ├── echoserver1.py ├── echoserver2.py ├── fortune.py ├── httpcs.py ├── jsonrpc.py └── netcat.py ├── lib └── gruvi │ ├── __init__.py │ ├── address.py │ ├── callbacks.py │ ├── compat.py │ ├── dbus.py │ ├── dllist.py │ ├── endpoints.py │ ├── errors.py │ ├── fibers.py │ ├── futures.py │ ├── http.py │ ├── hub.py │ ├── jsonrpc.py │ ├── local.py │ ├── logging.py │ ├── poll.py │ ├── process.py │ ├── protocols.py │ ├── ssl.py │ ├── sslcompat.py │ ├── stream.py │ ├── sync.py │ ├── transports.py │ ├── util.py │ └── vendor.py ├── requirements.txt ├── runtests.py ├── setup.cfg ├── setup.py ├── src ├── build_http.py ├── build_jsonrpc.py ├── http_parser.c ├── http_parser.h ├── json_splitter.c └── sslcompat.c ├── tasks.py ├── tests ├── bin │ ├── catn │ ├── echo │ ├── exitn │ ├── false │ ├── fortune │ ├── pwd │ ├── sleep │ └── true ├── examples.py ├── memory.py ├── openssl.cnf ├── perf_callbacks.py ├── perf_dbus.py ├── perf_fibers.py ├── perf_http.py ├── perf_jsonrpc.py ├── perf_ssl.py ├── support.py ├── test_address.py ├── test_callbacks.py ├── test_dbus.py ├── test_dllist.py ├── test_endpoints.py ├── test_fibers.py ├── test_futures.py ├── test_http.py ├── test_hub.py ├── test_jsonrpc.py ├── test_local.py ├── test_logging.py ├── test_poll.py ├── test_process.py ├── test_ssl.py ├── test_sslcompat.py ├── test_stream.py ├── test_sync.py └── test_transports.py ├── tox.ini └── vendor └── txdbus ├── __init__.py ├── authentication.py ├── error.py ├── marshal.py └── message.py /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | trash/ 4 | work/ 5 | *.egg-info 6 | *.pyc 7 | *.pyo 8 | *.so 9 | .*.swp 10 | .*.swo 11 | *~ 12 | __pycache__ 13 | *.pem 14 | docs/_build 15 | docs/html 16 | _version.py 17 | .tox 18 | tests/*.txt 19 | tests/server.* 20 | header 21 | .coverage 22 | htmlcov 23 | .eggs 24 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.3" 5 | - "3.4" 6 | - "3.5" 7 | - "3.6" 8 | env: 9 | - TOX_ENV=coverage VERBOSE=2 10 | matrix: 11 | include: 12 | - python: "3.5" 13 | env: TOX_ENV=docs 14 | - python: "3.5" 15 | env: TOX_ENV=flake8 16 | install: 17 | - pip install tox 18 | script: 19 | - tox -e $TOX_ENV 20 | - if [ $TOX_ENV = "coverage" ]; then pip install coveralls; coveralls; fi 21 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Main author: 2 | 3 | * Geert Jansen 4 | 5 | Gruvi includes some third party code: 6 | 7 | * The gruvi/txdbus package contains code from the "txdbus" project by Tom 8 | Cocagne (tom.cocagne@gmail.com). MIT licensed. Source code is at 9 | http://github.com/cocagne/txdbus. 10 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | Changes since latest version: 2 | 3 | * Verify SSL certificates by default. 4 | * New function: sslcompat.create_default_context() 5 | * Limit SSL support for Python <= 2.7.8. 6 | 7 | Changes in version 0.10.3: 8 | 9 | * Refactoring and cleanups of many places in the code. 10 | * Refactored and simplified flow control. 11 | * Made @switchpoint return a closure. 12 | * Documentation improvements. 13 | 14 | Changes in version 0.10.2: 15 | 16 | * New infrastructure for polling FDs. 17 | * Add Process.close() to explicitly close the process and stdio handles. 18 | * Remove redundant Process.kill() and Process.poll() 19 | * Use Memory BIO for faster and more compatible SSL implementation. 20 | * Add support for Python 3.5. 21 | * Move to new pyuv upstream (1.0.0-dev1, using libuv 1.0.0-rc2). 22 | * Improve test coverage. 23 | * Remove non-standard method Queue.clear(). 24 | * Drop support for Python 2.6. 25 | * Make futures thread-safe and add .cancel() method. 26 | * Make Event() follow threading API for timeouts. 27 | * New functions: wait(), as_completed() 28 | * Store callbacks in a linked list for fast removal. 29 | * Fix threading issues with timeouts for sync primitves. 30 | * Add test coverage via coveralls. 31 | * Add .alive property to Fiber. 32 | * http: communicate connection_lost() to waiters. 33 | * create_connection(): raise on callback error. 34 | * Workaround for Windows build failure (link error 1104). 35 | 36 | Changes in version 0.10.1: 37 | 38 | * Re-release of 0.10.0 with correct README file. 39 | 40 | Changes in version 0.10.0: 41 | 42 | * PEP-3156 style transports and protocols. 43 | * Incorporate SSL backports from Bluepass. 44 | * Replace Signal with stdlib-like Event and Condition. 45 | * Add support for working with child processes. 46 | * Add support for connecting protocols to a file descriptor. 47 | * Lots of small bug fixes and improvements. 48 | * Improved documentation. 49 | 50 | Changes in version 0.9.2: 51 | 52 | * Add support for JSON-RPC v2.0 53 | * Logger supports Python 2.7+ style docstrings on 2.6 54 | * Lots of small bug fixes and some cleanups 55 | 56 | Changes in version 0.9.1: 57 | 58 | * Add thread-safe synchronization primitives. 59 | * Add gruvi.futures. 60 | * Test suite now uses unittest. 61 | * Lots of smaller refactorings to make the API cleaner. 62 | * Remove dependency on six. 63 | * Switch from greenlet to python-fibers. 64 | * Fixed Windows support. 65 | 66 | Changes in version 0.9.0: 67 | 68 | * Almost complete rewrite. 69 | * First release of beta quality. 70 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2013 the Gruvi authors. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include setup.py runtests.py tasks.py 2 | include setup.cfg tox.ini 3 | include AUTHORS CHANGELOG LICENSE README.rst MANIFEST.in TODO 4 | include *requirements.txt 5 | include lib/gruvi/*.py 6 | include vendor/txdbus/*.py 7 | include src/*.py src/*.[ch] 8 | include tests/*.py tests/*.cnf tests/bin/* 9 | exclude tests/bin/*.* 10 | include docs/Makefile docs/*.py docs/*.rst 11 | include examples/*.py 12 | exclude __pycache__/* *.so 13 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ************************************** 2 | Gruvi: Async IO for Python, Simplified 3 | ************************************** 4 | 5 | .. image:: https://secure.travis-ci.org/geertj/gruvi.png 6 | :target: http://travis-ci.org/geertj/gruvi 7 | 8 | .. image:: https://coveralls.io/repos/geertj/gruvi/badge.png?branch=master 9 | :target: https://coveralls.io/r/geertj/gruvi?branch=master 10 | 11 | .. image:: https://badge.fury.io/py/gruvi.png 12 | :target: http://badge.fury.io/py/gruvi 13 | 14 | *Improved ergonomics for Python programmers wanting to use asynchronous IO.* 15 | 16 | Gruvi is an asynchronous IO library for Python. It focuses on the following 17 | desirable properties: 18 | 19 | * *Simple*. Async IO code should look just like normal code, with simple, 20 | sequential control flow and regular functions. 21 | 22 | * *Efficient*. An IO library should have a very low memory and CPU overhead, 23 | so that it can scale to small systems or to many concurrent connections. 24 | 25 | * *Powerful*. Common protocols like SSL/TLS and HTTP should be part of every 26 | IO library. 27 | 28 | Gruvi uses libuv_ (via pyuv_) as the underlying high-performance event-based 29 | I/O layer, and coroutines based on fibers_ to create a traditional sequential 30 | programming model on top of the libuv_ completion/callback model. 31 | 32 | Gruvi is similar in concept existing async IO frameworks like asyncio_, 33 | gevent_, and eventlet_. For a comparison, see Rationale_. 34 | 35 | Features 36 | ======== 37 | 38 | Gruvi has the following features: 39 | 40 | * Excellent platform support (mostly thanks to libuv). Linux, Mac OSX and 41 | Windows are all first-class citizens. 42 | * PEP-3156 compatible transport/protocol interface. 43 | * A traditional, sequential programming model based on green threads, where 44 | there is no distinction between asynchronous and normal functions. 45 | * Great SSL/TLS support, also on Windows. The `asynchronous SSL`_ support in the 46 | Python standard library came from Gruvi before it was included into the stdlib. 47 | * Small core and focus on low memory usage and fast performance. This makes 48 | Gruvi very suitable for mobile applications and embedded web servers. 49 | * A full suite of synchronization primitives including locks, conditions and queues. 50 | * Thread and fiber pools with a ``concurrent.futures`` interface. 51 | * Batteries includes: built-in client/server support for HTTP, JSON-RPC and D-BUS. 52 | * Support for Python 2.7.x and 3.3+. 53 | 54 | Example 55 | ======= 56 | 57 | An simple echo server, using a StreamServer_:: 58 | 59 | import gruvi 60 | 61 | def echo_handler(stream, transport, protocol): 62 | while True: 63 | buf = stream.read1() 64 | if not buf: 65 | break 66 | stream.write(buf) 67 | 68 | server = gruvi.StreamServer(echo_handler) 69 | server.listen(('localhost', 7777)) 70 | server.run() 71 | 72 | 73 | Requirements 74 | ============ 75 | 76 | You need Python 2.7 or 3.3+. 77 | 78 | The following operating systems are currently supported: 79 | 80 | * Linux (might work on other Posix OSs) 81 | * macOS (not currently tested via CI) 82 | * Windows (not currently tested via CI) 83 | 84 | Installation 85 | ============ 86 | 87 | Development install in a virtualenv:: 88 | 89 | $ git clone https://github.com/geertj/gruvi 90 | $ cd gruvi 91 | $ pip install -r requirements.txt 92 | $ python setup.py build 93 | $ python setup.py install 94 | 95 | To run the test suite:: 96 | 97 | $ python runtests.py unit 98 | 99 | For other installation options, see the Installation_ section in the manual. 100 | 101 | Documentation 102 | ============= 103 | 104 | The documentation is available on readthedocs_. 105 | 106 | License 107 | ======= 108 | 109 | Gruvi is free software, provided under the MIT license. 110 | 111 | Contact 112 | ======= 113 | 114 | Feel free to contact the author at geertj@gmail.com. You can also submit 115 | tickets or suggestions for improvements on Github_. 116 | 117 | .. _libuv: https://github.com/libuv/libuv 118 | .. _pyuv: http://pyuv.readthedocs.org/en/latest 119 | .. _fibers: http://python-fibers.readthedocs.org/en/latest 120 | .. _asyncio: http://docs.python.org/3.4/library/asyncio.html 121 | .. _gevent: http://gevent.org/ 122 | .. _eventlet: http://eventlet.net/ 123 | .. _Rationale: http://gruvi.readthedocs.org/en/latest/rationale.html 124 | .. _asynchronous SSL: https://docs.python.org/3/library/ssl.html#ssl.SSLObject 125 | .. _StreamServer: http://gruvi.readthedocs.org/en/latest/streams.html 126 | .. _Installation: http://gruvi.readthedocs.org/en/latest/install.html 127 | .. _readthedocs: https://gruvi.readthedocs.org/ 128 | .. _Github: https://github.com/geertj/gruvi 129 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | TODO: 2 | 3 | * Tests for Client/Server with do_handshake_on_connect=False 4 | - An Server with SSL should require SSL. Does it work if the client is not 5 | starting the handshake? 6 | - Do we rip out do_handshake_on_connect and remove support for unwrapped 7 | SslTransport? 8 | - SslTranport tests for _close_on_unwrap=False 9 | * Add connection refused tests to Client/Server tests. 10 | * Ensure eof_received() is always called for SSL transports (test_ssl.test_echo) 11 | * Threads that exit without calling Hub.close() are not garbage collected. Not 12 | sure how this can be implemented. I think it is not possible without this 13 | Python RFE: http://bugs.python.org/issue14073 14 | * gruvi.http needs better test coverage 15 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | sphinx 3 | wheel 4 | flake8 5 | tox 6 | detox 7 | invoke 8 | coverage 9 | coveralls 10 | twine 11 | flask 12 | -------------------------------------------------------------------------------- /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 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Gruvi.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Gruvi.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Gruvi" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Gruvi" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /docs/address.rst: -------------------------------------------------------------------------------- 1 | .. currentmodule:: gruvi 2 | 3 | ********* 4 | Addresses 5 | ********* 6 | 7 | Addresses in Gruvi are a location to connect to or listen on. Addresses can be 8 | local (e.g. when using a pipe), or remote (when using TCP or UDP). 9 | 10 | The following convention is used throughput Gruvi: 11 | 12 | * Pipe addresses are represented as ``str`` or ``bytes`` instances. On Unix 13 | these represent paths in the file system but on Windows there is no such 14 | correspondence. On Linux there is a special class of *abstract socket* 15 | addresses that start with a null byte (``'\x00'``). 16 | 17 | * IP addresses are represented as ``(node, service)`` tuples. The format is 18 | identical for IPv4 and IPv6, and both protocols are supported transparently. 19 | The host component of the tuple is either DNS name or a string format 20 | numerical IPv4/IPv6 address, while the port component is either an integer 21 | port number or a service name. 22 | 23 | In addition, some APIs also support connecting to a file descriptor or to a 24 | ``pyuv.Stream`` instance. 25 | 26 | Name Resolution 27 | =============== 28 | 29 | IP ``(node, service)`` tuples are resolved to numerical host addresses and port 30 | numbers using the :func:`getaddrinfo` function. The resulting, resolved address 31 | is called a *socket address*, and should be distinguished from a non-resolved 32 | addressed which we simply call "address". 33 | 34 | .. autofunction:: gruvi.getaddrinfo 35 | 36 | Note that the distinction between addresses and socket addresses is only 37 | present for IP addresses. Pipes don't use address resolution and their 38 | addresses and socket addresses are the same. 39 | 40 | A socket address can be resolved back to an address using :func:`getnameinfo`: 41 | 42 | .. autofunction:: gruvi.getnameinfo 43 | 44 | Gruvi API function always accept addresses, and address resolution is performed 45 | automatically. The only place where you will work with raw socket addresses is 46 | when you query the address of an existing socket, using e.g. the 47 | :meth:`~BaseTransport.get_extra_info` method of a transport. 48 | 49 | Note that the first two elements of an IP socket address are always the 50 | numerical-as-string address and the port number (for both IPv4 and IPv6). 51 | Because :func:`getaddrinfo` also accepts numerical-as-string addresses and port 52 | numbers, they also form a valid address and could be passed back to 53 | :func:`getaddrinfo` again. 54 | 55 | Some operating systems support a special convention to specify the scope ID for 56 | an IPv6 addressing using the ``'host%ifname'`` notation where ``'ifname'`` is 57 | the name of a network interface. Support for this is OS specific and neither 58 | libuv nor Gruvi try to present a portable interface for this. 59 | 60 | String Addresses 61 | ================ 62 | 63 | Gruvi contains two utility functions to transform addresses to and from a 64 | string representation. This is useful when logging or when accepting addresses 65 | from the command line. 66 | 67 | .. autofunction:: gruvi.saddr 68 | 69 | .. autofunction:: gruvi.paddr 70 | -------------------------------------------------------------------------------- /docs/authors.rst: -------------------------------------------------------------------------------- 1 | ******* 2 | AUTHORS 3 | ******* 4 | 5 | .. include:: ../AUTHORS 6 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | ********* 2 | CHANGELOG 3 | ********* 4 | 5 | .. include:: ../CHANGELOG 6 | -------------------------------------------------------------------------------- /docs/dbus.rst: -------------------------------------------------------------------------------- 1 | ******************************************** 2 | :mod:`gruvi.dbus` -- D-BUS Client and Server 3 | ******************************************** 4 | 5 | .. automodule:: gruvi.dbus 6 | :members: 7 | -------------------------------------------------------------------------------- /docs/example1.rst: -------------------------------------------------------------------------------- 1 | Example 1: curl like URL downloader 2 | =================================== 3 | 4 | .. literalinclude:: ../examples/curl.py 5 | :language: python 6 | :linenos: 7 | -------------------------------------------------------------------------------- /docs/example2.rst: -------------------------------------------------------------------------------- 1 | .. _example-stream-server: 2 | 3 | Example 2: echo server, using a StreamServer 4 | ============================================ 5 | 6 | .. literalinclude:: ../examples/echoserver1.py 7 | :language: python 8 | :linenos: 9 | -------------------------------------------------------------------------------- /docs/example3.rst: -------------------------------------------------------------------------------- 1 | Example 3: echo server, using a custom Protocol 2 | =============================================== 3 | 4 | .. literalinclude:: ../examples/echoserver2.py 5 | :language: python 6 | :linenos: 7 | -------------------------------------------------------------------------------- /docs/example4.rst: -------------------------------------------------------------------------------- 1 | Example 4: netcat client 2 | ======================== 3 | 4 | .. literalinclude:: ../examples/netcat.py 5 | :language: python 6 | :linenos: 7 | -------------------------------------------------------------------------------- /docs/example5.rst: -------------------------------------------------------------------------------- 1 | Example 5: fortune web service 2 | ============================== 3 | 4 | .. literalinclude:: ../examples/fortune.py 5 | :language: python 6 | :linenos: 7 | -------------------------------------------------------------------------------- /docs/examples.rst: -------------------------------------------------------------------------------- 1 | ******** 2 | Examples 3 | ******** 4 | 5 | .. toctree:: 6 | :maxdepth: 1 7 | 8 | example1 9 | example2 10 | example3 11 | example4 12 | example5 13 | -------------------------------------------------------------------------------- /docs/fibers.rst: -------------------------------------------------------------------------------- 1 | Fibers and the Hub 2 | ================== 3 | 4 | .. currentmodule:: gruvi 5 | 6 | Fibers are light weight execution contexts that are cooperatively scheduled. 7 | They form the basis for concurrency in Gruvi. Comparing fibers to threads: 8 | 9 | * Like threads, fibers represent an independent flow of control in a program. 10 | * Fibers are more light weight than threads, meaning you can run more of them. 11 | * Unlike threads, there can only ever be one active fiber (per thread, see 12 | below). This means that fibers give you *concurrency* but not *parallelism*. 13 | * Unlike threads, fibers are cooperatively scheduled. They are never preempted 14 | and have to yield control other fibers explicitly (called *switching*). 15 | 16 | Gruvi uses the python-fibers_ package as its fiber library. 17 | 18 | Fibers logically exist within threads. Each fiber has its own Python and 19 | C-level stack. When a Python program starts, there will be one thread called 20 | ``'Main'``, and this thread will have one fiber called ``'Root'``. Both are 21 | created automatically; the main thread by Python and the root fiber by the 22 | python-fibers package. 23 | 24 | Fibers are organized in a tree. Each fiber except the root fiber has a parent. 25 | When a fiber exits, either because its main function exits or because of an 26 | uncaught exception, control is passed to its parent. If the root fiber exits, 27 | the thread will exit. 28 | 29 | The hub and fiber scheduling 30 | ---------------------------- 31 | 32 | The Gruvi framework maintains a special fiber called the *Hub*. The hub runs an 33 | *event loop*, and acts as a central fiber scheduler. The event loop in Gruvi is 34 | provided by libuv_ through the pyuv_ bindings. 35 | 36 | From a high-level, the flow of a Gruvi program is as follows: 37 | 38 | 1. Run the current fiber (initially the root fiber). 39 | 2. When the fiber needs to wait for something, for example network data, 40 | the event to wait for is registered with the event loop, together with a 41 | *switch back* callback. 42 | 3. The fiber switches to the hub, which run the event loop. 43 | 4. When the event loop detects an event for which a fiber registered interest, 44 | it will will call the callback. This causes a switch back to the fiber that 45 | installed the event. 46 | 47 | The hub instance is automatically created when in is first needed. It can be 48 | retrieved using the following function: 49 | 50 | .. autofunction:: gruvi.get_hub 51 | 52 | The event loop is available as the ``.loop`` property: 53 | 54 | .. autoattribute:: Hub.loop 55 | 56 | Creating fibers and switching 57 | ----------------------------- 58 | 59 | To create a new fiber, instantiate the :class:`Fiber` class and pass it a main 60 | function. The :class:`Fiber` class is a thin wrapper on top of 61 | :class:`fibers.Fiber`, to which it adds a few behaviors: 62 | 63 | * The fiber is created as a child of the hub. 64 | * Only the hub is allowed to switch to this fiber. This prevent complex 65 | interactions where any fiber can switch to any other fiber. In other words, 66 | the hub is a real *hub*. 67 | 68 | .. autoclass:: gruvi.Fiber 69 | :members: 70 | 71 | The only two fibers in a Gruvi program that use :class:`fibers.Fiber` directly 72 | are the hub and the root fiber. All other fibers should be created as instances 73 | of :class:`Fiber`. 74 | 75 | When a fiber is created it doesn't run yet. To switch to the fiber, call its 76 | :meth:`~Fiber.start` method and call a function that will switch to the hub:: 77 | 78 | def say_hello(): 79 | print('Hello, there!') 80 | 81 | fiber = Fiber(say_hello) 82 | fiber.start() 83 | 84 | print('Starting fiber') 85 | sleep(0) 86 | print('Back in root fiber') 87 | 88 | The output of this will be:: 89 | 90 | Starting fiber 91 | Hello, there! 92 | Back in root fiber 93 | 94 | Working with the event loop 95 | --------------------------- 96 | 97 | To register interest in a certain event, you need to create the appropriate 98 | :class:`pyuv.Handle` instance and add it to the loop. The callback to the 99 | handle should cause a switch back to the current fiber. You also want to make 100 | sure you implement a timeout on the event, and that you clean up the handle in 101 | case it times out. Because this logic can be relatively tricky to get right, 102 | Gruvi provides the :class:`switch_back` context manager for this: 103 | 104 | .. autoclass:: switch_back 105 | :members: 106 | 107 | Lockless operation and switchpoints 108 | ----------------------------------- 109 | 110 | Functions that may cause a switch to happen are called *switch points*. In 111 | Gruvi these functions are marked by a special decorator: 112 | 113 | .. autofunction:: gruvi.switchpoint 114 | 115 | Knowing where switches may happen is important if you need to modify global 116 | state in a non-atomic way. If you can be sure that during the modification no 117 | switch points are called, then you don't need any locks. This lockless 118 | operation is one of the main benefits of green threads. Gruvi offers a context 119 | manager that can help with this: 120 | 121 | .. autoclass:: assert_no_switchpoints 122 | 123 | The :class:`assert_no_switchpoints` context manager should not be overused. 124 | Instead it is recommended to try and confine non-atomic changes to a global 125 | state to single functions. 126 | 127 | Utility functions 128 | ----------------- 129 | 130 | .. autofunction:: gruvi.current_fiber 131 | 132 | .. autofunction:: gruvi.spawn 133 | 134 | .. autofunction:: gruvi.sleep 135 | 136 | Fiber local data 137 | ---------------- 138 | 139 | .. autoclass:: gruvi.local 140 | 141 | Hub reference 142 | ------------- 143 | 144 | .. autoclass:: Hub 145 | :members: 146 | 147 | Mixing threads and fibers 148 | ------------------------- 149 | 150 | There are two common situations where you might want to mix threads and fibers: 151 | 152 | * When running CPU intensive code. In this case, you should run the code the 153 | :func:`CPU thread pool `. 154 | 155 | * When running third party code that performs blocking IO. In this case, run 156 | the code in the :func:`IO thread pool `. 157 | 158 | In both cases, running the code in a thread pool allows the hub to continue 159 | servicing IO for fibers. All other cases of mixing threads and fibers are 160 | generally a bad idea. 161 | 162 | The following code is Gruvi is thread safe: 163 | 164 | * The :meth:`Hub.run_callback` method. 165 | * All synchronization primitives in :ref:`sync-primitives`. 166 | 167 | All other code is **not** thread safe. If, for whatever reason, you must access 168 | this code from multiple threads, use locks to mediate access. 169 | 170 | .. _python-fibers: https://github.com/saghul/python-fibers 171 | .. _libuv: https://github.com/joyent/libuv 172 | .. _pyuv: https://pypi.python.org/pypi/pyuv 173 | .. _`Global Interpreter Lock`: https://wiki.python.org/moin/GlobalInterpreterLock 174 | -------------------------------------------------------------------------------- /docs/front.rst: -------------------------------------------------------------------------------- 1 | ************ 2 | Front Matter 3 | ************ 4 | 5 | .. currentmodule:: gruvi 6 | 7 | Package Namespace 8 | ================= 9 | 10 | The API of Gruvi is exposed through the ``gruvi`` package. The individual 11 | pieces that make up the API are actually defined in submodules, but those are 12 | pulled into the package scope by the package's ``__init__.py``. 13 | 14 | The symbol names in the gruvi package are designed to be unique and descriptive 15 | within the scope of Gruvi, but are not globally unique. Therefore you should 16 | use Gruvi as follows:: 17 | 18 | # Import the package 19 | import gruvi 20 | gruvi.sleep(1) 21 | 22 | # Or.. import individual symbols 23 | from gruvi import Process 24 | proc = Process() 25 | 26 | But never like this:: 27 | 28 | # Don't use this. It won't work because __all__ is set to [] 29 | from gruvi import * 30 | 31 | Gruvi includes a few important protocol implementations that are provided under 32 | its "batteries includes" philosophy. These are exposed as submodules under the 33 | Gruvi namespace. For example:: 34 | 35 | from gruvi.http import HttpClient 36 | client = HttpClient() 37 | 38 | External protocols for Gruvi are recommended to install themselves into the 39 | ``gruvi.ext`` namespace package. 40 | 41 | Exceptions 42 | ========== 43 | 44 | Errors are reported through exceptions. The following base exceptions are 45 | defined: 46 | 47 | .. autoexception:: gruvi.Error 48 | 49 | .. autoexception:: gruvi.Timeout 50 | 51 | .. autoexception:: gruvi.Cancelled 52 | 53 | More exceptions are defined by subsystems and they are documented with their 54 | respective subsystem. 55 | 56 | Functions that are a direct mapping of a pyuv function can also raise a pyuv 57 | exception. Gruvi does not try to capture these and transform them into a Gruvi 58 | exception, so in these cases you might want to capture the pyuv exception 59 | separately. The relevant pyuv base exception is: 60 | 61 | .. exception:: pyuv.error.UVError 62 | :noindex: 63 | 64 | Base exception for all pyuv errors. 65 | 66 | Timeouts 67 | ======== 68 | 69 | Many parts of the Gruvi API use timeouts. Timeouts follow the Python convention 70 | that they are expressed in seconds, and can be either passed as an integer or a 71 | floating point argument. The granularity of timeouts in Gruvi is approximately 72 | 1 millisecond due to the fact that the libuv API represents timeouts as an 73 | integer number of milliseconds. 74 | 75 | In addition the above, there are two other values for timeout arguments: 76 | 77 | * ``None`` means "no timeout". 78 | * ``-1`` means a *default* timeout. This arguments is accepted for methods 79 | of objects where the constructor allows you to set a default timeout. 80 | 81 | When a timeout occurs in a function or method, a :exc:`Timeout` exception is 82 | raised. 83 | 84 | Reference Counting 85 | ================== 86 | 87 | Memory management and the freeing of unused resources in Gruvi requires a 88 | little cooperation from the programmer. In summary, if a object has a 89 | ``close()`` method, you should call it before you are done with the object. 90 | You should never expect it to be called automatically for you by a destructor. 91 | If you do not call a ``close()`` method, then the memory associated with the 92 | object will likely not be fully reclaimed. 93 | 94 | The reason for this behavior is as follows. Gruvi, as an event-based IO 95 | library, uses a central event loop. The event loop contains a reference to all 96 | objects for which Gruvi interested in getting events. These events are 97 | registered as callbacks back into Gruvi objects. The event loop is provided by 98 | libuv, through the pyuv bindings. The libuv framework calls these objects 99 | "handles". 100 | 101 | Gruvi provides many objects that wrap a pyuv handle. For example, there are 102 | objects for a TCP socket or a process. These objects contain a reference to a 103 | pyuv handle, and while the handle is active, it has a callback back into the 104 | Gruvi object itself. This forms a cyclic reference in which destructors are not 105 | called. This means destructors cannot be used to call ``close()`` methods 106 | automatically. 107 | 108 | Some magic could be done by using weak references. However, the fact remains 109 | that a libuv handle will not be released until it is deactivated (and there's 110 | `good reasons`_ for that behavior). So even with weak references, you would 111 | still be required to call ``close()``, making the whole point of them moot. 112 | Don't be too concerned though, you will see that in practice it is not a big 113 | deal. 114 | 115 | .. _`good reasons`: https://github.com/saghul/pyuv/issues/63 116 | -------------------------------------------------------------------------------- /docs/futures.rst: -------------------------------------------------------------------------------- 1 | *************************** 2 | Asynchronous function calls 3 | *************************** 4 | 5 | Gruvi provides functionality to execute functions asynchronously outside the 6 | current flow of control. The API is a *futures* based interface, modeled after 7 | the :mod:`concurrent.futures` and :mod:`asyncio` packages in the Python 8 | standard library. 9 | 10 | .. currentmodule:: gruvi 11 | 12 | .. autoclass:: gruvi.Future 13 | :members: 14 | 15 | .. autoclass:: gruvi.PoolBase 16 | :members: 17 | 18 | .. autoclass:: gruvi.FiberPool 19 | 20 | .. autoclass:: gruvi.ThreadPool 21 | 22 | .. autofunction:: gruvi.get_io_pool 23 | 24 | .. autofunction:: gruvi.get_cpu_pool 25 | 26 | .. autofunction:: gruvi.blocking 27 | 28 | .. autofunction:: gruvi.wait 29 | 30 | .. autofunction:: gruvi.as_completed 31 | -------------------------------------------------------------------------------- /docs/http.rst: -------------------------------------------------------------------------------- 1 | ******************************************* 2 | :mod:`gruvi.http` -- HTTP Client and Server 3 | ******************************************* 4 | 5 | .. currentmodule:: gruvi.http 6 | 7 | .. automodule:: gruvi.http 8 | :members: 9 | 10 | .. autodata:: REQUEST 11 | :annotation: 12 | 13 | .. autodata:: RESPONSE 14 | :annotation: 15 | 16 | Example 17 | ======= 18 | 19 | .. literalinclude:: ../examples/httpcs.py 20 | :language: python 21 | -------------------------------------------------------------------------------- /docs/included.rst: -------------------------------------------------------------------------------- 1 | ******************************************** 2 | Included Protocols (specific high-level API) 3 | ******************************************** 4 | 5 | .. toctree:: 6 | :maxdepth: 1 7 | 8 | http 9 | jsonrpc 10 | dbus 11 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ################ 2 | Welcome to Gruvi 3 | ################ 4 | 5 | .. default-domain:: py 6 | 7 | .. include:: ../README.rst 8 | :start-line: 12 9 | :end-line: 26 10 | 11 | Documentation 12 | ############# 13 | 14 | .. toctree:: 15 | :maxdepth: 1 16 | 17 | rationale 18 | install 19 | reference 20 | examples 21 | 22 | Project files 23 | ############# 24 | 25 | .. toctree:: 26 | :maxdepth: 1 27 | 28 | readme 29 | changelog 30 | authors 31 | 32 | .. _libuv: https://github.com/libuv/libuv 33 | .. _pyuv: http://pyuv.readthedocs.org/en/latest 34 | .. _fibers: http://python-fibers.readthedocs.org/en/latest 35 | -------------------------------------------------------------------------------- /docs/install.rst: -------------------------------------------------------------------------------- 1 | .. _installation: 2 | 3 | ************ 4 | Installation 5 | ************ 6 | 7 | Gruvi uses the setuptools, so installation is relatively straightforward. The 8 | following Python / OS combinations are supported: 9 | 10 | ========== ================== =============================== 11 | OS Python versions Notes 12 | ========== ================== =============================== 13 | Posix 2.7, 3.3+ Only Linux is regularly tested 14 | Mac OSX 2.7, 3.3+ PPC is not tested 15 | Windows 2.7, 3.3+ No SSL backports on 2.7 16 | ========== ================== =============================== 17 | 18 | Gruvi and some of its dependencies contain C extensions. This means that you 19 | need to have a C compiler, and also the Python development files. Gruvi also 20 | uses CFFI_ so you need to have that installed as well. 21 | 22 | Installation using pip 23 | ********************** 24 | 25 | If you have the "pip" package manager, then you can install Gruvi directly from 26 | the Python Package Index:: 27 | 28 | $ pip install cffi 29 | $ pip install gruvi 30 | 31 | You need to install CFFI first because the Gruvi setup script depends on it. 32 | 33 | Installation from source 34 | ************************ 35 | 36 | The following instructions install Gruvi in a virtualenv development 37 | environment:: 38 | 39 | $ pyvenv gruvi-dev # "virtualenv gruvi-dev" with Python <= 3.3 40 | $ . gruvi-dev/bin/activate 41 | $ git clone https://github.com/geertj/gruvi 42 | $ cd gruvi 43 | $ pip install -r requirements.txt 44 | $ python setup.py develop 45 | 46 | Linux Specific Notes 47 | ******************** 48 | 49 | Most mainstream Linux versions have a supported version of Python installed by 50 | default, with the notable exception of RHEL/CentOS version 6. For this OS you 51 | can use `Software Collections`_ to install a supported Python version. 52 | 53 | To install the required dependencies on Debian/Ubuntu systems:: 54 | 55 | $ sudo apt-get update 56 | $ sudo apt-get install -y gcc python-dev python-virtualenv libffi-dev 57 | 58 | On Red Hat/Fedora type systems:: 59 | 60 | $ sudo yum install -y gcc python-devel python-virtualenv libffi-devel 61 | 62 | Mac OSX Specific Notes 63 | ********************** 64 | 65 | Recent versions of OSX have a supported Python version available. 66 | 67 | The easiest way to install a C compiler is to install Xcode from the Mac App 68 | Store. This is a free download. 69 | 70 | Since OSX Mavericks, the required libffi dependency has become part of OSX 71 | itself, so there's no need to install it separately. If you are on an earlier 72 | version of OSX, it's recommended to install libffi via Homebrew. 73 | 74 | Windows Specific Notes 75 | ********************** 76 | 77 | Windows does not come with a system provided version of Python, so you have to 78 | install one from python.org. It is recommended to use version 3.3 or 3.4. 79 | 80 | For the C compiler, I recommend to use Microsoft `Visual C++ 2010 Express`_. 81 | It is a free download, and it is the same compiler used to create the official 82 | Python builds for Windows since version 3.3. 83 | 84 | You can also use MinGW. In that case make sure you use either Python 2.7.6, 85 | 3.3.3, 3.4 or later. These version contain a fix for issue12641_. 86 | 87 | 88 | .. _CFFI: https://pypi.python.org/pypi/cffi 89 | .. _issue12641: http://bugs.python.org/issue12641 90 | .. _`Visual C++ 2010 Express`: https://www.microsoft.com/visualstudio/eng/downloads#d-2010-express 91 | .. _`Software Collections`: http://softwarecollections.org/ 92 | -------------------------------------------------------------------------------- /docs/jsonrpc.rst: -------------------------------------------------------------------------------- 1 | ************************************************** 2 | :mod:`gruvi.jsonrpc` -- JSON-RPC Client and Server 3 | ************************************************** 4 | 5 | .. currentmodule:: gruvi.jsonrpc 6 | 7 | .. automodule:: gruvi.jsonrpc 8 | :members: 9 | 10 | .. autodata:: REQUEST 11 | :annotation: 12 | 13 | .. autodata:: RESPONSE 14 | :annotation: 15 | 16 | .. autodata:: errorcode 17 | :annotation: {...} 18 | 19 | .. autofunction:: strerror 20 | 21 | Example 22 | ======= 23 | 24 | .. literalinclude:: ../examples/jsonrpc.py 25 | :language: python 26 | -------------------------------------------------------------------------------- /docs/lowlevel.rst: -------------------------------------------------------------------------------- 1 | **************************************** 2 | Transports and protocols (low-level API) 3 | **************************************** 4 | 5 | .. currentmodule:: gruvi 6 | 7 | Gruvi is built around the transport/protocol abstraction layers that are 8 | documented in :pep:`3156` and implemented by asyncio_. 9 | 10 | A transport is a standard interface to a communications channel. Different 11 | types of channels implement different interfaces. Since Gruvi uses libuv_ / 12 | pyuv_, its transports are mostly wrappers around the various 13 | :class:`pyuv.Handle` classes. Transport methods are always non-blocking. 14 | 15 | A protocol is a callback based interface that is connected to a transport. The 16 | transport calls specific callbacks on the protocol when specific events occur. 17 | The callbacks are always non-blocking, but a protocol may expose certain 18 | protocol operations are part of their API that are switch points. 19 | 20 | As a programmer you will use transports and protocols occasionally, but you 21 | will mostly use the higher-level interface provided by Gruvi. The exception is 22 | when adding support for a new protocol. 23 | 24 | Transports 25 | ========== 26 | 27 | The following transport classes are available: 28 | 29 | .. autoexception:: TransportError() 30 | 31 | .. autoclass:: BaseTransport() 32 | :members: 33 | 34 | .. autoclass:: Transport 35 | :members: 36 | 37 | .. autoclass:: DatagramTransport 38 | :members: 39 | 40 | SSL/TLS support 41 | =============== 42 | 43 | SSL and TLS support is available by means of a special :class:`SslTransport` 44 | transport: 45 | 46 | .. autoclass:: SslTransport 47 | :members: 48 | 49 | .. autofunction:: create_default_context 50 | 51 | Protocols 52 | ========= 53 | 54 | A Protocols is a collection of named callbacks. A protocol is are attached to a 55 | transport, and its callbacks are called by the transport when certain events 56 | happen. 57 | 58 | The following protocol classes are available: 59 | 60 | .. autoexception:: ProtocolError 61 | 62 | .. autoclass:: BaseProtocol 63 | :members: 64 | 65 | .. autoclass:: Protocol 66 | :members: 67 | 68 | .. autoclass:: DatagramProtocol 69 | :members: 70 | 71 | The following class does not exist in :pep:`3156` but is a useful base class 72 | for most protocols: 73 | 74 | .. autoclass:: MessageProtocol 75 | :members: 76 | 77 | Creating transports and protocols 78 | ================================= 79 | 80 | Transports and protocols operate in pairs; there is always exactly one protocol 81 | for each transport. A new transport/protocol pair can be created using the 82 | factory functions below: 83 | 84 | .. autofunction:: gruvi.create_connection 85 | 86 | .. autofunction:: gruvi.create_server 87 | 88 | Endpoints 89 | ========= 90 | 91 | Endpoints wrap transports and protocols for client and server side connections, 92 | and provide a more object-oriented way of working with them. 93 | 94 | .. autoclass:: gruvi.Endpoint 95 | :members: 96 | 97 | .. autoclass:: gruvi.Client 98 | :members: 99 | 100 | .. autoclass:: gruvi.Server 101 | :members: 102 | 103 | 104 | .. _libuv: https://github.com/joyent/libuv 105 | .. _pyuv: https://pypi.python.org/pypi/pyuv 106 | .. _asyncio: http://docs.python.org/3.4/library/asyncio.html 107 | -------------------------------------------------------------------------------- /docs/process.rst: -------------------------------------------------------------------------------- 1 | ********************** 2 | Working with Processes 3 | ********************** 4 | 5 | .. currentmodule:: gruvi 6 | 7 | .. attribute:: gruvi.PIPE = ... 8 | 9 | .. attribute:: gruvi.DEVNULL = ... 10 | 11 | .. autoclass:: gruvi.Process 12 | :members: 13 | -------------------------------------------------------------------------------- /docs/readme.rst: -------------------------------------------------------------------------------- 1 | ****** 2 | README 3 | ****** 4 | 5 | .. include:: ../README.rst 6 | :start-line: 13 7 | -------------------------------------------------------------------------------- /docs/reference.rst: -------------------------------------------------------------------------------- 1 | ********* 2 | Reference 3 | ********* 4 | 5 | .. module:: gruvi 6 | 7 | .. toctree:: 8 | :maxdepth: 2 9 | 10 | front 11 | fibers 12 | sync 13 | futures 14 | address 15 | lowlevel 16 | streams 17 | process 18 | included 19 | -------------------------------------------------------------------------------- /docs/serve.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, send_from_directory 2 | 3 | app = Flask(__name__) 4 | 5 | @app.route('/') 6 | def send_static(name): 7 | return send_from_directory('_build/html', name) 8 | 9 | if __name__ == "__main__": 10 | app.run(host='0.0.0.0', port='8080') 11 | -------------------------------------------------------------------------------- /docs/streams.rst: -------------------------------------------------------------------------------- 1 | ******************************** 2 | Streams (generic high-level API) 3 | ******************************** 4 | 5 | .. currentmodule:: gruvi 6 | 7 | Streams provide a generic, high-level API to work with connection-oriented 8 | transports using file-like, blocking and flow-controlled API. 9 | 10 | Utility classes 11 | =============== 12 | 13 | .. autoclass:: gruvi.StreamBuffer 14 | :members: 15 | 16 | .. autoclass:: gruvi.Stream 17 | :members: 18 | 19 | Stream Protocol 20 | =============== 21 | 22 | .. autoclass:: gruvi.StreamProtocol 23 | :members: 24 | 25 | Stream Client and Server 26 | ======================== 27 | 28 | .. autoclass:: gruvi.StreamClient 29 | :members: 30 | 31 | .. autoclass:: gruvi.StreamServer 32 | :members: 33 | -------------------------------------------------------------------------------- /docs/sync.rst: -------------------------------------------------------------------------------- 1 | .. _sync-primitives: 2 | 3 | ========================== 4 | Synchronization primitives 5 | ========================== 6 | 7 | Gruvi contains a set of primitives that can be used for synchronization between 8 | multiple fibers and threads. All primitives documented below are thread safe. 9 | They are modeled after the primitives in the Python :mod:`threading` and 10 | :mod:`queue` modules. 11 | 12 | .. currentmodule:: gruvi 13 | 14 | .. autoclass:: gruvi.Lock 15 | :members: 16 | :inherited-members: 17 | 18 | .. autoclass:: gruvi.RLock 19 | :members: 20 | :inherited-members: 21 | 22 | .. autoclass:: gruvi.Event 23 | :members: 24 | 25 | .. autoclass:: gruvi.Condition 26 | :members: 27 | 28 | .. autoexception:: gruvi.QueueEmpty 29 | 30 | .. autoexception:: gruvi.QueueFull 31 | 32 | .. autoclass:: gruvi.Queue 33 | :members: 34 | 35 | .. autoclass:: gruvi.LifoQueue 36 | :members: 37 | 38 | .. autoclass:: gruvi.PriorityQueue 39 | :members: 40 | -------------------------------------------------------------------------------- /examples/curl.py: -------------------------------------------------------------------------------- 1 | # Gruvi example program: a cURL like URL downloader 2 | 3 | import sys 4 | import argparse 5 | from gruvi.http import HttpClient, parse_url 6 | 7 | parser = argparse.ArgumentParser() 8 | parser.add_argument('url') 9 | args = parser.parse_args() 10 | 11 | url = parse_url(args.url) 12 | 13 | client = HttpClient() 14 | client.connect(url.addr, ssl=url.ssl) 15 | client.request('GET', url.target) 16 | 17 | resp = client.getresponse() 18 | if not 200 <= resp.status_code <= 399: 19 | sys.stderr.write('Error: got status {}\n'.format(resp.status_code)) 20 | sys.exit(1) 21 | 22 | stdout = getattr(sys.stdout, 'buffer', sys.stdout) 23 | 24 | while True: 25 | buf = resp.body.read(4096) 26 | if not buf: 27 | break 28 | stdout.write(buf) 29 | -------------------------------------------------------------------------------- /examples/echoserver1.py: -------------------------------------------------------------------------------- 1 | # Gruvi example program: echo server, using StreamServer 2 | 3 | import gruvi 4 | 5 | def echo_handler(stream, transport, protocol): 6 | peer = transport.get_extra_info('peername') 7 | print('New connection from {0}'.format(gruvi.saddr(peer))) 8 | while True: 9 | buf = stream.read1() 10 | if not buf: 11 | break 12 | stream.write(buf) 13 | print('Connection lost') 14 | 15 | server = gruvi.StreamServer(echo_handler) 16 | server.listen(('localhost', 0)) 17 | for addr in server.addresses: 18 | print('Listen on {0}'.format(gruvi.saddr(addr))) 19 | 20 | server.run() 21 | -------------------------------------------------------------------------------- /examples/echoserver2.py: -------------------------------------------------------------------------------- 1 | # Gruvi example program: an echo server, using a Protocol 2 | 3 | import gruvi 4 | 5 | class EchoProtocol(gruvi.Protocol): 6 | 7 | def connection_made(self, transport): 8 | self._transport = transport 9 | peer = transport.get_extra_info('peername') 10 | print('New connection from {0}'.format(gruvi.saddr(peer))) 11 | 12 | def data_received(self, data): 13 | self._transport.write(data) 14 | 15 | def eof_received(self): 16 | print('Connection lost') 17 | 18 | server = gruvi.create_server(EchoProtocol, ('localhost', 0)) 19 | for addr in server.addresses: 20 | print('Listen on {0}'.format(gruvi.saddr(addr))) 21 | 22 | server.run() 23 | -------------------------------------------------------------------------------- /examples/fortune.py: -------------------------------------------------------------------------------- 1 | # Gruvi example program: a "fortune" web service 2 | 3 | import locale 4 | import gruvi 5 | import contextlib 6 | 7 | def fortune_app(environ, start_response): 8 | print('New connection from {0}'.format(environ['REMOTE_ADDR'])) 9 | proc = gruvi.Process(encoding=locale.getpreferredencoding()) 10 | proc.spawn('fortune', stdout=gruvi.PIPE) 11 | with contextlib.closing(proc): 12 | fortune = proc.stdout.read() 13 | proc.wait() 14 | start_response('200 OK', [('Content-Type', 'text/plain; charset=utf-8')]) 15 | return [fortune.encode('utf-8')] 16 | 17 | server = gruvi.HttpServer(fortune_app) 18 | server.listen(('localhost', 0)) 19 | for addr in server.addresses: 20 | print('Listen on {0}'.format(gruvi.saddr(addr))) 21 | 22 | server.run() 23 | -------------------------------------------------------------------------------- /examples/httpcs.py: -------------------------------------------------------------------------------- 1 | # HTTP client and server example. 2 | 3 | from gruvi.http import HttpClient, HttpServer 4 | 5 | def handler(env, start_response): 6 | headers = [('Content-Type', 'text/plain; charset=UTF-8')] 7 | status = '200 OK' 8 | body = 'pong' 9 | start_response(status, headers) 10 | yield body.encode('utf-8') 11 | 12 | server = HttpServer(handler) 13 | server.listen(('localhost', 0)) 14 | addr = server.addresses[0] 15 | 16 | client = HttpClient() 17 | client.connect(addr) 18 | client.request('GET', '/ping') 19 | 20 | resp = client.getresponse() 21 | assert resp.status_code == 200 22 | 23 | body = resp.body.read() 24 | print('result = {}'.format(body.decode(resp.charset))) 25 | -------------------------------------------------------------------------------- /examples/jsonrpc.py: -------------------------------------------------------------------------------- 1 | # Ping-pong between JSON-RPC client and server. 2 | 3 | from gruvi.jsonrpc import JsonRpcClient, JsonRpcServer 4 | 5 | def handler(message, transport, protocol): 6 | method = message.get('method') 7 | if method == 'ping': 8 | protocol.send_response(message, 'pong') 9 | 10 | server = JsonRpcServer(handler) 11 | server.listen(('localhost', 0)) 12 | addr = server.addresses[0] 13 | 14 | client = JsonRpcClient() 15 | client.connect(addr) 16 | result = client.call_method('ping') 17 | 18 | print('result = {}'.format(result)) 19 | -------------------------------------------------------------------------------- /examples/netcat.py: -------------------------------------------------------------------------------- 1 | # Gruvi example program: a nc(1) like client, using StreamClient 2 | 3 | import sys 4 | import argparse 5 | import gruvi 6 | 7 | parser = argparse.ArgumentParser() 8 | parser.add_argument('hostname') 9 | parser.add_argument('port', type=int) 10 | parser.add_argument('--ssl', action='store_true') 11 | args = parser.parse_args() 12 | 13 | remote = gruvi.StreamClient() 14 | remote.connect((args.hostname, args.port), ssl=args.ssl) 15 | 16 | stdin = gruvi.StreamClient() 17 | stdin.connect(sys.stdin.fileno(), mode='r') 18 | stdout = gruvi.StreamClient() 19 | stdout.connect(sys.stdout.fileno(), mode='w') 20 | 21 | done = gruvi.Event() 22 | 23 | def stdin_reader(): 24 | while True: 25 | buf = stdin.read1() 26 | if not buf: 27 | print('Got EOF on stdin') 28 | break 29 | remote.write(buf) 30 | done.set() 31 | 32 | def remote_reader(): 33 | while True: 34 | buf = remote.read1() 35 | if not buf: 36 | print('Got EOF from remote') 37 | break 38 | stdout.write(buf) 39 | done.set() 40 | 41 | gruvi.spawn(stdin_reader) 42 | gruvi.spawn(remote_reader) 43 | 44 | done.wait() 45 | -------------------------------------------------------------------------------- /lib/gruvi/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Gruvi. Gruvi is free software available under the 3 | # terms of the MIT license. See the file "LICENSE" that was provided 4 | # together with this source file for the licensing terms. 5 | # 6 | # Copyright (c) 2012-2017 the Gruvi authors. See the file "AUTHORS" for a 7 | # complete list. 8 | 9 | from __future__ import absolute_import, print_function 10 | 11 | # Suppress warnings about 'import *' here. The submodules are designed to 12 | # export their symbols to a global package namespace like this. 13 | # flake8: noqa 14 | 15 | # should not use "from gruvi import *" 16 | __all__ = [] 17 | 18 | from sys import version_info as vi 19 | if vi[0] == 2 and vi[1] < 7 or vi[0] == 3 and vi[1] < 3: 20 | raise ImportError('Gruvi requires Python 2.7 or 3.3+') 21 | 22 | import pkg_resources 23 | try: 24 | __version__ = pkg_resources.require(__name__)[0].version 25 | except pkg_resources.DistributionNotFound: 26 | # Happens when the .egg-info directory does not exist yet. 27 | __version__ = None 28 | 29 | # clean up module namespace 30 | del absolute_import, print_function, vi, pkg_resources 31 | 32 | # import all the subpackages into the "gruvi" namespace 33 | from .errors import * 34 | from .hub import * 35 | from .fibers import * 36 | from .sync import * 37 | from .local import * 38 | from .ssl import * 39 | from .sslcompat import * 40 | from .futures import * 41 | from .transports import * 42 | from .protocols import * 43 | from .endpoints import * 44 | from .address import * 45 | from .process import * 46 | from .stream import * 47 | from .http import * 48 | from .jsonrpc import * 49 | from .dbus import * 50 | -------------------------------------------------------------------------------- /lib/gruvi/address.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Gruvi. Gruvi is free software available under the 3 | # terms of the MIT license. See the file "LICENSE" that was provided 4 | # together with this source file for the licensing terms. 5 | # 6 | # Copyright (c) 2012-2017 the Gruvi authors. See the file "AUTHORS" for a 7 | # complete list. 8 | 9 | from __future__ import absolute_import, print_function 10 | 11 | import six 12 | import pyuv 13 | 14 | from .hub import get_hub, switch_back, switchpoint 15 | 16 | __all__ = ['saddr', 'paddr', 'getaddrinfo', 'getnameinfo'] 17 | 18 | 19 | def saddr(address): 20 | """Return a string representation for an address. 21 | 22 | The *address* paramater can be a pipe name, an IP address tuple, or a 23 | socket address. 24 | 25 | The return value is always a ``str`` instance. 26 | """ 27 | if isinstance(address, six.string_types): 28 | return address 29 | elif isinstance(address, tuple) and len(address) >= 2 and ':' in address[0]: 30 | return '[{}]:{}'.format(address[0], address[1]) 31 | elif isinstance(address, tuple) and len(address) >= 2: 32 | return '{}:{}'.format(*address) 33 | else: 34 | raise TypeError('illegal address type: {!s}'.format(type(address))) 35 | 36 | 37 | def paddr(address): 38 | """Parse a string representation of an address. 39 | 40 | This function is the inverse of :func:`saddr`. 41 | """ 42 | if not isinstance(address, six.string_types): 43 | raise TypeError('expecting a string') 44 | if address.startswith('['): 45 | p1 = address.find(']:') 46 | if p1 == -1: 47 | raise ValueError 48 | return (address[1:p1], int(address[p1+2:])) 49 | elif ':' in address: 50 | p1 = address.find(':') 51 | return (address[:p1], int(address[p1+1:])) 52 | else: 53 | return address 54 | 55 | 56 | @switchpoint 57 | def getaddrinfo(node, service=0, family=0, socktype=0, protocol=0, flags=0, timeout=30): 58 | """Resolve an Internet *node* name and *service* into a socket address. 59 | 60 | The *family*, *socktype* and *protocol* are optional arguments that specify 61 | the address family, socket type and protocol, respectively. The *flags* 62 | argument allows you to pass flags to further modify the resolution process. 63 | See the :func:`socket.getaddrinfo` function for a detailed description of 64 | these arguments. 65 | 66 | The return value is a list of ``(family, socktype, proto, canonname, 67 | sockaddr)`` tuples. The fifth element (``sockaddr``) is the socket address. 68 | It will be a 2-tuple ``(addr, port)`` for an IPv4 address, and a 4-tuple 69 | ``(addr, port, flowinfo, scopeid)`` for an IPv6 address. 70 | 71 | The address resolution is performed in the libuv thread pool. 72 | """ 73 | hub = get_hub() 74 | with switch_back(timeout) as switcher: 75 | request = pyuv.dns.getaddrinfo(hub.loop, node, service, family, 76 | socktype, protocol, flags, callback=switcher) 77 | switcher.add_cleanup(request.cancel) 78 | result = hub.switch() 79 | result, error = result[0] 80 | if error: 81 | message = pyuv.errno.strerror(error) 82 | raise pyuv.error.UVError(error, message) 83 | return result 84 | 85 | 86 | @switchpoint 87 | def getnameinfo(sockaddr, flags=0, timeout=30): 88 | """Resolve a socket address *sockaddr* back to a ``(node, service)`` tuple. 89 | 90 | The *flags* argument can be used to modify the resolution process. See the 91 | :func:`socket.getnameinfo` function for more information. 92 | 93 | The address resolution is performed in the libuv thread pool. 94 | """ 95 | hub = get_hub() 96 | with switch_back(timeout) as switcher: 97 | request = pyuv.dns.getnameinfo(hub.loop, sockaddr, flags, callback=switcher) 98 | switcher.add_cleanup(request.cancel) 99 | result = hub.switch() 100 | result, error = result[0] 101 | if error: 102 | message = pyuv.errno.strerror(error) 103 | raise pyuv.error.UVError(error, message) 104 | return result 105 | -------------------------------------------------------------------------------- /lib/gruvi/callbacks.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Gruvi. Gruvi is free software available under the 3 | # terms of the MIT license. See the file "LICENSE" that was provided 4 | # together with this source file for the licensing terms. 5 | # 6 | # Copyright (c) 2012-2017 the Gruvi authors. See the file "AUTHORS" for a 7 | # complete list. 8 | 9 | from __future__ import absolute_import, print_function 10 | 11 | from . import logging 12 | from .dllist import dllist, Node 13 | 14 | # Many objects in Gruvi need to keep a list of callbacks. This module provides 15 | # a few utility functions to do this in an efficient way. 16 | # 17 | # * Callbacks are stored in a linked list. This allows to iterate over them in 18 | # insertion order and also allows removal from the middle assuming the node 19 | # handle is kept by the caller. 20 | # * The add/remove callback functions are implemented as "friend" functions 21 | # operating on an object. They store the callbacks in the '_callbacks' 22 | # property of the object. 23 | # * As a special case, a single callback is stored directly in the object as a 24 | # list node without the overhead of a dllist. 25 | 26 | 27 | def add_callback(obj, callback, args=()): 28 | """Add a callback to an object.""" 29 | callbacks = obj._callbacks 30 | node = Node(callback, args) 31 | # Store a single callback directly in _callbacks 32 | if callbacks is None: 33 | obj._callbacks = node 34 | return node 35 | # Otherwise use a dllist. 36 | if not isinstance(callbacks, dllist): 37 | obj._callbacks = dllist() 38 | obj._callbacks.insert(callbacks) 39 | callbacks = obj._callbacks 40 | callbacks.insert(node) 41 | return node 42 | 43 | 44 | def remove_callback(obj, handle): 45 | """Remove a callback from an object.""" 46 | callbacks = obj._callbacks 47 | if callbacks is handle: 48 | obj._callbacks = None 49 | elif isinstance(callbacks, dllist): 50 | callbacks.remove(handle) 51 | if not callbacks: 52 | obj._callbacks = None 53 | 54 | 55 | def has_callback(obj, handle): 56 | """Return whether a callback is currently registered for an object.""" 57 | callbacks = obj._callbacks 58 | if not callbacks: 59 | return False 60 | if isinstance(callbacks, Node): 61 | return handle is callbacks 62 | else: 63 | return handle in callbacks 64 | 65 | 66 | def pop_callback(obj): 67 | """Pop a single callback.""" 68 | callbacks = obj._callbacks 69 | if not callbacks: 70 | return 71 | if isinstance(callbacks, Node): 72 | node = callbacks 73 | obj._callbacks = None 74 | else: 75 | node = callbacks.first 76 | callbacks.remove(node) 77 | if not callbacks: 78 | obj._callbacks = None 79 | return node.data, node.extra 80 | 81 | 82 | def clear_callbacks(obj): 83 | """Remove all callbacks from an object.""" 84 | callbacks = obj._callbacks 85 | if isinstance(callbacks, dllist): 86 | # Help the garbage collector by clearing all links. 87 | callbacks.clear() 88 | obj._callbacks = None 89 | 90 | 91 | def walk_callbacks(obj, func, log=None): 92 | """Call func(callback, args) for all callbacks and keep only those 93 | callbacks for which the function returns True.""" 94 | callbacks = obj._callbacks 95 | if isinstance(callbacks, Node): 96 | node = callbacks 97 | try: 98 | if not func(node.data, node.extra): 99 | obj._callbacks = None 100 | except Exception: 101 | if log is None: 102 | log = logging.get_logger() 103 | log.exception('uncaught exception in callback') 104 | elif isinstance(callbacks, dllist): 105 | for node in callbacks: 106 | try: 107 | if func(node.data, node.extra): 108 | continue 109 | callbacks.remove(node) 110 | except Exception: 111 | if log is None: 112 | log = logging.get_logger() 113 | log.exception('uncaught exception in callback') 114 | if not callbacks: 115 | obj._callbacks = None 116 | 117 | 118 | def run_callbacks(obj, log=None): 119 | """Run callbacks.""" 120 | def run_callback(callback, args): 121 | return callback(*args) 122 | return walk_callbacks(obj, run_callback, log) 123 | -------------------------------------------------------------------------------- /lib/gruvi/compat.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Gruvi. Gruvi is free software available under the 3 | # terms of the MIT license. See the file "LICENSE" that was provided 4 | # together with this source file for the licensing terms. 5 | # 6 | # Copyright (c) 2012-2014 the Gruvi authors. See the file "AUTHORS" for a 7 | # complete list. 8 | 9 | from __future__ import absolute_import, print_function 10 | 11 | import os 12 | import io 13 | import sys 14 | import threading 15 | import socket 16 | import errno 17 | import pyuv 18 | 19 | 20 | # Some compatibility stuff that is not in six. 21 | 22 | PY3 = sys.version_info[0] == 3 23 | 24 | 25 | # Provide a get_thread_ident() that is the same on Python 2.x and 3.x 26 | 27 | try: 28 | get_thread_ident = threading._get_ident 29 | except AttributeError: 30 | get_thread_ident = threading.get_ident 31 | 32 | 33 | # Support for re-raising stored exceptions. In our callback based code it is 34 | # very common that we store an exception in a callback as an instance 35 | # attribute, and then raise that from the consumer API. This is problematic on 36 | # Python3 because the traceback in Py3K is attached to the exception itself (as 37 | # the __traceback__ attribute). The impact is that i) the traceback will also 38 | # be stored and never freed, and ii) if the exception is re-raised multiple times 39 | # a new traceback will be appended for every raise. 40 | # 41 | # The solution that we're using is is to have a saved_exc() call that returns a 42 | # new copy of the exception on Python3. 43 | 44 | if PY3: 45 | def saved_exc(exc): 46 | if isinstance(exc, type): 47 | return exc 48 | return type(exc)(*exc.args) 49 | else: 50 | def saved_exc(exc): 51 | return exc 52 | 53 | 54 | # Support write_through for TextIOWrapper on Python 2.x. The write_through 55 | # argument first appeared in Python 3.3. 56 | 57 | if PY3: 58 | 59 | TextIOWrapper = io.TextIOWrapper 60 | 61 | else: 62 | 63 | class TextIOWrapper(io.TextIOWrapper): 64 | 65 | def __init__(self, *args, **kwargs): 66 | self._write_through = kwargs.pop('write_through', False) 67 | super(TextIOWrapper, self).__init__(*args, **kwargs) 68 | 69 | def write(self, buf): 70 | super(TextIOWrapper, self).write(buf) 71 | if self._write_through: 72 | self.flush() 73 | 74 | def writelines(self, seq): 75 | super(TextIOWrapper, self).writelines(seq) 76 | if self._write_through: 77 | self.flush() 78 | 79 | 80 | # Needed until pyuv accepts PR #249 and #250 81 | 82 | def pyuv_pipe_helper(handle, handle_args, op, addr): 83 | if not isinstance(handle, pyuv.Pipe): 84 | return False 85 | # Store the 'ipc' constructor argument. 86 | if handle_args and not hasattr(handle, 'ipc'): 87 | handle.ipc = handle_args[0] 88 | if not sys.platform.startswith('linux') or '\x00' not in addr: 89 | return False 90 | # Connect or bind the socket. 91 | sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 92 | sock.setblocking(False) 93 | try: 94 | if op == 'connect': 95 | sock.connect(addr) 96 | elif op == 'bind': 97 | sock.bind(addr) 98 | fd = os.dup(sock.fileno()) 99 | except IOError as e: 100 | # Connecting to an AF_UNIX socket never gives EAGAIN on Linux. 101 | assert e.errno != errno.EAGAIN 102 | # Convert from Unix errno -> libuv errno via the symbolic error name 103 | errname = 'UV_{}'.format(errno.errocode.get(e.errno, 'UNKNOWN')) 104 | errnum = getattr(pyuv.errno, errname, pyuv.errno.UV_UNKNOWN) 105 | raise pyuv.error.PipeError(errnum, os.strerror(e.errno)) 106 | finally: 107 | sock.close() 108 | handle.open(fd) 109 | # Work around a bug in pyuv where abstract sockets names are reported as 110 | # bytes by dynamically patching getsockname(). The above PRs should fix this. 111 | if PY3: 112 | self = handle 113 | encoding = sys.getfilesystemencoding() 114 | def getsockname(): 115 | value = type(handle).getsockname(self) 116 | if isinstance(value, bytes): 117 | value = value.decode(encoding) 118 | return value 119 | handle.getsockname = getsockname 120 | return True 121 | -------------------------------------------------------------------------------- /lib/gruvi/dllist.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Gruvi. Gruvi is free software available under the 3 | # terms of the MIT license. See the file "LICENSE" that was provided 4 | # together with this source file for the licensing terms. 5 | # 6 | # Copyright (c) 2012-2017 the Gruvi authors. See the file "AUTHORS" for a 7 | # complete list. 8 | 9 | from __future__ import absolute_import, print_function 10 | 11 | from . import logging 12 | 13 | 14 | # Linked list implementation, used by gruvi.callbacks and others. 15 | 16 | if __debug__: 17 | 18 | def fmtnode(node): 19 | return '' \ 20 | .format(id(node._prev), id(node._next), node.data) 21 | 22 | def dump(dll): 23 | print('== Dumping dllist {!r}'.format(dll)) 24 | print('Size: {}'.format(dll._size)) 25 | print('First: {}'.format(fmtnode(dll.first) if dll.first else 'None')) 26 | print('Last: {}'.format(fmtnode(dll.last) if dll.last else 'None')) 27 | print('Nodes:') 28 | count = 0 29 | node = dll.first 30 | while node is not None: 31 | print('- {} [{}]'.format(fmtnode(node), count)) 32 | node = node._next 33 | count += 1 34 | print('Total nodes: {}'.format(count)) 35 | 36 | def check(dll): 37 | if dll.first is None: 38 | assert dll.last is None 39 | assert dll._size == 0 40 | return 41 | node = dll.first 42 | assert node._list is dll 43 | assert node._prev is None 44 | nnode = node._next 45 | count = 1 46 | while nnode is not None: 47 | assert nnode._list is dll 48 | assert nnode._prev is node 49 | node, nnode = nnode, nnode._next 50 | count += 1 51 | assert node is dll.last 52 | assert count == dll._size 53 | 54 | 55 | class Node(object): 56 | """A node in a doubly linked list.""" 57 | 58 | __slots__ = ('_prev', '_next', '_list', 'data', 'extra') 59 | 60 | def __init__(self, data=None, extra=None): 61 | self._prev = None 62 | self._next = None 63 | self._list = None 64 | self.data = data 65 | self.extra = extra 66 | 67 | 68 | class dllist(object): 69 | """A doubly linked list.""" 70 | 71 | __slots__ = ('_first', '_last', '_size') 72 | 73 | def __init__(self): 74 | self._first = None 75 | self._last = None 76 | self._size = 0 77 | 78 | @property 79 | def first(self): 80 | """The first node in the list.""" 81 | return self._first 82 | 83 | @property 84 | def last(self): 85 | """The last node in the list.""" 86 | return self._last 87 | 88 | def __len__(self): 89 | return self._size 90 | 91 | def __contains__(self, node): 92 | """Return whether *node* is contained in the list.""" 93 | if not isinstance(node, Node): 94 | raise TypeError('expecting Node instance') 95 | return node._list is self 96 | 97 | def remove(self, node): 98 | """Remove a node from the list.""" 99 | if not isinstance(node, Node): 100 | raise TypeError('expecting Node instance') 101 | if node._list is None: 102 | return 103 | if node._list is not self: 104 | raise RuntimeError('node is not contained in list') 105 | if node._next is None: 106 | self._last = node._prev # last node 107 | else: 108 | node._next._prev = node._prev 109 | if node._prev is None: 110 | self._first = node._next # first node 111 | else: 112 | node._prev._next = node._next 113 | node._list = node._prev = node._next = None 114 | self._size -= 1 115 | 116 | def insert(self, node, before=None): 117 | """Insert a new node in the list. 118 | 119 | If *before* is specified, the new node is inserted before this node. 120 | Otherwise, the node is inserted at the end of the list. 121 | """ 122 | node._list = self 123 | if self._first is None: 124 | self._first = self._last = node # first node in list 125 | self._size += 1 126 | return node 127 | if before is None: 128 | self._last._next = node # insert as last node 129 | node._prev = self._last 130 | self._last = node 131 | else: 132 | node._next = before 133 | node._prev = before._prev 134 | if node._prev: 135 | node._prev._next = node 136 | else: 137 | self._first = node # inserting as first node 138 | node._next._prev = node 139 | self._size += 1 140 | return node 141 | 142 | def __iter__(self): 143 | """Return an iterator/generator that yields all nodes. 144 | 145 | Note: it is safe to remove the current node while iterating but you 146 | should not remove the next one. 147 | """ 148 | node = self._first 149 | while node is not None: 150 | next_node = node._next 151 | yield node 152 | node = next_node 153 | 154 | def clear(self): 155 | """Remove all nodes from the list.""" 156 | node = self._first 157 | while node is not None: 158 | next_node = node._next 159 | node._list = node._prev = node._next = None 160 | node = next_node 161 | self._size = 0 162 | -------------------------------------------------------------------------------- /lib/gruvi/errors.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Gruvi. Gruvi is free software available under the 3 | # terms of the MIT license. See the file "LICENSE" that was provided 4 | # together with this source file for the licensing terms. 5 | # 6 | # Copyright (c) 2012-2014 the Gruvi authors. See the file "AUTHORS" for a 7 | # complete list. 8 | 9 | from __future__ import absolute_import, print_function 10 | 11 | __all__ = ['Error', 'Timeout', 'Cancelled'] 12 | 13 | 14 | class Error(Exception): 15 | """Base class for Gruvi exceptions.""" 16 | 17 | class Timeout(Error): 18 | """A timeout has occurred.""" 19 | 20 | class Cancelled(BaseException): 21 | """A fiber or calback was cancelled.""" 22 | -------------------------------------------------------------------------------- /lib/gruvi/fibers.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Gruvi. Gruvi is free software available under the 3 | # terms of the MIT license. See the file "LICENSE" that was provided 4 | # together with this source file for the licensing terms. 5 | # 6 | # Copyright (c) 2012-2017 the Gruvi authors. See the file "AUTHORS" for a 7 | # complete list. 8 | 9 | from __future__ import absolute_import, print_function 10 | 11 | import fibers 12 | 13 | from . import logging 14 | from .hub import get_hub, switchpoint 15 | from .sync import Event 16 | from .errors import Cancelled, Timeout 17 | from .callbacks import add_callback, remove_callback, run_callbacks 18 | 19 | __all__ = ['current_fiber', 'Fiber', 'spawn'] 20 | 21 | 22 | def current_fiber(): 23 | """Return the current fiber. 24 | 25 | Note: The root and hub fiber are "bare" :class:`fibers.Fiber` instances. 26 | Calling this method there returns the bare instance, not a 27 | :class:`gruvi.Fiber` instance. 28 | """ 29 | return fibers.current() 30 | 31 | 32 | class Fiber(fibers.Fiber): 33 | """An cooperatively scheduled execution context aka *green thread* aka 34 | *co-routine*.""" 35 | 36 | # This class is a very thin layer on top of fibers.Fiber. It adds a start() 37 | # method that schedules a switch via the hub. It also enforces that only 38 | # the hub may call switch(). 39 | # 40 | # All user created fibers should use this interface. The only fibers in a 41 | # Gruvi application that use the "raw" interface from the fibers package 42 | # are the root fiber and the Hub. 43 | 44 | __slots__ = ('_name', 'context', '_target', '_log', '_done', '_callbacks') 45 | 46 | def __init__(self, target, args=(), kwargs={}, name=None, hub=None): 47 | """ 48 | The *target* argument is the main function of the fiber. It must be a 49 | Python callable. The *args* and *kwargs* specify its arguments and 50 | keyword arguments, respectively. 51 | 52 | The *name* argument specifies the fiber name. This is purely a 53 | diagnositic tool is used e.g. in log messages. 54 | 55 | The *hub* argument can be used to override the hub that will be used to 56 | schedule this fiber. This argument is used by the unit tests and should 57 | not by needed. 58 | """ 59 | self._hub = hub or get_hub() 60 | super(Fiber, self).__init__(self.run, args, kwargs, self._hub) 61 | if name is None: 62 | fid = self._hub.data.setdefault('gruvi:next_fiber', 1) 63 | name = 'Fiber-{}'.format(fid) 64 | self._hub.data['gruvi:next_fiber'] += 1 65 | self._name = name 66 | self._target = target 67 | self._log = logging.get_logger() 68 | self._done = Event() 69 | self._callbacks = None 70 | 71 | @property 72 | def name(self): 73 | """The fiber's name.""" 74 | return self._name 75 | 76 | @property 77 | def alive(self): 78 | """Whether the fiber is alive.""" 79 | return self.is_alive() 80 | 81 | def start(self): 82 | """Schedule the fiber to be started in the next iteration of the 83 | event loop.""" 84 | target = getattr(self._target, '__qualname__', self._target.__name__) 85 | self._log.debug('starting fiber {}, target {}', self.name, target) 86 | self._hub.run_callback(self.switch) 87 | 88 | def switch(self, value=None): 89 | # Only the hub may call this. 90 | if self.current() is not self._hub: 91 | raise RuntimeError('only the Hub may switch() to a fiber') 92 | if self._hub._noswitch_depth: 93 | raise RuntimeError('attempt to switch from a no-switch section') 94 | if not self.is_alive(): 95 | self._log.warning('attempt to switch to a dead Fiber') 96 | return 97 | return super(Fiber, self).switch(value) 98 | 99 | def throw(self, typ, val=None, tb=None): 100 | # Only the hub may call this. 101 | if self.current() is not self._hub: 102 | raise RuntimeError('only the Hub may throw() into a fiber') 103 | return super(Fiber, self).throw(typ, val, tb) 104 | 105 | def cancel(self, message=None): 106 | """Schedule the fiber to be cancelled in the next iteration of the 107 | event loop. 108 | 109 | Cancellation works by throwing a :class:`~gruvi.Cancelled` exception 110 | into the fiber. If *message* is provided, it will be set as the value 111 | of the exception. 112 | """ 113 | if not self.is_alive(): 114 | return 115 | if message is None: 116 | message = 'cancelled by Fiber.cancel()' 117 | self._hub.run_callback(self.throw, Cancelled, Cancelled(message)) 118 | 119 | @switchpoint 120 | def join(self, timeout=None): 121 | """Wait until the fiber completes.""" 122 | if not self._done.wait(timeout): 123 | raise Timeout('timeout waiting for fiber to exit') 124 | 125 | def run(self, *args, **kwargs): 126 | # Target of the first :meth:`switch()` call. 127 | if self.current() is not self: 128 | raise RuntimeError('run() may only be called from self') 129 | try: 130 | self._target(*args, **kwargs) 131 | except Cancelled as e: 132 | self._log.debug('fiber was cancelled ({!s})', e) 133 | except BaseException: 134 | self._log.exception('uncaught exception in fiber') 135 | self._done.set() 136 | run_callbacks(self) 137 | 138 | # Support wait() 139 | 140 | def add_done_callback(self, callback, *args): 141 | if self._done.is_set(): 142 | callback(*args) 143 | return 144 | return add_callback(self, callback, args) 145 | 146 | def remove_done_callback(self, handle): 147 | remove_callback(self, handle) 148 | 149 | 150 | def spawn(func, *args, **kwargs): 151 | """Spawn a new fiber. 152 | 153 | A new :class:`Fiber` is created with main function *func* and positional 154 | arguments *args*. The keyword arguments are passed to the :class:`Fiber` 155 | constructor, not to the main function. The fiber is then scheduled to start 156 | by calling its :meth:`~Fiber.start` method. 157 | 158 | The fiber instance is returned. 159 | """ 160 | fiber = Fiber(func, args, **kwargs) 161 | fiber.start() 162 | return fiber 163 | -------------------------------------------------------------------------------- /lib/gruvi/local.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Gruvi. Gruvi is free software available under the 3 | # terms of the MIT license. See the file "LICENSE" that was provided 4 | # together with this source file for the licensing terms. 5 | # 6 | # Copyright (c) 2012-2013 the Gruvi authors. See the file "AUTHORS" for a 7 | # complete list. 8 | 9 | from __future__ import absolute_import, print_function 10 | 11 | import weakref 12 | import fibers 13 | 14 | __all__ = ['local'] 15 | 16 | 17 | class local(object): 18 | """Fiber-local data. 19 | 20 | To manage fiber-local data, instantiate this class and store attributes on 21 | it:: 22 | 23 | mydata = local() 24 | mydata.x = 10 25 | 26 | Attributes have a value or are unset independently for each fiber. 27 | """ 28 | 29 | def __init__(self): 30 | self.__dict__['_keys'] = weakref.WeakKeyDictionary() 31 | 32 | def __getattr__(self, key): 33 | current = fibers.current() 34 | try: 35 | return self._keys[current][key] 36 | except KeyError: 37 | raise AttributeError(key) 38 | 39 | def __setattr__(self, key, value): 40 | current = fibers.current() 41 | self._keys.setdefault(current, {})[key] = value 42 | 43 | def __delattr__(self, key): 44 | current = fibers.current() 45 | try: 46 | del self._keys[current][key] 47 | except KeyError: 48 | raise AttributeError(key) 49 | -------------------------------------------------------------------------------- /lib/gruvi/logging.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Gruvi. Gruvi is free software available under the 3 | # terms of the MIT license. See the file "LICENSE" that was provided 4 | # together with this source file for the licensing terms. 5 | # 6 | # Copyright (c) 2012-2017 the Gruvi authors. See the file "AUTHORS" for a 7 | # complete list. 8 | 9 | from __future__ import absolute_import, print_function 10 | 11 | import os 12 | import sys 13 | import logging 14 | import threading 15 | import fibers 16 | import six 17 | 18 | from . import util 19 | 20 | __all__ = ['get_logger'] 21 | 22 | # Add a new level: TRACE. 23 | logging.TRACE = 5 24 | assert logging.NOTSET < logging.TRACE < logging.DEBUG 25 | logging.addLevelName('TRACE', logging.TRACE) 26 | 27 | _logger_name = 'gruvi' 28 | _logger_dict = {} 29 | 30 | # The logging module documents this slight hack to disable finding caller 31 | # information (via sys._getframe()) for every logging call. In our logger we 32 | # only get logging information if needed (at the DEBUG level or higher), so we 33 | # can disable collecting it for every call. 34 | logging._srcfile = None 35 | 36 | 37 | def get_logger(context=None, name=None): 38 | """Return a logger for *context*. 39 | 40 | Return a :class:`ContextLogger` instance. The instance implements the 41 | standard library's :class:`logging.Logger` interface. 42 | """ 43 | # Many class instances have their own logger. Share them to save memory if 44 | # possible, i.e. when *context* is not set. 45 | if name is None: 46 | name = _logger_name 47 | if context is None and name in _logger_dict: 48 | return _logger_dict[name] 49 | if context is not None and not isinstance(context, six.string_types): 50 | context = util.objref(context) 51 | logger = logging.getLogger(name) 52 | logger = ContextLogger(logger, context) 53 | if context is None: 54 | _logger_dict[name] = logger 55 | return logger 56 | 57 | 58 | class ContextLogger(object): 59 | """A logger adapter that prepends a context string to log messages. 60 | 61 | It also supports passing arguments via '{}' format operations. 62 | """ 63 | 64 | __slots__ = ('_logger', '_context') 65 | 66 | # This is not based on logging.LoggingAdapter because the 2.x and 3.x 67 | # implementations differ quite a bit, which means we would need to 68 | # reimplement almost the entire thing anyway. 69 | 70 | def __init__(self, logger, context=None): 71 | self._logger = logger 72 | self._context = context or '' 73 | 74 | @property 75 | def context(self): 76 | """Return the logging context.""" 77 | return self._context 78 | 79 | def thread_info(self): 80 | """Return a string identifying the current thread and fiber.""" 81 | tid = threading.current_thread().name 82 | if tid == 'MainThread': 83 | tid = 'Main' 84 | current = fibers.current() 85 | fid = getattr(current, 'name') if current.parent else 'Root' 86 | return '{}/{}'.format(tid, fid) 87 | 88 | def frame_info(self): 89 | """Return a string identifying the current frame.""" 90 | if not self._logger.isEnabledFor(logging.DEBUG): 91 | return '' 92 | f = sys._getframe(3) 93 | fname = os.path.split(f.f_code.co_filename)[1] 94 | return '{}:{}'.format(fname, f.f_lineno) 95 | 96 | def log(self, level, msg, *args, **kwargs): 97 | if not self._logger.isEnabledFor(level): 98 | return 99 | prefix = '{}|{}|{}'.format(self.thread_info(), self.context or '-', self.frame_info()) 100 | if args: 101 | msg = msg.format(*args) 102 | msg = '[{}] {}'.format(prefix, msg) 103 | self._logger._log(level, msg, (), **kwargs) 104 | 105 | def trace(self, msg, *args, **kwargs): 106 | self.log(logging.TRACE, msg, *args, **kwargs) 107 | 108 | def debug(self, msg, *args, **kwargs): 109 | self.log(logging.DEBUG, msg, *args, **kwargs) 110 | 111 | def info(self, msg, *args, **kwargs): 112 | self.log(logging.INFO, msg, *args, **kwargs) 113 | 114 | def warning(self, msg, *args, **kwargs): 115 | self.log(logging.WARNING, msg, *args, **kwargs) 116 | 117 | def error(self, msg, *args, **kwargs): 118 | self.log(logging.ERROR, msg, *args, **kwargs) 119 | 120 | def critical(self, msg, *args, **kwargs): 121 | self.log(logging.CRITICAL, msg, *args, **kwargs) 122 | 123 | def exception(self, msg, *args, **kwargs): 124 | kwargs['exc_info'] = True 125 | self.log(logging.ERROR, msg, *args, **kwargs) 126 | -------------------------------------------------------------------------------- /lib/gruvi/poll.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Gruvi. Gruvi is free software available under the 3 | # terms of the MIT license. See the file "LICENSE" that was provided 4 | # together with this source file for the licensing terms. 5 | # 6 | # Copyright (c) 2012-2014 the Gruvi authors. See the file "AUTHORS" for a 7 | # complete list. 8 | 9 | from __future__ import absolute_import, print_function 10 | 11 | import pyuv 12 | from .callbacks import add_callback, remove_callback, walk_callbacks 13 | from .callbacks import has_callback, clear_callbacks 14 | 15 | __all__ = ['READABLE', 'WRITABLE'] 16 | 17 | 18 | READABLE = pyuv.UV_READABLE 19 | WRITABLE = pyuv.UV_WRITABLE 20 | 21 | 22 | if __debug__: 23 | 24 | def dump(mp): 25 | print('== Dumping MultiPoll {!r}'.format(mp)) 26 | # [total, nreaders, nwriters, nreadwrite, disabled] 27 | stats = [0, 0, 0, 0, 0] 28 | def walker(callback, args): 29 | stats[0] += 1 30 | if args == READABLE: 31 | stats[1] += 1 32 | elif args == WRITABLE: 33 | stats[2] += 1 34 | elif args == READABLE|WRITABLE: 35 | stats[3] += 1 36 | elif not args: 37 | stats[4] += 1 38 | return True 39 | walk_callbacks(mp, walker) 40 | print('Using Poll: {!r}'.format(mp._poll)) 41 | print('Poll FD: {}'.format(mp._poll.fileno())) 42 | print('Total callbacks: {}'.format(stats[0])) 43 | print('Callbacks registered for read: {}'.format(stats[1])) 44 | print('Callbacks registered for write: {}'.format(stats[2])) 45 | print('Callbacks registered for read|write: {}'.format(stats[3])) 46 | print('Callbacks that are inactive: {}'.format(stats[4])) 47 | 48 | def check(mp): 49 | counts = [0, 0] # [readable, writable] 50 | errors = [0] 51 | def walker(callback, events): 52 | if events & READABLE: 53 | counts[0] += 1 54 | if events & WRITABLE: 55 | counts[1] += 1 56 | if events & ~(READABLE|WRITABLE): 57 | errors[0] += 1 58 | return True 59 | walk_callbacks(mp, walker) 60 | assert mp._readers == counts[0] 61 | assert mp._writers == counts[1] 62 | assert errors[0] == 0 63 | events = (READABLE if counts[0] > 0 else 0) \ 64 | | (WRITABLE if counts[1] > 0 else 0) 65 | assert mp._events == events 66 | 67 | 68 | class MultiPoll(object): 69 | """A version of :class:`pyuv.Poll` that allows connecting multiple readers 70 | and writers to the same file descriptor. 71 | """ 72 | 73 | __slots__ = ('_poll', '_readers', '_writers', '_callbacks', '_events') 74 | 75 | def __init__(self, loop, fd): 76 | self._poll = pyuv.Poll(loop, fd) 77 | self._readers = 0 78 | self._writers = 0 79 | self._callbacks = None 80 | self._events = 0 81 | 82 | def _run_callbacks(self, handle, events, error): 83 | # Run those callbacks that were registered for *events*. 84 | if error: 85 | return 86 | def run_callback(callback, args): 87 | masked = args & events 88 | if masked: 89 | callback(self._poll.fileno(), masked) 90 | return True # Keep callback, poll callbacks are persistent 91 | walk_callbacks(self, run_callback) 92 | 93 | def _sync(self): 94 | # Synchronize the current set of events with the pyuv.Poll object. 95 | events = (READABLE if self._readers > 0 else 0) \ 96 | | (WRITABLE if self._writers > 0 else 0) 97 | if events == self._events: 98 | return 99 | if self._events: 100 | self._poll.stop() 101 | if events: 102 | self._poll.start(events, self._run_callbacks) 103 | self._events = events 104 | 105 | def add_callback(self, events, callback): 106 | """Add a new callback.""" 107 | if self._poll is None: 108 | raise RuntimeError('poll instance is closed') 109 | if events & ~(READABLE|WRITABLE): 110 | raise ValueError('illegal event mask: {}'.format(events)) 111 | if events & READABLE: 112 | self._readers += 1 113 | if events & WRITABLE: 114 | self._writers += 1 115 | handle = add_callback(self, callback, events) 116 | self._sync() 117 | return handle 118 | 119 | def remove_callback(self, handle): 120 | """Remove a callback.""" 121 | if self._poll is None: 122 | raise RuntimeError('poll instance is closed') 123 | remove_callback(self, handle) 124 | if handle.extra & READABLE: 125 | self._readers -= 1 126 | if handle.extra & WRITABLE: 127 | self._writers -= 1 128 | self._sync() 129 | 130 | def update_callback(self, handle, events): 131 | """Update the event mask for a callback.""" 132 | if self._poll is None: 133 | raise RuntimeError('poll instance is closed') 134 | if not has_callback(self, handle): 135 | raise ValueError('no such callback') 136 | if events & ~(READABLE|WRITABLE): 137 | raise ValueError('illegal event mask: {}'.format(events)) 138 | if handle.extra == events: 139 | return 140 | if handle.extra & READABLE: 141 | self._readers -= 1 142 | if handle.extra & WRITABLE: 143 | self._writers -= 1 144 | if events & READABLE: 145 | self._readers += 1 146 | if events & WRITABLE: 147 | self._writers += 1 148 | handle.extra = events 149 | self._sync() 150 | 151 | def close(self): 152 | """Close the poll instance.""" 153 | if self._poll is None: 154 | return 155 | self._poll.close() 156 | self._poll = None 157 | self._readers = 0 158 | self._writers = 0 159 | self._events = 0 160 | clear_callbacks(self) 161 | 162 | 163 | class Poller(object): 164 | """A flexible file descriptor watcher. 165 | 166 | A Poller can watch multiple file descriptors, and each file descriptors can 167 | have multiple callbacks registered to it. 168 | 169 | This object is useful when integrating with third party libraries that 170 | support asynchronous IO by exposing a file descriptor that can be added to 171 | the event loop. 172 | 173 | Normally you should not instantiate this class yourself. Instead, use the 174 | per-loop instance that is available as :attr:`Hub.poller`. 175 | """ 176 | 177 | def __init__(self, loop): 178 | self._loop = loop 179 | self._mpoll = {} 180 | 181 | def add_callback(self, fd, events, callback): 182 | """Add a new callback. 183 | 184 | The file descriptor *fd* will be watched for the events specified by 185 | the *events* parameter, which should be a bitwise OR of the constants 186 | ``READABLE`` and ``WRITABLE``. Whenever one or more of the specified 187 | events occur, *callback* will be called with a single integer argument 188 | containing the bitwise OR of the current events. 189 | 190 | The return value of this method is an opaque handle that can be used 191 | to remove the callback or to update the events associated with it. 192 | """ 193 | if self._mpoll is None: 194 | raise RuntimeError('Poller instance is closed') 195 | try: 196 | mpoll = self._mpoll[fd] 197 | except KeyError: 198 | mpoll = self._mpoll[fd] = MultiPoll(self._loop, fd) 199 | handle = mpoll.add_callback(events, callback) 200 | return handle 201 | 202 | def remove_callback(self, fd, handle): 203 | """Remove a callback added by :meth:`~Poller.add_callback`. 204 | 205 | If this is the last callback that is registered for the fd then this 206 | will deallocate the ``MultiPoll`` instance and close the libuv handle. 207 | """ 208 | if self._mpoll is None: 209 | raise RuntimeError('Poller instance is closed') 210 | mpoll = self._mpoll.get(fd) 211 | if mpoll is None: 212 | raise ValueError('not watching fd {}'.format(fd)) 213 | mpoll.remove_callback(handle) 214 | 215 | def update_callback(self, fd, handle, events): 216 | """Update the event mask associated with an existing callback. 217 | 218 | If you want to temporarily disable a callback then you can use this 219 | method with an *events* argument of ``0``. This is more efficient than 220 | removing the callback and adding it again later. 221 | """ 222 | if self._mpoll is None: 223 | raise RuntimeError('Poller instance is closed') 224 | mpoll = self._mpoll.get(fd) 225 | if mpoll is None: 226 | raise ValueError('not watching fd {}'.format(fd)) 227 | mpoll.update_callback(handle, events) 228 | 229 | def close(self): 230 | """Close all active poll instances and remove all callbacks.""" 231 | if self._mpoll is None: 232 | return 233 | for mpoll in self._mpoll.values(): 234 | mpoll.close() 235 | self._mpoll.clear() 236 | self._mpoll = None 237 | -------------------------------------------------------------------------------- /lib/gruvi/protocols.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Gruvi. Gruvi is free software available under the 3 | # terms of the MIT license. See the file "LICENSE" that was provided 4 | # together with this source file for the licensing terms. 5 | # 6 | # Copyright (c) 2012-2017 the Gruvi authors. See the file "AUTHORS" for a 7 | # complete list. 8 | 9 | from __future__ import absolute_import, print_function 10 | 11 | from . import logging 12 | from .sync import Queue 13 | from .errors import Error 14 | from .hub import get_hub 15 | from .fibers import spawn 16 | 17 | __all__ = ['ProtocolError', 'BaseProtocol', 'Protocol', 'DatagramProtocol', 18 | 'MessageProtocol'] 19 | 20 | 21 | class ProtocolError(Error): 22 | """A protocol error.""" 23 | 24 | 25 | class BaseProtocol(object): 26 | """Base class for all protocols.""" 27 | 28 | def __init__(self, timeout=None): 29 | """The *timeout* argument specifies a default timeout for various 30 | protocol operations.""" 31 | self._timeout = timeout 32 | self._log = logging.get_logger() 33 | self._error = None 34 | 35 | def connection_made(self, transport): 36 | """Called when a connection is made.""" 37 | 38 | def connection_lost(self, exc): 39 | """Called when a connection is lost.""" 40 | 41 | def pause_writing(self): 42 | """Called when the write buffer in the transport has exceeded the high 43 | water mark. The protocol should stop writing new data.""" 44 | 45 | def resume_writing(self): 46 | """Called when the write buffer in the transport has fallen below the 47 | low water mark. The protocol can start writing data again.""" 48 | 49 | 50 | class Protocol(BaseProtocol): 51 | """Base class for connection oriented protocols.""" 52 | 53 | def data_received(self, data): 54 | """Called when a new chunk of data is received.""" 55 | 56 | def eof_received(self): 57 | """Called when an EOF is received.""" 58 | 59 | 60 | class DatagramProtocol(BaseProtocol): 61 | """Base classs for datagram oriented protocols.""" 62 | 63 | def datagram_received(self, data, addr): 64 | """Called when a new datagram is received.""" 65 | 66 | def error_received(self, exc): 67 | """Called when an error has occurred.""" 68 | 69 | 70 | class MessageProtocol(Protocol): 71 | """Base class for message oriented protocols.""" 72 | 73 | max_queue_size = 10 74 | 75 | def __init__(self, message_handler=None, timeout=None): 76 | super(MessageProtocol, self).__init__(timeout=timeout) 77 | self._message_handler = message_handler 78 | self._hub = get_hub() 79 | self._queue = Queue() 80 | self._dispatcher = None 81 | 82 | def _maybe_pause_transport(self): 83 | # Stop transport from calling data_received() if queue is at capacity 84 | if self._queue.qsize() >= self.max_queue_size: 85 | self._transport.pause_reading() 86 | 87 | def _maybe_resume_transport(self): 88 | # Resume data_received if queue has room. 89 | if self._queue.qsize() < self.max_queue_size: 90 | self._transport.resume_reading() 91 | 92 | def _dispatch_loop(self): 93 | # Call message handler for HTTP requests. Runs in a separate fiber. 94 | self._log.debug('dispatcher starting') 95 | try: 96 | while True: 97 | message = self._queue.get() 98 | if self._transport is None: 99 | break 100 | self._maybe_resume_transport() 101 | self._message_handler(message, self._transport, self) 102 | finally: 103 | self._log.debug('dispatcher exiting, closing transport') 104 | if self._transport is not None: 105 | self._transport.close() 106 | 107 | def connection_made(self, transport): 108 | # Protocol callback 109 | self._transport = transport 110 | if self._message_handler: 111 | self._dispatcher = spawn(self._dispatch_loop) 112 | 113 | def connection_lost(self, exc): 114 | # Protocol callback 115 | if self._dispatcher: 116 | self._dispatcher.cancel() 117 | self._transport = None 118 | -------------------------------------------------------------------------------- /lib/gruvi/sslcompat.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Gruvi. Gruvi is free software available under the 3 | # terms of the MIT license. See the file "LICENSE" that was provided 4 | # together with this source file for the licensing terms. 5 | # 6 | # Copyright (c) 2012-2017 the Gruvi authors. See the file "AUTHORS" for a 7 | # complete list. 8 | 9 | from __future__ import absolute_import, print_function 10 | 11 | import sys 12 | import socket 13 | import _ssl 14 | import ssl 15 | import six 16 | 17 | if sys.version_info[:2] < (3, 5): 18 | from . import _sslcompat 19 | 20 | __all__ = ['create_default_context'] 21 | 22 | 23 | if hasattr(ssl, 'CertificateError'): 24 | # Python 2.7.9+ and Python 3.3+ 25 | CertificateError = ssl.CertificateError 26 | 27 | else: 28 | # Python 2.7.x, x <= 8 29 | CertificateError = ValueError 30 | 31 | 32 | if hasattr(ssl, 'SSLContext'): 33 | # Python 2.7.9+ and Python 3.3+ 34 | SSLContext = ssl.SSLContext 35 | 36 | else: 37 | # Python 2.7.x, x <= 8 38 | 39 | class SSLContext(object): 40 | 41 | # This isn't a real SSLContext so it doesn't do things like session 42 | # caching. The purpose is to store keyword arguments to _ssl.sslwrap() 43 | # while having an external interfact that is a subset of the real SSLContext. 44 | 45 | def __init__(self, protocol=ssl.PROTOCOL_SSLv23): 46 | # [keyfile, certfile, cert_reqs, ssl_version, ca_certs, ciphers] 47 | self._sslwrap_args = [None, None, ssl.CERT_NONE, protocol, None, ssl._DEFAULT_CIPHERS] 48 | 49 | @property 50 | def protocol(self): 51 | return self._sslwrap_args[3] 52 | 53 | @property 54 | def verify_mode(self): 55 | return self._sslwrap_args[2] 56 | 57 | @verify_mode.setter 58 | def verify_mode(self, cert_reqs): 59 | self._sslwrap_args[2] = cert_reqs 60 | 61 | def load_verify_locations(self, ca_certs): 62 | self._sslwrap_args[4] = ca_certs 63 | 64 | def load_cert_chain(self, certfile, keyfile): 65 | self._sslwrap_args[0] = keyfile 66 | self._sslwrap_args[1] = certfile 67 | 68 | def set_ciphers(self, ciphers): 69 | self._sslwrap_args[5] = ciphers 70 | 71 | 72 | if hasattr(ssl, 'MemoryBIO'): 73 | # Python 3.5+ 74 | MemoryBIO = ssl.MemoryBIO 75 | 76 | else: 77 | # Python 2.7, Python 3.3/3.4 78 | MemoryBIO = _sslcompat.MemoryBIO 79 | 80 | 81 | if hasattr(ssl, 'SSLObject'): 82 | # Python 3.5+ 83 | SSLObject = ssl.SSLObject 84 | 85 | else: 86 | 87 | # Python 2.7, 3.3 and 3.4 88 | # SSLObject was copied and adapted from Python 3.5 89 | 90 | class SSLObject(object): 91 | """An :class:`ssl.SSLObject` implementation for Python 2.7, 3.3 and 3.4.""" 92 | 93 | def __init__(self, sslobj, context, server_side, server_hostname): 94 | self._sslobj = sslobj 95 | self._context = context 96 | self._server_side = server_side 97 | self._server_hostname = server_hostname 98 | 99 | @property 100 | def context(self): 101 | return self._context 102 | 103 | @context.setter 104 | def context(self, ctx): 105 | if not hasattr(self._sslobj, 'context'): 106 | raise NotImplementedError('operation not supported by the _ssl module') 107 | self._sslobj.context = ctx 108 | self._context = ctx 109 | 110 | @property 111 | def server_side(self): 112 | return self._server_side 113 | 114 | @property 115 | def server_hostname(self): 116 | return self._server_hostname 117 | 118 | def read(self, len=1024, buffer=None): 119 | if buffer is not None: 120 | v = self._sslobj.read(len, buffer) 121 | else: 122 | v = self._sslobj.read(len) 123 | return v 124 | 125 | def write(self, data): 126 | return self._sslobj.write(data) 127 | 128 | def getpeercert(self, binary_form=False): 129 | return self._sslobj.peer_certificate(binary_form) 130 | 131 | def selected_npn_protocol(self): 132 | if hasattr(self._sslobj, 'selected_npn_protocol'): 133 | return self._sslobj.selected_npn_protocol() 134 | 135 | def selected_alpn_protocol(self): 136 | if hasattr(self._sslobj, 'selected_alpn_protocol'): 137 | return self._sslobj.selected_alpn_protocol() 138 | 139 | def cipher(self): 140 | return self._sslobj.cipher() 141 | 142 | def compression(self): 143 | if hasattr(self._sslobj, 'compression'): 144 | return self._sslobj.compression() 145 | 146 | def pending(self): 147 | return self._sslobj.pending() 148 | 149 | def do_handshake(self, block=False): 150 | self._sslobj.do_handshake() 151 | if getattr(self.context, 'check_hostname', False): 152 | if not self.server_hostname: 153 | raise ValueError('check_hostname needs server_hostname argument') 154 | ssl.match_hostname(self.getpeercert(), self.server_hostname) 155 | 156 | def unwrap(self): 157 | self._sslobj.shutdown() 158 | 159 | def get_channel_binding(self, cb_type="tls-unique"): 160 | if cb_type not in getattr(ssl, 'CHANNEL_BINDING_TYPES', []): 161 | raise ValueError('Unsupported channel binding type') 162 | if cb_type != 'tls-unique': 163 | raise NotImplementedError('{0} channel binding not implemented'.format(cb_type)) 164 | return self._sslobj.tls_unique_cb() 165 | 166 | def version(self): 167 | if hasattr(self._sslobj, 'version'): 168 | return self._sslobj.version() 169 | 170 | 171 | def create_default_context(purpose=None, **kwargs): 172 | """Create a new SSL context in the most secure way available on the current 173 | Python version. See :func:`ssl.create_default_context`.""" 174 | if hasattr(ssl, 'create_default_context'): 175 | # Python 2.7.9+, Python 3.4+: take a server_side boolean or None, in 176 | # addition to the ssl.Purpose.XX values. This allows a user to write 177 | # code that works on all supported Python versions. 178 | if purpose is None or purpose is False: 179 | purpose = ssl.Purpose.SERVER_AUTH 180 | elif purpose is True: 181 | purpose = ssl.Purpose.CLIENT_AUTH 182 | return ssl.create_default_context(purpose, **kwargs) 183 | # Python 2.7.8, Python 3.3 184 | context = SSLContext(ssl.PROTOCOL_SSLv23) 185 | if kwargs.get('cafile'): 186 | context.load_verify_locations(kwargs['cafile']) 187 | return context 188 | 189 | 190 | def get_dummy_socket(): 191 | # Return a dummy socket that can be wrapped by _ssl, before we replace the 192 | # socket BIO with our backport of the memory BIO. 193 | sock = socket.socket() 194 | if six.PY2: 195 | sock = sock._sock 196 | sock.setblocking(False) 197 | return sock 198 | 199 | _sock = None 200 | 201 | 202 | def wrap_bio(ctx, incoming, outgoing, server_side=False, server_hostname=None): 203 | # Create a new SSL protocol instance from a context and a BIO pair. 204 | if hasattr(ctx, 'wrap_bio'): 205 | # Python 3.5+ 206 | return ctx.wrap_bio(incoming, outgoing, server_side, server_hostname) 207 | # Allocate a single global dummy socket to wrap on Python < 3.5 208 | global _sock 209 | if _sock is None: 210 | _sock = get_dummy_socket() 211 | if hasattr(ctx, '_wrap_socket'): 212 | # Python 2.7.9+, Python 3.3+ 213 | sslobj = ctx._wrap_socket(_sock, server_side, server_hostname) 214 | else: 215 | # Python 2.7.x, x <= 8 216 | sslobj = _ssl.sslwrap(_sock, server_side, *ctx._sslwrap_args) 217 | _sslcompat.replace_bio(sslobj, incoming, outgoing) 218 | pyobj = SSLObject(sslobj, ctx, server_side, server_hostname) 219 | return pyobj 220 | 221 | 222 | def get_reason(exc): 223 | # Return the reason code from an SSLError exception. 224 | # On Python 3.x the reason is available via exc.reason. 225 | if hasattr(exc, 'reason'): 226 | return exc.reason 227 | # .. but on 2.x we have to parse the error string (fortunately it is there) 228 | message = exc.args[1] 229 | p0 = message.find('error:') 230 | if p0 == -1: 231 | return 232 | p0 += 6 233 | p1 = message.find(':', p0) 234 | assert p1 != -1 235 | code = int(message[p0:p1], 16) & 0xfff 236 | return _sslcompat.errorcode.get(code, message[p0:p1]) 237 | -------------------------------------------------------------------------------- /lib/gruvi/util.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Gruvi. Gruvi is free software available under the 3 | # terms of the MIT license. See the file "LICENSE" that was provided 4 | # together with this source file for the licensing terms. 5 | # 6 | # Copyright (c) 2012-2017 the Gruvi authors. See the file "AUTHORS" for a 7 | # complete list. 8 | 9 | from __future__ import absolute_import, print_function 10 | 11 | import os 12 | import sys 13 | import re 14 | import functools 15 | 16 | from weakref import WeakKeyDictionary 17 | 18 | __all__ = [] 19 | 20 | 21 | class EnvBool(object): 22 | """A boolean-like object that takes its value from the environment variable 23 | *name*.""" 24 | 25 | _zero_values = ('0', 'n', 'f', 'false') 26 | 27 | def __init__(self, name): 28 | self._name = name 29 | 30 | @classmethod 31 | def new(cls, name): 32 | # Optimize away $DEBUG when interpreter is running with -O 33 | if name == 'DEBUG' and not __debug__: 34 | return False 35 | return cls(name) 36 | 37 | def __nonzero__(self): 38 | value = os.environ.get(self._name, '0') 39 | return value.lower() not in self._zero_values 40 | 41 | __bool__ = __nonzero__ 42 | 43 | 44 | class AbsentType(object): 45 | """A type that represents the absence of a value. 46 | 47 | Useful in parsing protocols where there's a difference between a NULL 48 | value, and an absent value. 49 | """ 50 | 51 | def __nonzero__(self): 52 | return False 53 | 54 | __bool__ = __nonzero__ 55 | 56 | def __repr__(self): 57 | return 'Absent' 58 | 59 | __str__ = __repr__ 60 | 61 | Absent = AbsentType() 62 | 63 | 64 | def docfrom(base): 65 | """Decorator to set a function's docstring from another function.""" 66 | def setdoc(func): 67 | func.__doc__ = (getattr(base, '__doc__') or '') + (func.__doc__ or '') 68 | return func 69 | return setdoc 70 | 71 | 72 | _objrefs = WeakKeyDictionary() # obj -> objref 73 | _lastids = {} # classname -> lastid 74 | 75 | def objref(obj): 76 | """Return a string that uniquely and compactly identifies an object.""" 77 | ref = _objrefs.get(obj) 78 | if ref is None: 79 | clsname = obj.__class__.__name__.split('.')[-1] 80 | seqno = _lastids.setdefault(clsname, 1) 81 | ref = '{}-{}'.format(clsname, seqno) 82 | _objrefs[obj] = ref 83 | _lastids[clsname] += 1 84 | return ref 85 | 86 | 87 | re_lu = re.compile('[A-Z]+[a-z0-9]+') 88 | 89 | def split_cap_words(s): 90 | """Split the CamelCase string *s* into words.""" 91 | return re_lu.findall(s) 92 | 93 | 94 | def delegate_method(other, method, name=None): 95 | """Add a method to the current class that delegates to another method. 96 | 97 | The *other* argument must be a property that returns the instance to 98 | delegate to. Due to an implementation detail, the property must be defined 99 | in the current class. The *method* argument specifies a method to delegate 100 | to. It can be any callable as long as it takes the instances as its first 101 | argument. 102 | 103 | It is a common paradigm in Gruvi to expose protocol methods onto clients. 104 | This keeps most of the logic into the protocol, but prevents the user from 105 | having to type ``'client.protocol.*methodname*'`` all the time. 106 | 107 | For example:: 108 | 109 | class MyClient(Client): 110 | 111 | protocol = Client.protocol 112 | 113 | delegate_method(protocol, MyProtocol.method) 114 | """ 115 | frame = sys._getframe(1) 116 | classdict = frame.f_locals 117 | 118 | @functools.wraps(method) 119 | def delegate(self, *args, **kwargs): 120 | other_self = other.__get__(self) 121 | return method(other_self, *args, **kwargs) 122 | 123 | if getattr(method, '__switchpoint__', False): 124 | delegate.__switchpoint__ = True 125 | 126 | if name is None: 127 | name = method.__name__ 128 | propname = None 129 | for key in classdict: 130 | if classdict[key] is other: 131 | propname = key 132 | break 133 | # If we know the property name, replace the docstring with a small 134 | # reference instead of copying the function docstring. 135 | if propname: 136 | qname = getattr(method, '__qualname__', method.__name__) 137 | if '.' in qname: 138 | delegate.__doc__ = 'A shorthand for ``self.{propname}.{name}()``.' \ 139 | .format(name=name, propname=propname) 140 | else: 141 | delegate.__doc__ = 'A shorthand for ``{name}({propname}, ...)``.' \ 142 | .format(name=name, propname=propname) 143 | classdict[name] = delegate 144 | -------------------------------------------------------------------------------- /lib/gruvi/vendor.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Gruvi. Gruvi is free software available under the 3 | # terms of the MIT license. See the file "LICENSE" that was provided 4 | # together with this source file for the licensing terms. 5 | # 6 | # Copyright (c) 2012-2017 the Gruvi authors. See the file "AUTHORS" for a 7 | # complete list. 8 | 9 | from __future__ import absolute_import, print_function 10 | 11 | 12 | from os.path import abspath, split, join, exists 13 | 14 | def up(path): 15 | return split(path)[0] 16 | 17 | pathdir = up(up(abspath(__file__))) 18 | vendordir = join(pathdir, 'gruvi_vendor') 19 | 20 | if not exists(vendordir): 21 | # This is a development install. The setuptools "develop" command appears 22 | # not to correctly handle multiple entries in package_dir. Therefore, pull 23 | # in the vendorized packages directly from the top of the source tree. 24 | vendordir = join(up(pathdir), 'vendor') 25 | 26 | __package__ = 'vendor' 27 | __path__ = [vendordir] 28 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | cffi >= 1.0 2 | fibers >= 1.0 3 | pyuv >= 1.1 4 | six 5 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Gruvi. Gruvi is free software available under the 3 | # terms of the MIT license. See the file "LICENSE" that was provided 4 | # together with this source file for the licensing terms. 5 | # 6 | # Copyright (c) 2012-2014 the Gruvi authors. See the file "AUTHORS" for a 7 | # complete list. 8 | 9 | from __future__ import absolute_import, print_function 10 | 11 | import os 12 | import sys 13 | 14 | from argparse import ArgumentParser 15 | from unittest import TestLoader, TextTestRunner, TestSuite 16 | 17 | 18 | parser = ArgumentParser() 19 | parser.add_argument('-v', '--verbose', help='be more verbose', action='count', default=1) 20 | parser.add_argument('-f', '--failfast', help='stop on first failure', action='store_true') 21 | parser.add_argument('-b', '--buffer', help='buffer stdout and stderr', action='store_true') 22 | parser.add_argument('suite', nargs='+', help='name of test suite to run', metavar='suite', 23 | choices=('all', 'unit', 'performance', 'memory', 'examples')) 24 | args = parser.parse_args() 25 | 26 | if 'all' in args.suite: 27 | args.suite = ['unit', 'performance', 'memory', 'examples'] 28 | 29 | # $VERBOSE can override -v 30 | try: 31 | verbose = int(os.environ['VERBOSE']) 32 | except (KeyError, ValueError): 33 | verbose = args.verbose 34 | os.environ['VERBOSE'] = str(verbose) 35 | 36 | # Change directory to tests/ irrespective of where we're called from. 37 | topdir = os.path.split(os.path.abspath(__file__))[0] 38 | testdir = os.path.join(topdir, 'tests') 39 | os.chdir(testdir) 40 | 41 | sys.path.insert(0, testdir) 42 | from support import TestCase, MemoryTest, PerformanceTest 43 | 44 | suite = TestSuite() 45 | 46 | for name in args.suite: 47 | TestCase.setup_loader() 48 | if name == 'unit': 49 | pattern = 'test_*.py' 50 | elif name == 'performance': 51 | pattern = 'perf_*.py' 52 | PerformanceTest.setup_loader() 53 | PerformanceTest.start_new_results() 54 | elif name == 'memory': 55 | pattern = 'memory.py' 56 | MemoryTest.setup_loader() 57 | MemoryTest.start_new_results() 58 | elif name == 'examples': 59 | pattern = 'examples.py' 60 | loader = TestLoader() 61 | tests = loader.discover('.', pattern) 62 | suite.addTest(tests) 63 | 64 | runner = TextTestRunner(verbosity=verbose, buffer=args.buffer, failfast=args.failfast) 65 | result = runner.run(suite) 66 | if result.errors or result.failures: 67 | sys.exit(1) 68 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [build_ext] 2 | #inplace=1 3 | 4 | [flake8] 5 | ignore = E126,E127,E128,E226,E227,E301,E302,E306,E702 6 | exclude = build,work,trash,txdbus,docs/conf.py,__pycache__,.git 7 | max-line-length = 99 8 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of gruvi. Gruvi is free software available under the 3 | # terms of the MIT license. See the file "LICENSE" that was provided 4 | # together with this source file for the licensing terms. 5 | # 6 | # Copyright (c) 2012-2017 the gruvi authors. See the file "AUTHORS" for a 7 | # complete list. 8 | 9 | from __future__ import absolute_import, print_function 10 | 11 | import os 12 | import sys 13 | 14 | from setuptools import setup, Extension 15 | 16 | 17 | version_info = { 18 | 'name': 'gruvi', 19 | 'version': '0.10.3', 20 | 'description': 'Pythonic async IO with libuv and fibers', 21 | 'author': 'Geert Jansen', 22 | 'author_email': 'geertj@gmail.com', 23 | 'url': 'https://github.com/geertj/gruvi', 24 | 'license': 'MIT', 25 | 'classifiers': [ 26 | 'Development Status :: 4 - Beta', 27 | 'License :: OSI Approved :: MIT License', 28 | 'Operating System :: POSIX', 29 | 'Operating System :: Microsoft :: Windows', 30 | 'Operating System :: MacOS :: MacOS X', 31 | 'Programming Language :: Python :: 2.7', 32 | 'Programming Language :: Python :: 3.5', 33 | 'Programming Language :: Python :: 3.6' 34 | ] 35 | } 36 | 37 | topdir, _ = os.path.split(os.path.abspath(__file__)) 38 | 39 | 40 | def get_requirements(): 41 | """Parse a requirements.txt file and return as a list.""" 42 | with open(os.path.join(topdir, 'requirements.txt')) as fin: 43 | lines = fin.readlines() 44 | lines = [line.strip() for line in lines] 45 | return lines 46 | 47 | 48 | def main(): 49 | os.chdir(topdir) 50 | ext_modules = [] 51 | # Note that on Windows it's more involved to compile _sslcompat because 52 | # there's no system provided OpenSSL and you need to match the version that 53 | # was used to compile your Python. 54 | if sys.version_info[:2] < (3, 5): 55 | ext_modules.append(Extension('_sslcompat', ['src/sslcompat.c'], 56 | libraries=['ssl', 'crypto'])) 57 | setup( 58 | packages=['gruvi', 'gruvi_vendor.txdbus'], 59 | package_dir={'': 'lib', 'gruvi_vendor': 'vendor'}, 60 | setup_requires=['cffi >= 1.0.0'], 61 | install_requires=get_requirements(), 62 | cffi_modules=['src/build_http.py:ffi', 'src/build_jsonrpc.py:ffi'], 63 | ext_package='gruvi', 64 | ext_modules=ext_modules, 65 | zip_safe=False, 66 | **version_info 67 | ) 68 | 69 | 70 | if __name__ == '__main__': 71 | main() 72 | -------------------------------------------------------------------------------- /src/build_http.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Gruvi. Gruvi is free software available under the 3 | # terms of the MIT license. See the file "LICENSE" that was provided 4 | # together with this source file for the licensing terms. 5 | # 6 | # Copyright (c) 2012-2017 the Gruvi authors. See the file "AUTHORS" for a 7 | # complete list. 8 | 9 | from __future__ import absolute_import, print_function 10 | 11 | import os.path 12 | from cffi import FFI 13 | 14 | parent, _ = os.path.split(os.path.abspath(__file__)) 15 | topdir, _ = os.path.split(parent) 16 | 17 | 18 | ffi = FFI() 19 | 20 | ffi.set_source('http_ffi', """ 21 | #include 22 | #include 23 | #include "src/http_parser.h" 24 | #include "src/http_parser.c" 25 | 26 | unsigned char http_message_type(http_parser *p) { return p->type; } 27 | unsigned int http_status_code(http_parser *p) { return p->status_code; } 28 | unsigned int http_method(http_parser *p) { return p->method; } 29 | unsigned char http_errno(http_parser *p) { return p->http_errno; } 30 | unsigned char http_is_upgrade(http_parser *p) { return p->upgrade; } 31 | """, include_dirs=[topdir]) 32 | 33 | ffi.cdef(""" 34 | int strcasecmp(const char *, const char *); 35 | 36 | typedef struct http_parser http_parser; 37 | typedef struct http_parser_settings http_parser_settings; 38 | 39 | typedef int (*http_data_cb) (http_parser*, const char *at, size_t length); 40 | typedef int (*http_cb) (http_parser*); 41 | 42 | enum http_parser_type { HTTP_REQUEST, HTTP_RESPONSE, HTTP_BOTH, ... }; 43 | 44 | struct http_parser { 45 | unsigned short http_major; 46 | unsigned short http_minor; 47 | void *data; 48 | ...; 49 | }; 50 | 51 | struct http_parser_settings { 52 | http_cb on_message_begin; 53 | http_data_cb on_url; 54 | http_data_cb on_status; 55 | http_data_cb on_header_field; 56 | http_data_cb on_header_value; 57 | http_cb on_headers_complete; 58 | http_data_cb on_body; 59 | http_cb on_message_complete; 60 | ...; 61 | }; 62 | 63 | enum http_parser_url_fields { UF_SCHEMA, UF_HOST, UF_PORT, UF_PATH, 64 | UF_QUERY, UF_FRAGMENT, UF_USERINFO, UF_MAX }; 65 | 66 | struct http_parser_url { 67 | uint16_t field_set; 68 | uint16_t port; 69 | 70 | struct { 71 | uint16_t off; 72 | uint16_t len; 73 | } field_data[UF_MAX]; 74 | ...; 75 | }; 76 | 77 | void http_parser_init(http_parser *parser, enum http_parser_type type); 78 | size_t http_parser_execute(http_parser *parser, 79 | const http_parser_settings *settings, 80 | const char *data, 81 | size_t len); 82 | 83 | int http_should_keep_alive(const http_parser *parser); 84 | const char *http_method_str(enum http_method m); 85 | const char *http_errno_name(enum http_errno err); 86 | 87 | void http_parser_url_init(struct http_parser_url *u); 88 | int http_parser_parse_url(const char *buf, size_t buflen, 89 | int is_connect, struct http_parser_url *u); 90 | 91 | /* Extra functions to extract bitfields not supported by cffi */ 92 | unsigned char http_message_type(http_parser *parser); 93 | unsigned int http_status_code(http_parser *parser); 94 | unsigned int http_method(http_parser *parser); 95 | unsigned char http_errno(http_parser *parser); 96 | unsigned char http_is_upgrade(http_parser *parser); 97 | 98 | """) 99 | 100 | 101 | if __name__ == '__main__': 102 | ffi.compile() 103 | -------------------------------------------------------------------------------- /src/build_jsonrpc.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Gruvi. Gruvi is free software available under the 3 | # terms of the MIT license. See the file "LICENSE" that was provided 4 | # together with this source file for the licensing terms. 5 | # 6 | # Copyright (c) 2012-2015 the Gruvi authors. See the file "AUTHORS" for a 7 | # complete list. 8 | 9 | from __future__ import absolute_import, print_function 10 | 11 | import os.path 12 | from cffi import FFI 13 | 14 | parent, _ = os.path.split(os.path.abspath(__file__)) 15 | topdir, _ = os.path.split(parent) 16 | 17 | 18 | ffi = FFI() 19 | 20 | ffi.set_source('jsonrpc_ffi', """ 21 | #include "src/json_splitter.c" 22 | """, include_dirs=[topdir]) 23 | 24 | ffi.cdef(""" 25 | #define OK ... 26 | #define INCOMPLETE ... 27 | #define ERROR ... 28 | 29 | struct split_context { 30 | const char *buf; 31 | int buflen; 32 | int offset; 33 | int error; 34 | ...; 35 | }; 36 | 37 | int json_split(struct split_context *ctx); 38 | """) 39 | 40 | 41 | if __name__ == '__main__': 42 | ffi.compile() 43 | -------------------------------------------------------------------------------- /src/json_splitter.c: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Gruvi. Gruvi is free software available under the 3 | * terms of the MIT license. See the file "LICENSE" that was provided 4 | * together with this source file for the licensing terms. 5 | * 6 | * Copyright (c) 2012-2013 the Gruvi authors. See the file "AUTHORS" for a 7 | * complete list. 8 | * 9 | * This file contains a fast incremental JSON splitter. It is exposed to 10 | * Python via CFFI. 11 | */ 12 | 13 | #include 14 | #include 15 | 16 | #define OK 0 17 | #define INCOMPLETE 1 18 | #define ERROR 2 19 | 20 | enum state { s_preamble = 0, s_object, s_string, s_string_escape }; 21 | 22 | struct split_context 23 | { 24 | const char *buf; 25 | int buflen; 26 | int offset; 27 | int state; 28 | int depth; 29 | int error; 30 | }; 31 | 32 | int json_split(struct split_context *ctx) 33 | { 34 | char ch; 35 | 36 | ctx->error = 0; 37 | while (ctx->offset < ctx->buflen) 38 | { 39 | ch = ctx->buf[ctx->offset]; 40 | 41 | switch (ctx->state) 42 | { 43 | case s_preamble: 44 | if (ch == '{') { 45 | ctx->state = s_object; 46 | ctx->depth = 1; 47 | } else if (!isspace(ch)) 48 | ctx->error = ERROR; 49 | break; 50 | case s_object: 51 | if (ch == '{') 52 | ctx->depth += 1; 53 | else if (ch == '}') { 54 | ctx->depth -= 1; 55 | } else if (ch == '"') 56 | ctx->state = s_string; 57 | break; 58 | case s_string: 59 | if (ch == '"') 60 | ctx->state = s_object; 61 | else if (ch == '\\') 62 | ctx->state = s_string_escape; 63 | break; 64 | case s_string_escape: 65 | ctx->state = s_string; 66 | break; 67 | } 68 | 69 | if (ctx->error) 70 | break; 71 | ctx->offset += 1; 72 | 73 | if (ctx->state == s_object && ctx->depth == 0) 74 | break; 75 | } 76 | 77 | if (!ctx->error) { 78 | if (ctx->state == s_object && ctx->depth == 0) 79 | ctx->state = s_preamble; 80 | else 81 | ctx->error = INCOMPLETE; 82 | } 83 | 84 | return ctx->error; 85 | } 86 | -------------------------------------------------------------------------------- /tasks.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Gruvi. Gruvi is free software available under the 3 | # terms of the MIT license. See the file "LICENSE" that was provided 4 | # together with this source file for the licensing terms. 5 | # 6 | # Copyright (c) 2012-2014 the Gruvi authors. See the file "AUTHORS" for a 7 | # complete list. 8 | 9 | from __future__ import absolute_import, print_function 10 | 11 | from invoke import run, task 12 | 13 | 14 | @task 15 | def clean(ctx): 16 | run('find . -name __pycache__ | xargs rm -rf || :', echo=True) 17 | run('find . -name \*.so | xargs rm -f', echo=True) 18 | run('find . -name \*.pyc | xargs rm -f', echo=True) 19 | run('find . -name \*.egg-info | xargs rm -rf', echo=True) 20 | run('rm -rf build dist', echo=True) 21 | run('rm -rf docs/_build/*', echo=True) 22 | 23 | 24 | @task(clean) 25 | def develop(ctx): 26 | run('python setup.py build', echo=True) 27 | if develop: 28 | run('python setup.py develop', echo=True) 29 | 30 | 31 | @task 32 | def checksdist(ctx): 33 | from setup import version_info 34 | run('git ls-files | sort > files.git') 35 | run('rm -rf lib/*.egg-info') 36 | run('python setup.py sdist >/dev/null 2>&1') 37 | run('tar tfz dist/{name}-{version}.tar.gz' 38 | ' | sed -e \'s/^{name}-{version}\///\' -e \'/\/$/d\' -e \'/^$/d\'' 39 | ' | sort > files.sdist'.format(**version_info)) 40 | run('diff -u files.git files.sdist || true') 41 | run('rm files.git; rm files.sdist') 42 | 43 | 44 | @task 45 | def buildwheels(ctx): 46 | run('tox -e py27 -- python setup.py bdist_wheel') 47 | run('tox -e py35 -- python setup.py bdist_wheel') 48 | run('tox -e py36 -- python setup.py bdist_wheel') 49 | -------------------------------------------------------------------------------- /tests/bin/catn: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import sys 5 | 6 | fd = sys.argv[1] if len(sys.argv) > 1 else '0' 7 | fdout = sys.argv[2] if len(sys.argv) > 2 else fd if len(sys.argv) > 1 else '1' 8 | 9 | if len(sys.argv) > 3 or not fd.isdigit() or not fdout.isdigit(): 10 | sys.stderr.write('Usage: catn [ []]\n') 11 | sys.exit(1) 12 | 13 | fd = int(fd) 14 | fdout = int(fdout) 15 | 16 | while True: 17 | buf = os.read(fd, 4096) 18 | if not buf: 19 | break 20 | nbytes = 0 21 | while nbytes < len(buf): 22 | nbytes += os.write(fdout, buf[nbytes:]) 23 | 24 | sys.exit(0) 25 | -------------------------------------------------------------------------------- /tests/bin/echo: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import sys 5 | 6 | for i in range(len(sys.argv)-1): 7 | arg = sys.argv[i+1] 8 | if arg.startswith('$'): 9 | arg = os.environ.get(arg[1:], '') 10 | if i > 0: 11 | sys.stdout.write(' ') 12 | sys.stdout.write(arg) 13 | 14 | sys.stdout.write('\n') 15 | sys.exit(0) 16 | -------------------------------------------------------------------------------- /tests/bin/exitn: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | 5 | acc = 0 6 | for arg in sys.argv[1:]: 7 | if not arg.isdigit(): 8 | sys.stderr.write('Usage: exitn []...\n') 9 | sys.exit(1) 10 | acc += int(arg) 11 | sys.exit(acc) 12 | -------------------------------------------------------------------------------- /tests/bin/false: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | sys.exit(1) 5 | -------------------------------------------------------------------------------- /tests/bin/fortune: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | fortune = """\ 4 | My religion consists of a humble admiration of the illimitable superior 5 | spirit who reveals himself in the slight details we are able to perceive 6 | with our frail and feeble mind. 7 | -- Albert Einstein 8 | """ 9 | 10 | import sys 11 | sys.stdout.write(fortune) 12 | sys.exit(0) 13 | -------------------------------------------------------------------------------- /tests/bin/pwd: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | print(os.getcwd()) 5 | -------------------------------------------------------------------------------- /tests/bin/sleep: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | import time 5 | 6 | try: 7 | delay = float(sys.argv[1]) 8 | except (IndexError, ValueError): 9 | sys.stderr.write('Usage: sleep \n') 10 | sys.exit(1) 11 | 12 | time.sleep(delay) 13 | sys.exit(0) 14 | -------------------------------------------------------------------------------- /tests/bin/true: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | sys.exit(0) 5 | -------------------------------------------------------------------------------- /tests/examples.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Gruvi. Gruvi is free software available under the 3 | # terms of the MIT license. See the file "LICENSE" that was provided 4 | # together with this source file for the licensing terms. 5 | # 6 | # Copyright (c) 2012-2017 the Gruvi authors. See the file "AUTHORS" for a 7 | # complete list. 8 | 9 | from __future__ import absolute_import, print_function 10 | 11 | import os 12 | import sys 13 | import signal 14 | import unittest 15 | 16 | from support import TestCase 17 | 18 | import gruvi 19 | from gruvi import Process, PIPE, DEVNULL, StreamClient, HttpClient 20 | 21 | 22 | class TestExamples(TestCase): 23 | # Tests for the examples in examples/* 24 | 25 | @classmethod 26 | def setUpClass(cls): 27 | super(TestExamples, cls).setUpClass() 28 | exampledir = os.path.join(cls.topdir, 'examples') 29 | os.chdir(exampledir) 30 | 31 | def test_curl(self): 32 | proc = Process(encoding='utf-8') 33 | proc.spawn([sys.executable, 'curl.py', 'http://www.python.org'], stdout=PIPE) 34 | stdout, stderr = proc.communicate(timeout=30) 35 | self.assertEqual(proc.returncode, 0) 36 | self.assertEqual(stdout, '') 37 | proc.close() 38 | 39 | def test_curl_ssl(self): 40 | proc = Process(encoding='utf-8') 41 | proc.spawn([sys.executable, 'curl.py', 'https://www.python.org'], stdout=PIPE) 42 | stdout, stderr = proc.communicate(timeout=30) 43 | self.assertEqual(proc.returncode, 0) 44 | self.assertTrue('s close_notify alert 108 | # Communicate the close_notify to the server. 109 | serverssl, appdata = server.feed_ssldata(clientssl[0]) 110 | # b'' means close notify: acknowledge it 111 | self.assertEqual(serverssl, []) 112 | self.assertEqual(appdata, [b'']) 113 | serverssl = server.shutdown() 114 | # Now we should have a close_notify 115 | self.assertEqual(len(serverssl), 1) 116 | self.assertGreater(len(serverssl[0]), 0) # the s->c close_notify 117 | self.assertEqual(appdata, [b'']) 118 | self.assertFalse(server.wrapped) 119 | # Send back the server response to the client. 120 | clientssl, appdata = client.feed_ssldata(serverssl[0]) 121 | self.assertEqual(clientssl, []) 122 | self.assertEqual(appdata, [b'']) 123 | self.assertFalse(client.wrapped) 124 | 125 | def test_unwrapped(self): 126 | # Send data over an unencrypted channel. 127 | client, server = self.client, self.server 128 | buf = b'x' * 1000 129 | received = communicate(buf, client, server, [], []) 130 | self.assertEqual(received, buf) 131 | 132 | def test_unwrapped_after_wrapped(self): 133 | # Send data over SSL, then unwrap, and send data in the clear. 134 | client, server = self.client, self.server 135 | buf = b'x' * 1000 136 | # Send some data in the clear. 137 | received = communicate(buf, client, server, [], []) 138 | self.assertEqual(received, buf) 139 | # Now start the handshake and send some encrypted data. 140 | clientssl = client.do_handshake() 141 | server.do_handshake() 142 | received = communicate(buf, client, server, clientssl, []) 143 | self.assertTrue(client.wrapped) 144 | self.assertTrue(server.wrapped) 145 | self.assertEqual(received, buf) 146 | # Move back to clear again. 147 | clientssl = client.shutdown() 148 | received = communicate(buf, client, server, clientssl, []) 149 | self.assertEqual(received, b'') 150 | serverssl = server.shutdown() 151 | received = communicate(buf, client, server, [], serverssl) 152 | self.assertFalse(client.wrapped) 153 | self.assertFalse(server.wrapped) 154 | self.assertEqual(received, buf) 155 | # And back to encrypted again.. 156 | clientssl = client.do_handshake() 157 | server.do_handshake() 158 | received = communicate(buf, client, server, clientssl, []) 159 | self.assertTrue(client.wrapped) 160 | self.assertTrue(server.wrapped) 161 | self.assertEqual(received, buf) 162 | 163 | def test_simultaneous_shutdown(self): 164 | # Test a simultaenous shutdown. 165 | client, server = self.client, self.server 166 | buf = b'x' * 1000 167 | # Start an encrypted session. 168 | clientssl = client.do_handshake() 169 | server.do_handshake() 170 | received = communicate(buf, client, server, clientssl, []) 171 | self.assertEqual(received, buf) 172 | # Tear it down concurrently. 173 | clientssl = client.shutdown() 174 | serverssl = server.shutdown() 175 | received = communicate(buf, client, server, clientssl, serverssl) 176 | self.assertFalse(client.wrapped) 177 | self.assertFalse(server.wrapped) 178 | self.assertEqual(received, buf) # this was sent in the clear 179 | 180 | 181 | class SslTransportTest(TransportTest): 182 | 183 | def setUp(self): 184 | if not os.access(self.cafile, os.R_OK): 185 | raise SkipTest('no certificate available') 186 | super(SslTransportTest, self).setUp() 187 | 188 | def create_transport(self, handle, protocol, server_side): 189 | context = self.server_context if server_side else self.client_context 190 | ssl_args = {} 191 | if not server_side: 192 | ssl_args['server_hostname'] = 'localhost' 193 | transport = SslTransport(handle, context, server_side, **ssl_args) 194 | transport.start(protocol) 195 | return transport 196 | 197 | 198 | class TestSslTcpTransport(SslTransportTest, EventLoopTest): 199 | 200 | def create_handle(self): 201 | return pyuv.TCP(self.loop) 202 | 203 | def bind_handle(self, handle): 204 | host = socket.gethostbyname('localhost') 205 | handle.bind((host, 0)) 206 | return handle.getsockname() 207 | 208 | 209 | class TestSslPipeTransport(SslTransportTest, EventLoopTest): 210 | 211 | def create_handle(self): 212 | return pyuv.Pipe(self.loop) 213 | 214 | def bind_handle(self, handle): 215 | addr = self.pipename('test-pipe') 216 | handle.bind(addr) 217 | return addr 218 | 219 | 220 | if __name__ == '__main__': 221 | unittest.main() 222 | -------------------------------------------------------------------------------- /tests/test_sslcompat.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Gruvi. Gruvi is free software available under the 3 | # terms of the MIT license. See the file "LICENSE" that was provided 4 | # together with this source file for the licensing terms. 5 | # 6 | # Copyright (c) 2012-2014 the Gruvi authors. See the file "AUTHORS" for a 7 | # complete list. 8 | 9 | from __future__ import absolute_import, print_function 10 | 11 | import six 12 | import unittest 13 | 14 | from gruvi.sslcompat import MemoryBIO 15 | from support import UnitTest 16 | 17 | 18 | class MemoryBIOTests(UnitTest): 19 | 20 | def test_read_write(self): 21 | bio = MemoryBIO() 22 | bio.write(b'foo') 23 | self.assertEqual(bio.read(), b'foo') 24 | self.assertEqual(bio.read(), b'') 25 | bio.write(b'foo') 26 | bio.write(b'bar') 27 | self.assertEqual(bio.read(), b'foobar') 28 | self.assertEqual(bio.read(), b'') 29 | bio.write(b'baz') 30 | self.assertEqual(bio.read(2), b'ba') 31 | self.assertEqual(bio.read(1), b'z') 32 | self.assertEqual(bio.read(1), b'') 33 | 34 | def test_eof(self): 35 | bio = MemoryBIO() 36 | self.assertFalse(bio.eof) 37 | self.assertEqual(bio.read(), b'') 38 | self.assertFalse(bio.eof) 39 | bio.write(b'foo') 40 | self.assertFalse(bio.eof) 41 | bio.write_eof() 42 | self.assertFalse(bio.eof) 43 | self.assertEqual(bio.read(2), b'fo') 44 | self.assertFalse(bio.eof) 45 | self.assertEqual(bio.read(1), b'o') 46 | self.assertTrue(bio.eof) 47 | self.assertEqual(bio.read(), b'') 48 | self.assertTrue(bio.eof) 49 | 50 | def test_pending(self): 51 | bio = MemoryBIO() 52 | self.assertEqual(bio.pending, 0) 53 | bio.write(b'foo') 54 | self.assertEqual(bio.pending, 3) 55 | for i in range(3): 56 | bio.read(1) 57 | self.assertEqual(bio.pending, 3-i-1) 58 | for i in range(3): 59 | bio.write(b'x') 60 | self.assertEqual(bio.pending, i+1) 61 | bio.read() 62 | self.assertEqual(bio.pending, 0) 63 | 64 | def test_buffer_types(self): 65 | bio = MemoryBIO() 66 | bio.write(b'foo') 67 | self.assertEqual(bio.read(), b'foo') 68 | bio.write(bytearray(b'bar')) 69 | self.assertEqual(bio.read(), b'bar') 70 | bio.write(memoryview(b'baz')) 71 | self.assertEqual(bio.read(), b'baz') 72 | 73 | def test_error_types(self): 74 | bio = MemoryBIO() 75 | if six.PY3: 76 | self.assertRaises(TypeError, bio.write, 'foo') 77 | self.assertRaises(TypeError, bio.write, None) 78 | self.assertRaises(TypeError, bio.write, True) 79 | self.assertRaises(TypeError, bio.write, 1) 80 | 81 | 82 | if __name__ == '__main__': 83 | unittest.main() 84 | -------------------------------------------------------------------------------- /tests/test_transports.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Gruvi. Gruvi is free software available under the 3 | # terms of the MIT license. See the file "LICENSE" that was provided 4 | # together with this source file for the licensing terms. 5 | # 6 | # Copyright (c) 2012-2017 the Gruvi authors. See the file "AUTHORS" for a 7 | # complete list. 8 | 9 | from __future__ import absolute_import, print_function 10 | 11 | import socket 12 | import unittest 13 | import pyuv 14 | 15 | from support import UnitTest 16 | from gruvi.transports import TransportError, Transport, DatagramTransport 17 | 18 | 19 | class ProtocolLogger(object): 20 | """Utility protocol class that implements both the stream and datagram 21 | protocols and that logs all callbacks.""" 22 | 23 | def __init__(self): 24 | self.events = [] 25 | self.transport = None 26 | 27 | def get_events(self, typ): 28 | return [ev for ev in self.events if ev[0] == typ] 29 | 30 | def connection_made(self, transport): 31 | self.events.append(('connection_made', transport)) 32 | self.transport = transport 33 | 34 | def data_received(self, data): 35 | # collapse data_received for easier verification 36 | if self.events[-1][0] == 'data_received': 37 | self.events[-1] = ('data_received', self.events[-1][1] + data) 38 | else: 39 | self.events.append(('data_received', data)) 40 | 41 | def eof_received(self): 42 | self.events.append(('eof_received',)) 43 | 44 | def connection_lost(self, exc=None): 45 | self.events.append(('connection_lost', exc)) 46 | 47 | def datagram_received(self, data, addr): 48 | self.events.append(('datagram_received', data, addr)) 49 | 50 | def error_received(self, exc): 51 | self.events.append(('error_received', exc)) 52 | 53 | def resume_writing(self): 54 | pass 55 | 56 | def pause_writing(self): 57 | pass 58 | 59 | 60 | class EchoServer(ProtocolLogger): 61 | 62 | def data_received(self, data): 63 | super(EchoServer, self).data_received(data) 64 | self.transport.write(data) 65 | 66 | 67 | class EventLoopTest(UnitTest): 68 | 69 | def setUp(self): 70 | super(EventLoopTest, self).setUp() 71 | self.loop = pyuv.Loop() 72 | self.errors = [] 73 | 74 | def catch_errors(self, callback): 75 | # Run *callback*. If it raises an exception, store it and stop the 76 | # loop. 77 | def run_callback(*args): 78 | try: 79 | callback(*args) 80 | except Exception as e: 81 | self.errors.append(e) 82 | self.loop.stop() 83 | return run_callback 84 | 85 | def run_loop(self, timeout): 86 | # Run the loop for at most *timeout* seconds. Re-raise any exception 87 | # that was catch by a "catch_errors" callback. 88 | timer = pyuv.Timer(self.loop) 89 | def stop_loop(handle): 90 | self.loop.stop() 91 | timer.start(stop_loop, timeout, 0) 92 | self.loop.run() 93 | if self.errors: 94 | raise self.errors[0] 95 | 96 | 97 | class TransportTest(object): 98 | 99 | def create_handle(self): 100 | raise NotImplementedError 101 | 102 | def bind_handle(self, handle): 103 | raise NotImplementedError 104 | 105 | def create_transport(self, handle, protocol, server_side): 106 | transport = Transport(handle) 107 | transport.start(protocol) 108 | return transport 109 | 110 | def test_echo(self): 111 | # Test a simple echo server. The client writes some data end then 112 | # writes an EOF (if the transport supports writing EOF). The server 113 | # echos and upon receipt of EOF will close the connection. 114 | @self.catch_errors 115 | def echo_server(handle, error): 116 | if error: 117 | raise TransportError.from_errno(error) 118 | client = self.create_handle() 119 | handle.accept(client) 120 | protocols[0] = EchoServer() 121 | transports[0] = self.create_transport(client, protocols[0], True) 122 | @self.catch_errors 123 | def echo_client(handle, error): 124 | if error: 125 | raise TransportError.from_errno(error) 126 | protocols[1] = ProtocolLogger() 127 | trans = transports[1] = self.create_transport(handle, protocols[1], False) 128 | trans.write(b'foo\n') 129 | trans.write(b'bar\n') 130 | trans.writelines([b'qux', b'quux']) 131 | if trans.can_write_eof(): 132 | trans.write_eof() 133 | transports = [None, None] 134 | protocols = [None, None] 135 | server = self.create_handle() 136 | addr = self.bind_handle(server) 137 | server.listen(echo_server) 138 | client = self.create_handle() 139 | client.connect(addr, echo_client) 140 | self.run_loop(0.1) 141 | strans, ctrans = transports 142 | sproto, cproto = protocols 143 | self.assertIsInstance(strans, Transport) 144 | self.assertIsInstance(ctrans, Transport) 145 | self.assertIsInstance(sproto, EchoServer) 146 | self.assertIsInstance(cproto, ProtocolLogger) 147 | ctrans.close() 148 | self.run_loop(0.1) 149 | sevents = sproto.events 150 | self.assertIn(len(sevents), (3, 4)) 151 | self.assertEqual(sevents[0], ('connection_made', strans)) 152 | self.assertEqual(sevents[1], ('data_received', b'foo\nbar\nquxquux')) 153 | if strans.can_write_eof(): 154 | self.assertEqual(sevents[2], ('eof_received',)) 155 | self.assertEqual(sevents[-1][0], 'connection_lost') 156 | cevents = cproto.events 157 | self.assertIn(len(cevents), (3, 4)) 158 | self.assertEqual(cevents[0], ('connection_made', ctrans)) 159 | self.assertEqual(cevents[1], ('data_received', b'foo\nbar\nquxquux')) 160 | if ctrans.can_write_eof(): 161 | self.assertEqual(cevents[2], ('eof_received',)) 162 | self.assertEqual(cevents[-1][0], 'connection_lost') 163 | 164 | 165 | class TestTcpTransport(TransportTest, EventLoopTest): 166 | 167 | def create_handle(self): 168 | return pyuv.TCP(self.loop) 169 | 170 | def bind_handle(self, handle): 171 | host = socket.gethostbyname('localhost') 172 | handle.bind((host, 0)) 173 | return handle.getsockname() 174 | 175 | 176 | class TestPipeTransport(TransportTest, EventLoopTest): 177 | 178 | def create_handle(self): 179 | return pyuv.Pipe(self.loop) 180 | 181 | def bind_handle(self, handle): 182 | addr = self.pipename('test-pipe') 183 | handle.bind(addr) 184 | return addr 185 | 186 | 187 | class TestUdpTransport(EventLoopTest): 188 | 189 | def create_handle(self): 190 | return pyuv.UDP(self.loop) 191 | 192 | def bind_handle(self, handle): 193 | host = socket.gethostbyname('localhost') 194 | handle.bind((host, 0)) 195 | return handle.getsockname() 196 | 197 | def create_transport(self, handle, protocol): 198 | transport = DatagramTransport(handle) 199 | transport.start(protocol) 200 | return transport 201 | 202 | def test_echo(self): 203 | server = self.create_handle() 204 | saddr = self.bind_handle(server) 205 | sproto = ProtocolLogger() 206 | strans = self.create_transport(server, sproto) 207 | client = self.create_handle() 208 | caddr = self.bind_handle(client) 209 | cproto = ProtocolLogger() 210 | ctrans = self.create_transport(client, cproto) 211 | # Try 5 times (since UDP is lossy) 212 | for i in range(5): 213 | ctrans.sendto(b'foo', saddr) 214 | for i in range(5): 215 | strans.sendto(b'bar', caddr) 216 | self.run_loop(0.1) 217 | sevents = sproto.get_events('datagram_received') 218 | self.assertGreater(len(sevents), 0) 219 | for event in sevents: 220 | self.assertEqual(event[1], b'foo') 221 | self.assertEqual(event[2], caddr) 222 | cevents = cproto.get_events('datagram_received') 223 | self.assertGreater(len(cevents), 0) 224 | for event in cevents: 225 | self.assertEqual(event[1], b'bar') 226 | self.assertEqual(event[2], saddr) 227 | 228 | 229 | if __name__ == '__main__': 230 | unittest.main() 231 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27, py33, py34, py35, py36, docs, flake8, coverage 3 | 4 | [testenv] 5 | commands = {posargs:python runtests.py all} 6 | deps = -rrequirements.txt 7 | passenv = VERBOSE DEBUG 8 | 9 | [testenv:docs] 10 | changedir = docs 11 | commands = sphinx-build -nW -b html . {envtmpdir}/html 12 | deps = -rrequirements.txt 13 | sphinx 14 | 15 | [testenv:flake8] 16 | usedevelop = True 17 | commands = flake8 --exit-zero lib tests examples 18 | deps = flake8 19 | 20 | # Coverage runs in develop mode. Otherwise we get very long and 21 | # ugly path names in the report. 22 | 23 | [testenv:coverage] 24 | usedevelop = True 25 | commands = 26 | coverage run --include "lib/gruvi/*" runtests.py all 27 | deps =-rrequirements.txt 28 | coverage 29 | invoke 30 | -------------------------------------------------------------------------------- /vendor/txdbus/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Gruvi. Gruvi is free software available under the 3 | # terms of the MIT license. See the file "LICENSE" that was provided 4 | # together with this source file for the licensing terms. 5 | # 6 | # Copyright (c) 2012-2014 the Gruvi authors. See the file "AUTHORS" for a 7 | # complete list. 8 | 9 | from __future__ import absolute_import, print_function 10 | 11 | from .error import * 12 | from .message import (DBusMessage, MethodCallMessage, MethodReturnMessage, 13 | ErrorMessage, SignalMessage, parseMessage) 14 | from .authentication import ClientAuthenticator, BusAuthenticator 15 | 16 | # A few renames to have a consistent naming scheme 17 | DbusMessage = DBusMessage 18 | DbusException = DBusException 19 | DbusAuthenticationFailed = DBusAuthenticationFailed 20 | -------------------------------------------------------------------------------- /vendor/txdbus/error.py: -------------------------------------------------------------------------------- 1 | """ 2 | DBus errors 3 | @author: Tom Cocagne 4 | """ 5 | 6 | class DBusException (Exception): 7 | """ 8 | Base class for all expected DBus exceptions 9 | """ 10 | pass 11 | 12 | 13 | class DBusAuthenticationFailed (DBusException): 14 | pass 15 | 16 | 17 | class MarshallingError (DBusException): 18 | """ 19 | Thrown when errors are encountered by the marshalling/unmarshalling 20 | code 21 | """ 22 | pass 23 | 24 | 25 | class TimeOut (DBusException): 26 | """ 27 | Used to indicate a timeout for remote DBus method calls that 28 | request a timeout value 29 | """ 30 | pass 31 | 32 | 33 | class IntrospectionFailed (DBusException): 34 | """ 35 | Thrown if remote object introspection fails 36 | """ 37 | pass 38 | 39 | 40 | class RemoteError (DBusException): 41 | """ 42 | Thrown in response to errors encountered during a remote 43 | method invocation. 44 | 45 | @ivar errName: DBus error name 46 | @type errName: C{string} 47 | """ 48 | message = '' 49 | 50 | def __init__(self, errName): 51 | self.errName = errName 52 | 53 | def __str__(self): 54 | return '%s: %s' % (self.errName, self.message) if self.message else self.errName 55 | 56 | 57 | class FailedToAcquireName(DBusException): 58 | """ 59 | Indicates a failed attempt to acquire a bus name 60 | """ 61 | def __init__(self, new_name, returnCode): 62 | head = 'Failed to acquire bus name "%s": ' % (new_name,) 63 | if returnCode == 2: 64 | tail = 'Queued for name acquisition' 65 | elif returnCode == 3: 66 | tail = 'Name in use' 67 | else: 68 | tail = 'Unknown reason' 69 | 70 | DBusException.__init__(self, head + tail) 71 | 72 | self.returnCode = returnCode 73 | 74 | --------------------------------------------------------------------------------