├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTORS.md ├── LICENSE ├── README.rst ├── _config.yml ├── docs ├── Makefile ├── api.rst ├── conf.py ├── http.rst ├── index.rst └── make.bat ├── pyrabbit2 ├── __init__.py ├── api.py └── http.py ├── requirements.txt ├── requirements3.txt ├── setup.py ├── tests ├── test_httpclient.py └── test_pyrabbit.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | .tox 2 | .idea 3 | .coverage 4 | *.swp 5 | *.pyc 6 | *egg-info* 7 | build 8 | _build 9 | _static 10 | _templates 11 | dist 12 | *.log 13 | 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.4" 5 | - "3.5" 6 | - "3.6" 7 | 8 | addons: 9 | apt: 10 | sources: 11 | - sourceline: deb https://packages.erlang-solutions.com/ubuntu trusty contrib 12 | key_url: https://packages.erlang-solutions.com/ubuntu/erlang_solutions.asc 13 | - sourceline: deb https://dl.bintray.com/rabbitmq/debian trusty main 14 | key_url: https://dl.bintray.com/rabbitmq/Keys/rabbitmq-release-signing-key.asc 15 | packages: 16 | - esl-erlang=1:20.1 17 | 18 | before_install: 19 | - sudo mv /opt/jdk_switcher/jdk_switcher.sh /tmp 20 | - sudo apt-get install rabbitmq-server=3.7.4-1 21 | - sudo mv /tmp/jdk_switcher.sh /opt/jdk_switcher/ 22 | - sudo rabbitmq-plugins enable rabbitmq_management 23 | - sudo rabbitmqctl add_user test test 24 | - sudo rabbitmqctl set_user_tags test administrator 25 | - sudo rabbitmqctl set_permissions -p / test \".*\" \".*\" \".*\" 26 | - sudo rabbitmq-plugins enable rabbitmq_shovel 27 | - chmod +x tests/test_pyrabbit.py 28 | 29 | install: 30 | - if [[ $TRAVIS_PYTHON_VERSION != '2.7' ]]; then pip install -r requirements3.txt; fi 31 | - if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then pip install -r requirements.txt; fi 32 | - cd tests 33 | script: 34 | - if [[ $TRAVIS_PYTHON_VERSION != '2.7' ]]; then python3 test_pyrabbit.py; fi 35 | - if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then python test_pyrabbit.py; fi 36 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 1.0.0 2 | ---------------- 3 | Features 4 | - Support shovel (Create, Read, Update, Delete) 5 | - Support users account SHA256 + salt (Create, Update, Move, Delete) 6 | - For security not support plain text users passwords. https://www.rabbitmq.com/passwords.html 7 | - Show names extensions on RMQ Servers 8 | - Show definitions 9 | 10 | Tests 11 | - Add support Travis CI https://travis-ci.org/deslum/pyrabbit2 12 | - Add new unittests and modify old 13 | - Testing API and HTTP modules in pyrabbit2 14 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | If you've contributed, let it be known! 2 | 3 | - @bkjones 4 | - @spil-sean 5 | - @davidszotten 6 | - @inviscid 7 | - @deslum 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011, Brian K. Jones (bkjones@gmail.com) 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | * Neither the name of the author nor the names of its 13 | contributors may be used to endorse or promote products derived from this 14 | software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 19 | IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 20 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 21 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 22 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 23 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 24 | OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 25 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | 2 | PyRabbit2 3 | ------------------- 4 | 5 | .. image:: https://travis-ci.org/deslum/pyrabbit2.svg?branch=master 6 | :target: https://travis-ci.org/deslum/pyrabbit2 7 | 8 | .. image:: https://img.shields.io/badge/python-2.x-yellow.svg 9 | :target: https://pypi.python.org/pypi/pyrabbit2/ 10 | 11 | .. image:: https://img.shields.io/badge/python-3.x-orange.svg 12 | :target: https://pypi.python.org/pypi/pyrabbit2/ 13 | 14 | .. image:: https://img.shields.io/badge/license-New%20BSD-blue.svg 15 | :target: https://raw.githubusercontent.com/deslum/pyrabbit2/master/LICENSE 16 | 17 | This project is fork project pyrabbit https://github.com/bkjones/pyrabbit 18 | 19 | I fork project, because he don't update many years. 20 | 21 | Pyrabbit2 is a module to make it easy to interface w/ RabbitMQ's HTTP Management 22 | API. It's tested against RabbitMQ 3.7.4 using Python 2.7-3.6. It has 23 | a pretty solid set of tests, and I use tox to test across Python versions. 24 | 25 | PyRabbit2 is on PyPI, which makes it installable using pip or easy_install. 26 | 27 | Features: 28 | ---------- 29 | 30 | * Users (Create, Read, Update, Delete) 31 | * User acess SHA256 + salt 32 | * Permissions 33 | * Polices 34 | * Support SSL 35 | * Vhosts (Create, Read, Update, Delete) 36 | * Exchanges (Create, Read, Update, Delete) 37 | * Bindings (Create, Read, Update, Delete) 38 | * Queues (Create, Read, Update, Delete) 39 | * Shovel 40 | * Work with cluster nodes 41 | * Many features support RabbitMQ API https://pulse.mozilla.org/api/ 42 | 43 | Install 44 | ----------- 45 | :: 46 | 47 | pip3 install pyrabbit2 48 | 49 | 50 | Documentation 51 | -------------- 52 | 53 | http://pyrabbit.readthedocs.org 54 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | 15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " singlehtml to make a single large HTML file" 22 | @echo " pickle to make pickle files" 23 | @echo " json to make JSON files" 24 | @echo " htmlhelp to make HTML files and a HTML help project" 25 | @echo " qthelp to make HTML files and a qthelp project" 26 | @echo " devhelp to make HTML files and a Devhelp project" 27 | @echo " epub to make an epub" 28 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 29 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 30 | @echo " text to make text files" 31 | @echo " man to make manual pages" 32 | @echo " changes to make an overview of all changed/added/deprecated items" 33 | @echo " linkcheck to check all external links for integrity" 34 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 35 | 36 | clean: 37 | -rm -rf $(BUILDDIR)/* 38 | 39 | html: 40 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 41 | @echo 42 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 43 | 44 | dirhtml: 45 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 48 | 49 | singlehtml: 50 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 51 | @echo 52 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 53 | 54 | pickle: 55 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 56 | @echo 57 | @echo "Build finished; now you can process the pickle files." 58 | 59 | json: 60 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 61 | @echo 62 | @echo "Build finished; now you can process the JSON files." 63 | 64 | htmlhelp: 65 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 66 | @echo 67 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 68 | ".hhp project file in $(BUILDDIR)/htmlhelp." 69 | 70 | qthelp: 71 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 72 | @echo 73 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 74 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 75 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/PyRabbit.qhcp" 76 | @echo "To view the help file:" 77 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PyRabbit.qhc" 78 | 79 | devhelp: 80 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 81 | @echo 82 | @echo "Build finished." 83 | @echo "To view the help file:" 84 | @echo "# mkdir -p $$HOME/.local/share/devhelp/PyRabbit" 85 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PyRabbit" 86 | @echo "# devhelp" 87 | 88 | epub: 89 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 90 | @echo 91 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 92 | 93 | latex: 94 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 95 | @echo 96 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 97 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 98 | "(use \`make latexpdf' here to do that automatically)." 99 | 100 | latexpdf: 101 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 102 | @echo "Running LaTeX files through pdflatex..." 103 | make -C $(BUILDDIR)/latex all-pdf 104 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 105 | 106 | text: 107 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 108 | @echo 109 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 110 | 111 | man: 112 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 113 | @echo 114 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 115 | 116 | changes: 117 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 118 | @echo 119 | @echo "The overview file is in $(BUILDDIR)/changes." 120 | 121 | linkcheck: 122 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 123 | @echo 124 | @echo "Link check complete; look for any errors in the above output " \ 125 | "or in $(BUILDDIR)/linkcheck/output.txt." 126 | 127 | doctest: 128 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 129 | @echo "Testing of doctests in the sources finished, look at the " \ 130 | "results in $(BUILDDIR)/doctest/output.txt." 131 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | =============== 2 | The api Module 3 | =============== 4 | 5 | The api module is the only module meant for use by users of the module, although that is not enforced. It exposes 6 | an interface for getting at all of the functionality provided by the RabbitMQ API. Where that is *not* true, it will be shortly ;-) 7 | 8 | .. automodule:: pyrabbit.api 9 | :members: 10 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # PyRabbit documentation build configuration file, created by 4 | # sphinx-quickstart on Tue Sep 20 22:48:33 2011. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | #sys.path.insert(0, os.path.abspath('.')) 20 | sys.path.insert(0, os.path.abspath('..')) 21 | # -- General configuration ----------------------------------------------------- 22 | 23 | # If your documentation needs a minimal Sphinx version, state it here. 24 | #needs_sphinx = '1.0' 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be extensions 27 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 28 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.coverage', 'sphinx.ext.viewcode'] 29 | 30 | # Add any paths that contain templates here, relative to this directory. 31 | #templates_path = ['_templates'] 32 | 33 | # The suffix of source filenames. 34 | source_suffix = '.rst' 35 | 36 | # The encoding of source files. 37 | #source_encoding = 'utf-8-sig' 38 | 39 | # The master toctree document. 40 | master_doc = 'index' 41 | 42 | # General information about the project. 43 | project = u'PyRabbit' 44 | copyright = u'2011-2015, Brian K. Jones' 45 | 46 | # The version info for the project you're documenting, acts as replacement for 47 | # |version| and |release|, also used in various other places throughout the 48 | # built documents. 49 | # 50 | # The short X.Y version. 51 | version = '1.1.0' 52 | # The full version, including alpha/beta/rc tags. 53 | release = '1.1.0' 54 | 55 | # The language for content autogenerated by Sphinx. Refer to documentation 56 | # for a list of supported languages. 57 | #language = None 58 | 59 | # There are two options for replacing |today|: either, you set today to some 60 | # non-false value, then it is used: 61 | #today = '' 62 | # Else, today_fmt is used as the format for a strftime call. 63 | #today_fmt = '%B %d, %Y' 64 | 65 | # List of patterns, relative to source directory, that match files and 66 | # directories to ignore when looking for source files. 67 | exclude_patterns = ['_build'] 68 | 69 | # The reST default role (used for this markup: `text`) to use for all documents. 70 | #default_role = None 71 | 72 | # If true, '()' will be appended to :func: etc. cross-reference text. 73 | #add_function_parentheses = True 74 | 75 | # If true, the current module name will be prepended to all description 76 | # unit titles (such as .. function::). 77 | #add_module_names = True 78 | 79 | # If true, sectionauthor and moduleauthor directives will be shown in the 80 | # output. They are ignored by default. 81 | #show_authors = False 82 | 83 | # The name of the Pygments (syntax highlighting) style to use. 84 | pygments_style = 'sphinx' 85 | 86 | # A list of ignored prefixes for module index sorting. 87 | #modindex_common_prefix = [] 88 | 89 | 90 | # -- Options for HTML output --------------------------------------------------- 91 | 92 | # The theme to use for HTML and HTML Help pages. See the documentation for 93 | # a list of builtin themes. 94 | html_theme = 'nature' 95 | 96 | # Theme options are theme-specific and customize the look and feel of a theme 97 | # further. For a list of options available for each theme, see the 98 | # documentation. 99 | #html_theme_options = {} 100 | 101 | # Add any paths that contain custom themes here, relative to this directory. 102 | #html_theme_path = [] 103 | 104 | # The name for this set of Sphinx documents. If None, it defaults to 105 | # " v documentation". 106 | #html_title = None 107 | 108 | # A shorter title for the navigation bar. Default is the same as html_title. 109 | #html_short_title = None 110 | 111 | # The name of an image file (relative to this directory) to place at the top 112 | # of the sidebar. 113 | #html_logo = None 114 | 115 | # The name of an image file (within the static path) to use as favicon of the 116 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 117 | # pixels large. 118 | #html_favicon = None 119 | 120 | # Add any paths that contain custom static files (such as style sheets) here, 121 | # relative to this directory. They are copied after the builtin static files, 122 | # so a file named "default.css" will overwrite the builtin "default.css". 123 | #html_static_path = ['_static'] 124 | 125 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 126 | # using the given strftime format. 127 | #html_last_updated_fmt = '%b %d, %Y' 128 | 129 | # If true, SmartyPants will be used to convert quotes and dashes to 130 | # typographically correct entities. 131 | #html_use_smartypants = True 132 | 133 | # Custom sidebar templates, maps document names to template names. 134 | #html_sidebars = {} 135 | 136 | # Additional templates that should be rendered to pages, maps page names to 137 | # template names. 138 | #html_additional_pages = {} 139 | 140 | # If false, no module index is generated. 141 | #html_domain_indices = True 142 | 143 | # If false, no index is generated. 144 | #html_use_index = True 145 | 146 | # If true, the index is split into individual pages for each letter. 147 | #html_split_index = False 148 | 149 | # If true, links to the reST sources are added to the pages. 150 | #html_show_sourcelink = True 151 | 152 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 153 | #html_show_sphinx = True 154 | 155 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 156 | #html_show_copyright = True 157 | 158 | # If true, an OpenSearch description file will be output, and all pages will 159 | # contain a tag referring to it. The value of this option must be the 160 | # base URL from which the finished HTML is served. 161 | #html_use_opensearch = '' 162 | 163 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 164 | #html_file_suffix = None 165 | 166 | # Output file base name for HTML help builder. 167 | htmlhelp_basename = 'PyRabbitdoc' 168 | 169 | 170 | # -- Options for LaTeX output -------------------------------------------------- 171 | 172 | # The paper size ('letter' or 'a4'). 173 | #latex_paper_size = 'letter' 174 | 175 | # The font size ('10pt', '11pt' or '12pt'). 176 | #latex_font_size = '10pt' 177 | 178 | # Grouping the document tree into LaTeX files. List of tuples 179 | # (source start file, target name, title, author, documentclass [howto/manual]). 180 | latex_documents = [ 181 | ('index', 'PyRabbit.tex', u'PyRabbit Documentation', 182 | u'Brian K. Jones', 'manual'), 183 | ] 184 | 185 | # The name of an image file (relative to this directory) to place at the top of 186 | # the title page. 187 | #latex_logo = None 188 | 189 | # For "manual" documents, if this is true, then toplevel headings are parts, 190 | # not chapters. 191 | #latex_use_parts = False 192 | 193 | # If true, show page references after internal links. 194 | #latex_show_pagerefs = False 195 | 196 | # If true, show URL addresses after external links. 197 | #latex_show_urls = False 198 | 199 | # Additional stuff for the LaTeX preamble. 200 | #latex_preamble = '' 201 | 202 | # Documents to append as an appendix to all manuals. 203 | #latex_appendices = [] 204 | 205 | # If false, no module index is generated. 206 | #latex_domain_indices = True 207 | 208 | 209 | # -- Options for manual page output -------------------------------------------- 210 | 211 | # One entry per manual page. List of tuples 212 | # (source start file, name, description, authors, manual section). 213 | man_pages = [ 214 | ('index', 'pyrabbit', u'PyRabbit Documentation', 215 | [u'Brian K. Jones'], 1) 216 | ] 217 | -------------------------------------------------------------------------------- /docs/http.rst: -------------------------------------------------------------------------------- 1 | =============== 2 | The http Module 3 | =============== 4 | 5 | The http module wraps the underlying raw HTTP request module (requests currently) to separate the job of exposing the API from the job of dealing with lower-level interfacing with the HTTP client in use today. 6 | 7 | .. automodule:: pyrabbit.http 8 | :members: 9 | :undoc-members: 10 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. PyRabbit documentation master file, created by 2 | sphinx-quickstart on Tue Sep 20 22:48:33 2011. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to PyRabbit's documentation! 7 | ==================================== 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | api 15 | http 16 | 17 | Indices and tables 18 | ================== 19 | 20 | * :ref:`genindex` 21 | * :ref:`modindex` 22 | * :ref:`search` 23 | 24 | ================== 25 | PyRabbit Overview 26 | ================== 27 | 28 | pyrabbit is a module to make it easy to interface w/ RabbitMQ's HTTP Management 29 | API. It's tested against RabbitMQ 2.4.1 using Python 2.6-3.2. Yes, it works 30 | great w/ Python 3! 31 | 32 | Here's a quick demo:: 33 | 34 | >>> from pyrabbit.api import Client 35 | >>> cl = Client('localhost:55672', 'guest', 'guest') 36 | >>> cl.is_alive() 37 | True 38 | >>> cl.create_vhost('example_vhost') 39 | True 40 | >>> [i['name'] for i in cl.get_all_vhosts()] 41 | [u'/', u'diabolica', u'example_vhost', u'testvhost'] 42 | >>> cl.get_vhost_names() 43 | [u'/', u'diabolica', u'example_vhost', u'testvhost'] 44 | >>> cl.set_vhost_permissions('example_vhost', 'guest', '.*', '.*', '.*') 45 | True 46 | >>> cl.create_exchange('example_vhost', 'example_exchange', 'direct') 47 | True 48 | >>> cl.get_exchange('example_vhost', 'example_exchange') 49 | {u'name': u'example_exchange', u'durable': True, u'vhost': u'example_vhost', u'internal': False, u'arguments': {}, u'type': u'direct', u'auto_delete': False} 50 | >>> cl.create_queue('example_queue', 'example_vhost') 51 | True 52 | >>> cl.create_binding('example_vhost', 'example_exchange', 'example_queue', 'my.rtkey') 53 | True 54 | >>> cl.publish('example_vhost', 'example_exchange', 'my.rtkey', 'example message payload') 55 | True 56 | >>> cl.get_messages('example_vhost', 'example_queue') 57 | [{u'payload': u'example message payload', u'exchange': u'example_exchange', u'routing_key': u'my.rtkey', u'payload_bytes': 23, u'message_count': 2, u'payload_encoding': u'string', u'redelivered': False, u'properties': []}] 58 | >>> cl.delete_vhost('example_vhost') 59 | True 60 | >>> [i['name'] for i in cl.get_all_vhosts()] 61 | [u'/', u'diabolica', u'testvhost'] 62 | 63 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | if NOT "%PAPER%" == "" ( 11 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 12 | ) 13 | 14 | if "%1" == "" goto help 15 | 16 | if "%1" == "help" ( 17 | :help 18 | echo.Please use `make ^` where ^ is one of 19 | echo. html to make standalone HTML files 20 | echo. dirhtml to make HTML files named index.html in directories 21 | echo. singlehtml to make a single large HTML file 22 | echo. pickle to make pickle files 23 | echo. json to make JSON files 24 | echo. htmlhelp to make HTML files and a HTML help project 25 | echo. qthelp to make HTML files and a qthelp project 26 | echo. devhelp to make HTML files and a Devhelp project 27 | echo. epub to make an epub 28 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 29 | echo. text to make text files 30 | echo. man to make manual pages 31 | echo. changes to make an overview over all changed/added/deprecated items 32 | echo. linkcheck to check all external links for integrity 33 | echo. doctest to run all doctests embedded in the documentation if enabled 34 | goto end 35 | ) 36 | 37 | if "%1" == "clean" ( 38 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 39 | del /q /s %BUILDDIR%\* 40 | goto end 41 | ) 42 | 43 | if "%1" == "html" ( 44 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 45 | if errorlevel 1 exit /b 1 46 | echo. 47 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 48 | goto end 49 | ) 50 | 51 | if "%1" == "dirhtml" ( 52 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 53 | if errorlevel 1 exit /b 1 54 | echo. 55 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 56 | goto end 57 | ) 58 | 59 | if "%1" == "singlehtml" ( 60 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 61 | if errorlevel 1 exit /b 1 62 | echo. 63 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 64 | goto end 65 | ) 66 | 67 | if "%1" == "pickle" ( 68 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 69 | if errorlevel 1 exit /b 1 70 | echo. 71 | echo.Build finished; now you can process the pickle files. 72 | goto end 73 | ) 74 | 75 | if "%1" == "json" ( 76 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 77 | if errorlevel 1 exit /b 1 78 | echo. 79 | echo.Build finished; now you can process the JSON files. 80 | goto end 81 | ) 82 | 83 | if "%1" == "htmlhelp" ( 84 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 85 | if errorlevel 1 exit /b 1 86 | echo. 87 | echo.Build finished; now you can run HTML Help Workshop with the ^ 88 | .hhp project file in %BUILDDIR%/htmlhelp. 89 | goto end 90 | ) 91 | 92 | if "%1" == "qthelp" ( 93 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 94 | if errorlevel 1 exit /b 1 95 | echo. 96 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 97 | .qhcp project file in %BUILDDIR%/qthelp, like this: 98 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\PyRabbit.qhcp 99 | echo.To view the help file: 100 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\PyRabbit.ghc 101 | goto end 102 | ) 103 | 104 | if "%1" == "devhelp" ( 105 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 106 | if errorlevel 1 exit /b 1 107 | echo. 108 | echo.Build finished. 109 | goto end 110 | ) 111 | 112 | if "%1" == "epub" ( 113 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 117 | goto end 118 | ) 119 | 120 | if "%1" == "latex" ( 121 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 122 | if errorlevel 1 exit /b 1 123 | echo. 124 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 125 | goto end 126 | ) 127 | 128 | if "%1" == "text" ( 129 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 130 | if errorlevel 1 exit /b 1 131 | echo. 132 | echo.Build finished. The text files are in %BUILDDIR%/text. 133 | goto end 134 | ) 135 | 136 | if "%1" == "man" ( 137 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 138 | if errorlevel 1 exit /b 1 139 | echo. 140 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 141 | goto end 142 | ) 143 | 144 | if "%1" == "changes" ( 145 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 146 | if errorlevel 1 exit /b 1 147 | echo. 148 | echo.The overview file is in %BUILDDIR%/changes. 149 | goto end 150 | ) 151 | 152 | if "%1" == "linkcheck" ( 153 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 154 | if errorlevel 1 exit /b 1 155 | echo. 156 | echo.Link check complete; look for any errors in the above output ^ 157 | or in %BUILDDIR%/linkcheck/output.txt. 158 | goto end 159 | ) 160 | 161 | if "%1" == "doctest" ( 162 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 163 | if errorlevel 1 exit /b 1 164 | echo. 165 | echo.Testing of doctests in the sources finished, look at the ^ 166 | results in %BUILDDIR%/doctest/output.txt. 167 | goto end 168 | ) 169 | 170 | :end 171 | -------------------------------------------------------------------------------- /pyrabbit2/__init__.py: -------------------------------------------------------------------------------- 1 | from .api import Client 2 | -------------------------------------------------------------------------------- /pyrabbit2/api.py: -------------------------------------------------------------------------------- 1 | """ 2 | The api module houses the Client class, which provides the main interface 3 | developers will use to interact with RabbitMQ. It also contains errors and 4 | decorators used by the class. 5 | """ 6 | 7 | from . import http 8 | # import functools # UNUSED 9 | import json 10 | try: 11 | # python 2.x 12 | from urllib import quote 13 | except ImportError: 14 | # python 3.x 15 | from urllib.parse import quote 16 | 17 | 18 | class APIError(Exception): 19 | 20 | """Denotes a failure due to unexpected or invalid 21 | input/output between the client and the API 22 | 23 | """ 24 | pass 25 | 26 | 27 | class PermissionError(Exception): 28 | 29 | """ 30 | Raised if the operation requires admin permissions, and the user used to 31 | instantiate the Client class does not have admin privileges. 32 | """ 33 | pass 34 | 35 | 36 | class Client(object): 37 | 38 | """ 39 | Abstraction of the RabbitMQ Management HTTP API. 40 | 41 | HTTP calls are delegated to the HTTPClient class for ease of testing, 42 | cleanliness, separation of duty, flexibility, etc. 43 | """ 44 | urls = {'overview': 'overview', 45 | 'all_queues': 'queues', 46 | 'all_exchanges': 'exchanges', 47 | 'all_channels': 'channels', 48 | 'all_connections': 'connections', 49 | 'all_nodes': 'nodes', 50 | 'all_vhosts': 'vhosts', 51 | 'all_users': 'users', 52 | 'all_permissions': 'permissions', 53 | 'all_bindings': 'bindings', 54 | 'whoami': 'whoami', 55 | 'queues_by_vhost': 'queues/%s', 56 | 'queues_by_name': 'queues/%s/%s', 57 | 'queue_action': 'queues/%s/%s/actions', 58 | 'exchanges_by_vhost': 'exchanges/%s', 59 | 'exchange_by_name': 'exchanges/%s/%s', 60 | 'live_test': 'aliveness-test/%s', 61 | 'purge_queue': 'queues/%s/%s/contents', 62 | 'channels_by_name': 'channels/%s', 63 | 'connections_by_name': 'connections/%s', 64 | 'bindings_by_source_exch': 'exchanges/%s/%s/bindings/source', 65 | 'bindings_by_dest_exch': 'exchanges/%s/%s/bindings/destination', 66 | 'bindings_on_queue': 'queues/%s/%s/bindings', 67 | 'bindings_between_exch_queue': 'bindings/%s/e/%s/q/%s', 68 | 'bindings_between_exch_exch': 'bindings/%s/e/%s/e/%s', 69 | 'rt_bindings_between_exch_queue': 'bindings/%s/e/%s/q/%s/%s', 70 | 'rt_bindings_between_exch_exch': 'bindings/%s/e/%s/e/%s/%s', 71 | 'get_from_queue': 'queues/%s/%s/get', 72 | 'publish_to_exchange': 'exchanges/%s/%s/publish', 73 | 'vhosts_by_name': 'vhosts/%s', 74 | 'vhost_permissions': 'permissions/%s/%s', 75 | 'users_by_name': 'users/%s', 76 | 'user_permissions': 'users/%s/permissions', 77 | 'vhost_permissions_get': 'vhosts/%s/permissions', 78 | 'shovel': 'parameters/shovel/%s/%s', 79 | 'all_shovels': 'parameters/shovel', 80 | 'policy': 'policies/%s/%s', 81 | 'all_policies': 'policies', 82 | 'definitions': 'definitions', 83 | 'extensions': 'extensions', 84 | 'cluster-name': 'cluster-name', 85 | } 86 | 87 | json_headers = {"content-type": "application/json"} 88 | 89 | def __init__(self, api_url, user, passwd, timeout=5, scheme='http', 90 | verify=True, cert=None): 91 | """ 92 | :param string api_url: base url for the broker API 93 | :param string user: Username used to authenticate to the API. 94 | :param string passwd: Password used to authenticate to the API. 95 | :param int timeout: Integer number of seconds to wait for each call. 96 | :param string scheme: HTTP scheme used to make the connection 97 | 98 | Populates server attributes using passed-in parameters and 99 | the HTTP API's 'overview' information. 100 | """ 101 | self.api_url = api_url 102 | self.user = user 103 | self.passwd = passwd 104 | self.timeout = timeout 105 | self.scheme = scheme 106 | self.verify = verify 107 | self.cert = cert 108 | self.http = http.HTTPClient( 109 | self.api_url, 110 | self.user, 111 | self.passwd, 112 | timeout=self.timeout, 113 | scheme=self.scheme, 114 | verify=self.verify, 115 | cert=self.cert 116 | ) 117 | 118 | return 119 | 120 | def _call(self, path, method, body=None, headers=None, params=None): 121 | """ 122 | Wrapper around http.do_call that transforms some HTTPError into 123 | our own exceptions 124 | """ 125 | try: 126 | resp = self.http.do_call(path, method, body, headers, params) 127 | except http.HTTPError as err: 128 | if err.status == 401: 129 | raise PermissionError('Insufficient permissions to query ' + 130 | '%s with user %s :%s' % (path, self.user, err)) 131 | raise 132 | return resp 133 | 134 | def get_shovel(self, vhost, shovel_name): 135 | """ 136 | Create a shovel on a given vhost. 137 | 138 | :param string vhost: virtual host where working shovel 139 | :param string shovel_name: shovel name 140 | :returns: boolean 141 | """ 142 | vhost = quote(vhost, '') 143 | shovel_name = quote(shovel_name, '') 144 | path = Client.urls['shovel'] % (vhost, shovel_name) 145 | shovel = self._call(path, 'GET') 146 | return shovel 147 | 148 | def get_all_shovels(self): 149 | """ 150 | Get all shovels on a given server. 151 | :returns: list all shovels 152 | """ 153 | path = Client.urls['all_shovels'] 154 | shovel = self._call(path, 'GET') 155 | if not shovel: 156 | shovel = list() 157 | return shovel 158 | 159 | def delete_shovel(self, vhost, shovel_name): 160 | """ 161 | Delete shovel by vhost and shovel_name. 162 | 163 | :param string vhost: vhost hosing the shovel 164 | :param string shovel_name: deleting shovel name 165 | :returns: boolean 166 | """ 167 | vhost = quote(vhost, '') 168 | shovel_name = quote(shovel_name, '') 169 | path = Client.urls['shovel'] % (vhost, shovel_name) 170 | shovel = self._call(path, 'DELETE') 171 | return shovel 172 | 173 | def create_shovel(self, vhost, shovel_name, **kwargs): 174 | """ 175 | Create shovel. https://www.rabbitmq.com/shovel-dynamic.html 176 | 177 | :param string vhost: shovel's vhost 178 | :param string shovel_name: name shovel 179 | :params dict shovel params 180 | example {"src-uri":"amqp://admin:admin@rabbit.test.com:5672","src-queue":"test_queue","dest-uri":"amqp://test1:test1@rabbit2.test.com:5672","dest-queue":"test_queue","prefetch-count":500,"reconnect-delay":1,"add-forward-headers":false,"ack-mode":"on-confirm","delete-after":"never"} 181 | :returns: http_code 182 | """ 183 | params = {"value": kwargs} 184 | vhost = quote(vhost, '') 185 | shovel_name = quote(shovel_name, '') 186 | body = json.dumps(params) 187 | path = Client.urls['shovel'] % (vhost, shovel_name) 188 | shovel = self._call(path, 'PUT', body=body, headers=Client.json_headers) 189 | return shovel 190 | 191 | def get_policy(self, vhost, policy_name): 192 | """ 193 | Create a policy on a given vhost. 194 | 195 | :param string vhost: virtual host where working shovel 196 | :param string policy_name: policy name 197 | :returns: boolean 198 | """ 199 | vhost = quote(vhost, '') 200 | policy_name = quote(policy_name, '') 201 | path = Client.urls['policy'] % (vhost, policy_name) 202 | policy = self._call(path, 'GET') 203 | return policy 204 | 205 | def get_all_policies(self): 206 | """ 207 | Get all policies on a given server. 208 | :returns: list all policies 209 | """ 210 | path = Client.urls['all_policies'] 211 | policies = self._call(path, 'GET') 212 | if not policies: 213 | policies = list() 214 | return policies 215 | 216 | def delete_policy(self, vhost, policy_name): 217 | """ 218 | Delete policy by vhost and policy_name. 219 | 220 | :param string vhost: vhost hosing the policy 221 | :param string policy_name: deleting policy name 222 | :returns: boolean 223 | """ 224 | vhost = quote(vhost, '') 225 | policy_name = quote(policy_name, '') 226 | path = Client.urls['policy'] % (vhost, policy_name) 227 | policy = self._call(path, 'DELETE') 228 | return policy 229 | 230 | def create_policy(self, vhost, policy_name, **kwargs): 231 | """ 232 | Create policy. https://www.rabbitmq.com/parameters.html 233 | 234 | :param string vhost: policy's vhost 235 | :param string policy_name: name policy 236 | :params dict policy params 237 | example {"pattern": "^hf\.", "definition": {"federation-upstream-set":"all", "ha-mode": "all"}, "priority": 1, "apply-to": "queues"} 238 | :returns: http_code 239 | """ 240 | vhost = quote(vhost, '') 241 | policy_name = quote(policy_name, '') 242 | body = json.dumps(kwargs) 243 | path = Client.urls['policy'] % (vhost, policy_name) 244 | policy = self._call(path, 'PUT', body=body, headers=Client.json_headers) 245 | return policy 246 | 247 | def is_alive(self, vhost='%2F'): 248 | """ 249 | Uses the aliveness-test API call to determine if the 250 | server is alive and the vhost is active. The broker (not this code) 251 | creates a queue and then sends/consumes a message from it. 252 | 253 | :param string vhost: There should be no real reason to ever change 254 | this from the default value, but it's there if you need to. 255 | :returns bool: True if alive, False otherwise 256 | :raises: HTTPError if *vhost* doesn't exist on the broker. 257 | 258 | """ 259 | uri = Client.urls['live_test'] % vhost 260 | 261 | try: 262 | resp = self._call(uri, 'GET') 263 | except http.HTTPError as err: 264 | if err.status == 404: 265 | raise APIError("No vhost named '%s'" % vhost) 266 | raise 267 | 268 | if resp['status'] == 'ok': 269 | return True 270 | else: 271 | return False 272 | 273 | def get_whoami(self): 274 | """ 275 | A convenience function used in the event that you need to confirm that 276 | the broker thinks you are who you think you are. 277 | 278 | :returns dict whoami: Dict structure contains: 279 | * administrator: whether the user is has admin privileges 280 | * name: user name 281 | * auth_backend: backend used to determine admin rights 282 | """ 283 | path = Client.urls['whoami'] 284 | whoami = self._call(path, 'GET') 285 | return whoami 286 | 287 | def get_overview(self): 288 | """ 289 | :rtype: dict 290 | 291 | Data in the 'overview' depends on the privileges of the creds used, 292 | but typically contains information about the management plugin version, 293 | some high-level message stats, and aggregate queue totals. Admin-level 294 | creds gets you information about the cluster node, listeners, etc. 295 | 296 | """ 297 | overview = self._call(Client.urls['overview'], 'GET') 298 | return overview 299 | 300 | def get_nodes(self): 301 | """ 302 | :rtype: dict 303 | 304 | Returns a list of dictionaries, each containing the details of each 305 | node of the cluster. 306 | 307 | 308 | """ 309 | nodes = self._call(Client.urls['all_nodes'], 'GET') 310 | return nodes 311 | 312 | def get_users(self): 313 | """ 314 | Returns a list of dictionaries, each containing the attributes of a 315 | different RabbitMQ user. 316 | 317 | :returns: a list of dictionaries, each representing a user. 318 | 319 | """ 320 | users = self._call(Client.urls['all_users'], 'GET') 321 | return users 322 | 323 | ################################################ 324 | # VHOSTS 325 | ################################################ 326 | def get_all_vhosts(self): 327 | """ 328 | Lists the names of all RabbitMQ vhosts. 329 | 330 | :returns: a list of dicts, each dict representing a vhost 331 | on the broker. 332 | 333 | """ 334 | vhosts = self._call(Client.urls['all_vhosts'], 'GET') 335 | return vhosts 336 | 337 | def get_vhost_names(self): 338 | """ 339 | A convenience function for getting back only the vhost names instead of 340 | the larger vhost dicts. 341 | 342 | :returns list vhost_names: A list of just the vhost names. 343 | """ 344 | vhosts = self.get_all_vhosts() 345 | vhost_names = [i['name'] for i in vhosts] 346 | return vhost_names 347 | 348 | def get_vhost(self, vname): 349 | """ 350 | Returns the attributes of a single named vhost in a dict. 351 | 352 | :param string vname: Name of the vhost to get. 353 | :returns dict vhost: Attribute dict for the named vhost 354 | 355 | """ 356 | 357 | vname = quote(vname, '') 358 | path = Client.urls['vhosts_by_name'] % vname 359 | vhost = self._call(path, 'GET', headers=Client.json_headers) 360 | return vhost 361 | 362 | def create_vhost(self, vname): 363 | """ 364 | Creates a vhost on the server to house exchanges. 365 | 366 | :param string vname: The name to give to the vhost on the server 367 | :returns: boolean 368 | """ 369 | vname = quote(vname, '') 370 | path = Client.urls['vhosts_by_name'] % vname 371 | return self._call(path, 'PUT', 372 | headers=Client.json_headers) 373 | 374 | def delete_vhost(self, vname): 375 | """ 376 | Deletes a vhost from the server. Note that this also deletes any 377 | exchanges or queues that belong to this vhost. 378 | 379 | :param string vname: Name of the vhost to delete from the server. 380 | """ 381 | vname = quote(vname, '') 382 | path = Client.urls['vhosts_by_name'] % vname 383 | return self._call(path, 'DELETE') 384 | 385 | ############################################### 386 | # PERMISSIONS 387 | ############################################### 388 | def get_permissions(self): 389 | """ 390 | :returns: list of dicts, or an empty list if there are no permissions. 391 | """ 392 | path = Client.urls['all_permissions'] 393 | conns = self._call(path, 'GET') 394 | return conns 395 | 396 | def get_vhost_permissions(self, vname): 397 | """ 398 | :returns: list of dicts, or an empty list if there are no permissions. 399 | 400 | :param string vname: Name of the vhost to set perms on. 401 | """ 402 | vname = quote(vname, '') 403 | path = Client.urls['vhost_permissions_get'] % (vname,) 404 | conns = self._call(path, 'GET') 405 | return conns 406 | 407 | def get_user_permissions(self, username): 408 | """ 409 | :returns: list of dicts, or an empty list if there are no permissions. 410 | 411 | :param string username: User to set permissions for. 412 | """ 413 | 414 | path = Client.urls['user_permissions'] % (username,) 415 | conns = self._call(path, 'GET') 416 | return conns 417 | 418 | def set_vhost_permissions(self, vname, username, config, rd, wr): 419 | """ 420 | Set permissions for a given username on a given vhost. Both 421 | must already exist. 422 | 423 | :param string vname: Name of the vhost to set perms on. 424 | :param string username: User to set permissions for. 425 | :param string config: Permission pattern for configuration operations 426 | for this user in this vhost. 427 | :param string rd: Permission pattern for read operations for this user 428 | in this vhost 429 | :param string wr: Permission pattern for write operations for this user 430 | in this vhost. 431 | 432 | Permission patterns are regex strings. If you're unfamiliar with this, 433 | you should definitely check out this section of the RabbitMQ docs: 434 | 435 | http://www.rabbitmq.com/admin-guide.html#access-control 436 | """ 437 | vname = quote(vname, '') 438 | body = json.dumps({"configure": config, "read": rd, "write": wr}) 439 | path = Client.urls['vhost_permissions'] % (vname, username) 440 | return self._call(path, 'PUT', body, 441 | headers=Client.json_headers) 442 | 443 | def delete_permission(self, vname, username): 444 | """ 445 | Delete permission for a given username on a given vhost. Both 446 | must already exist. 447 | 448 | :param string vname: Name of the vhost to set perms on. 449 | :param string username: User to set permissions for. 450 | """ 451 | vname = quote(vname, '') 452 | path = Client.urls['vhost_permissions'] % (vname, username) 453 | return self._call(path, 'DELETE') 454 | 455 | def get_permission(self, vname, username): 456 | """ 457 | :returns: dicts of permissions. 458 | 459 | :param string vname: Name of the vhost to set perms on. 460 | :param string username: User to set permissions for. 461 | """ 462 | vname = quote(vname, '') 463 | path = Client.urls['vhost_permissions'] % (vname, username) 464 | return self._call(path, 'GET') 465 | 466 | ############################################### 467 | # EXCHANGES 468 | ############################################### 469 | def get_exchanges(self, vhost=None): 470 | """ 471 | :returns: A list of dicts 472 | :param string vhost: A vhost to query for exchanges, or None (default), 473 | which triggers a query for all exchanges in all vhosts. 474 | 475 | """ 476 | if vhost: 477 | vhost = quote(vhost, '') 478 | path = Client.urls['exchanges_by_vhost'] % vhost 479 | else: 480 | path = Client.urls['all_exchanges'] 481 | 482 | exchanges = self._call(path, 'GET') 483 | return exchanges 484 | 485 | def get_exchange(self, vhost, name): 486 | """ 487 | Gets a single exchange which requires a vhost and name. 488 | 489 | :param string vhost: The vhost containing the target exchange 490 | :param string name: The name of the exchange 491 | :returns: dict 492 | 493 | """ 494 | vhost = quote(vhost, '') 495 | name = quote(name, '') 496 | path = Client.urls['exchange_by_name'] % (vhost, name) 497 | exch = self._call(path, 'GET') 498 | return exch 499 | 500 | def create_exchange(self, 501 | vhost, 502 | name, 503 | xtype, 504 | auto_delete=False, 505 | durable=True, 506 | internal=False, 507 | arguments=None): 508 | """ 509 | Creates an exchange in the given vhost with the given name. As per the 510 | RabbitMQ API documentation, a JSON body also needs to be included that 511 | "looks something like this": 512 | 513 | {"type":"direct", 514 | "auto_delete":false, 515 | "durable":true, 516 | "internal":false, 517 | "arguments":[]} 518 | 519 | On success, the API returns a 204 with no content, in which case this 520 | function returns True. If any other response is received, it's raised. 521 | 522 | :param string vhost: Vhost to create the exchange in. 523 | :param string name: Name of the proposed exchange. 524 | :param string type: The AMQP exchange type. 525 | :param bool auto_delete: Whether or not the exchange should be 526 | dropped when the no. of consumers drops to zero. 527 | :param bool durable: Whether you want this exchange to persist a 528 | broker restart. 529 | :param bool internal: Whether or not this is a queue for use by the 530 | broker only. 531 | :param list arguments: If given, should be a list. If not given, an 532 | empty list is sent. 533 | 534 | """ 535 | 536 | vhost = quote(vhost, '') 537 | name = quote(name, '') 538 | path = Client.urls['exchange_by_name'] % (vhost, name) 539 | base_body = {"type": xtype, "auto_delete": auto_delete, 540 | "durable": durable, "internal": internal, 541 | "arguments": arguments or list()} 542 | 543 | body = json.dumps(base_body) 544 | self._call(path, 'PUT', body, 545 | headers=Client.json_headers) 546 | return True 547 | 548 | def publish(self, vhost, xname, rt_key, payload, payload_enc='string', 549 | properties=None): 550 | """ 551 | Publish a message to an exchange. 552 | 553 | :param string vhost: vhost housing the target exchange 554 | :param string xname: name of the target exchange 555 | :param string rt_key: routing key for message 556 | :param string payload: the message body for publishing 557 | :param string payload_enc: encoding of the payload. The only choices 558 | here are 'string' and 'base64'. 559 | :param dict properties: a dict of message properties 560 | :returns: boolean indicating success or failure. 561 | """ 562 | vhost = quote(vhost, '') 563 | xname = quote(xname, '') 564 | path = Client.urls['publish_to_exchange'] % (vhost, xname) 565 | body = json.dumps({'routing_key': rt_key, 'payload': payload, 566 | 'payload_encoding': payload_enc, 567 | 'properties': properties or {}}) 568 | result = self._call(path, 'POST', body) 569 | return result['routed'] 570 | 571 | def delete_exchange(self, vhost, name): 572 | """ 573 | Delete the named exchange from the named vhost. The API returns a 204 574 | on success, in which case this method returns True, otherwise the 575 | error is raised. 576 | 577 | :param string vhost: Vhost where target exchange was created 578 | :param string name: The name of the exchange to delete. 579 | :returns bool: True on success. 580 | """ 581 | vhost = quote(vhost, '') 582 | name = quote(name, '') 583 | path = Client.urls['exchange_by_name'] % (vhost, name) 584 | self._call(path, 'DELETE') 585 | return True 586 | 587 | ############################################# 588 | # QUEUES 589 | ############################################# 590 | def get_queues(self, vhost=None, pattern=None, regex=False): 591 | """ 592 | Get all queues, or all queues in a vhost if vhost is not None. 593 | Returns a list. 594 | 595 | :param string vhost: The virtual host to list queues for. If This is 596 | None (the default), all queues for the broker instance 597 | are returned. 598 | :param string pattern: Name pattern to filter queues 599 | :param boolean regex: True if pattern is regex 600 | :returns: A list of dicts, each representing a queue. 601 | :rtype: list of dicts 602 | 603 | """ 604 | if vhost: 605 | vhost = quote(vhost, '') 606 | path = Client.urls['queues_by_vhost'] % vhost 607 | else: 608 | path = Client.urls['all_queues'] 609 | 610 | if pattern: 611 | cur_page = 1 612 | num_pages = 1 613 | queues = list() 614 | params = { 615 | 'use_regex': 'true' if regex else 'false', 616 | 'name': pattern, 617 | 'pagination': True, 618 | } 619 | while cur_page <= num_pages: 620 | params['page'] = cur_page 621 | result = self._call(path, 'GET', params=params) 622 | queues.extend(result['items']) 623 | cur_page += 1 624 | num_pages = result['page_count'] 625 | else: 626 | queues = self._call(path, 'GET') 627 | return queues or list() 628 | 629 | def get_queue(self, vhost, name): 630 | """ 631 | Get a single queue, which requires both vhost and name. 632 | 633 | :param string vhost: The virtual host for the queue being requested. 634 | If the vhost is '/', note that it will be translated to '%2F' to 635 | conform to URL encoding requirements. 636 | :param string name: The name of the queue being requested. 637 | :returns: A dictionary of queue properties. 638 | :rtype: dict 639 | 640 | """ 641 | vhost = quote(vhost, '') 642 | name = quote(name, '') 643 | path = Client.urls['queues_by_name'] % (vhost, name) 644 | queue = self._call(path, 'GET') 645 | return queue 646 | 647 | def get_queue_depth(self, vhost, name): 648 | """ 649 | Get the number of messages currently in a queue. This is a convenience 650 | function that just calls :meth:`Client.get_queue` and pulls 651 | out/returns the 'messages' field from the dictionary it returns. 652 | 653 | :param string vhost: The vhost of the queue being queried. 654 | :param string name: The name of the queue to query. 655 | :returns: Number of messages in the queue 656 | :rtype: integer 657 | 658 | """ 659 | vhost = quote(vhost, '') 660 | name = quote(name, '') 661 | path = Client.urls['queues_by_name'] % (vhost, name) 662 | queue = self._call(path, 'GET') 663 | depth = queue['messages'] 664 | 665 | return depth 666 | 667 | def get_queue_depths(self, vhost, names=None): 668 | """ 669 | Get the number of messages currently sitting in either the queue 670 | names listed in 'names', or all queues in 'vhost' if no 'names' are 671 | given. 672 | 673 | :param str vhost: Vhost where queues in 'names' live. 674 | :param list names: OPTIONAL - Specific queues to show depths for. If 675 | None, show depths for all queues in 'vhost'. 676 | :returns: Nomber of messages in the queues 677 | :rtype: dict 678 | """ 679 | 680 | depths = {} 681 | vhost = quote(vhost, '') 682 | if not names: 683 | # get all queues in vhost 684 | path = Client.urls['queues_by_vhost'] % vhost 685 | queues = self._call(path, 'GET') 686 | for queue in queues: 687 | depth = queue['messages'] 688 | depths[queue['name']] = depth 689 | else: 690 | # get the named queues only. 691 | for name in names: 692 | depth = self.get_queue_depth(vhost, name) 693 | depths[name] = depth 694 | 695 | return depths 696 | 697 | def purge_queues(self, queues): 698 | """ 699 | Purge all messages from one or more queues. 700 | 701 | :param list queues: A list of ('qname', 'vhost') tuples. 702 | :returns: True on success 703 | 704 | """ 705 | for name, vhost in queues: 706 | vhost = quote(vhost, '') 707 | name = quote(name, '') 708 | path = Client.urls['purge_queue'] % (vhost, name) 709 | self._call(path, 'DELETE') 710 | return True 711 | 712 | def purge_queue(self, vhost, name): 713 | """ 714 | Purge all messages from a single queue. This is a convenience method 715 | so you aren't forced to supply a list containing a single tuple to 716 | the purge_queues method. 717 | 718 | :param string vhost: The vhost of the queue being purged. 719 | :param string name: The name of the queue being purged. 720 | :rtype: None 721 | 722 | """ 723 | vhost = quote(vhost, '') 724 | name = quote(name, '') 725 | path = Client.urls['purge_queue'] % (vhost, name) 726 | return self._call(path, 'DELETE') 727 | 728 | def create_queue(self, vhost, name, **kwargs): 729 | """ 730 | Create a queue. The API documentation specifies that all of the body 731 | elements are optional, so this method only requires arguments needed 732 | to form the URI 733 | 734 | :param string vhost: The vhost to create the queue in. 735 | :param string name: The name of the queue 736 | 737 | More on these operations can be found at: 738 | http://www.rabbitmq.com/amqp-0-9-1-reference.html 739 | 740 | """ 741 | 742 | vhost = quote(vhost, '') 743 | name = quote(name, '') 744 | path = Client.urls['queues_by_name'] % (vhost, name) 745 | 746 | body = json.dumps(kwargs) 747 | 748 | return self._call(path, 749 | 'PUT', 750 | body, 751 | headers=Client.json_headers) 752 | 753 | def delete_queue(self, vhost, qname): 754 | """ 755 | Deletes the named queue from the named vhost. 756 | 757 | :param string vhost: Vhost housing the queue to be deleted. 758 | :param string qname: Name of the queue to delete. 759 | 760 | Note that if you just want to delete the messages from a queue, you 761 | should use purge_queue instead of deleting/recreating a queue. 762 | """ 763 | vhost = quote(vhost, '') 764 | qname = quote(qname, '') 765 | path = Client.urls['queues_by_name'] % (vhost, qname) 766 | return self._call(path, 'DELETE', headers=Client.json_headers) 767 | 768 | def queue_action(self, vhost, name, **kwargs): 769 | """ 770 | Currently the actions which are supported are sync and cancel_sync 771 | 772 | :param string vhost: The vhost to create the queue in. 773 | :param string name: The name of the queue 774 | """ 775 | 776 | vhost = quote(vhost, '') 777 | name = quote(name, '') 778 | path = Client.urls['queue_action'] % (vhost, name) 779 | 780 | body = json.dumps(kwargs) 781 | 782 | return self._call(path, 783 | 'POST', 784 | body, 785 | headers=Client.json_headers) 786 | 787 | def get_messages(self, vhost, qname, count=1, 788 | requeue=False, truncate=None, encoding='auto'): 789 | """ 790 | Gets messages from the queue. 791 | :param string vhost: Name of vhost containing the queue 792 | :param string qname: Name of the queue to consume from 793 | :param int count: Number of messages to get. 794 | :param bool requeue: Whether to requeue the message after getting it. 795 | This will cause the 'redelivered' flag to be set in the message on 796 | the queue. 797 | :param int truncate: The length, in bytes, beyond which the server will 798 | truncate the message before returning it. 799 | :returns: list of dicts. messages[msg-index]['payload'] will contain 800 | the message body. 801 | """ 802 | 803 | vhost = quote(vhost, '') 804 | base_body = {'count': count, 'requeue': requeue, 'encoding': encoding} 805 | 806 | # 3.7.X now uses ackmode to denote the requeuing capability 807 | if requeue: 808 | base_body['ackmode'] = 'ack_requeue_true' 809 | else: 810 | base_body['ackmode'] = 'ack_requeue_false' 811 | 812 | if truncate: 813 | base_body['truncate'] = truncate 814 | body = json.dumps(base_body) 815 | 816 | qname = quote(qname, '') 817 | path = Client.urls['get_from_queue'] % (vhost, qname) 818 | messages = self._call(path, 'POST', body, 819 | headers=Client.json_headers) 820 | return messages 821 | 822 | ######################################### 823 | # CONNS/CHANS & BINDINGS 824 | ######################################### 825 | def get_connections(self): 826 | """ 827 | :returns: list of dicts, or an empty list if there are no connections. 828 | """ 829 | path = Client.urls['all_connections'] 830 | conns = self._call(path, 'GET') 831 | return conns 832 | 833 | def get_connection(self, name): 834 | """ 835 | Get a connection by name. To get the names, use get_connections. 836 | 837 | :param string name: Name of connection to get 838 | :returns dict conn: A connection attribute dictionary. 839 | 840 | """ 841 | name = quote(name, '') 842 | path = Client.urls['connections_by_name'] % name 843 | conn = self._call(path, 'GET') 844 | return conn 845 | 846 | def delete_connection(self, name): 847 | """ 848 | Close the named connection. The API returns a 204 on success, 849 | in which case this method returns True, otherwise the 850 | error is raised. 851 | 852 | :param string name: The name of the connection to delete. 853 | :returns bool: True on success. 854 | """ 855 | name = quote(name, '') 856 | path = Client.urls['connections_by_name'] % name 857 | self._call(path, 'DELETE') 858 | return True 859 | 860 | def get_channels(self): 861 | """ 862 | Return a list of dicts containing details about broker connections. 863 | :returns: list of dicts 864 | """ 865 | path = Client.urls['all_channels'] 866 | chans = self._call(path, 'GET') 867 | return chans 868 | 869 | def get_channel(self, name): 870 | """ 871 | Get a channel by name. To get the names, use get_channels. 872 | 873 | :param string name: Name of channel to get 874 | :returns dict conn: A channel attribute dictionary. 875 | 876 | """ 877 | name = quote(name, '') 878 | path = Client.urls['channels_by_name'] % name 879 | chan = self._call(path, 'GET') 880 | return chan 881 | 882 | def get_bindings(self): 883 | """ 884 | :returns: list of dicts 885 | 886 | """ 887 | path = Client.urls['all_bindings'] 888 | bindings = self._call(path, 'GET') 889 | return bindings 890 | 891 | def get_queue_bindings(self, vhost, qname): 892 | """ 893 | Return a list of dicts, one dict per binding. The dict format coming 894 | from RabbitMQ for queue named 'testq' is: 895 | 896 | {"source":"sourceExch","vhost":"/","destination":"testq", 897 | "destination_type":"queue","routing_key":"*.*","arguments":{}, 898 | "properties_key":"%2A.%2A"} 899 | """ 900 | vhost = quote(vhost, '') 901 | qname = quote(qname, '') 902 | path = Client.urls['bindings_on_queue'] % (vhost, qname) 903 | bindings = self._call(path, 'GET') 904 | return bindings 905 | 906 | def get_bindings_from_exchange(self, vhost, exch): 907 | pass 908 | 909 | def get_bindings_to_exchange(self, vhost, exch): 910 | pass 911 | 912 | def get_bindings_between_exch_and_queue(self, vhost, exch, queue): 913 | pass 914 | 915 | def create_binding(self, vhost, exchange, queue, rt_key=None, args=None): 916 | """ 917 | Creates a binding between an exchange and a queue on a given vhost. 918 | 919 | :param string vhost: vhost housing the exchange/queue to bind 920 | :param string exchange: the target exchange of the binding 921 | :param string queue: the queue to bind to the exchange 922 | :param string rt_key: the routing key to use for the binding 923 | :param dict args: extra arguments to associate w/ the binding. 924 | :returns: boolean 925 | """ 926 | 927 | vhost = quote(vhost, '') 928 | exchange = quote(exchange, '') 929 | queue = quote(queue, '') 930 | body = json.dumps({'routing_key': rt_key, 'arguments': args or []}) 931 | path = Client.urls['bindings_between_exch_queue'] % (vhost, 932 | exchange, 933 | queue) 934 | binding = self._call(path, 'POST', body=body, 935 | headers=Client.json_headers) 936 | return binding 937 | 938 | def create_exchange_binding(self, vhost, source, destination, rt_key=None, args=None): 939 | """ 940 | Creates a binding between two exchanges on a given vhost. 941 | :param string vhost: vhost housing the exchange/queue to bind 942 | :param string source: the source exchange of the binding 943 | :param string destination: the target echnage of the binding 944 | :param string rt_key: the routing key to use for the binding 945 | :param dict args: extra arguments to associate w/ the binding. 946 | :returns: boolean 947 | """ 948 | 949 | vhost = quote(vhost, '') 950 | source = quote(source, '') 951 | destination = quote(destination, '') 952 | body = json.dumps({'routing_key': rt_key, 'arguments': args or []}) 953 | path = Client.urls['bindings_between_exch_exch'] % (vhost, 954 | source, 955 | destination) 956 | binding = self._call(path, 'POST', body=body, 957 | headers=Client.json_headers) 958 | return binding 959 | 960 | def delete_binding(self, vhost, exchange, queue, rt_key): 961 | """ 962 | Deletes a binding between an exchange and a queue on a given vhost. 963 | 964 | :param string vhost: vhost housing the exchange/queue to bind 965 | :param string exchange: the target exchange of the binding 966 | :param string queue: the queue to bind to the exchange 967 | :param string rt_key: the routing key to use for the binding 968 | """ 969 | 970 | vhost = quote(vhost, '') 971 | exchange = quote(exchange, '') 972 | queue = quote(queue, '') 973 | # body = '' # UNUSED 974 | path = Client.urls['rt_bindings_between_exch_queue'] % (vhost, 975 | exchange, 976 | queue, 977 | rt_key) 978 | return self._call(path, 'DELETE', headers=Client.json_headers) 979 | 980 | def delete_exchange_binding(self, vhost, source, destination, rt_key): 981 | """ 982 | Deletes a binding between two exchanges on a given vhost. 983 | :param string vhost: vhost housing the exchange/queue to bind 984 | :param string source: the source exchange of the binding 985 | :param string destination: the target exchange of the binding 986 | :param string rt_key: the routing key to use for the binding 987 | """ 988 | 989 | vhost = quote(vhost, '') 990 | source = quote(source, '') 991 | destination = quote(destination, '') 992 | # body = '' # UNUSED 993 | path = Client.urls['rt_bindings_between_exch_exch'] % (vhost, 994 | source, 995 | destination, 996 | rt_key) 997 | return self._call(path, 'DELETE', headers=Client.json_headers) 998 | 999 | def create_user(self, username, password=None, password_hash=None, tags=""): 1000 | """ 1001 | Creates a user. 1002 | 1003 | :param string username: The name to give to the new user 1004 | :param string password: Plain text password 1005 | :param password_hash: Password for the new user sha256 with salt see more http://www.rabbitmq.com/passwords.html#password-generation 1006 | :param string tags: Comma-separated list of tags for the user 1007 | :returns: boolean 1008 | """ 1009 | path = Client.urls['users_by_name'] % username 1010 | body = None 1011 | if bool(password): 1012 | body = json.dumps({'password': password, 'tags': tags}) 1013 | elif bool(password_hash): 1014 | body = json.dumps({'password_hash': password_hash, 'hashing_algorithm': 'rabbit_password_hashing_sha256', 'tags': tags}) 1015 | else: 1016 | raise APIError("password or password_hash should be present.") 1017 | return self._call(path, 'PUT', body=body, 1018 | headers=Client.json_headers) 1019 | 1020 | def delete_user(self, username): 1021 | """ 1022 | Deletes a user from the server. 1023 | 1024 | :param string username: Name of the user to delete from the server. 1025 | """ 1026 | path = Client.urls['users_by_name'] % username 1027 | return self._call(path, 'DELETE') 1028 | 1029 | def get_definitions(self): 1030 | """ 1031 | The server definitions - exchanges, queues, bindings, users, virtual hosts, permissions and parameters. 1032 | 1033 | :return: list dicts 1034 | """ 1035 | path = Client.urls['definitions'] 1036 | return self._call(path, 'GET') 1037 | 1038 | def get_extensions(self): 1039 | """ 1040 | A list of extensions to the management plugin. 1041 | :return: list dicts Example: [{"javascript":"tracing.js"},{"javascript":"shovel.js"},{"javascript":"dispatcher.js"}] 1042 | """ 1043 | path = Client.urls['extensions'] 1044 | return self._call(path, 'GET') 1045 | 1046 | def get_cluster_name(self): 1047 | """ 1048 | Name identifying this RabbitMQ cluster. 1049 | """ 1050 | path = Client.urls['cluster-name'] 1051 | return self._call(path, 'GET') 1052 | -------------------------------------------------------------------------------- /pyrabbit2/http.py: -------------------------------------------------------------------------------- 1 | 2 | import json 3 | import os 4 | import socket 5 | import requests 6 | import requests.exceptions 7 | from requests.auth import HTTPBasicAuth 8 | try: 9 | from urlparse import urljoin, urlparse, urlunparse 10 | except ImportError: 11 | from urllib.parse import urljoin, urlparse, urlunparse 12 | 13 | class HTTPError(Exception): 14 | """ 15 | An error response from the API server. This should be an 16 | HTTP error of some kind (404, 500, etc). 17 | 18 | """ 19 | def __init__(self, content, status=None, reason=None, path=None, body=None): 20 | #HTTP status code 21 | self.status = status 22 | # human readable HTTP status 23 | self.reason = reason 24 | self.path = path 25 | self.body = body 26 | self.detail = None 27 | 28 | # Actual, useful reason for failure returned by RabbitMQ 29 | self.detail=None 30 | if content and content.get('reason'): 31 | self.detail = content['reason'] 32 | 33 | self.output = "%s - %s (%s) (%s) (%s)" % (self.status, 34 | self.reason, 35 | self.detail, 36 | self.path, 37 | repr(self.body)) 38 | 39 | def __str__(self): 40 | return self.output 41 | 42 | 43 | class NetworkError(Exception): 44 | """Denotes a failure to communicate with the REST API 45 | 46 | """ 47 | pass 48 | 49 | 50 | class HTTPClient(object): 51 | """ 52 | A wrapper for requests. Abstracts away 53 | things like path building, return value parsing, etc., 54 | so the api module code stays clean and easy to read/use. 55 | 56 | """ 57 | 58 | def __init__(self, api_url, uname, passwd, timeout=5, scheme='http', 59 | verify=True, cert=None): 60 | """ 61 | :param string api_url: The base URL for the broker API. 62 | :param string uname: Username credential used to authenticate. 63 | :param string passwd: Password used to authenticate w/ REST API 64 | :param int timeout: Integer number of seconds to wait for each call. 65 | :param string scheme: HTTP scheme used to connect 66 | :param string verify: Path to CA certificate bundle (False to disable) 67 | :param string cert: Path to client certificate 68 | 69 | """ 70 | self.auth = HTTPBasicAuth(uname, passwd) 71 | self.timeout = timeout 72 | api_url = '%s://%s/api/' % (scheme, api_url) 73 | self.base_url = api_url 74 | self.verify = verify 75 | self.cert = cert 76 | 77 | def do_call(self, path, method, body=None, headers=None, params=None): 78 | """ 79 | Send an HTTP request to the REST API. 80 | 81 | :param string path: A URL 82 | :param string method: The HTTP method (GET, POST, etc.) to use 83 | in the request. 84 | :param string body: A string representing any data to be sent in the 85 | body of the HTTP request. 86 | :param dictionary headers: 87 | "{header-name: header-value}" dictionary. 88 | :param dictionary params: query parameters 89 | 90 | """ 91 | url = urljoin(self.base_url, path) 92 | try: 93 | resp = requests.request(method, url, data=body, headers=headers, 94 | auth=self.auth, timeout=self.timeout, 95 | verify=self.verify, cert=self.cert, 96 | params=params) 97 | except requests.exceptions.Timeout as out: 98 | raise NetworkError("Timeout while trying to connect to RabbitMQ") 99 | except requests.exceptions.RequestException as err: 100 | # All other requests exceptions inherit from RequestException 101 | raise NetworkError("Error during request %s %s" % (type(err), err)) 102 | 103 | try: 104 | content = resp.json() 105 | except ValueError as out: 106 | content = None 107 | 108 | # 'success' HTTP status codes are 200-206 109 | if resp.status_code < 200 or resp.status_code > 206: 110 | raise HTTPError(content, resp.status_code, resp.text, path, body) 111 | else: 112 | if content is not None: 113 | return content 114 | else: 115 | return resp.status_code 116 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | nose 2 | requests 3 | mock 4 | unittest2 5 | -------------------------------------------------------------------------------- /requirements3.txt: -------------------------------------------------------------------------------- 1 | nose 2 | requests 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | version = '1.0.7' 4 | 5 | setup(name='pyrabbit2', 6 | version=version, 7 | description="A Pythonic interface to the RabbitMQ Management HTTP API", 8 | 9 | long_description="""\ 10 | 11 | Fork module to communicate the RabbitMQ HTTP Management API https://github.com/bkjones/pyrabbit 12 | 13 | The main documentation lives at http://pyrabbit.readthedocs.org 14 | 15 | There's no way to easily write programs against RabbitMQs management API 16 | without resorting to some messy urllib boilerplate code involving HTTP 17 | Basic authentication and parsing the JSON responses, etc. Pyrabbit 18 | abstracts this away & provides an intuitive, easy way to work with the 19 | data that lives inside of RabbitMQ, and manipulate the resources there.""", 20 | 21 | classifiers=[ 22 | "Intended Audience :: Developers", 23 | "License :: OSI Approved :: MIT License", 24 | "Natural Language :: English", 25 | "Programming Language :: Python", 26 | "Programming Language :: Python :: 2", 27 | "Programming Language :: Python :: 2.6", 28 | "Programming Language :: Python :: 2.7", 29 | "Programming Language :: Python :: 3.5", 30 | "Programming Language :: Python :: 3.6", 31 | "Topic :: Internet :: WWW/HTTP", 32 | "Topic :: Software Development :: Libraries :: Python Modules", 33 | ], 34 | keywords='python http amqp rabbit rabbitmq management', 35 | install_requires = ['requests'], 36 | author='Brian K. Jones, Yuri Bukatkin', 37 | author_email='bkjones@gmail.com, windowod@gmail.com', 38 | url='https://github.com/deslum/pyrabbit2', 39 | download_url='https://github.com/deslum/pyrabbit2/archive/master.zip', 40 | license='MIT', 41 | packages=find_packages(exclude='tests'), 42 | include_package_data=False, 43 | zip_safe=False 44 | ) 45 | -------------------------------------------------------------------------------- /tests/test_httpclient.py: -------------------------------------------------------------------------------- 1 | try: 2 | import unittest2 as unittest 3 | except ImportError: 4 | import unittest 5 | 6 | import sys 7 | sys.path.append('..') 8 | from pyrabbit2 import http 9 | 10 | 11 | 12 | class TestHTTPClient(unittest.TestCase): 13 | """ 14 | Except for the init test, these are largely functional tests that 15 | require a RabbitMQ management API to be available on localhost 16 | 17 | """ 18 | testhost = 'localhost:15672' 19 | testuser = 'guest' 20 | testpass = 'guest' 21 | def setUp(self): 22 | self.c = http.HTTPClient(self.testhost, self.testuser, self.testpass) 23 | 24 | def test_client_init(self): 25 | c = http.HTTPClient(self.testhost, self.testuser, self.testpass) 26 | self.assertIsInstance(c, http.HTTPClient) 27 | 28 | def test_client_init_sets_credentials(self): 29 | self.assertEqual(self.c.auth.username, self.testuser) 30 | self.assertEqual(self.c.auth.password, self.testpass) 31 | 32 | def test_client_init_sets_default_timeout(self): 33 | self.assertEqual(self.c.timeout, 5) 34 | 35 | def test_client_init_with_timeout(self): 36 | c = http.HTTPClient(self.testhost, self.testuser, self.testpass, 1) 37 | self.assertEqual(c.timeout, 1) 38 | 39 | if __name__ == "__main__": 40 | unittest.main(testRunner=unittest.TextTestRunner()) 41 | -------------------------------------------------------------------------------- /tests/test_pyrabbit.py: -------------------------------------------------------------------------------- 1 | """Main test file for the pyrabbit Client.""" 2 | 3 | import json 4 | 5 | try: 6 | #python 2.x 7 | import unittest2 as unittest 8 | except ImportError: 9 | #python 3.x 10 | import unittest 11 | 12 | import sys 13 | import requests 14 | sys.path.append('..') 15 | import pyrabbit2 16 | from mock import Mock, patch 17 | 18 | host_and_port = 'localhost:15672' 19 | user = 'guest' 20 | password = 'guest' 21 | 22 | class TestClient(unittest.TestCase): 23 | def setUp(self): 24 | self.client = pyrabbit2.api.Client(host_and_port, user, password) 25 | 26 | def tearDown(self): 27 | del self.client 28 | 29 | def test_server_init_200(self): 30 | self.assertIsInstance(self.client, pyrabbit2.api.Client) 31 | self.assertEqual(self.client.api_url, host_and_port) 32 | 33 | def test_server_is_alive_default_vhost(self): 34 | response = {'status': 'ok'} 35 | self.client.http.do_call = Mock(return_value=response) 36 | self.assertTrue(self.client.is_alive()) 37 | 38 | def test_get_vhosts_200(self): 39 | self.client.http.do_call = Mock(return_value=[]) 40 | vhosts = self.client.get_all_vhosts() 41 | self.assertIsInstance(vhosts, list) 42 | 43 | def test_get_all_queues(self): 44 | self.client.http.do_call = Mock(return_value=[]) 45 | queues = self.client.get_queues() 46 | self.assertIsInstance(queues, list) 47 | 48 | def test_get_nodes(self): 49 | self.client.http.do_call = Mock(return_value=[]) 50 | nodes = self.client.get_nodes() 51 | self.assertIsInstance(nodes, list) 52 | 53 | def test_purge_queues(self): 54 | self.client.http.do_call = Mock(return_value=True) 55 | self.assertTrue(self.client.purge_queues(['q1', 'q2'])) 56 | 57 | def test_get_queue(self): 58 | self.client.http.do_call = Mock(return_value=True) 59 | self.assertTrue(self.client.get_queue('', 'q1')) 60 | 61 | def test_get_all_exchanges(self): 62 | xchs = [{'name': 'foo', 'vhost': '/', 'type': 'direct', 63 | 'durable': False, 'auto_delete': False, 'internal': False, 64 | 'arguments': {}}, 65 | 66 | {'name': 'bar', 'vhost': '/', 'type': 'direct', 67 | 'durable': False, 'auto_delete': False, 'internal': False, 68 | 'arguments': {}},] 69 | self.client.http.do_call = Mock(return_value=xchs) 70 | xlist = self.client.get_exchanges() 71 | self.assertIsInstance(xlist, list) 72 | self.assertEqual(len(xlist), 2) 73 | 74 | def test_get_named_exchange(self): 75 | xch = {'name': 'foo', 'vhost': '/', 'type': 'direct', 76 | 'durable': False, 'auto_delete': False, 'internal': False, 77 | 'arguments': {}} 78 | self.client.http.do_call = Mock(return_value=xch) 79 | myexch = self.client.get_exchange('%2F', 'foo') 80 | self.assertEqual(myexch['name'], 'foo') 81 | 82 | def test_get_users(self): 83 | with patch('pyrabbit2.http.HTTPClient.do_call') as do_call: 84 | self.assertTrue(self.client.get_users()) 85 | 86 | def test_get_queue_depth(self): 87 | q = {'messages': 4} 88 | self.client.http.do_call = Mock(return_value=q) 89 | depth = self.client.get_queue_depth('/', 'test') 90 | self.assertEqual(depth, q['messages']) 91 | 92 | def test_get_queue_depth_2(self): 93 | """ 94 | An integration test that includes the HTTP client's do_call 95 | method and json decoding operations. 96 | 97 | """ 98 | q = {'messages': 8} 99 | json_q = json.dumps(q) 100 | 101 | with patch('requests.request') as req: 102 | resp = requests.Response() 103 | resp._content = json_q.encode() 104 | resp.status_code = 200 105 | req.return_value = resp 106 | depth = self.client.get_queue_depth('/', 'test') 107 | self.assertEqual(depth, q['messages']) 108 | 109 | 110 | def test_purge_queue(self): 111 | self.client.http.do_call = Mock(return_value=True) 112 | self.assertTrue(self.client.purge_queue('vname', 'qname')) 113 | 114 | def test_create_queue(self): 115 | self.client.http.do_call = Mock(return_value=True) 116 | self.assertTrue(self.client.create_queue('qname', 'vname')) 117 | 118 | def test_get_connections(self): 119 | self.client.http.do_call = Mock(return_value=True) 120 | self.assertTrue(self.client.get_connections()) 121 | 122 | def test_get_connection(self): 123 | self.client.http.do_call = Mock(return_value=True) 124 | self.assertTrue(self.client.get_connection('cname')) 125 | 126 | def test_delete_connection(self): 127 | self.client.http.do_call = Mock(return_value=True) 128 | self.assertTrue(self.client.delete_connection('127.0.0.1:1234 -> 127.0.0.1:5678 (1)')) 129 | 130 | def test_get_channels(self): 131 | self.client.http.do_call = Mock(return_value=True) 132 | self.assertTrue(self.client.get_channels()) 133 | 134 | def test_get_channel(self): 135 | self.client.http.do_call = Mock(return_value=True) 136 | self.assertTrue(self.client.get_channel('127.0.0.1:1234 -> 127.0.0.1:5678 (1)')) 137 | 138 | def test_get_bindings(self): 139 | self.client.http.do_call = Mock(return_value=True) 140 | self.assertTrue(self.client.get_bindings()) 141 | 142 | def test_create_binding(self): 143 | self.client.http.do_call = Mock(return_value=True) 144 | self.assertTrue(self.client.create_binding('vhost', 145 | 'exch', 146 | 'queue', 147 | 'rt_key')) 148 | 149 | def test_delete_binding(self): 150 | self.client.http.do_call = Mock(return_value=True) 151 | self.assertTrue(self.client.delete_binding('vhost', 152 | 'exch', 153 | 'queue', 154 | 'rt_key')) 155 | 156 | def test_publish(self): 157 | self.client.http.do_call = Mock(return_value={'routed': 'true'}) 158 | self.assertTrue(self.client.publish('vhost', 'xname', 'rt_key', 159 | 'payload')) 160 | 161 | def test_create_vhost(self): 162 | self.client.http.do_call = Mock(return_value=True) 163 | self.assertTrue(self.client.create_vhost('vname')) 164 | 165 | def test_delete_vhost(self): 166 | self.client.http.do_call = Mock(return_value=True) 167 | self.assertTrue(self.client.delete_vhost('vname')) 168 | 169 | def test_create_user(self): 170 | self.client.http.do_call = Mock(return_value=True) 171 | self.assertTrue(self.client.create_user('user', 'password')) 172 | 173 | def test_delete_user(self): 174 | self.client.http.do_call = Mock(return_value=True) 175 | self.assertTrue(self.client.delete_user('user')) 176 | 177 | def test_get_permissions(self): 178 | self.client.http.do_call = Mock(return_value=True) 179 | self.assertTrue(self.client.get_permissions()) 180 | 181 | def test_get_vhost_permissions(self): 182 | self.client.http.do_call = Mock(return_value=True) 183 | self.assertTrue(self.client.get_vhost_permissions('vname')) 184 | 185 | def test_get_user_permissions(self): 186 | self.client.http.do_call = Mock(return_value=True) 187 | self.assertTrue(self.client.get_user_permissions('username')) 188 | 189 | def test_delete_permission(self): 190 | self.client.http.do_call = Mock(return_value=True) 191 | self.assertTrue(self.client.delete_permission('vname', 'username')) 192 | 193 | def test_get_permission(self): 194 | self.client.http.do_call = Mock(return_value=True) 195 | self.assertTrue(self.client.get_permission('vname', 'username')) 196 | 197 | def test_is_alive(self): 198 | with patch('pyrabbit2.http.HTTPClient.do_call') as do_call: 199 | do_call.return_value = {'status': 'ok'} 200 | self.assertTrue(self.client.is_alive()) 201 | 202 | def test_definitions(self): 203 | def_result = self.client.get_definitions() 204 | self.assertIsInstance(def_result, dict) 205 | self.assertNotEqual(def_result.get("rabbit_version", 0), 0) 206 | 207 | def test_extensions(self): 208 | ext_result = self.client.get_extensions() 209 | self.assertIsInstance(ext_result, list) 210 | self.assertIsInstance(ext_result[0], dict) 211 | 212 | def test_get_cluster_name(self): 213 | result = self.client.get_cluster_name() 214 | self.assertIsInstance(result, dict) 215 | self.assertNotEqual(result.get("name", 0), 0) 216 | 217 | 218 | 219 | class TestLiveServer(unittest.TestCase): 220 | def setUp(self): 221 | self.rabbit = pyrabbit2.api.Client(host_and_port, user, password) 222 | self.vhost_name = 'pyrabbit_test_vhost' 223 | self.exchange_name = 'pyrabbit_test_exchange' 224 | self.queue_name = 'pyrabbit_test_queue' 225 | self.rt_key = 'pyrabbit-roundtrip' 226 | self.payload = 'pyrabbit test message payload' 227 | self.user = 'guest' 228 | 229 | def test_round_trip(self): 230 | """ 231 | This does a 'round trip' test, which consists of the following steps: 232 | 233 | * Create a vhost, and verify creation 234 | * Give 'guest' all perms on vhost 235 | * Create an exchange in that vhost, verify creation 236 | * Create a queue 237 | * Create a binding between the queue and exchange 238 | * Publish a message to the exchange that makes it to the queue 239 | * Grab that message from the queue (verify it's the same message) 240 | * Delete binding and verify we don't receive messages 241 | * Delete the exchange 242 | * Delete the vhost 243 | """ 244 | 245 | # create a vhost, verify creation, and grant all perms to 'guest'. 246 | self.rabbit.create_vhost(self.vhost_name) 247 | vhosts = [i['name'] for i in self.rabbit.get_all_vhosts()] 248 | self.assertIn(self.vhost_name, vhosts) 249 | self.rabbit.set_vhost_permissions(self.vhost_name, self.user, 250 | '.*', '.*', '.*') 251 | 252 | # create an exchange, and verify creation. 253 | self.rabbit.create_exchange(self.vhost_name, 254 | self.exchange_name, 255 | 'direct') 256 | self.assertEqual(self.exchange_name, 257 | self.rabbit.get_exchange(self.vhost_name, 258 | self.exchange_name)['name']) 259 | 260 | # create a queue and verify it was created 261 | self.rabbit.create_queue(self.vhost_name,self.queue_name) 262 | self.assertEqual(self.queue_name, 263 | self.rabbit.get_queue(self.vhost_name, 264 | self.queue_name)['name']) 265 | 266 | # bind the queue and exchange 267 | self.rabbit.create_binding(self.vhost_name, self.exchange_name, 268 | self.queue_name, self.rt_key) 269 | 270 | # publish a message, and verify by getting it back. 271 | self.rabbit.publish(self.vhost_name, self.exchange_name, self.rt_key, 272 | self.payload) 273 | messages = self.rabbit.get_messages(self.vhost_name, self.queue_name) 274 | self.assertEqual(messages[0]['payload'], self.payload) 275 | 276 | # delete binding and verify we don't get the message 277 | self.rabbit.delete_binding(self.vhost_name, self.exchange_name, 278 | self.queue_name, self.rt_key) 279 | self.rabbit.publish(self.vhost_name, self.exchange_name, self.rt_key, 280 | self.payload) 281 | messages = self.rabbit.get_messages(self.vhost_name, self.queue_name) 282 | self.assertEqual(messages, []) 283 | 284 | # Clean up. 285 | self.rabbit.delete_exchange(self.vhost_name, self.exchange_name) 286 | self.rabbit.delete_vhost(self.vhost_name) 287 | 288 | 289 | class TestShovel(unittest.TestCase): 290 | 291 | def setUp(self): 292 | self.rabbit = pyrabbit2.api.Client(host_and_port, user, password) 293 | self.vhost_name = '/' 294 | self.exchange_name = 'pyrabbit_test_exchange' 295 | self.queue_name = 'pyrabbit_test_queue' 296 | self.rt_key = 'pyrabbit-roundtrip' 297 | self.payload = 'pyrabbit test message payload' 298 | self.user = 'guest' 299 | self.shovel_name = 'pyrabbit2_test_shovel' 300 | 301 | def tearDown(self): 302 | del self.rabbit 303 | 304 | def test_create_shovel(self): 305 | kwargs = {} 306 | kwargs['src-uri'] = 'amqp://admin:admin@rabbit.test.com:5672' 307 | kwargs['src-queue'] = 'test_queue' 308 | kwargs['dest-uri'] = 'amqp://test1:test1@rabbit2.test.com:5672' 309 | kwargs['dest-queue'] = 'test_queue' 310 | kwargs['prefetch-count'] = 500 311 | kwargs['reconnect-delay'] = 1 312 | kwargs['add-forward-headers'] = False 313 | kwargs['ack-mode'] = 'on-confirm' 314 | kwargs['delete-after'] = 'never' 315 | self.assertIsInstance(self.rabbit.create_shovel(self.vhost_name, self.shovel_name, **kwargs), int) 316 | 317 | def test_get_shovel(self): 318 | shovel = self.rabbit.get_shovel(self.vhost_name, self.shovel_name) 319 | self.assertEqual(shovel['name'], self.shovel_name) 320 | 321 | def test_get_all_shovels(self): 322 | shovel = self.rabbit.get_all_shovels().pop() 323 | self.assertEqual(shovel['name'], self.shovel_name) 324 | 325 | def test_update_shovel(self): 326 | kwargs = {} 327 | kwargs['src-uri'] = 'amqp://admin:admin@rabbit.test.com:15672' 328 | kwargs['src-queue'] = 'test' 329 | kwargs['dest-uri'] = 'amqp://test1:test1@rabbit2.test.com:15672' 330 | kwargs['dest-queue'] = 'test' 331 | kwargs['prefetch-count'] = 250 332 | kwargs['reconnect-delay'] = 100 333 | kwargs['add-forward-headers'] = True 334 | kwargs['ack-mode'] = 'on-confirm' 335 | kwargs['delete-after'] = 'never' 336 | self.assertIsInstance(self.rabbit.create_shovel(self.vhost_name, self.shovel_name, **kwargs), int) 337 | 338 | def test_xdelete_shovel(self): 339 | result = self.rabbit.delete_shovel(self.vhost_name, self.shovel_name) 340 | self.assertIsInstance(result, int) 341 | 342 | if __name__ == "__main__": 343 | unittest.main(testRunner=unittest.TextTestRunner()) 344 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py26,py27,py34,py35,py36 3 | 4 | [testenv:py36] 5 | deps= 6 | nose 7 | requests 8 | mock 9 | commands=nosetests [] 10 | 11 | [testenv:py35] 12 | deps= 13 | nose 14 | requests 15 | mock 16 | commands=nosetests [] 17 | 18 | [testenv:py34] 19 | deps= 20 | nose 21 | requests 22 | mock 23 | commands=nosetests [] 24 | 25 | [testenv:py27] 26 | deps= 27 | nose 28 | requests 29 | mock 30 | changedir=tests 31 | commands=nosetests [] 32 | 33 | [testenv:py26] 34 | deps= 35 | nose 36 | requests 37 | mock 38 | unittest2 39 | changedir=tests 40 | commands=nosetests [] 41 | --------------------------------------------------------------------------------