pax_global_header00006660000000000000000000000064131476275530014527gustar00rootroot0000000000000052 comment=2f6b27beb1799344e7283040d4e9730fe7913a3d tinyrpc-0.6/000077500000000000000000000000001314762755300130645ustar00rootroot00000000000000tinyrpc-0.6/LICENSE000066400000000000000000000020421314762755300140670ustar00rootroot00000000000000Copyright (c) 2013 Marc Brinkmann Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. tinyrpc-0.6/MANIFEST.in000066400000000000000000000000231314762755300146150ustar00rootroot00000000000000include README.rst tinyrpc-0.6/README.rst000066400000000000000000000056211314762755300145570ustar00rootroot00000000000000tinyrpc: A small and modular way of handling web-related RPC ============================================================ Motivation ---------- As of this writing (in Jan 2013) there are a few jsonrpc_ libraries already out there on PyPI_, most of them handling one specific use case (e.g. json via WSGI, using Twisted, or TCP-sockets). None of the libraries, however, made it easy to reuse the jsonrpc_-parsing bits and substitute a different transport (i.e. going from json_ via TCP_ to an implementation using WebSockets_ or 0mq_). In the end, all these libraries have their own dispatching interfaces and a custom implementation of handling jsonrpc_. ``tinyrpc`` aims to do better by dividing the problem into cleanly interchangeable parts that allow easy addition of new transport methods, RPC protocols or dispatchers. Documentation ------------- You'll quickly find that ``tinyrpc`` has more documentation and tests than core code, hence the name. See the documentation at for more details, especially the Structure-section to get a birds-eye view. Installation ------------ .. code-block:: sh pip install tinyrpc will install ``tinyrpc`` with its default dependencies. Optional dependencies --------------------- Depending on the protocols and transports you want to use additional dependencies are required. You can instruct pip to install these dependencies by specifying extras to the basic install command. .. code-block:: sh pip install tinyrpc[httpclient, wsgi] will install ``tinyrpc`` with dependencies for the httpclient and wsgi transports. Available extras are: +------------+-------------------------------------------------------+ | Option | Needed to use objects of class | +============+=======================================================+ | gevent | optional in RPCClient, required by RPCServerGreenlets | +------------+-------------------------------------------------------+ | httpclient | HttpPostClientTransport, HttpWebSocketClientTransport | +------------+-------------------------------------------------------+ | jsonext | optional in JSONRPCProtocol | +------------+-------------------------------------------------------+ | websocket | WSServerTransport | +------------+-------------------------------------------------------+ | wsgi | WsgiServerTransport | +------------+-------------------------------------------------------+ | zmq | ZmqServerTransport, ZmqClientTransport | +------------+-------------------------------------------------------+ .. _jsonrpc: http://www.jsonrpc.org/ .. _PyPI: http://pypi.python.org .. _json: http://www.json.org/ .. _TCP: http://en.wikipedia.org/wiki/Transmission_Control_Protocol .. _WebSockets: http://en.wikipedia.org/wiki/WebSocket .. _0mq: http://www.zeromq.org/ tinyrpc-0.6/docs/000077500000000000000000000000001314762755300140145ustar00rootroot00000000000000tinyrpc-0.6/docs/Makefile000066400000000000000000000127001314762755300154540ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/tinyrpc.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/tinyrpc.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/tinyrpc" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/tinyrpc" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." tinyrpc-0.6/docs/client.rst000066400000000000000000000007431314762755300160300ustar00rootroot00000000000000RPC Client ========== :py:class:`~tinyrpc.client.RPCClient` instances are high-level handlers for making remote procedure calls to servers. Other than :py:class:`~tinyrpc.client.RPCProxy` objects, they are what most user applications interact with. Clients needs to be instantiated with a protocol and a transport to function. Proxies are syntactic sugar for using clients. .. autoclass:: tinyrpc.client.RPCClient :members: .. autoclass:: tinyrpc.client.RPCProxy :members: tinyrpc-0.6/docs/conf.py000066400000000000000000000176161314762755300153260ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # tinyrpc documentation build configuration file, created by # sphinx-quickstart on Wed Jan 23 19:15:13 2013. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys, os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.insert(0, os.path.abspath('.')) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'tinyrpc' copyright = u'2013, Marc Brinkmann' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = '0.6' # The full version, including alpha/beta/rc tags. release = '0.6dev' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'default' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'tinyrpcdoc' # -- Options for LaTeX output -------------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'tinyrpc.tex', u'tinyrpc Documentation', u'Marc Brinkmann', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'tinyrpc', u'tinyrpc Documentation', [u'Marc Brinkmann'], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'tinyrpc', u'tinyrpc Documentation', u'Marc Brinkmann', 'tinyrpc', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { 'https://docs.python.org/': None, 'https://pyzmq.readthedocs.io/en/latest/': None, 'http://docs.python-requests.org/en/latest/': None, 'http://werkzeug.pocoo.org/docs/': None, } tinyrpc-0.6/docs/dispatch.rst000066400000000000000000000055131314762755300163510ustar00rootroot00000000000000Dispatching =========== Dispatching in ``tinyrpc`` is very similiar to url-routing in web frameworks. Functions are registered with a specific name and made public, i.e. callable, to remote clients. Examples -------- Exposing a few functions: ~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: python from tinyrpc.dispatch import RPCDispatcher dispatch = RPCDispatcher() @dispatch.public def foo(): # ... @dispatch.public def bar(arg): # ... # later on, assuming we know we want to call foo(*args, **kwargs): f = dispatch.get_method('foo') f(*args, **kwargs) Using prefixes and instance registration: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: python from tinyrpc.dispatch import public class SomeWebsite(object): def __init__(self, ...): # note: this method will not be exposed def secret(self): # another unexposed method @public def get_user_info(self, user): # ... # using a different name @public('get_user_comment') def get_comment(self, comment_id): # ... The code above declares an RPC interface for ``SomeWebsite`` objects, consisting of two visible methods: ``get_user_info(user)`` and ``get_user_comment(commend_id)``. These can be used with a dispatcher now: .. code-block:: python def hello(): # ... website1 = SomeWebsite(...) website2 = SomeWebsite(...) from tinyrpc.dispatch import RPCDispatcher dispatcher = RPCDispatcher() # directly register version method @dispatcher.public def version(): # ... # add earlier defined method dispatcher.add_method(hello) # register the two website instances dispatcher.register_instance(website1, 'sitea.') dispatcher.register_instance(website2, 'siteb.') In the example above, the :py:class:`~tinyrpc.dispatch.RPCDispatcher` now knows a total of six registered methods: ``version``, ``hello``, ``sitea.get_user_info``, ``sitea.get_user_comment``, ``siteb.get_user_info``, ``siteb.get_user_comment``. Automatic dispatching ~~~~~~~~~~~~~~~~~~~~~ When writing a server application, a higher level dispatching method is available with :py:func:`~tinyrpc.dispatch.RPCDispatcher.dispatch`: .. code-block:: python from tinyrpc.dispatch import RPCDispatcher dispatcher = RPCDispatcher() # register methods like in the examples above # ... # now assumes that a valid RPCRequest has been obtained, as `request` response = dispatcher.dispatch(request) # response can be directly processed back to the client, all Exceptions have # been handled already API reference ------------- .. autoclass:: tinyrpc.dispatch.RPCDispatcher :members: Classes can be made to support an RPC interface without coupling it to a dispatcher using a decorator: .. autofunction:: tinyrpc.dispatch.public tinyrpc-0.6/docs/exceptions.rst000066400000000000000000000001221314762755300167220ustar00rootroot00000000000000Exception reference ------------------- .. automodule:: tinyrpc.exc :members: tinyrpc-0.6/docs/index.rst000066400000000000000000000077461314762755300156730ustar00rootroot00000000000000tinyrpc: A modular RPC library ============================== ``tinyrpc`` is a library for making and handling RPC calls in python. Its initial scope is handling jsonrpc_, although it aims to be very well-documented and modular to make it easy to add support for further protocols. A feature is support of multiple transports (or none at all) and providing clever syntactic sugar for writing dispatchers. Quickstart examples ------------------- The source contains all of these examples in a working fashion in the examples subfolder. HTTP based ~~~~~~~~~~ A client making JSONRPC calls via HTTP (this requires :py:mod:`requests` to be installed): .. code-block:: python from tinyrpc.protocols.jsonrpc import JSONRPCProtocol from tinyrpc.transports.http import HttpPostClientTransport from tinyrpc import RPCClient rpc_client = RPCClient( JSONRPCProtocol(), HttpPostClientTransport('http://example.org/jsonrpc/2.0/') ) time_server = rpc_client.get_proxy() # ... # call a method called 'get_time_in' with a single string argument time_in_berlin = time_server.get_time_in('Europe/Berlin') These can be answered by a server implemented as follows: .. code-block:: python import gevent import gevent.wsgi import gevent.queue from tinyrpc.protocols.jsonrpc import JSONRPCProtocol from tinyrpc.transports.wsgi import WsgiServerTransport from tinyrpc.server.gevent import RPCServerGreenlets from tinyrpc.dispatch import RPCDispatcher dispatcher = RPCDispatcher() transport = WsgiServerTransport(queue_class=gevent.queue.Queue) # start wsgi server as a background-greenlet wsgi_server = gevent.wsgi.WSGIServer(('127.0.0.1', 80), transport.handle) gevent.spawn(wsgi_server.serve_forever) rpc_server = RPCServerGreenlets( transport, JSONRPCProtocol(), dispatcher ) @dispatcher.public def reverse_string(s): return s[::-1] # in the main greenlet, run our rpc_server rpc_server.serve_forever() 0mq ~~~ An example using :py:mod:`zmq` is very similiar, differing only in the instantiation of the transport: .. code-block:: python import zmq from tinyrpc.protocols.jsonrpc import JSONRPCProtocol from tinyrpc.transports.zmq import ZmqClientTransport from tinyrpc import RPCClient ctx = zmq.Context() rpc_client = RPCClient( JSONRPCProtocol(), ZmqClientTransport.create(ctx, 'tcp://127.0.0.1:5001') ) remote_server = rpc_client.get_proxy() # call a method called 'reverse_string' with a single string argument result = remote_server.reverse_string('Hello, World!') print "Server answered:", result Matching server: .. code-block:: python import zmq from tinyrpc.protocols.jsonrpc import JSONRPCProtocol from tinyrpc.transports.zmq import ZmqServerTransport from tinyrpc.server import RPCServer from tinyrpc.dispatch import RPCDispatcher ctx = zmq.Context() dispatcher = RPCDispatcher() transport = ZmqServerTransport.create(ctx, 'tcp://127.0.0.1:5001') rpc_server = RPCServer( transport, JSONRPCProtocol(), dispatcher ) @dispatcher.public def reverse_string(s): return s[::-1] rpc_server.serve_forever() Further examples ---------------- In :doc:`protocols`, you can find client and server examples on how to use just the protocol parsing parts of ``tinyrpc``. The :py:class:`~tinyrpc.dispatch.RPCDispatcher` should be useful on its own (or at least easily replaced with one of your choosing), see :doc:`dispatch` for details. Table of contents ----------------- .. toctree:: :maxdepth: 2 structure protocols dispatch transports client server exceptions People ------ Creator ~~~~~~~ - Marc Brinkmann: https://github.com/mbr Maintainer ~~~~~~~~~~ - Leo Noordergraaf: https://github.com/lnoor Contributors ~~~~~~~~~~~~ - Guilherme Salgado: https://github.com/gsalgado - jnnk: https://github.com/jnnk - Satoshi Kobayashi: https://github.com/satosi-k .. _jsonrpc: http://jsonrpc.org tinyrpc-0.6/docs/make.bat000066400000000000000000000117521314762755300154270ustar00rootroot00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\tinyrpc.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\tinyrpc.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) :end tinyrpc-0.6/docs/protocols.rst000066400000000000000000000120471314762755300165760ustar00rootroot00000000000000The protocol layer ================== Any protocol is implemented by deriving from :py:class:`~tinyrpc.RPCProtocol` and implementing all of its members: .. autoclass:: tinyrpc.RPCProtocol :members: These require implementations of the following classes as well: .. autoclass:: tinyrpc.RPCRequest :members: .. autoclass:: tinyrpc.RPCResponse :members: .. autoclass:: tinyrpc.BadRequestError :members: Every protocol deals with multiple kinds of structures: ``data`` arguments are always byte strings, either messages or replies, that are sent via or received from a transport. There are two protocol-specific subclasses of :py:class:`~tinyrpc.RPCRequest` and :py:class:`~tinyrpc.RPCResponse`, these represent well-formed requests and responses. Finally, if an error occurs during parsing of a request, a :py:class:`~tinyrpc.BadRequestError` instance must be thrown. These need to be subclassed for each protocol as well, since they generate error replies. Batch protocols --------------- Some protocols may support batch requests. In this case, they need to derive from :py:class:`~tinyrpc.RPCBatchProtocol`. Batch protocols differ in that their :py:func:`~tinyrpc.RPCProtocol.parse_request` method may return an instance of :py:class:`~tinyrpc.RPCBatchRequest`. They also possess an addional method in :py:func:`~tinyrpc.RPCBatchProtocol.create_batch_request`. Handling a batch request is slightly different, while it supports :py:func:`~tinyrpc.RPCBatchRequest.error_respond`, to make actual responses, :py:func:`~tinyrpc.RPCBatchRequest.create_batch_response` needs to be used. No assumptions are made whether or not it is okay for batch requests to be handled in parallel. This is up to the server/dispatch implementation, which must be chosen appropriately. .. autoclass:: tinyrpc.RPCBatchProtocol :members: .. autoclass:: tinyrpc.RPCBatchRequest :members: .. autoclass:: tinyrpc.RPCBatchResponse :members: Supported protocols ------------------- Any supported protocol is used by instantiating its class and calling the interface of :py:class:`~tinyrpc.RPCProtocol`. Note that constructors are not part of the interface, any protocol may have specific arguments for its instances. Protocols usually live in their own module because they may need to import optional modules that needn't be a dependency for all of ``tinyrpc``. Example ------- The following example shows how to use the :py:class:`~tinyrpc.protcols.jsonrpc.JSONRPCProtocol` class in a custom application, without using any other components: Server ~~~~~~ .. code-block:: python from tinyrpc.protocols.jsonrpc import JSONRPCProtocol from tinyrpc import BadRequestError, RPCBatchRequest rpc = JSONRPCProtocol() # the code below is valid for all protocols, not just JSONRPC: def handle_incoming_message(self, data): try: request = rpc.parse_request(data) except BadRequestError as e: # request was invalid, directly create response response = e.error_respond(e) else: # we got a valid request # the handle_request function is user-defined # and returns some form of response if hasattr(request, create_batch_response): response = request.create_batch_response( handle_request(req) for req in request ) else: response = handle_request(request) # now send the response to the client if response != None: send_to_client(response.serialize()) def handle_request(request): try: # do magic with method, args, kwargs... return request.respond(result) except Exception as e: # for example, a method wasn't found return request.error_respond(e) Client ~~~~~~ .. code-block:: python from tinyrpc.protocols.jsonrpc import JSONRPCProtocol rpc = JSONRPCProtocol() # again, code below is protocol-independent # assuming you want to call method(*args, **kwargs) request = rpc.create_request(method, args, kwargs) reply = send_to_server_and_get_reply(request) response = rpc.parse_reply(reply) if hasattr(response, 'error'): # error handling... else: # the return value is found in response.result do_something_with(response.result) Another example, this time using batch requests: .. code-block:: python # or using batch requests: requests = rpc.create_batch_request([ rpc.create_request(method_1, args_1, kwargs_1) rpc.create_request(method_2, args_2, kwargs_2) # ... ]) reply = send_to_server_and_get_reply(request) responses = rpc.parse_reply(reply) for responses in response: if hasattr(reponse, 'error'): # ... Finally, one-way requests are requests where the client does not expect an answer: .. code-block:: python request = rpc.create_request(method, args, kwargs, one_way=True) send_to_server(request) # done JSON-RPC ~~~~~~~~ .. autoclass:: tinyrpc.protocols.jsonrpc.JSONRPCProtocol :members: .. _jsonrpc: http://jsonrpc.org tinyrpc-0.6/docs/server.rst000066400000000000000000000011101314762755300160450ustar00rootroot00000000000000Server implementations ====================== Like :doc:`client`, servers are top-level instances that most user code should interact with. They provide runnable functions that are combined with transports, protocols and dispatchers to form a complete RPC system. .. automodule:: tinyrpc.server :members: .. py:class:: tinyrpc.server.gevent.RPCServerGreenlets Asynchronous RPCServer. This implementation of :py:class:`~tinyrpc.server.RPCServer` uses :py:func:`gevent.spawn` to spawn new client handlers, result in asynchronous handling of clients using greenlets. tinyrpc-0.6/docs/structure.rst000066400000000000000000000023211314762755300166040ustar00rootroot00000000000000Structure of tinyrpc ==================== ``tinyrpc`` architectually considers three layers: Transport, Protocol and Dispatch. The Transport-layer is responsible for receiving and sending messages. No assumptions are made about messages, except that they are of a fixed size. Messages are received and possibly passed on a Python strings. In an RPC context, messages coming in (containing requests) are simply called messages, a message sent in reply is called a reply. Replies are always serialized responses. On the Protocol-layer messages are decoded into a format that is protocol independent, i.e. incoming messages are turned into requests or vice verse, while outgoing messages can be turned from responses into replies or the other way around. The Dispatch-layer performs the actual method calling and serializes the return value. These can be routed back through the Protocol- and Transport-layer to return the answer to the calling client. Each layer is useful "on its own" and can be used seperately. If you simply need to decode a jsonrpc_ message, without passing it on or sending it through a transport, any :py:class:`~tinyrpc.RPCProtocol`-class is completely usable on its own. .. _jsonrpc: http://jsonrpc.org tinyrpc-0.6/docs/transports.rst000066400000000000000000000024461314762755300167730ustar00rootroot00000000000000Transports ========== Transports are somewhat low level interface concerned with transporting messages across through different means. "Messages" in this case are simple strings. All transports need to support two different interfaces: .. autoclass:: tinyrpc.transports.ServerTransport :members: .. autoclass:: tinyrpc.transports.ClientTransport :members: Note that these transports are of relevance when using ``tinyrpc``-built in facilities. They can be coopted for any other purpose, if you simply need reliable server-client message passing as well. Transport implementations ------------------------- A few transport implementations are included with ``tinyrpc``: 0mq ~~~ Based on :py:mod:`zmq`, supports 0mq based sockets. Highly recommended: .. autoclass:: tinyrpc.transports.zmq.ZmqServerTransport :members: .. autoclass:: tinyrpc.transports.zmq.ZmqClientTransport :members: HTTP ~~~~ There is only an HTTP client, no server (use WSGI instead). .. autoclass:: tinyrpc.transports.http.HttpPostClientTransport :members: WSGI ~~~~ .. autoclass:: tinyrpc.transports.wsgi.WsgiServerTransport :members: CGI ~~~ .. autoclass:: tinyrpc.transports.cgi.CGIServerTransport :members: Callback ~~~~~~~~ .. autoclass:: tinyrpc.transports.callback.CallbackServerTransport :members: tinyrpc-0.6/examples/000077500000000000000000000000001314762755300147025ustar00rootroot00000000000000tinyrpc-0.6/examples/http_client_example.py000066400000000000000000000007571314762755300213150ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- from tinyrpc.protocols.jsonrpc import JSONRPCProtocol from tinyrpc.transports.http import HttpPostClientTransport from tinyrpc import RPCClient rpc_client = RPCClient( JSONRPCProtocol(), HttpPostClientTransport('http://127.0.0.1:5000/') ) remote_server = rpc_client.get_proxy() # call a method called 'reverse_string' with a single string argument result = remote_server.reverse_string('Hello, World!') print "Server answered:", result tinyrpc-0.6/examples/http_server_example.py000066400000000000000000000014201314762755300213310ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- import gevent import gevent.wsgi import gevent.queue from tinyrpc.protocols.jsonrpc import JSONRPCProtocol from tinyrpc.transports.wsgi import WsgiServerTransport from tinyrpc.server.gevent import RPCServerGreenlets from tinyrpc.dispatch import RPCDispatcher dispatcher = RPCDispatcher() transport = WsgiServerTransport(queue_class=gevent.queue.Queue) # start wsgi server as a background-greenlet wsgi_server = gevent.wsgi.WSGIServer(('127.0.0.1', 5000), transport.handle) gevent.spawn(wsgi_server.serve_forever) rpc_server = RPCServerGreenlets( transport, JSONRPCProtocol(), dispatcher ) @dispatcher.public def reverse_string(s): return s[::-1] # in the main greenlet, run our rpc_server rpc_server.serve_forever() tinyrpc-0.6/examples/zmq_client_example.py000066400000000000000000000010171314762755300211330ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- import zmq from tinyrpc.protocols.jsonrpc import JSONRPCProtocol from tinyrpc.transports.zmq import ZmqClientTransport from tinyrpc import RPCClient ctx = zmq.Context() rpc_client = RPCClient( JSONRPCProtocol(), ZmqClientTransport.create(ctx, 'tcp://127.0.0.1:5001') ) remote_server = rpc_client.get_proxy() # call a method called 'reverse_string' with a single string argument result = remote_server.reverse_string('Hello, World!') print "Server answered:", result tinyrpc-0.6/examples/zmq_server_example.py000066400000000000000000000010261314762755300211630ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- import zmq from tinyrpc.protocols.jsonrpc import JSONRPCProtocol from tinyrpc.transports.zmq import ZmqServerTransport from tinyrpc.server import RPCServer from tinyrpc.dispatch import RPCDispatcher ctx = zmq.Context() dispatcher = RPCDispatcher() transport = ZmqServerTransport.create(ctx, 'tcp://127.0.0.1:5001') rpc_server = RPCServer( transport, JSONRPCProtocol(), dispatcher ) @dispatcher.public def reverse_string(s): return s[::-1] rpc_server.serve_forever() tinyrpc-0.6/setup.py000066400000000000000000000017331314762755300146020ustar00rootroot00000000000000import os from setuptools import setup, find_packages def read(fname): return open(os.path.join(os.path.dirname(__file__), fname)).read() setup( name='tinyrpc', version='0.6', description='A small, modular, transport and protocol neutral RPC ' 'library that, among other things, supports JSON-RPC and zmq.', long_description=read('README.rst'), packages=find_packages(exclude=['tests', 'examples']), keywords='json rpc json-rpc jsonrpc 0mq zmq zeromq', author='Marc Brinkmann', author_email='git@marcbrinkmann.de', maintainer='Leo Noordergraaf', maintainer_email='leo@noordergraaf.net', url='http://github.com/mbr/tinyrpc', license='MIT', install_requires=['six'], extras_require={ 'gevent': ['gevent'], 'httpclient': ['requests', 'websocket-client'], 'websocket': ['gevent-websocket'], 'wsgi': ['werkzeug'], 'zmq': ['pyzmq'], 'jsonext': ['jsonext'] } ) tinyrpc-0.6/tests/000077500000000000000000000000001314762755300142265ustar00rootroot00000000000000tinyrpc-0.6/tests/_compat.py000066400000000000000000000004771314762755300162320ustar00rootroot00000000000000# from http://stackoverflow.com/questions/28215214/how-to-add-custom-renames-in-six import six mod = six.MovedModule('mock', 'mock', 'unittest.mock') six.add_move(mod) six._importer._add_module(mod, "moves." + mod.name) # issue open at https://bitbucket.org/gutworth/six/issue/116/enable-importing-from-within-custom tinyrpc-0.6/tests/test_client.py000066400000000000000000000067551314762755300171320ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- import pytest import _compat from six.moves.mock import Mock from tinyrpc.exc import RPCError from tinyrpc.client import RPCClient, RPCProxy from tinyrpc.protocols import RPCProtocol, RPCResponse, RPCErrorResponse from tinyrpc.transports import ClientTransport @pytest.fixture(params=['test_method1', 'method2', 'CamelCasedMethod']) def method_name(request): return request.param @pytest.fixture(params=[(), ('foo', None, 42), (1,)]) def method_args(request): return request.param @pytest.fixture(params=[(), (('foo', 'bar'), ('x', None), ('y', 42)), (('q', 1),)]) def method_kwargs(request): return dict(request.param or {}) @pytest.fixture(params=['', 'NoDot', 'dot.']) def prefix(request): return request.param @pytest.fixture(params=[True, False]) def one_way_setting(request): return request.param @pytest.fixture def mock_client(): return Mock(RPCClient) @pytest.fixture def mock_protocol(): mproto = Mock(RPCProtocol) foo = Mock(RPCResponse) foo.result = None mproto.parse_reply = Mock(return_value=foo) return mproto @pytest.fixture def mock_transport(): return Mock(ClientTransport) @pytest.fixture() def client(mock_protocol, mock_transport): return RPCClient(mock_protocol, mock_transport) @pytest.fixture def m_proxy(mock_client, prefix, one_way_setting): return RPCProxy(mock_client, prefix, one_way_setting) def test_proxy_calls_correct_method(m_proxy, mock_client, prefix, method_kwargs, method_args, method_name, one_way_setting): getattr(m_proxy, method_name)(*method_args, **method_kwargs) mock_client.call.assert_called_with( prefix + method_name, method_args, method_kwargs, one_way=one_way_setting ) def test_client_uses_correct_protocol(client, mock_protocol, method_name, method_args, method_kwargs, one_way_setting): client.call(method_name, method_args, method_kwargs, one_way_setting) assert mock_protocol.create_request.called def test_client_uses_correct_transport(client, mock_protocol, method_name, method_args, method_kwargs, one_way_setting, mock_transport): client.call(method_name, method_args, method_kwargs, one_way_setting) assert mock_transport.send_message.called def test_client_passes_correct_reply(client, mock_protocol, method_name, method_args, method_kwargs, one_way_setting, mock_transport): transport_return = '023hoisdfh' mock_transport.send_message = Mock(return_value=transport_return) client.call(method_name, method_args, method_kwargs, one_way_setting) if one_way_setting: mock_protocol.parse_reply.assert_not_called() else: mock_protocol.parse_reply.assert_called_with(transport_return) def test_client_raises_error_replies(client, mock_protocol, method_name, method_args, method_kwargs, one_way_setting): error_response = RPCErrorResponse() error_response.error = 'foo' mock_protocol.parse_reply = Mock(return_value=error_response) if not one_way_setting: with pytest.raises(RPCError): client.call(method_name, method_args, method_kwargs, one_way_setting) tinyrpc-0.6/tests/test_dispatch.py000066400000000000000000000141011314762755300174330ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- import _compat from six.moves.mock import Mock, MagicMock import pytest import inspect from tinyrpc.dispatch import RPCDispatcher, public from tinyrpc import RPCRequest, RPCBatchRequest, RPCBatchResponse from tinyrpc.protocols.jsonrpc import JSONRPCProtocol, JSONRPCInvalidParamsError @pytest.fixture def dispatch(): return RPCDispatcher() @pytest.fixture() def subdispatch(): return RPCDispatcher() @pytest.fixture() def mock_request(method='subtract', args=None, kwargs=None): mock_request = Mock(RPCRequest) mock_request.method = method mock_request.args = args or [4, 6] mock_request.kwargs = kwargs or {} return mock_request def test_function_decorating_without_paramters(dispatch): @dispatch.public def foo(bar): pass assert dispatch.get_method('foo') == foo def test_function_decorating_with_empty_paramters(dispatch): @dispatch.public() def foo(bar): pass assert dispatch.get_method('foo') == foo def test_function_decorating_with_paramters(dispatch): @dispatch.public(name='baz') def foo(bar): pass with pytest.raises(KeyError): dispatch.get_method('foo') assert dispatch.get_method('baz') == foo def test_subdispatchers(dispatch, subdispatch): @dispatch.public() def foo(bar): pass @subdispatch.public(name='foo') def subfoo(bar): pass dispatch.add_subdispatch(subdispatch, 'sub.') assert dispatch.get_method('foo') == foo assert dispatch.get_method('sub.foo') == subfoo def test_object_method_marking(): class Foo(object): def foo1(self): pass @public def foo2(self): pass @public(name='baz') def foo3(self): pass f = Foo() assert not hasattr(f.foo1, '_rpc_public_name') assert f.foo2._rpc_public_name == 'foo2' assert f.foo3._rpc_public_name == 'baz' def test_object_method_register(dispatch): class Foo(object): def foo1(self): pass @public def foo2(self): pass @public(name='baz') def foo3(self): pass f = Foo() dispatch.register_instance(f) with pytest.raises(KeyError): assert dispatch.get_method('foo1') assert dispatch.get_method('foo2') == f.foo2 assert dispatch.get_method('baz') == f.foo3 def test_object_method_register_with_prefix(dispatch): class Foo(object): def foo1(self): pass @public def foo2(self): pass @public(name='baz') def foo3(self): pass f = Foo() dispatch.register_instance(f, 'myprefix') with pytest.raises(KeyError): assert dispatch.get_method('foo1') with pytest.raises(KeyError): assert dispatch.get_method('myprefixfoo1') with pytest.raises(KeyError): assert dispatch.get_method('foo2') with pytest.raises(KeyError): assert dispatch.get_method('foo3') assert dispatch.get_method('myprefixfoo2') == f.foo2 assert dispatch.get_method('myprefixbaz') == f.foo3 def test_dispatch_calls_method_and_responds(dispatch, mock_request): m = Mock() m.subtract = Mock(return_value=-2) dispatch.add_method(m.subtract, 'subtract') response = dispatch.dispatch(mock_request) assert m.subtract.called mock_request.respond.assert_called_with(-2) def test_dispatch_handles_in_function_exceptions(dispatch, mock_request): m = Mock() m.subtract = Mock(return_value=-2) class MockError(Exception): pass m.subtract.side_effect = MockError('mock error') dispatch.add_method(m.subtract, 'subtract') response = dispatch.dispatch(mock_request) assert m.subtract.called mock_request.error_respond.assert_called_with(m.subtract.side_effect) def test_batch_dispatch(dispatch): method1 = Mock(return_value='rv1') method2 = Mock(return_value=None) dispatch.add_method(method1, 'method1') dispatch.add_method(method2, 'method2') batch_request = RPCBatchRequest() batch_request.error_respond = Mock(return_value='ERROR') batch_request.append(mock_request('method1', args=[1,2])) batch_request.append(mock_request('non_existant_method', args=[5,6])) batch_request.append(mock_request('method2', args=[3,4])) batch_request.create_batch_response = lambda: RPCBatchResponse() assert batch_request.error_respond.call_count == 0 response = dispatch.dispatch(batch_request) # assert all methods are called method1.assert_called_with(1, 2) method2.assert_called_with(3, 4) # FIXME: could use better checking? def test_dispatch_raises_key_error(dispatch): with pytest.raises(KeyError): dispatch.get_method('foo') @pytest.fixture(params=[ ('fn_a', [4, 6], {}, -2), ('fn_a', [4], {}, JSONRPCInvalidParamsError), ('fn_a', [], {'a':4, 'b':6}, -2), ('fn_a', [4], {'b':6}, -2), ('fn_b', [4, 6], {}, -2), ('fn_b', [], {'a':4, 'b':6}, JSONRPCInvalidParamsError), ('fn_b', [4], {}, IndexError), # a[1] doesn't exist, can't be detected beforehand ('fn_c', [4, 6], {}, JSONRPCInvalidParamsError), ('fn_c', [], {'a':4, 'b':6}, -2), ('fn_c', [], {'a':4}, KeyError) # a['b'] doesn't exist, can't be detected beforehand ]) def invoke_with(request): return request.param def test_argument_error(dispatch, invoke_with): method, args, kwargs, result = invoke_with protocol = JSONRPCProtocol() @dispatch.public def fn_a(a, b): return a-b @dispatch.public def fn_b(*a): return a[0]-a[1] @dispatch.public def fn_c(**a): return a['a']-a['b'] mock_request = Mock(RPCRequest) mock_request.args = args mock_request.kwargs = kwargs mock_request.method = method dispatch._dispatch(mock_request, getattr(protocol, '_caller', None)) if inspect.isclass(result) and issubclass(result, Exception): assert type(mock_request.error_respond.call_args[0][0]) == result else: mock_request.respond.assert_called_with(result) tinyrpc-0.6/tests/test_jsonrpc.py000066400000000000000000000343331314762755300173230ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- import json import six import pytest from tinyrpc import MethodNotFoundError, InvalidRequestError, ServerError, \ RPCError, RPCResponse, InvalidReplyError from tinyrpc.protocols.jsonrpc import JSONRPCParseError, \ JSONRPCInvalidRequestError, \ JSONRPCMethodNotFoundError, \ JSONRPCInvalidParamsError, \ JSONRPCInternalError def _json_equal(a, b): da = json.loads(a) db = json.loads(b) return da == db @pytest.fixture def prot(): from tinyrpc.protocols.jsonrpc import JSONRPCProtocol return JSONRPCProtocol() @pytest.mark.parametrize(('data', 'attrs'), [ # examples from the spec, parsing only ("""{"jsonrpc": "2.0", "method": "subtract", "params": [42, 23], "id": 1}""", {'method': 'subtract', 'args': [42, 23], 'unique_id': 1} ), ("""{"jsonrpc": "2.0", "method": "subtract", "params": [23, 42], "id": 2}""", {'method': 'subtract', 'args': [23, 42], 'unique_id': 2} ), ("""{"jsonrpc": "2.0", "method": "subtract", "params": {"subtrahend": 23, "minuend": 42}, "id": 3}""", {'method': 'subtract', 'kwargs': {'subtrahend': 23, 'minuend': 42}, 'unique_id': 3} ), ("""{"jsonrpc": "2.0", "method": "subtract", "params": {"minuend": 42, "subtrahend": 23}, "id": 4}""", {'method': 'subtract', 'kwargs': {'minuend': 42, 'subtrahend': 23}, 'unique_id': 4}, ), ("""{"jsonrpc": "2.0", "method": "update", "params": [1,2,3,4,5]}""", {'method': 'update', 'args': [1, 2, 3, 4, 5]} ), ("""{"jsonrpc": "2.0", "method": "foobar"}""", {'method': 'foobar'} ), ]) def test_parsing_good_request_samples(prot, data, attrs): req = prot.parse_request(data) for k, v in six.iteritems(attrs): assert getattr(req, k) == v @pytest.mark.parametrize('invalid_json', [ '{"jsonrpc": "2.0", "method": "foobar, "params": "bar", "baz]', 'garbage', ]) def test_parsing_invalid_json(prot, invalid_json): with pytest.raises(JSONRPCParseError): prot.parse_request(invalid_json) def test_parsing_invalid_arguments(prot): with pytest.raises(JSONRPCInvalidParamsError): prot.parse_request( """{"jsonrpc": "2.0", "method": "update", "params": 9}""" ) @pytest.mark.parametrize(('data', 'id', 'result'), [ ("""{"jsonrpc": "2.0", "result": 19, "id": 1}""", 1, 19, ), ("""{"jsonrpc": "2.0", "result": -19, "id": 2}""", 2, -19, ), ("""{"jsonrpc": "2.0", "result": 19, "id": 3}""", 3, 19, ), ("""{"jsonrpc": "2.0", "result": 19, "id": 4}""", 4, 19, ), ]) def test_good_reply_samples(prot, data, id, result): reply = prot.parse_reply(data) assert reply.unique_id == id assert reply.result == result @pytest.mark.parametrize(('exc', 'code', 'message'), [ (JSONRPCParseError, -32700, 'Parse error'), (JSONRPCInvalidRequestError, -32600, 'Invalid Request'), (JSONRPCMethodNotFoundError, -32601, 'Method not found'), (JSONRPCInvalidParamsError, -32602, 'Invalid params'), (JSONRPCInternalError, -32603, 'Internal error'), # generic errors #(InvalidRequestError, -32600, 'Invalid Request'), #(MethodNotFoundError, -32601, 'Method not found'), #(ServerError, -32603, 'Internal error'), ]) def test_proper_construction_of_error_codes(prot, exc, code, message): request = prot.parse_request( """{"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"}""" ) reply = exc().error_respond().serialize() err = json.loads(reply) assert err['error']['code'] == code assert err['error']['message'] == message def test_notification_yields_None_response(prot): data = """{"jsonrpc": "2.0", "method": "update", "params": [1,2,3,4,5]}""" req = prot.parse_request(data) # updates should never cause retries assert req.respond(True) == None def test_batch_empty_array(prot): with pytest.raises(JSONRPCInvalidRequestError): prot.parse_request("""[]""") def test_batch_invalid_array(prot): assert isinstance(prot.parse_request("""[1]""")[0], JSONRPCInvalidRequestError) def test_batch_invalid_batch(prot): for r in prot.parse_request("""[1, 2, 3]"""): assert isinstance(r, JSONRPCInvalidRequestError) def test_batch_good_examples(prot): data = """ [ {"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"}, {"jsonrpc": "2.0", "method": "notify_hello", "params": [7]}, {"jsonrpc": "2.0", "method": "subtract", "params": [42,23], "id": "2"}, {"foo": "boo"}, {"jsonrpc": "2.0", "method": "foo.get", "params": {"name": "myself"}, "id": "5"}, {"jsonrpc": "2.0", "method": "get_data", "id": "9"} ] """ results = prot.parse_request(data) assert isinstance(results, list) assert results[0].method == 'sum' assert results[0].args == [1, 2, 4] assert results[0].unique_id == "1" assert results[1].method == 'notify_hello' assert results[1].args == [7] assert results[1].unique_id == None assert results[2].method == 'subtract' assert results[2].args == [42, 23] assert results[2].unique_id == "2" assert isinstance(results[3], JSONRPCInvalidRequestError) assert results[4].method == 'foo.get' assert results[4].kwargs == {'name': 'myself'} assert results[4].unique_id == "5" assert results[5].method == 'get_data' assert results[5].args == [] assert results[5].kwargs == {} assert results[5].unique_id == "9" def test_unique_ids(prot): req1 = prot.create_request('foo', [1, 2]) req2 = prot.create_request('foo', [1, 2]) assert req1.unique_id != req2.unique_id def test_out_of_order(prot): req = prot.create_request('foo', ['a', 'b'], None) rep = req.respond(1) assert req.unique_id == rep.unique_id def test_request_generation(prot): jdata = json.loads(prot.create_request('subtract', [42, 23]).serialize()) assert jdata['method'] == 'subtract' assert jdata['params'] == [42, 23] assert jdata['id'] != None assert jdata['jsonrpc'] == '2.0' def test_jsonrpc_spec_v2_example1(prot): # reset id counter prot._id_counter = 0 request = prot.create_request('subtract', [42, 23]) assert _json_equal( """{"jsonrpc": "2.0", "method": "subtract", "params": [42, 23], "id": 1}""", request.serialize() ) reply = request.respond(19) assert _json_equal( """{"jsonrpc": "2.0", "result": 19, "id": 1}""", reply.serialize() ) request = prot.create_request('subtract', [23, 42]) assert _json_equal( """{"jsonrpc": "2.0", "method": "subtract", "params": [23, 42], "id": 2}""", request.serialize() ) reply = request.respond(-19) assert _json_equal( """{"jsonrpc": "2.0", "result": -19, "id": 2}""", reply.serialize() ) def test_jsonrpc_spec_v2_example2(prot): # reset id counter prot._id_counter = 2 request = prot.create_request('subtract', kwargs={'subtrahend': 23, 'minuend': 42}) assert _json_equal( """{"jsonrpc": "2.0", "method": "subtract", "params": {"subtrahend": 23, "minuend": 42}, "id": 3}""", request.serialize() ) reply = request.respond(19) assert _json_equal( """{"jsonrpc": "2.0", "result": 19, "id": 3}""", reply.serialize() ) request = prot.create_request('subtract', kwargs={'subtrahend': 23, 'minuend': 42}) assert _json_equal( """{"jsonrpc": "2.0", "method": "subtract", "params": {"minuend": 42, "subtrahend": 23}, "id": 4}""", request.serialize() ) reply = request.respond(-19) assert _json_equal( """{"jsonrpc": "2.0", "result": -19, "id": 4}""", reply.serialize() ) def test_jsonrpc_spec_v2_example3(prot): request = prot.create_request('update', [1, 2, 3, 4, 5], one_way=True) assert _json_equal( """{"jsonrpc": "2.0", "method": "update", "params": [1,2,3,4,5]}""", request.serialize() ) request = prot.create_request('foobar', one_way=True) assert _json_equal( """{"jsonrpc": "2.0", "method": "foobar"}""", request.serialize() ) def test_jsonrpc_spec_v2_example4(prot): request = prot.create_request('foobar') request.unique_id = str(1) assert _json_equal( """{"jsonrpc": "2.0", "method": "foobar", "id": "1"}""", request.serialize() ) response = request.error_respond(MethodNotFoundError('foobar')) assert _json_equal( """{"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found"}, "id": "1"}""", response.serialize() ) def test_jsonrpc_spec_v2_example5(prot): try: prot.parse_request( """{"jsonrpc": "2.0", "method": "foobar, "params": "bar", "baz]""") assert False # parsing must fail except JSONRPCParseError as error: e = error response = e.error_respond() assert _json_equal( """{"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error"}, "id": null}""", response.serialize() ) def test_jsonrpc_spec_v2_example6(prot): try: prot.parse_request( """{"jsonrpc": "2.0", "method": 1, "params": "bar"}""") assert False # parsing must fail except JSONRPCInvalidRequestError as error: e = error response = e.error_respond() assert _json_equal( """{"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null}""", response.serialize() ) def test_jsonrpc_spec_v2_example7(prot): try: prot.parse_request("""[ {"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"}, {"jsonrpc": "2.0", "method" ]""") assert False except JSONRPCParseError as error: e = error response = e.error_respond() assert _json_equal( """{"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error"}, "id": null}""", response.serialize() ) def test_jsonrpc_spec_v2_example8(prot): try: prot.parse_request("""[]""") assert False except JSONRPCInvalidRequestError as error: e = error response = e.error_respond() assert _json_equal("""{"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null}""", response.serialize()) def test_jsonrpc_spec_v2_example9(prot): requests = prot.parse_request("""[1]""") assert isinstance(requests[0], JSONRPCInvalidRequestError) responses = requests.create_batch_response() responses.append(requests[0].error_respond()) assert _json_equal("""[ {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null} ]""", responses.serialize()) def test_jsonrpc_spec_v2_example10(prot): requests = prot.parse_request("""[1, 2, 3]""") assert isinstance(requests[0], JSONRPCInvalidRequestError) assert isinstance(requests[1], JSONRPCInvalidRequestError) assert isinstance(requests[2], JSONRPCInvalidRequestError) responses = requests.create_batch_response() responses.append(requests[0].error_respond()) responses.append(requests[1].error_respond()) responses.append(requests[2].error_respond()) assert _json_equal("""[ {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null}, {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null}, {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null} ]""", responses.serialize()) def test_jsonrpc_spec_v2_example11(prot): requests = prot.parse_request("""[ {"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"}, {"jsonrpc": "2.0", "method": "notify_hello", "params": [7]}, {"jsonrpc": "2.0", "method": "subtract", "params": [42,23], "id": "2"}, {"foo": "boo"}, {"jsonrpc": "2.0", "method": "foo.get", "params": {"name": "myself"}, "id": "5"}, {"jsonrpc": "2.0", "method": "get_data", "id": "9"} ]""") assert isinstance(requests[3], JSONRPCInvalidRequestError) responses = requests.create_batch_response() responses.append(requests[0].respond(7)) responses.append(requests[2].respond(19)) responses.append(requests[3].error_respond()) responses.append(requests[4].error_respond(MethodNotFoundError('foo.get'))) responses.append(requests[5].respond(['hello', 5])) assert _json_equal("""[ {"jsonrpc": "2.0", "result": 7, "id": "1"}, {"jsonrpc": "2.0", "result": 19, "id": "2"}, {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null}, {"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found"}, "id": "5"}, {"jsonrpc": "2.0", "result": ["hello", 5], "id": "9"} ]""", responses.serialize()) def test_jsonrpc_spec_v2_example12(prot): reqs = [] reqs.append(prot.create_request('notify_sum', [1, 2, 4], one_way=True)) reqs.append(prot.create_request('notify_hello', [7], one_way=True)) request = prot.create_batch_request(reqs) assert request.create_batch_response() == None def test_can_get_custom_error_messages_out(prot): request = prot.create_request('foo') custom_msg = 'join the army, they said. see the world, they said.' e = Exception(custom_msg) response = request.error_respond(e) data = json.loads(response.serialize()) assert data['error']['message'] == custom_msg def test_accepts_empty_but_not_none_args_kwargs(prot): request = prot.create_request('foo', args=[], kwargs={}) def test_missing_jsonrpc_version_on_request(prot): with pytest.raises(JSONRPCInvalidRequestError): prot.parse_request('{"method": "sum", "params": [1,2,4], "id": "1"}') def test_missing_jsonrpc_version_on_reply(prot): with pytest.raises(InvalidReplyError): prot.parse_reply('{"result": 7, "id": "1"}') tinyrpc-0.6/tests/test_protocols.py000066400000000000000000000027661314762755300176760ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- import pytest from tinyrpc.protocols.jsonrpc import JSONRPCProtocol from tinyrpc import RPCErrorResponse @pytest.fixture(params=['jsonrpc']) def protocol(request): if 'jsonrpc': return JSONRPCProtocol() raise RuntimeError('Bad protocol name in test case') def test_protocol_returns_strings(protocol): req = protocol.create_request('foo', ['bar']) assert isinstance(req.serialize(), str) def test_procotol_responds_strings(protocol): req = protocol.create_request('foo', ['bar']) rep = req.respond(42) err_rep = req.error_respond(Exception('foo')) assert isinstance(rep.serialize(), str) assert isinstance(err_rep.serialize(), str) def test_one_way(protocol): req = protocol.create_request('foo', None, {'a': 'b'}, True) assert req.respond(None) == None def test_raises_on_args_and_kwargs(protocol): with pytest.raises(Exception): protocol.create_request('foo', ['arg1', 'arg2'], {'kw_key': 'kw_value'}) def test_supports_no_args(protocol): protocol.create_request('foo') def test_creates_error_response(protocol): req = protocol.create_request('foo', ['bar']) err_rep = req.error_respond(Exception('foo')) assert hasattr(err_rep, 'error') def test_parses_error_response(protocol): req = protocol.create_request('foo', ['bar']) err_rep = req.error_respond(Exception('foo')) parsed = protocol.parse_reply(err_rep.serialize()) assert hasattr(parsed, 'error') tinyrpc-0.6/tests/test_server.py000066400000000000000000000033541314762755300171520ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- import pytest import _compat from six.moves.mock import Mock, call from tinyrpc.server import RPCServer from tinyrpc.transports import ServerTransport from tinyrpc.protocols import RPCProtocol, RPCResponse from tinyrpc.dispatch import RPCDispatcher CONTEXT='sapperdeflap' RECMSG='out of receive_message' PARMSG='out of parse_request' SERMSG='out of serialize' @pytest.fixture def transport(): transport = Mock(ServerTransport) transport.receive_message = Mock(return_value=(CONTEXT, RECMSG)) return transport @pytest.fixture def protocol(): protocol = Mock(RPCProtocol) protocol.parse_request = Mock(return_value=PARMSG) return protocol @pytest.fixture() def response(): response = Mock(RPCResponse) response.serialize = Mock(return_value=SERMSG) return response @pytest.fixture def dispatcher(response): dispatcher = Mock(RPCDispatcher) dispatcher.dispatch = Mock(return_value=response) return dispatcher def test_handle_message(transport, protocol, dispatcher): server = RPCServer(transport, protocol, dispatcher) server.receive_one_message() transport.receive_message.assert_called() protocol.parse_request.assert_called_with(RECMSG) dispatcher.dispatch.assert_called_with(PARMSG, None) dispatcher.dispatch().serialize.assert_called() transport.send_reply.assert_called_with(CONTEXT, SERMSG) def test_handle_message_callback(transport, protocol, dispatcher): server = RPCServer(transport, protocol, dispatcher) server.trace = Mock(return_value=None) server.receive_one_message() assert server.trace.call_args_list == [call('-->', CONTEXT, RECMSG), call('<--', CONTEXT, SERMSG)] server.trace.assert_called() tinyrpc-0.6/tests/test_transport.py000066400000000000000000000060571314762755300177030ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- import pytest import six import zmq import zmq.green from tinyrpc.transports import ServerTransport, ClientTransport from tinyrpc.transports.zmq import ZmqServerTransport, ZmqClientTransport class DummyServerTransport(ServerTransport): def __init__(self): self.messages = [] self.clients = {} def receive_message(self): return self.messages.pop() def send_reply(self, context, message): if not isinstance(message, str): raise TypeError('Message must be str().') self.clients[context].messages.append(message) class DummyClientTransport(ClientTransport): def __init__(self, server): self.server = server self.id = id(self) self.server.clients[self.id] = self self.messages = [] def send_message(self, message): if not isinstance(message, str): raise TypeError('Message must be str().') self.server.messages.append((self.id, message)) def receive_reply(self): return self.messages.pop() ZMQ_ENDPOINT = 'inproc://example2' @pytest.fixture(scope='session') def zmq_context(request): ctx = zmq.Context() def fin(): request.addfinalizer(ctx.destroy()) return ctx @pytest.fixture(scope='session') def zmq_green_context(request): ctx = zmq.Context() def fin(): request.addfinalizer(ctx.destroy()) return ctx @pytest.fixture(params=['dummy', 'zmq', 'zmq.green']) def transport(request, zmq_context, zmq_green_context): if request.param == 'dummy': server = DummyServerTransport() client = DummyClientTransport(server) elif request.param in ('zmq', 'zmq.green'): ctx = zmq_context if request.param == 'zmq' else zmq_green_context server = ZmqServerTransport.create(ctx, ZMQ_ENDPOINT) client = ZmqClientTransport.create(ctx, ZMQ_ENDPOINT) def fin(): server.socket.close() client.socket.close() request.addfinalizer(fin) else: raise ValueError('Invalid transport.') return (client, server) SAMPLE_MESSAGES = ['asdf', 'loremipsum' * 1500, '', '\x00', 'b\x00a', '\r\n', '\n', u'\u1234'.encode('utf8')] BAD_MESSAGES = [u'asdf', u'', 1234, 1.2, None, True, False, ('foo',)] @pytest.fixture(scope='session', params=SAMPLE_MESSAGES) def sample_msg(request): return request.param @pytest.fixture(scope='session', params=SAMPLE_MESSAGES) def sample_msg2(request): return request.param @pytest.fixture(scope='session', params=BAD_MESSAGES) def bad_msg(request): return request.param @pytest.mark.skipif(six.PY3, reason='Somehow fails on PY3') def test_transport_rejects_bad_values(transport, sample_msg, bad_msg): client, server = transport with pytest.raises(TypeError): client.send_message(bad_msg) # FIXME: these tests need to be rethought, as they no longer work properly with # the change to the interface of ClientTransport # FIXME: the actual client needs tests as well tinyrpc-0.6/tests/test_wsgi_transport.py000066400000000000000000000106111314762755300207230ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- import pytest import six import gevent import gevent.queue import gevent.monkey from gevent.pywsgi import WSGIServer import requests from tinyrpc.transports.wsgi import WsgiServerTransport from tinyrpc.transports.http import HttpPostClientTransport TEST_SERVER_ADDR = ('127.0.0.1', 49294) @pytest.fixture(scope='module', autouse=True) def monkey_patches(request): # ugh? ugh. ugh. ugh! import socket # if six.PY2: # import httplib # else: # import http.client as httplib # FIXME: httplib=True has been removed in more recent gevent versions gevent.monkey.patch_all( socket=True, dns=False, time=False, select=False, thread=False, os=True, httplib=False, ssl=False, aggressive=False) def fin(): six.moves.reload_module(socket) # six.moves.reload_module(httplib) request.addfinalizer(fin) @pytest.fixture() def wsgi_server(request): app = WsgiServerTransport(queue_class=gevent.queue.Queue) server = WSGIServer(TEST_SERVER_ADDR, app.handle) def fin(): server.stop() server_greenlet.join() request.addfinalizer(fin) server_greenlet = gevent.spawn(server.serve_forever) gevent.sleep(0) # wait for server to come up return (app, 'http://%s:%d' % TEST_SERVER_ADDR) def test_server_supports_post_only(wsgi_server): transport, addr = wsgi_server r = requests.get(addr) # we expect a "not supported" response assert r.status_code == 405 r = requests.head(addr) # we expect a "not supported" response assert r.status_code == 405 @pytest.mark.skipif(six.PY3, reason='Somehow fails on PY3') @pytest.mark.parametrize(('msg',), [('foo',), ('',), ('bar',), ('1234',), ('{}',), ('{',), ('\x00\r\n',)]) def test_server_receives_messages(wsgi_server, msg): transport, addr = wsgi_server def consumer(): context, received_msg = transport.receive_message() assert received_msg == msg reply = 'reply:' + msg transport.send_reply(context, reply) gevent.spawn(consumer) r = requests.post(addr, data=msg) assert r.content == 'reply:' + msg @pytest.fixture def sessioned_client(): session = requests.Session() adapter = requests.adapters.HTTPAdapter(pool_maxsize=100) session.mount('http://', adapter) client = HttpPostClientTransport( 'http://%s:%d' % TEST_SERVER_ADDR, post_method=session.post ) return client @pytest.fixture def non_sessioned_client(): client = HttpPostClientTransport('http://%s:%d' % TEST_SERVER_ADDR) return client @pytest.mark.skipif(six.PY3, reason='Somehow fails on PY3') @pytest.mark.parametrize(('msg',), [('foo',), ('',), ('bar',), ('1234',), ('{}',), ('{',), ('\x00\r\n',)]) def test_sessioned_http_sessioned_client(wsgi_server, sessioned_client, msg): transport, addr = wsgi_server def consumer(): context, received_msg = transport.receive_message() assert received_msg == msg reply = 'reply:' + msg transport.send_reply(context, reply) gevent.spawn(consumer) result = sessioned_client.send_message(msg) assert result == 'reply:' + msg @pytest.mark.skipif(True,reason='tmp') def test_exhaust_ports(wsgi_server, non_sessioned_client): """ This raises a > ConnectionError: HTTPConnectionPool(host='127.0.0.1', port=49294): > Max retries exceeded with url: / (Caused by > NewConnectionError(' object at 0x7f6f86246210>: Failed to establish a new connection: > [Errno 99] Cannot assign requested address',)) """ transport, addr = wsgi_server def consumer(): context, received_msg = transport.receive_message() reply = 'reply:' + received_msg transport.send_reply(context, reply) def send_and_receive(i): try: gevent.spawn(consumer) msg = 'msg_%s' % i result = non_sessioned_client.send_message(msg) return result == 'reply:' + msg except Exception as e: return e pool = gevent.pool.Pool(500) with pytest.raises(requests.ConnectionError): for result in pool.imap_unordered(send_and_receive, xrange(55000)): assert result if isinstance(result, Exception): raise result tinyrpc-0.6/tinyrpc/000077500000000000000000000000001314762755300145545ustar00rootroot00000000000000tinyrpc-0.6/tinyrpc/__init__.py000066400000000000000000000001611314762755300166630ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- from .protocols import * from .exc import * from .client import * tinyrpc-0.6/tinyrpc/client.py000066400000000000000000000126731314762755300164150ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- import sys from collections import namedtuple from .exc import RPCError RPCCall = namedtuple('RPCCall', 'method args kwargs') """Defines the elements of a RPC call. RPCCall is used with :py:meth:`~tinyrpc.client.RPCClient.call_all` to provide the list of requests to be processed. Each request contains the elements defined in this tuple. """ RPCCallTo = namedtuple('RPCCallTo', 'transport method args kwargs') """Defines the elements of a RPC call directed to multiple transports. RPCCallTo is used with :py:meth:`~tinyrpc.client.RPCClient.call_all` to provide the list of requests to be processed. """ class RPCClient(object): """Client for making RPC calls to connected servers. :param protocol: An :py:class:`~tinyrpc.RPCProtocol` instance. :param transport: A :py:class:`~tinyrpc.transports.ClientTransport` instance. """ def __init__(self, protocol, transport): self.protocol = protocol self.transport = transport def _send_and_handle_reply(self, req, one_way, transport=None, no_exception=False): tport = self.transport if transport is None else transport # sends ... reply = tport.send_message(req.serialize()) if one_way: # ... and be done return # ... or process the reply response = self.protocol.parse_reply(reply) if not no_exception and hasattr(response, 'error'): raise RPCError('Error calling remote procedure: %s' %\ response.error) return response def call(self, method, args, kwargs, one_way=False): """Calls the requested method and returns the result. If an error occured, an :py:class:`~tinyrpc.exc.RPCError` instance is raised. :param method: Name of the method to call. :param args: Arguments to pass to the method. :param kwargs: Keyword arguments to pass to the method. :param one_way: Whether or not a reply is desired. """ req = self.protocol.create_request(method, args, kwargs, one_way) rep = self._send_and_handle_reply(req, one_way) if one_way: return return rep.result def call_all(self, requests): """Calls the methods in the request in parallel. When the :py:mod:`gevent` module is already loaded it is assumed to be correctly initialized, including monkey patching if necessary. In that case the RPC calls defined by ``requests`` is performed in parallel otherwise the methods are called sequentially. :param requests: A listof either RPCCall or RPCCallTo elements. When RPCCallTo is used each element defines a transport. Otherwise the default transport set when RPCClient is created is used. :return: A list with replies matching the order of the requests. """ threads = [] if 'gevent' in sys.modules: # assume that gevent is available and functional, make calls in parallel import gevent for r in requests: req = self.protocol.create_request(r.method, r.args, r.kwargs) tr = r.transport.transport if len(r) == 4 else None threads.append(gevent.spawn(self._send_and_handle_reply, req, False, tr, True)) gevent.joinall(threads) return [t.value for t in threads] else: # call serially for r in requests: req = self.protocol.create_request(r.method, r.args, r.kwargs) tr = r.transport.transport if len(r) == 4 else None threads.append(self._send_and_handle_reply(req, False, tr, True)) return threads def get_proxy(self, prefix='', one_way=False): """Convenience method for creating a proxy. :param prefix: Passed on to :py:class:`~tinyrpc.client.RPCProxy`. :param one_way: Passed on to :py:class:`~tinyrpc.client.RPCProxy`. :return: :py:class:`~tinyrpc.client.RPCProxy` instance.""" return RPCProxy(self, prefix, one_way) def batch_call(self, calls): """Experimental, use at your own peril.""" req = self.protocol.create_batch_request() for call_args in calls: req.append(self.protocol.create_request(*call_args)) return self._send_and_handle_reply(req) class RPCProxy(object): """Create a new remote proxy object. Proxies allow calling of methods through a simpler interface. See the documentation for an example. :param client: An :py:class:`~tinyrpc.client.RPCClient` instance. :param prefix: Prefix to prepend to every method name. :param one_way: Passed to every call of :py:func:`~tinyrpc.client.call`. """ def __init__(self, client, prefix='', one_way=False): self.client = client self.prefix = prefix self.one_way = one_way def __getattr__(self, name): """Returns a proxy function that, when called, will call a function name ``name`` on the client associated with the proxy. """ proxy_func = lambda *args, **kwargs: self.client.call( self.prefix + name, args, kwargs, one_way=self.one_way ) return proxy_func tinyrpc-0.6/tinyrpc/dispatch/000077500000000000000000000000001314762755300163535ustar00rootroot00000000000000tinyrpc-0.6/tinyrpc/dispatch/__init__.py000066400000000000000000000157341314762755300204760ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- import six import inspect from ..exc import * def public(name=None): """Set RPC name on function. This function decorator will set the ``_rpc_public_name`` attribute on a function, causing it to be picked up if an instance of its parent class is registered using :py:func:`~tinyrpc.dispatch.RPCDispatcher.register_instance`. ``@public`` is a shortcut for ``@public()``. :param name: The name to register the function with. """ # called directly with function if callable(name): f = name f._rpc_public_name = f.__name__ return f def _(f): f._rpc_public_name = name or f.__name__ return f return _ class RPCDispatcher(object): """Stores name-to-method mappings.""" def __init__(self): self.method_map = {} self.subdispatchers = {} def add_subdispatch(self, dispatcher, prefix=''): """Adds a subdispatcher, possibly in its own namespace. :param dispatcher: The dispatcher to add as a subdispatcher. :param prefix: A prefix. All of the new subdispatchers methods will be available as prefix + their original name. """ self.subdispatchers.setdefault(prefix, []).append(dispatcher) def add_method(self, f, name=None): """Add a method to the dispatcher. :param f: Callable to be added. :param name: Name to register it with. If ``None``, ``f.__name__`` will be used. """ assert callable(f), "method argument must be callable" # catches a few programming errors that are # commonly silently swallowed otherwise if not name: name = f.__name__ if name in self.method_map: raise RPCError('Name %s already registered') self.method_map[name] = f def dispatch(self, request, caller=None): """Fully handle request. The dispatch method determines which method to call, calls it and returns a response containing a result. No exceptions will be thrown, rather, every exception will be turned into a response using :py:func:`~tinyrpc.RPCRequest.error_respond`. If a method isn't found, a :py:exc:`~tinyrpc.exc.MethodNotFoundError` response will be returned. If any error occurs outside of the requested method, a :py:exc:`~tinyrpc.exc.ServerError` without any error information will be returend. If the method is found and called but throws an exception, the exception thrown is used as a response instead. This is the only case in which information from the exception is possibly propagated back to the client, as the exception is part of the requested method. :py:class:`~tinyrpc.RPCBatchRequest` instances are handled by handling all its children in order and collecting the results, then returning an :py:class:`~tinyrpc.RPCBatchResponse` with the results. To allow for custom processing around calling the method (i.e. custom error handling), the optional parameter ``caller`` may be provided with a callable. When present invoking the method is deferred to this callable. :param request: An :py:func:`~tinyrpc.RPCRequest`. :param caller: An optional callable used to invoke the method. :return: An :py:func:`~tinyrpc.RPCResponse`. """ if hasattr(request, 'create_batch_response'): results = [self._dispatch(req, caller) for req in request] response = request.create_batch_response() if response != None: response.extend(results) return response else: return self._dispatch(request, caller) def _dispatch(self, request, caller): try: try: method = self.get_method(request.method) except KeyError as e: return request.error_respond(MethodNotFoundError(e)) # we found the method try: if caller is not None: result = caller(method, request.args, request.kwargs) else: result = method(*request.args, **request.kwargs) except Exception as e: # an error occured within the method, return it return request.error_respond(e) # respond with result return request.respond(result) except Exception as e: # unexpected error, do not let client know what happened return request.error_respond(ServerError()) def get_method(self, name): """Retrieve a previously registered method. Checks if a method matching ``name`` has been registered. If :py:func:`get_method` cannot find a method, every subdispatcher with a prefix matching the method name is checked as well. If a method isn't found, a :py:class:`KeyError` is thrown. :param name: Callable to find. :param return: The callable. """ if name in self.method_map: return self.method_map[name] for prefix, subdispatchers in six.iteritems(self.subdispatchers): if name.startswith(prefix): for sd in subdispatchers: try: return sd.get_method(name[len(prefix):]) except KeyError: pass raise KeyError(name) def public(self, name=None): """Convenient decorator. Allows easy registering of functions to this dispatcher. Example: .. code-block:: python dispatch = RPCDispatcher() @dispatch.public def foo(bar): # ... class Baz(object): def not_exposed(self): # ... @dispatch.public(name='do_something') def visible_method(arg1) # ... :param name: Name to register callable with """ if callable(name): self.add_method(name) return name def _(f): self.add_method(f, name=name) return f return _ def register_instance(self, obj, prefix=''): """Create new subdispatcher and register all public object methods on it. To be used in conjunction with the :py:func:`tinyrpc.dispatch.public` decorator (*not* :py:func:`tinyrpc.dispatch.RPCDispatcher.public`). :param obj: The object whose public methods should be made available. :param prefix: A prefix for the new subdispatcher. """ dispatch = self.__class__() for name, f in inspect.getmembers( obj, lambda f: callable(f) and hasattr(f, '_rpc_public_name') ): dispatch.add_method(f, f._rpc_public_name) # add to dispatchers self.add_subdispatch(dispatch, prefix) tinyrpc-0.6/tinyrpc/exc.py000066400000000000000000000023271314762755300157110ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- class RPCError(Exception): """Base class for all excetions thrown by :py:mod:`tinyrpc`.""" class BadRequestError(RPCError): """Base class for all errors that caused the processing of a request to abort before a request object could be instantiated.""" def error_respond(self): """Create :py:class:`~tinyrpc.RPCErrorResponse` to respond the error. :return: A error responce instance or ``None``, if the protocol decides to drop the error silently.""" raise RuntimeError('Not implemented') class BadReplyError(RPCError): """Base class for all errors that caused processing of a reply to abort before it could be turned in a response object.""" class InvalidRequestError(BadRequestError): """A request made was malformed (i.e. violated the specification) and could not be parsed.""" class InvalidReplyError(BadReplyError): """A reply received was malformed (i.e. violated the specification) and could not be parsed into a response.""" class MethodNotFoundError(RPCError): """The desired method was not found.""" class ServerError(RPCError): """An internal error in the RPC system occured.""" tinyrpc-0.6/tinyrpc/protocols/000077500000000000000000000000001314762755300166005ustar00rootroot00000000000000tinyrpc-0.6/tinyrpc/protocols/__init__.py000066400000000000000000000125341314762755300207160ustar00rootroot00000000000000#!/usr/bin/env python from ..exc import * class RPCRequest(object): unique_id = None """A unique ID to remember the request by. Protocol specific, may or may not be set. This value should only be set by :py:func:`~tinyrpc.RPCProtocol.create_request`. The ID allows client to receive responses out-of-order and still allocate them to the correct request. Only supported if the parent protocol has :py:attr:`~tinyrpc.RPCProtocol.supports_out_of_order` set to ``True``. """ method = None """The name of the method to be called.""" args = [] """The positional arguments of the method call.""" kwargs = {} """The keyword arguments of the method call.""" def error_respond(self, error): """Creates an error response. Create a response indicating that the request was parsed correctly, but an error has occured trying to fulfill it. :param error: An exception or a string describing the error. :return: A response or ``None`` to indicate that no error should be sent out. """ raise NotImplementedError() def respond(self, result): """Create a response. Call this to return the result of a successful method invocation. This creates and returns an instance of a protocol-specific subclass of :py:class:`~tinyrpc.RPCResponse`. :param result: Passed on to new response instance. :return: A response or ``None`` to indicate this request does not expect a response. """ raise NotImplementedError() def serialize(self): """Returns a serialization of the request. :return: A string to be passed on to a transport. """ raise NotImplementedError() class RPCBatchRequest(list): """Multiple requests batched together. A batch request is a subclass of :py:class:`list`. Protocols that support multiple requests in a single message use this to group them together. Handling a batch requests is done in any order, responses must be gathered in a batch response and be in the same order as their respective requests. Any item of a batch request is either a request or a subclass of :py:class:`~tinyrpc.BadRequestError`, which indicates that there has been an error in parsing the request. """ def create_batch_response(self): """Creates a response suitable for responding to this request. :return: An :py:class:`~tinyrpc.RPCBatchResponse` or ``None``, if no response is expected.""" raise NotImplementedError() def serialize(self): raise NotImplementedError() class RPCResponse(object): """RPC call response class. Base class for all deriving responses. Has an attribute ``result`` containing the result of the RPC call, unless an error occured, in which case an attribute ``error`` will contain the error message.""" unique_id = None def serialize(self): """Returns a serialization of the response. :return: A reply to be passed on to a transport. """ raise NotImplementedError() class RPCErrorResponse(RPCResponse): pass class RPCBatchResponse(list): """Multiple response from a batch request. See :py:class:`~tinyrpc.RPCBatchRequest` on how to handle. Items in a batch response need to be :py:class:`~tinyrpc.RPCResponse` instances or None, meaning no reply should generated for the request. """ def serialize(self): """Returns a serialization of the batch response.""" raise NotImplementedError() class RPCProtocol(object): """Base class for all protocol implementations.""" supports_out_of_order = False """If true, this protocol can receive responses out of order correctly. Note that this usually depends on the generation of unique_ids, the generation of these may or may not be thread safe, depending on the protocol. Ideally, only once instance of RPCProtocol should be used per client.""" def create_request(self, method, args=None, kwargs=None, one_way=False): """Creates a new RPCRequest object. It is up to the implementing protocol whether or not ``args``, ``kwargs``, one of these, both at once or none of them are supported. :param method: The method name to invoke. :param args: The positional arguments to call the method with. :param kwargs: The keyword arguments to call the method with. :param one_way: The request is an update, i.e. it does not expect a reply. :return: A new :py:class:`~tinyrpc.RPCRequest` instance. """ raise NotImplementedError() def parse_request(self, data): """Parses a request given as a string and returns an :py:class:`RPCRequest` instance. :return: An instanced request. """ raise NotImplementedError() def parse_reply(self, data): """Parses a reply and returns an :py:class:`RPCResponse` instance. :return: An instanced response. """ raise NotImplementedError() class RPCBatchProtocol(RPCProtocol): def create_batch_request(self, requests=None): """Create a new :py:class:`tinyrpc.RPCBatchRequest` object. :param requests: A list of requests. """ raise NotImplementedError() tinyrpc-0.6/tinyrpc/protocols/jsonrpc.py000066400000000000000000000232071314762755300206340ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- from .. import RPCBatchProtocol, RPCRequest, RPCResponse, RPCErrorResponse,\ InvalidRequestError, MethodNotFoundError, ServerError,\ InvalidReplyError, RPCError, RPCBatchRequest, RPCBatchResponse import json import six import inspect import sys if 'jsonext' in sys.modules: # jsonext was imported before this file, assume the intent is that # it is used in place of the regular json encoder. import jsonext json_dumps = jsonext.dumps else: json_dumps = json.dumps class FixedErrorMessageMixin(object): def __init__(self, *args, **kwargs): if not args: args = [self.message] super(FixedErrorMessageMixin, self).__init__(*args, **kwargs) def error_respond(self): response = JSONRPCErrorResponse() response.error = self.message response.unique_id = None response._jsonrpc_error_code = self.jsonrpc_error_code return response class JSONRPCParseError(FixedErrorMessageMixin, InvalidRequestError): jsonrpc_error_code = -32700 message = 'Parse error' class JSONRPCInvalidRequestError(FixedErrorMessageMixin, InvalidRequestError): jsonrpc_error_code = -32600 message = 'Invalid Request' class JSONRPCMethodNotFoundError(FixedErrorMessageMixin, MethodNotFoundError): jsonrpc_error_code = -32601 message = 'Method not found' class JSONRPCInvalidParamsError(FixedErrorMessageMixin, InvalidRequestError): jsonrpc_error_code = -32602 message = 'Invalid params' class JSONRPCInternalError(FixedErrorMessageMixin, InvalidRequestError): jsonrpc_error_code = -32603 message = 'Internal error' class JSONRPCServerError(FixedErrorMessageMixin, InvalidRequestError): jsonrpc_error_code = -32000 message = '' class JSONRPCSuccessResponse(RPCResponse): def _to_dict(self): return { 'jsonrpc': JSONRPCProtocol.JSON_RPC_VERSION, 'id': self.unique_id, 'result': self.result, } def serialize(self): return json_dumps(self._to_dict()) class JSONRPCErrorResponse(RPCErrorResponse): def _to_dict(self): return { 'jsonrpc': JSONRPCProtocol.JSON_RPC_VERSION, 'id': self.unique_id, 'error': { 'message': str(self.error), 'code': self._jsonrpc_error_code, } } def serialize(self): return json_dumps(self._to_dict()) def _get_code_and_message(error): assert isinstance(error, (Exception, six.string_types)) if isinstance(error, Exception): if hasattr(error, 'jsonrpc_error_code'): code = error.jsonrpc_error_code msg = str(error) elif isinstance(error, InvalidRequestError): code = JSONRPCInvalidRequestError.jsonrpc_error_code msg = JSONRPCInvalidRequestError.message elif isinstance(error, MethodNotFoundError): code = JSONRPCMethodNotFoundError.jsonrpc_error_code msg = JSONRPCMethodNotFoundError.message else: # allow exception message to propagate code = JSONRPCServerError.jsonrpc_error_code msg = str(error) else: code = -32000 msg = error return code, msg class JSONRPCRequest(RPCRequest): def error_respond(self, error): if self.unique_id is None: return None response = JSONRPCErrorResponse() code, msg = _get_code_and_message(error) response.error = msg response.unique_id = self.unique_id response._jsonrpc_error_code = code return response def respond(self, result): if self.unique_id is None: return None response = JSONRPCSuccessResponse() response.result = result response.unique_id = self.unique_id return response def _to_dict(self): jdata = { 'jsonrpc': JSONRPCProtocol.JSON_RPC_VERSION, 'method': self.method, } if self.args: jdata['params'] = self.args if self.kwargs: jdata['params'] = self.kwargs if hasattr(self, 'unique_id') and self.unique_id is not None: jdata['id'] = self.unique_id return jdata def serialize(self): return json_dumps(self._to_dict()) class JSONRPCBatchRequest(RPCBatchRequest): def create_batch_response(self): if self._expects_response(): return JSONRPCBatchResponse() def _expects_response(self): for request in self: if isinstance(request, Exception): return True if request.unique_id != None: return True return False def serialize(self): return json_dumps([req._to_dict() for req in self]) class JSONRPCBatchResponse(RPCBatchResponse): def serialize(self): return json_dumps([resp._to_dict() for resp in self if resp != None]) class JSONRPCProtocol(RPCBatchProtocol): """JSONRPC protocol implementation. Currently, only version 2.0 is supported.""" JSON_RPC_VERSION = "2.0" _ALLOWED_REPLY_KEYS = sorted(['id', 'jsonrpc', 'error', 'result']) _ALLOWED_REQUEST_KEYS = sorted(['id', 'jsonrpc', 'method', 'params']) def __init__(self, *args, **kwargs): super(JSONRPCProtocol, self).__init__(*args, **kwargs) self._id_counter = 0 def _get_unique_id(self): self._id_counter += 1 return self._id_counter def create_batch_request(self, requests=None): return JSONRPCBatchRequest(requests or []) def create_request(self, method, args=None, kwargs=None, one_way=False): if args and kwargs: raise InvalidRequestError('Does not support args and kwargs at '\ 'the same time') request = JSONRPCRequest() if not one_way: request.unique_id = self._get_unique_id() request.method = method request.args = args request.kwargs = kwargs return request def parse_reply(self, data): if six.PY3 and isinstance(data, bytes): # zmq won't accept unicode strings, and this is the other # end; decoding non-unicode strings back into unicode data = data.decode() try: rep = json.loads(data) except Exception as e: raise InvalidReplyError(e) for k in six.iterkeys(rep): if not k in self._ALLOWED_REPLY_KEYS: raise InvalidReplyError('Key not allowed: %s' % k) if not 'jsonrpc' in rep: raise InvalidReplyError('Missing jsonrpc (version) in response.') if rep['jsonrpc'] != self.JSON_RPC_VERSION: raise InvalidReplyError('Wrong JSONRPC version') if not 'id' in rep: raise InvalidReplyError('Missing id in response') if ('error' in rep) == ('result' in rep): raise InvalidReplyError( 'Reply must contain exactly one of result and error.' ) if 'error' in rep: response = JSONRPCErrorResponse() error = rep['error'] response.error = error['message'] response._jsonrpc_error_code = error['code'] else: response = JSONRPCSuccessResponse() response.result = rep.get('result', None) response.unique_id = rep['id'] return response def parse_request(self, data): if six.PY3 and isinstance(data, bytes): # zmq won't accept unicode strings, and this is the other # end; decoding non-unicode strings back into unicode data = data.decode() try: req = json.loads(data) except Exception as e: raise JSONRPCParseError() if isinstance(req, list): # batch request requests = JSONRPCBatchRequest() for subreq in req: try: requests.append(self._parse_subrequest(subreq)) except RPCError as e: requests.append(e) except Exception as e: requests.append(JSONRPCInvalidRequestError()) if not requests: raise JSONRPCInvalidRequestError() return requests else: return self._parse_subrequest(req) def _parse_subrequest(self, req): for k in six.iterkeys(req): if not k in self._ALLOWED_REQUEST_KEYS: raise JSONRPCInvalidRequestError() if req.get('jsonrpc', None) != self.JSON_RPC_VERSION: raise JSONRPCInvalidRequestError() if not isinstance(req['method'], six.string_types): raise JSONRPCInvalidRequestError() request = JSONRPCRequest() request.method = str(req['method']) request.unique_id = req.get('id', None) params = req.get('params', None) if params != None: if isinstance(params, list): request.args = req['params'] elif isinstance(params, dict): request.kwargs = req['params'] else: raise JSONRPCInvalidParamsError() return request def _caller(self, method, args, kwargs): # custom dispatcher called by RPCDispatcher._dispatch() # when provided with the address of a custom dispatcher. # Used to generate a customized error message when the # function signature doesn't match the parameter list. try: inspect.getcallargs(method, *args, **kwargs) except TypeError: raise JSONRPCInvalidParamsError() else: return method(*args, **kwargs) tinyrpc-0.6/tinyrpc/server/000077500000000000000000000000001314762755300160625ustar00rootroot00000000000000tinyrpc-0.6/tinyrpc/server/__init__.py000066400000000000000000000075371314762755300202070ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # FIXME: needs unittests # FIXME: needs checks for out-of-order, concurrency, etc as attributes from tinyrpc.exc import RPCError class RPCServer(object): """High level RPC server. :param transport: The :py:class:`~tinyrpc.transports.RPCTransport` to use. :param protocol: The :py:class:`~tinyrpc.RPCProtocol` to use. :param dispatcher: The :py:class:`~tinyrpc.dispatch.RPCDispatcher` to use. """ trace = None """Trace incoming and outgoing messages. When this attribute is set to a callable this callable will be called directly after a message has been received and immediately after a reply is sent. The callable should accept three positional parameters: * *direction*: string, either '-->' for incoming or '<--' for outgoing data. * *context*: the context returned by :py:meth:`~tinyrpc.transport.RPCTransport.receive_message`. * *message*: the message string itself. Example:: def my_trace(direction, context, message): logger.debug('%s%s', direction, message) server = RPCServer(transport, protocol, dispatcher) server.trace = my_trace server.serve_forever will log all incoming and outgoing traffic of the RPC service. """ def __init__(self, transport, protocol, dispatcher): self.transport = transport self.protocol = protocol self.dispatcher = dispatcher self.trace = None def serve_forever(self): """Handle requests forever. Starts the server loop continuously calling :py:meth:`receive_one_message` to process the next incoming request. """ while True: self.receive_one_message() def receive_one_message(self): """Handle a single request. Polls the transport for a new message. After a new message has arrived :py:meth:`_spawn` is called with a handler function and arguments to handle the request. The handler function will try to decode the message using the supplied protocol, if that fails, an error response will be sent. After decoding the message, the dispatcher will be asked to handle the resultung request and the return value (either an error or a result) will be sent back to the client using the transport. """ context, message = self.transport.receive_message() if callable(self.trace): self.trace('-->', context, message) # assuming protocol is threadsafe and dispatcher is theadsafe, as # long as its immutable def handle_message(context, message): try: request = self.protocol.parse_request(message) except RPCError as e: response = e.error_respond() else: response = self.dispatcher.dispatch( request, getattr(self.protocol, '_caller', None) ) # send reply if response is not None: result = response.serialize() if callable(self.trace): self.trace('<--', context, result) self.transport.send_reply(context, result) self._spawn(handle_message, context, message) def _spawn(self, func, *args, **kwargs): """Spawn a handler function. This function is overridden in subclasses to provide concurrency. In the base implementation, it simply calls the supplied function ``func`` with ``*args`` and ``**kwargs``. This results in a single-threaded, single-process, synchronous server. :param func: A callable to call. :param args: Arguments to ``func``. :param kwargs: Keyword arguments to ``func``. """ func(*args, **kwargs) tinyrpc-0.6/tinyrpc/server/gevent.py000066400000000000000000000004611314762755300177250ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- from __future__ import absolute_import import gevent from . import RPCServer class RPCServerGreenlets(RPCServer): # documentation in docs because of dependencies def _spawn(self, func, *args, **kwargs): gevent.spawn(func, *args, **kwargs) tinyrpc-0.6/tinyrpc/transports/000077500000000000000000000000001314762755300167735ustar00rootroot00000000000000tinyrpc-0.6/tinyrpc/transports/__init__.py000066400000000000000000000034111314762755300211030ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- class ServerTransport(object): """Base class for all server transports.""" def receive_message(self): """Receive a message from the transport. Blocks until another message has been received. May return a context opaque to clients that should be passed on :py:func:`~tinyrpc.transport.ServerTransport.send_reply` to identify the client later on. :return: A tuple consisting of ``(context, message)``. """ raise NotImplementedError() def send_reply(self, context, reply): """Sends a reply to a client. The client is usually identified by passing ``context`` as returned from the original :py:func:`~tinyrpc.transport.Transport.receive_message` call. Messages must be strings, it is up to the sender to convert the beforehand. A non-string value raises a :py:exc:`TypeError`. :param context: A context returned by :py:func:`~tinyrpc.transport.ServerTransport.receive_message`. :param reply: A string to send back as the reply. """ raise NotImplementedError class ClientTransport(object): """Base class for all client transports.""" def send_message(self, message, expect_reply=True): """Send a message to the server and possibly receive a reply. Sends a message to the connected server. Messages must be strings, it is up to the sender to convert the beforehand. A non-string value raises a :py:exc:`TypeError`. This function will block until one reply has been received. :param message: A string to send. :return: A string containing the server reply. """ raise NotImplementedError tinyrpc-0.6/tinyrpc/transports/callback.py000066400000000000000000000046351314762755300211110ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- """ callback extends the tinyrpc package. The CallbackServerTransport uses the provided callbacks to implement communication with the counterpart. (c) 2016, Leo Noordergraaf, Nextpertise BV This code is made available under the same license as tinyrpc itself. """ import json from . import ServerTransport class CallbackServerTransport(ServerTransport): """Callback server transport. Used when tinyrpc is part of a system where it cannot directly attach to a socket or stream. The methods :py:meth:`receive_message` and :py:meth:`send_reply` are implemented by callback functions that were passed to :py:meth:`__init__`. This transport is also useful for testing the other modules of tinyrpc. """ def __init__(self, reader, writer): """Install callbacks. :param callable reader: Expected to return a string or other data structure. A string is assumed to be json and is passed on as is, otherwise the returned value is converted into a json string. :param callable writer: Expected to accept a single parameter of type string. """ super(CallbackServerTransport, self).__init__() self.reader = reader self.writer = writer def receive_message(self): """Receive a message from the transport. Uses the callback function :py:attr:`reader` to obtain a json string. May return a context opaque to clients that should be passed on :py:meth:`send_reply` to identify the client later on. :return: A tuple consisting of ``(context, message)``. """ data = self.reader() if type(data) != str: # Turn non-string to string or die trying data = json.dumps(data) return None, data def send_reply(self, context, reply): """Sends a reply to a client. The client is usually identified by passing ``context`` as returned from the original :py:meth:`receive_message` call. Messages must be a string, it is up to the sender to convert it beforehand. A non-string value raises a :py:exc:`TypeError`. :param context: A context returned by :py:meth:`receive_message`. :param reply: A string to send back as the reply. """ self.writer(reply) tinyrpc-0.6/tinyrpc/transports/cgi.py000066400000000000000000000060041314762755300201070ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- """ cgi extends the tinyrpc package. The CGIServerTransport adds CGI as a supported server protocol that can be used with the regular HTTP client. (c) 2016, Leo Noordergraaf, Nextpertise BV This code is made available under the same license as tinyrpc itself. """ from __future__ import print_function import os import sys import json import cgi try: import urllib.parse as urlparse except ImportError: import urllib as urlparse from . import ServerTransport class CGIServerTransport(ServerTransport): """CGI transport. Reading stdin is blocking but, given that we've been called, something is waiting. The transport accepts both GET and POST request. A POST request provides the entire JSON-RPC request in the body of the HTTP request. A GET request provides the elements of the JSON-RPC request in separate query parameters and only the params field contains a JSON object or array. i.e. curl 'http://server?jsonrpc=2.0&id=1&method="doit"¶ms={"arg"="something"}' """ def receive_message(self): """Receive a message from the transport. Blocks until another message has been received. May return a context opaque to clients that should be passed on :py:func:`~tinyrpc.transport.CGIServerTransport.send_reply` to identify the client later on. :return: A tuple consisting of ``(context, message)``. """ if 'CONTENT_LENGTH' in os.environ: # POST content_length = int(os.environ['CONTENT_LENGTH']) request_json = sys.stdin.read(content_length) request_json = urlparse.unquote(request_json) else: # GET fields = cgi.FieldStorage() jsonrpc = fields.getfirst("jsonrpc") id = fields.getfirst("id") method = fields.getfirst("method") params = fields.getfirst("params") # Create request string request_json = json.dumps({ 'jsonrpc': jsonrpc, 'id': id, 'method': method, 'params': params }) return None, request_json def send_reply(self, context, reply): """Sends a reply to a client. The client is usually identified by passing ``context`` as returned from the original :py:func:`~tinyrpc.transport.Transport.receive_message` call. Messages must be strings, it is up to the sender to convert the beforehand. A non-string value raises a :py:exc:`TypeError`. :param context: A context returned by :py:func:`~tinyrpc.transport.CGIServerTransport.receive_message`. :param reply: A string to send back as the reply. """ # context isn't used with cgi print("Content-Type: application/json") print("Cache-Control: no-cache") print("Pragma: no-cache") print("Content-Length: %d" % len(reply)) print() print(reply) tinyrpc-0.6/tinyrpc/transports/http.py000066400000000000000000000043421314762755300203270ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- import requests import geventwebsocket as websocket from . import ClientTransport class HttpPostClientTransport(ClientTransport): """HTTP POST based client transport. Requires :py:mod:`requests`. Submits messages to a server using the body of an ``HTTP`` ``POST`` request. Replies are taken from the responses body. :param endpoint: The URL to send ``POST`` data to. :param post_method: allows to replace `requests.post` with another method, e.g. the post method of a `requests.Session()` instance. :param kwargs: Additional parameters for :py:func:`requests.post`. """ def __init__(self, endpoint, post_method=None, **kwargs): self.endpoint = endpoint self.request_kwargs = kwargs if post_method is None: self.post = requests.post else: self.post = post_method def send_message(self, message, expect_reply=True): if not isinstance(message, str): raise TypeError('str expected') r = self.post(self.endpoint, data=message, **self.request_kwargs) if expect_reply: return r.content class HttpWebSocketClientTransport(ClientTransport): """HTTP WebSocket based client transport. Requires :py:mod:`websocket-python`. Submits messages to a server using the body of an ``HTTP`` ``WebSocket`` message. Replies are taken from the response of the websocket. The connection is establish on the ``__init__`` because the protocol is connection oriented, you need to close the connection calling the close method. :param endpoint: The URL to connect the websocket. :param kwargs: Additional parameters for :py:func:`websocket.send`. """ def __init__(self, endpoint, **kwargs): self.endpoint = endpoint self.request_kwargs = kwargs self.ws = websocket.create_connection(self.endpoint, **kwargs) def send_message(self, message, expect_reply=True): if not isinstance(message, str): raise TypeError('str expected') self.ws.send(message) r = self.ws.recv() if expect_reply: return r def close(self): if self.ws is not None: self.ws.close() tinyrpc-0.6/tinyrpc/transports/websocket.py000066400000000000000000000056741314762755300213470ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- import Queue from . import ServerTransport from geventwebsocket.resource import WebSocketApplication, Resource class WSServerTransport(ServerTransport): ''' Requires :py:mod:`geventwebsocket`. Due to the nature of WS, this transport has a few pecularities: It must be run in a thread, greenlet or some other form of concurrent execution primitive. This is due to :py:prop:`~tinyrpc.transports.websocket.WSServerTransport.handle` which is a :py:class:`geventwebsocket.resource.Resource` that joins a wsgi handler for the / and a WebSocket handler for the /ws path. These resource is used in combination with a :py:class:`geventwebsocket.server.WebSocketServer` that blocks while waiting for a call to :py:func:`~tinyrpc.transports.wsgi.WSServerTransport.send_reply`. The parameter ``queue_class`` must be used to supply a proper queue class for the chosen concurrency mechanism (i.e. when using :py:mod:`gevent`, set it to :py:class:`gevent.queue.Queue`). :param queue_class: The Queue class to use. :param wsgi_handler: Can be used to change the standard response to a http request to the / ''' def __init__(self, queue_class=Queue.Queue, wsgi_handler=None): self._queue_class = queue_class self.messages = queue_class() def static_wsgi_app(environ, start_response): start_response("200 OK", [("Content-Type", "text/html")]) return 'Ready for WebSocket connection in /ws' self.handle = Resource( {'/': static_wsgi_app if wsgi_handler is None else wsgi_handler, '/ws': WSApplicationFactory(self.messages, queue_class)}) def receive_message(self): return self.messages.get() def send_reply(self, context, reply): context.put(reply) class WSApplicationFactory(object): ''' Creates WebSocketApplications with a messages queue and the queue_class needed for the communication with the WSServerTransport. ''' def __init__(self, messages, queue_class): self.messages = messages self._queue_class = queue_class def __call__(self, ws): ''' The fake __init__ for the WSApplication ''' app = WSApplication(ws) app.messages = self.messages app._queue_class = self._queue_class return app @classmethod def protocol(cls): return WebSocketApplication.protocol() class WSApplication(WebSocketApplication): ''' This class is the bridge between the WSServerTransport and the WebSocket protocol implemented by :py:class:`geventwebsocket.resource.WebSocketApplication` ''' def on_message(self, msg, *args, **kwargs): # create new context context = self._queue_class() self.messages.put((context, msg)) response = context.get() self.ws.send(response, *args, **kwargs) tinyrpc-0.6/tinyrpc/transports/wsgi.py000066400000000000000000000062051314762755300203210ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- from six.moves import queue as Queue from werkzeug.wrappers import Response, Request from . import ServerTransport class WsgiServerTransport(ServerTransport): """WSGI transport. Requires :py:mod:`werkzeug`. Due to the nature of WSGI, this transport has a few pecularities: It must be run in a thread, greenlet or some other form of concurrent execution primitive. This is due to :py:func:`~tinyrpc.transports.wsgi.WsgiServerTransport.handle` blocking while waiting for a call to :py:func:`~tinyrpc.transports.wsgi.WsgiServerTransport.send_reply`. The parameter ``queue_class`` must be used to supply a proper queue class for the chosen concurrency mechanism (i.e. when using :py:mod:`gevent`, set it to :py:class:`gevent.queue.Queue`). :param max_content_length: The maximum request content size allowed. Should be set to a sane value to prevent DoS-Attacks. :param queue_class: The Queue class to use. :param allow_origin: The ``Access-Control-Allow-Origin`` header. Defaults to ``*`` (so change it if you need actual security). """ def __init__(self, max_content_length=4096, queue_class=Queue.Queue, allow_origin='*'): self._queue_class = queue_class self.messages = queue_class() self.max_content_length = max_content_length self.allow_origin = allow_origin def receive_message(self): return self.messages.get() def send_reply(self, context, reply): if not isinstance(reply, str): raise TypeError('str expected') context.put(reply) def handle(self, environ, start_response): """WSGI handler function. The transport will serve a request by reading the message and putting it into an internal buffer. It will then block until another concurrently running function sends a reply using :py:func:`~tinyrpc.transports.WsgiServerTransport.send_reply`. The reply will then be sent to the client being handled and handle will return. """ request = Request(environ) request.max_content_length = self.max_content_length access_control_headers = { 'Access-Control-Allow-Methods': 'POST', 'Access-Control-Allow-Origin': self.allow_origin, 'Access-Control-Allow-Headers': \ 'Content-Type, X-Requested-With, Accept, Origin' } if request.method == 'OPTIONS': response = Response(headers=access_control_headers) elif request.method == 'POST': # message is encoded in POST, read it... msg = request.stream.read() # create new context context = self._queue_class() self.messages.put((context, msg)) # ...and send the reply response = Response(context.get(), headers=access_control_headers) else: # nothing else supported at the moment response = Response('Only POST supported', 405) return response(environ, start_response) tinyrpc-0.6/tinyrpc/transports/zmq.py000066400000000000000000000050541314762755300201600ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- from __future__ import absolute_import # needed for zmq import import six import zmq from . import ServerTransport, ClientTransport class ZmqServerTransport(ServerTransport): """Server transport based on a :py:const:`zmq.ROUTER` socket. :param socket: A :py:const:`zmq.ROUTER` socket instance, bound to an endpoint. """ def __init__(self, socket): self.socket = socket def receive_message(self): msg = self.socket.recv_multipart() return msg[:-1], msg[-1] def send_reply(self, context, reply): if six.PY3 and isinstance(reply, six.string_types): # zmq won't accept unicode strings reply = reply.encode() self.socket.send_multipart(context + [reply]) @classmethod def create(cls, zmq_context, endpoint): """Create new server transport. Instead of creating the socket yourself, you can call this function and merely pass the :py:class:`zmq.core.context.Context` instance. By passing a context imported from :py:mod:`zmq.green`, you can use green (gevent) 0mq sockets as well. :param zmq_context: A 0mq context. :param endpoint: The endpoint clients will connect to. """ socket = zmq_context.socket(zmq.ROUTER) socket.bind(endpoint) return cls(socket) class ZmqClientTransport(ClientTransport): """Client transport based on a :py:const:`zmq.REQ` socket. :param socket: A :py:const:`zmq.REQ` socket instance, connected to the server socket. """ def __init__(self, socket): self.socket = socket def send_message(self, message, expect_reply=True): if six.PY3 and isinstance(message, six.string_types): # pyzmq won't accept unicode strings message = message.encode() self.socket.send(message) if expect_reply: return self.socket.recv() @classmethod def create(cls, zmq_context, endpoint): """Create new client transport. Instead of creating the socket yourself, you can call this function and merely pass the :py:class:`zmq.core.context.Context` instance. By passing a context imported from :py:mod:`zmq.green`, you can use green (gevent) 0mq sockets as well. :param zmq_context: A 0mq context. :param endpoint: The endpoint the server is bound to. """ socket = zmq_context.socket(zmq.REQ) socket.connect(endpoint) return cls(socket) tinyrpc-0.6/tox.ini000066400000000000000000000002601314762755300143750ustar00rootroot00000000000000[tox] envlist = py27,py33,py34 [testenv] deps= py27: mock>=1.3.0 six pytest werkzeug gevent-websocket gevent requests zmq commands=py.test -rs