From 92d64728d858d695e3e6d68ee243da81955fdd63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Schneider?= Date: Sat, 1 Nov 2014 18:56:02 +0100 Subject: [PATCH] Initial import from tarball --- Changelog | 484 +++++ LICENSE | 458 ++++ MANIFEST.in | 6 + PKG-INFO | 128 ++ README.rst | 104 + amqp.egg-info/PKG-INFO | 128 ++ amqp.egg-info/SOURCES.txt | 67 + amqp.egg-info/dependency_links.txt | 1 + amqp.egg-info/not-zip-safe | 1 + amqp.egg-info/top_level.txt | 1 + amqp/__init__.py | 70 + amqp/abstract_channel.py | 93 + amqp/basic_message.py | 124 ++ amqp/channel.py | 2537 ++++++++++++++++++++++ amqp/connection.py | 1004 +++++++++ amqp/exceptions.py | 258 +++ amqp/five.py | 188 ++ amqp/method_framing.py | 231 ++ amqp/protocol.py | 13 + amqp/serialization.py | 510 +++++ amqp/transport.py | 294 +++ amqp/utils.py | 102 + demo/amqp_clock.py | 78 + demo/demo_receive.py | 83 + demo/demo_send.py | 66 + docs/.static/.keep | 0 docs/.templates/page.html | 4 + docs/.templates/sidebarintro.html | 4 + docs/.templates/sidebarlogo.html | 3 + docs/Makefile | 81 + docs/_ext/applyxrefs.py | 92 + docs/_ext/literals_to_xrefs.py | 173 ++ docs/_theme/celery/static/celery.css_t | 401 ++++ docs/_theme/celery/theme.conf | 5 + docs/changelog.rst | 484 +++++ docs/conf.py | 127 ++ docs/includes/intro.txt | 96 + docs/index.rst | 22 + docs/reference/amqp.abstract_channel.rst | 11 + docs/reference/amqp.basic_message.rst | 11 + docs/reference/amqp.channel.rst | 11 + docs/reference/amqp.connection.rst | 11 + docs/reference/amqp.exceptions.rst | 11 + docs/reference/amqp.five.rst | 11 + docs/reference/amqp.method_framing.rst | 11 + docs/reference/amqp.protocol.rst | 11 + docs/reference/amqp.serialization.rst | 11 + docs/reference/amqp.transport.rst | 11 + docs/reference/amqp.utils.rst | 11 + docs/reference/index.rst | 23 + docs/templates/readme.txt | 5 + extra/README | 10 + extra/generate_skeleton_0_8.py | 377 ++++ extra/release/bump_version.py | 181 ++ extra/release/sphinx-to-rst.py | 75 + extra/update_comments_from_spec.py | 76 + funtests/run_all.py | 38 + funtests/settings.py | 91 + funtests/test_basic_message.py | 132 ++ funtests/test_channel.py | 317 +++ funtests/test_connection.py | 127 ++ funtests/test_exceptions.py | 47 + funtests/test_serialization.py | 411 ++++ funtests/test_with.py | 70 + requirements/docs.txt | 2 + requirements/pkgutils.txt | 5 + requirements/test.txt | 5 + setup.cfg | 5 + setup.py | 132 ++ 69 files changed, 10771 insertions(+) create mode 100644 Changelog create mode 100644 LICENSE create mode 100644 MANIFEST.in create mode 100644 PKG-INFO create mode 100644 README.rst create mode 100644 amqp.egg-info/PKG-INFO create mode 100644 amqp.egg-info/SOURCES.txt create mode 100644 amqp.egg-info/dependency_links.txt create mode 100644 amqp.egg-info/not-zip-safe create mode 100644 amqp.egg-info/top_level.txt create mode 100644 amqp/__init__.py create mode 100644 amqp/abstract_channel.py create mode 100644 amqp/basic_message.py create mode 100644 amqp/channel.py create mode 100644 amqp/connection.py create mode 100644 amqp/exceptions.py create mode 100644 amqp/five.py create mode 100644 amqp/method_framing.py create mode 100644 amqp/protocol.py create mode 100644 amqp/serialization.py create mode 100644 amqp/transport.py create mode 100644 amqp/utils.py create mode 100755 demo/amqp_clock.py create mode 100755 demo/demo_receive.py create mode 100755 demo/demo_send.py create mode 100644 docs/.static/.keep create mode 100644 docs/.templates/page.html create mode 100644 docs/.templates/sidebarintro.html create mode 100644 docs/.templates/sidebarlogo.html create mode 100644 docs/Makefile create mode 100644 docs/_ext/applyxrefs.py create mode 100644 docs/_ext/literals_to_xrefs.py create mode 100644 docs/_theme/celery/static/celery.css_t create mode 100644 docs/_theme/celery/theme.conf create mode 100644 docs/changelog.rst create mode 100644 docs/conf.py create mode 100644 docs/includes/intro.txt create mode 100644 docs/index.rst create mode 100644 docs/reference/amqp.abstract_channel.rst create mode 100644 docs/reference/amqp.basic_message.rst create mode 100644 docs/reference/amqp.channel.rst create mode 100644 docs/reference/amqp.connection.rst create mode 100644 docs/reference/amqp.exceptions.rst create mode 100644 docs/reference/amqp.five.rst create mode 100644 docs/reference/amqp.method_framing.rst create mode 100644 docs/reference/amqp.protocol.rst create mode 100644 docs/reference/amqp.serialization.rst create mode 100644 docs/reference/amqp.transport.rst create mode 100644 docs/reference/amqp.utils.rst create mode 100644 docs/reference/index.rst create mode 100644 docs/templates/readme.txt create mode 100644 extra/README create mode 100755 extra/generate_skeleton_0_8.py create mode 100755 extra/release/bump_version.py create mode 100755 extra/release/sphinx-to-rst.py create mode 100644 extra/update_comments_from_spec.py create mode 100755 funtests/run_all.py create mode 100644 funtests/settings.py create mode 100755 funtests/test_basic_message.py create mode 100755 funtests/test_channel.py create mode 100755 funtests/test_connection.py create mode 100755 funtests/test_exceptions.py create mode 100755 funtests/test_serialization.py create mode 100644 funtests/test_with.py create mode 100644 requirements/docs.txt create mode 100644 requirements/pkgutils.txt create mode 100644 requirements/test.txt create mode 100644 setup.cfg create mode 100644 setup.py diff --git a/Changelog b/Changelog new file mode 100644 index 0000000..e036942 --- /dev/null +++ b/Changelog @@ -0,0 +1,484 @@ +Changes +======= + +py-amqp is fork of amqplib used by Kombu containing additional features and improvements. +The previous amqplib changelog is here: +http://code.google.com/p/py-amqplib/source/browse/CHANGES + +.. _version-1.4.5: + +1.4.5 +===== +:release-date: 2014-04-15 09:00 P.M UTC +:release-by: Ask Solem + +- Can now deserialize more AMQP types. + + Now handles types ``short string``, ``short short int``, + ``short short unsigned int``, ``short int``, ``short unsigned int``, + ``long unsigned int``, ``long long int``, ``long long unsigned int`` + and ``float`` which for some reason was missing, even in the original + amqplib module. + +- SSL: Workaround for Python SSL bug. + + A bug in the python socket library causes ``ssl.read/write()`` + on a closed socket to raise :exc:`AttributeError` instead of + :exc:`IOError`. + + Fix contributed by Craig Jellick. + +- ``Transport.__del_`` now handles errors occurring at late interpreter + shutdown (Issue #36). + +.. _version-1.4.4: + +1.4.4 +===== +:release-date: 2014-03-03 04:00 P.M UTC +:release-by: Ask Solem + +- SSL transport accidentally disconnected after read timeout. + + Fix contributed by Craig Jellick. + +.. _version-1.4.3: + +1.4.3 +===== +:release-date: 2014-02-09 03:00 P.M UTC +:release-by: Ask Solem + +- Fixed bug where more data was requested from the socket + than was actually needed. + + Contributed by Ionel Cristian Mărieș. + +.. _version-1.4.2: + +1.4.2 +===== +:release-date: 2014-01-23 05:00 P.M UTC + +- Heartbeat negotiation would use heartbeat value from server even + if heartbeat disabled (Issue #31). + +.. _version-1.4.1: + +1.4.1 +===== +:release-date: 2014-01-14 09:30 P.M UTC +:release-by: Ask Solem + +- Fixed error occurring when heartbeats disabled. + +.. _version-1.4.0: + +1.4.0 +===== +:release-date: 2014-01-13 03:00 P.M UTC +:release-by: Ask Solem + +- Heartbeat implementation improved (Issue #6). + + The new heartbeat behavior is the same approach as taken by the + RabbitMQ java library. + + This also means that clients should preferably call the ``heartbeat_tick`` + method more frequently (like every second) instead of using the old + ``rate`` argument (which is now ignored). + + - Heartbeat interval is negotiated with the server. + - Some delay is allowed if the heartbeat is late. + - Monotonic time is used to keep track of the heartbeat + instead of relying on the caller to call the checking function + at the right time. + + Contributed by Dustin J. Mitchell. + +- NoneType is now supported in tables and arrays. + + Contributed by Dominik Fässler. + +- SSLTransport: Now handles ``ENOENT``. + + Fix contributed by Adrien Guinet. + +.. _version-1.3.3: + +1.3.3 +===== +:release-date: 2013-11-11 03:30 P.M UTC +:release-by: Ask Solem + +- SSLTransport: Now keeps read buffer if an exception is raised + (Issue #26). + + Fix contributed by Tommie Gannert. + +.. _version-1.3.2: + +1.3.2 +===== +:release-date: 2013-10-29 02:00 P.M UTC +:release-by: Ask Solem + +- Message.channel is now a channel object (not the channel id). + +- Bug in previous version caused the socket to be flagged as disconnected + at EAGAIN/EINTR. + +.. _version-1.3.1: + +1.3.1 +===== +:release-date: 2013-10-24 04:00 P.M UTC +:release-by: Ask Solem + +- Now implements Connection.connected (Issue #22). + +- Fixed bug where ``str(AMQPError)`` did not return string. + +.. _version-1.3.0: + +1.3.0 +===== +:release-date: 2013-09-04 02:39 P.M UTC +:release-by: Ask Solem + +- Now sets ``Message.channel`` on delivery (Issue #12) + + amqplib used to make the channel object available + as ``Message.delivery_info['channel']``, but this was removed + in py-amqp. librabbitmq sets ``Message.channel``, + which is a more reasonable solution in our opinion as that + keeps the delivery info intact. + +- New option to wait for publish confirmations (Issue #3) + + There is now a new Connection ``confirm_publish`` that will + force any ``basic_publish`` call to wait for confirmation. + + Enabling publisher confirms like this degrades performance + considerably, but can be suitable for some applications + and now it's possible by configuration. + +- ``queue_declare`` now returns named tuple of type + :class:`~amqp.protocol.basic_declare_ok_t`. + + Supporting fields: ``queue``, ``message_count``, and + ``consumer_count``. + +- Contents of ``Channel.returned_messages`` is now named tuples. + + Supporting fields: ``reply_code``, ``reply_text``, ``exchange``, + ``routing_key``, and ``message``. + +- Sockets now set to close on exec using the ``FD_CLOEXEC`` flag. + + Currently only supported on platforms supporting this flag, + which does not include Windows. + + Contributed by Tommie Gannert. + +.. _version-1.2.1: + +1.2.1 +===== +:release-date: 2013-08-16 05:30 P.M UTC +:release-by: Ask Solem + +- Adds promise type: :meth:`amqp.utils.promise` + +- Merges fixes from 1.0.x + +.. _version-1.2.0: + +1.2.0 +===== +:release-date: 2012-11-12 04:00 P.M UTC +:release-by: Ask Solem + +- New exception hierarchy: + + - :class:`~amqp.AMQPError` + - :class:`~amqp.ConnectionError` + - :class:`~amqp.RecoverableConnectionError` + - :class:`~amqp.ConsumerCancelled` + - :class:`~amqp.ConnectionForced` + - :class:`~amqp.ResourceError` + - :class:`~IrrecoverableConnectionError` + - :class:`~amqp.ChannelNotOpen` + - :class:`~amqp.FrameError` + - :class:`~amqp.FrameSyntaxError` + - :class:`~amqp.InvalidCommand` + - :class:`~amqp.InvalidPath` + - :class:`~amqp.NotAllowed` + - :class:`~amqp.UnexpectedFrame` + - :class:`~amqp.AMQPNotImplementedError` + - :class:`~amqp.InternalError` + - :class:`~amqp.ChannelError` + - :class:`~RecoverableChannelError` + - :class:`~amqp.ContentTooLarge` + - :class:`~amqp.NoConsumers` + - :class:`~amqp.ResourceLocked` + - :class:`~IrrecoverableChannelError` + - :class:`~amqp.AccessRefused` + - :class:`~amqp.NotFound` + - :class:`~amqp.PreconditionFailed` + + +.. _version-1.1.0: + +1.1.0 +===== +:release-date: 2012-11-08 10:36 P.M UTC +:release-by: Ask Solem + +- No longer supports Python 2.5 + +- Fixed receiving of float table values. + +- Now Supports Python 3 and Python 2.6+ in the same source code. + +- Python 3 related fixes. + +.. _version-1.0.13: + +1.0.13 +====== +:release-date: 2013-07-31 04:00 P.M BST +:release-by: Ask Solem + +- Fixed problems with the SSL transport (Issue #15). + + Fix contributed by Adrien Guinet. + +- Small optimizations + +.. _version-1.0.12: + +1.0.12 +====== +:release-date: 2013-06-25 02:00 P.M BST +:release-by: Ask Solem + +- Fixed another Python 3 compatibility problem. + +.. _version-1.0.11: + +1.0.11 +====== +:release-date: 2013-04-11 06:00 P.M BST +:release-by: Ask Solem + +- Fixed Python 3 incompatibility in ``amqp/transport.py``. + +.. _version-1.0.10: + +1.0.10 +====== +:release-date: 2013-03-21 03:30 P.M UTC +:release-by: Ask Solem + +- Fixed Python 3 incompatibility in ``amqp/serialization.py``. + (Issue #11). + +.. _version-1.0.9: + +1.0.9 +===== +:release-date: 2013-03-08 10:40 A.M UTC +:release-by: Ask Solem + +- Publisher ack callbacks should now work after typo fix (Issue #9). + +- ``channel(explicit_id)`` will now claim that id from the array + of unused channel ids. + +- Fixes Jython compatibility. + +.. _version-1.0.8: + +1.0.8 +===== +:release-date: 2013-02-08 01:00 P.M UTC +:release-by: Ask Solem + +- Fixed SyntaxError on Python 2.5 + +.. _version-1.0.7: + +1.0.7 +===== +:release-date: 2013-02-08 01:00 P.M UTC +:release-by: Ask Solem + +- Workaround for bug on some Python 2.5 installations where (2**32) is 0. + +- Can now serialize the ARRAY type. + + Contributed by Adam Wentz. + +- Fixed tuple format bug in exception (Issue #4). + +.. _version-1.0.6: + +1.0.6 +===== +:release-date: 2012-11-29 01:14 P.M UTC +:release-by: Ask Solem + +- ``Channel.close`` is now ignored if the connection attribute is None. + +.. _version-1.0.5: + +1.0.5 +===== +:release-date: 2012-11-21 04:00 P.M UTC +:release-by: Ask Solem + +- ``Channel.basic_cancel`` is now ignored if the channel was already closed. + +- ``Channel.events`` is now a dict of sets:: + + >>> channel.events['basic_return'].add(on_basic_return) + >>> channel.events['basic_return'].discard(on_basic_return) + +.. _version-1.0.4: + +1.0.4 +===== +:release-date: 2012-11-13 04:00 P.M UTC +:release-by: Ask Solem + +- Fixes Python 2.5 support + +.. _version-1.0.3: + +1.0.3 +===== +:release-date: 2012-11-12 04:00 P.M UTC +:release-by: Ask Solem + +- Now can also handle float in headers/tables when receiving messages. + +- Now uses :class:`array.array` to keep track of unused channel ids. + +- The :data:`~amqp.exceptions.METHOD_NAME_MAP` has been updated for + amqp/0.9.1 and Rabbit extensions. + +- Removed a bunch of accidentally included images. + +.. _version-1.0.2: + +1.0.2 +===== +:release-date: 2012-11-06 05:00 P.M UTC +:release-by: Ask Solem + +- Now supports float values in headers/tables. + +.. _version-1.0.1: + +1.0.1 +===== +:release-date: 2012-11-05 01:00 P.M UTC +:release-by: Ask Solem + +- Connection errors no longer includes :exc:`AttributeError`. + +- Fixed problem with using the SSL transport in a non-blocking context. + + Fix contributed by Mher Movsisyan. + + +.. _version-1.0.0: + +1.0.0 +===== +:release-date: 2012-11-05 01:00 P.M UTC +:release-by: Ask Solem + +- Channels are now restored on channel error, so that the connection does not + have to closed. + +.. _version-0.9.4: + +Version 0.9.4 +============= + +- Adds support for ``exchange_bind`` and ``exchange_unbind``. + + Contributed by Rumyana Neykova + +- Fixed bugs in funtests and demo scripts. + + Contributed by Rumyana Neykova + +.. _version-0.9.3: + +Version 0.9.3 +============= + +- Fixed bug that could cause the consumer to crash when reading + large message payloads asynchronously. + +- Serialization error messages now include the invalid value. + +.. _version-0.9.2: + +Version 0.9.2 +============= + +- Consumer cancel notification support was broken (Issue #1) + + Fix contributed by Andrew Grangaard + +.. _version-0.9.1: + +Version 0.9.1 +============= + +- Supports draining events from multiple channels (``Connection.drain_events``) +- Support for timeouts +- Support for heartbeats + - ``Connection.heartbeat_tick(rate=2)`` must called at regular intervals + (half of the heartbeat value if rate is 2). + - Or some other scheme by using ``Connection.send_heartbeat``. +- Supports RabbitMQ extensions: + - Consumer Cancel Notifications + - by default a cancel results in ``ChannelError`` being raised + - but not if a ``on_cancel`` callback is passed to ``basic_consume``. + - Publisher confirms + - ``Channel.confirm_select()`` enables publisher confirms. + - ``Channel.events['basic_ack'].append(my_callback)`` adds a callback + to be called when a message is confirmed. This callback is then + called with the signature ``(delivery_tag, multiple)``. +- Support for ``basic_return`` +- Uses AMQP 0-9-1 instead of 0-8. + - ``Channel.access_request`` and ``ticket`` arguments to methods + **removed**. + - Supports the ``arguments`` argument to ``basic_consume``. + - ``internal`` argument to ``exchange_declare`` removed. + - ``auto_delete`` argument to ``exchange_declare`` deprecated + - ``insist`` argument to ``Connection`` removed. + - ``Channel.alerts`` has been removed. + - Support for ``Channel.basic_recover_async``. + - ``Channel.basic_recover`` deprecated. +- Exceptions renamed to have idiomatic names: + - ``AMQPException`` -> ``AMQPError`` + - ``AMQPConnectionException`` -> ConnectionError`` + - ``AMQPChannelException`` -> ChannelError`` + - ``Connection.known_hosts`` removed. + - ``Connection`` no longer supports redirects. + - ``exchange`` argument to ``queue_bind`` can now be empty + to use the "default exchange". +- Adds ``Connection.is_alive`` that tries to detect + whether the connection can still be used. +- Adds ``Connection.connection_errors`` and ``.channel_errors``, + a list of recoverable errors. +- Exposes the underlying socket as ``Connection.sock``. +- Adds ``Channel.no_ack_consumers`` to keep track of consumer tags + that set the no_ack flag. +- Slightly better at error recovery diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3b473db --- /dev/null +++ b/LICENSE @@ -0,0 +1,458 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..d17e9d2 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,6 @@ +include README.rst Changelog LICENSE +recursive-include docs * +recursive-include demo *.py +recursive-include extra README *.py +recursive-include funtests *.py +recursive-include requirements *.txt diff --git a/PKG-INFO b/PKG-INFO new file mode 100644 index 0000000..c71dc31 --- /dev/null +++ b/PKG-INFO @@ -0,0 +1,128 @@ +Metadata-Version: 1.1 +Name: amqp +Version: 1.4.5 +Summary: Low-level AMQP client for Python (fork of amqplib) +Home-page: http://github.com/celery/py-amqp +Author: Ask Solem +Author-email: pyamqp@celeryproject.org +License: LGPL +Description: ===================================================================== + Python AMQP 0.9.1 client library + ===================================================================== + + :Version: 1.4.5 + :Web: http://amqp.readthedocs.org/ + :Download: http://pypi.python.org/pypi/amqp/ + :Source: http://github.com/celery/py-amqp/ + :Keywords: amqp, rabbitmq + + About + ===== + + This is a fork of amqplib_ which was originally written by Barry Pederson. + It is maintained by the Celery_ project, and used by `kombu`_ as a pure python + alternative when `librabbitmq`_ is not available. + + This library should be API compatible with `librabbitmq`_. + + .. _amqplib: http://pypi.python.org/pypi/amqplib + .. _Celery: http://celeryproject.org/ + .. _kombu: http://kombu.readthedocs.org/ + .. _librabbitmq: http://pypi.python.org/pypi/librabbitmq + + Differences from `amqplib`_ + =========================== + + - Supports draining events from multiple channels (``Connection.drain_events``) + - Support for timeouts + - Channels are restored after channel error, instead of having to close the + connection. + - Support for heartbeats + + - ``Connection.heartbeat_tick(rate=2)`` must called at regular intervals + (half of the heartbeat value if rate is 2). + - Or some other scheme by using ``Connection.send_heartbeat``. + - Supports RabbitMQ extensions: + - Consumer Cancel Notifications + - by default a cancel results in ``ChannelError`` being raised + - but not if a ``on_cancel`` callback is passed to ``basic_consume``. + - Publisher confirms + - ``Channel.confirm_select()`` enables publisher confirms. + - ``Channel.events['basic_ack'].append(my_callback)`` adds a callback + to be called when a message is confirmed. This callback is then + called with the signature ``(delivery_tag, multiple)``. + - Exchange-to-exchange bindings: ``exchange_bind`` / ``exchange_unbind``. + - ``Channel.confirm_select()`` enables publisher confirms. + - ``Channel.events['basic_ack'].append(my_callback)`` adds a callback + to be called when a message is confirmed. This callback is then + called with the signature ``(delivery_tag, multiple)``. + - Support for ``basic_return`` + - Uses AMQP 0-9-1 instead of 0-8. + - ``Channel.access_request`` and ``ticket`` arguments to methods + **removed**. + - Supports the ``arguments`` argument to ``basic_consume``. + - ``internal`` argument to ``exchange_declare`` removed. + - ``auto_delete`` argument to ``exchange_declare`` deprecated + - ``insist`` argument to ``Connection`` removed. + - ``Channel.alerts`` has been removed. + - Support for ``Channel.basic_recover_async``. + - ``Channel.basic_recover`` deprecated. + - Exceptions renamed to have idiomatic names: + - ``AMQPException`` -> ``AMQPError`` + - ``AMQPConnectionException`` -> ConnectionError`` + - ``AMQPChannelException`` -> ChannelError`` + - ``Connection.known_hosts`` removed. + - ``Connection`` no longer supports redirects. + - ``exchange`` argument to ``queue_bind`` can now be empty + to use the "default exchange". + - Adds ``Connection.is_alive`` that tries to detect + whether the connection can still be used. + - Adds ``Connection.connection_errors`` and ``.channel_errors``, + a list of recoverable errors. + - Exposes the underlying socket as ``Connection.sock``. + - Adds ``Channel.no_ack_consumers`` to keep track of consumer tags + that set the no_ack flag. + - Slightly better at error recovery + + Further + ======= + + - Differences between AMQP 0.8 and 0.9.1 + + http://www.rabbitmq.com/amqp-0-8-to-0-9-1.html + + - AMQP 0.9.1 Quick Reference + + http://www.rabbitmq.com/amqp-0-9-1-quickref.html + + - RabbitMQ Extensions + + http://www.rabbitmq.com/extensions.html + + - For more information about AMQP, visit + + http://www.amqp.org + + - For other Python client libraries see: + + http://www.rabbitmq.com/devtools.html#python-dev + + .. image:: https://d2weczhvl823v0.cloudfront.net/celery/celery/trend.png + :alt: Bitdeli badge + :target: https://bitdeli.com/free + +Platform: any +Classifier: Development Status :: 5 - Production/Stable +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.6 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.0 +Classifier: Programming Language :: Python :: 3.1 +Classifier: Programming Language :: Python :: 3.2 +Classifier: Programming Language :: Python :: 3.3 +Classifier: License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL) +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: OS Independent diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..661d6bd --- /dev/null +++ b/README.rst @@ -0,0 +1,104 @@ +===================================================================== + Python AMQP 0.9.1 client library +===================================================================== + +:Version: 1.4.5 +:Web: http://amqp.readthedocs.org/ +:Download: http://pypi.python.org/pypi/amqp/ +:Source: http://github.com/celery/py-amqp/ +:Keywords: amqp, rabbitmq + +About +===== + +This is a fork of amqplib_ which was originally written by Barry Pederson. +It is maintained by the Celery_ project, and used by `kombu`_ as a pure python +alternative when `librabbitmq`_ is not available. + +This library should be API compatible with `librabbitmq`_. + +.. _amqplib: http://pypi.python.org/pypi/amqplib +.. _Celery: http://celeryproject.org/ +.. _kombu: http://kombu.readthedocs.org/ +.. _librabbitmq: http://pypi.python.org/pypi/librabbitmq + +Differences from `amqplib`_ +=========================== + +- Supports draining events from multiple channels (``Connection.drain_events``) +- Support for timeouts +- Channels are restored after channel error, instead of having to close the + connection. +- Support for heartbeats + + - ``Connection.heartbeat_tick(rate=2)`` must called at regular intervals + (half of the heartbeat value if rate is 2). + - Or some other scheme by using ``Connection.send_heartbeat``. +- Supports RabbitMQ extensions: + - Consumer Cancel Notifications + - by default a cancel results in ``ChannelError`` being raised + - but not if a ``on_cancel`` callback is passed to ``basic_consume``. + - Publisher confirms + - ``Channel.confirm_select()`` enables publisher confirms. + - ``Channel.events['basic_ack'].append(my_callback)`` adds a callback + to be called when a message is confirmed. This callback is then + called with the signature ``(delivery_tag, multiple)``. + - Exchange-to-exchange bindings: ``exchange_bind`` / ``exchange_unbind``. + - ``Channel.confirm_select()`` enables publisher confirms. + - ``Channel.events['basic_ack'].append(my_callback)`` adds a callback + to be called when a message is confirmed. This callback is then + called with the signature ``(delivery_tag, multiple)``. +- Support for ``basic_return`` +- Uses AMQP 0-9-1 instead of 0-8. + - ``Channel.access_request`` and ``ticket`` arguments to methods + **removed**. + - Supports the ``arguments`` argument to ``basic_consume``. + - ``internal`` argument to ``exchange_declare`` removed. + - ``auto_delete`` argument to ``exchange_declare`` deprecated + - ``insist`` argument to ``Connection`` removed. + - ``Channel.alerts`` has been removed. + - Support for ``Channel.basic_recover_async``. + - ``Channel.basic_recover`` deprecated. +- Exceptions renamed to have idiomatic names: + - ``AMQPException`` -> ``AMQPError`` + - ``AMQPConnectionException`` -> ConnectionError`` + - ``AMQPChannelException`` -> ChannelError`` + - ``Connection.known_hosts`` removed. + - ``Connection`` no longer supports redirects. + - ``exchange`` argument to ``queue_bind`` can now be empty + to use the "default exchange". +- Adds ``Connection.is_alive`` that tries to detect + whether the connection can still be used. +- Adds ``Connection.connection_errors`` and ``.channel_errors``, + a list of recoverable errors. +- Exposes the underlying socket as ``Connection.sock``. +- Adds ``Channel.no_ack_consumers`` to keep track of consumer tags + that set the no_ack flag. +- Slightly better at error recovery + +Further +======= + +- Differences between AMQP 0.8 and 0.9.1 + + http://www.rabbitmq.com/amqp-0-8-to-0-9-1.html + +- AMQP 0.9.1 Quick Reference + + http://www.rabbitmq.com/amqp-0-9-1-quickref.html + +- RabbitMQ Extensions + + http://www.rabbitmq.com/extensions.html + +- For more information about AMQP, visit + + http://www.amqp.org + +- For other Python client libraries see: + + http://www.rabbitmq.com/devtools.html#python-dev + +.. image:: https://d2weczhvl823v0.cloudfront.net/celery/celery/trend.png + :alt: Bitdeli badge + :target: https://bitdeli.com/free diff --git a/amqp.egg-info/PKG-INFO b/amqp.egg-info/PKG-INFO new file mode 100644 index 0000000..c71dc31 --- /dev/null +++ b/amqp.egg-info/PKG-INFO @@ -0,0 +1,128 @@ +Metadata-Version: 1.1 +Name: amqp +Version: 1.4.5 +Summary: Low-level AMQP client for Python (fork of amqplib) +Home-page: http://github.com/celery/py-amqp +Author: Ask Solem +Author-email: pyamqp@celeryproject.org +License: LGPL +Description: ===================================================================== + Python AMQP 0.9.1 client library + ===================================================================== + + :Version: 1.4.5 + :Web: http://amqp.readthedocs.org/ + :Download: http://pypi.python.org/pypi/amqp/ + :Source: http://github.com/celery/py-amqp/ + :Keywords: amqp, rabbitmq + + About + ===== + + This is a fork of amqplib_ which was originally written by Barry Pederson. + It is maintained by the Celery_ project, and used by `kombu`_ as a pure python + alternative when `librabbitmq`_ is not available. + + This library should be API compatible with `librabbitmq`_. + + .. _amqplib: http://pypi.python.org/pypi/amqplib + .. _Celery: http://celeryproject.org/ + .. _kombu: http://kombu.readthedocs.org/ + .. _librabbitmq: http://pypi.python.org/pypi/librabbitmq + + Differences from `amqplib`_ + =========================== + + - Supports draining events from multiple channels (``Connection.drain_events``) + - Support for timeouts + - Channels are restored after channel error, instead of having to close the + connection. + - Support for heartbeats + + - ``Connection.heartbeat_tick(rate=2)`` must called at regular intervals + (half of the heartbeat value if rate is 2). + - Or some other scheme by using ``Connection.send_heartbeat``. + - Supports RabbitMQ extensions: + - Consumer Cancel Notifications + - by default a cancel results in ``ChannelError`` being raised + - but not if a ``on_cancel`` callback is passed to ``basic_consume``. + - Publisher confirms + - ``Channel.confirm_select()`` enables publisher confirms. + - ``Channel.events['basic_ack'].append(my_callback)`` adds a callback + to be called when a message is confirmed. This callback is then + called with the signature ``(delivery_tag, multiple)``. + - Exchange-to-exchange bindings: ``exchange_bind`` / ``exchange_unbind``. + - ``Channel.confirm_select()`` enables publisher confirms. + - ``Channel.events['basic_ack'].append(my_callback)`` adds a callback + to be called when a message is confirmed. This callback is then + called with the signature ``(delivery_tag, multiple)``. + - Support for ``basic_return`` + - Uses AMQP 0-9-1 instead of 0-8. + - ``Channel.access_request`` and ``ticket`` arguments to methods + **removed**. + - Supports the ``arguments`` argument to ``basic_consume``. + - ``internal`` argument to ``exchange_declare`` removed. + - ``auto_delete`` argument to ``exchange_declare`` deprecated + - ``insist`` argument to ``Connection`` removed. + - ``Channel.alerts`` has been removed. + - Support for ``Channel.basic_recover_async``. + - ``Channel.basic_recover`` deprecated. + - Exceptions renamed to have idiomatic names: + - ``AMQPException`` -> ``AMQPError`` + - ``AMQPConnectionException`` -> ConnectionError`` + - ``AMQPChannelException`` -> ChannelError`` + - ``Connection.known_hosts`` removed. + - ``Connection`` no longer supports redirects. + - ``exchange`` argument to ``queue_bind`` can now be empty + to use the "default exchange". + - Adds ``Connection.is_alive`` that tries to detect + whether the connection can still be used. + - Adds ``Connection.connection_errors`` and ``.channel_errors``, + a list of recoverable errors. + - Exposes the underlying socket as ``Connection.sock``. + - Adds ``Channel.no_ack_consumers`` to keep track of consumer tags + that set the no_ack flag. + - Slightly better at error recovery + + Further + ======= + + - Differences between AMQP 0.8 and 0.9.1 + + http://www.rabbitmq.com/amqp-0-8-to-0-9-1.html + + - AMQP 0.9.1 Quick Reference + + http://www.rabbitmq.com/amqp-0-9-1-quickref.html + + - RabbitMQ Extensions + + http://www.rabbitmq.com/extensions.html + + - For more information about AMQP, visit + + http://www.amqp.org + + - For other Python client libraries see: + + http://www.rabbitmq.com/devtools.html#python-dev + + .. image:: https://d2weczhvl823v0.cloudfront.net/celery/celery/trend.png + :alt: Bitdeli badge + :target: https://bitdeli.com/free + +Platform: any +Classifier: Development Status :: 5 - Production/Stable +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.6 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.0 +Classifier: Programming Language :: Python :: 3.1 +Classifier: Programming Language :: Python :: 3.2 +Classifier: Programming Language :: Python :: 3.3 +Classifier: License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL) +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: OS Independent diff --git a/amqp.egg-info/SOURCES.txt b/amqp.egg-info/SOURCES.txt new file mode 100644 index 0000000..dd32cc2 --- /dev/null +++ b/amqp.egg-info/SOURCES.txt @@ -0,0 +1,67 @@ +Changelog +LICENSE +MANIFEST.in +README.rst +setup.py +amqp/__init__.py +amqp/abstract_channel.py +amqp/basic_message.py +amqp/channel.py +amqp/connection.py +amqp/exceptions.py +amqp/five.py +amqp/method_framing.py +amqp/protocol.py +amqp/serialization.py +amqp/transport.py +amqp/utils.py +amqp.egg-info/PKG-INFO +amqp.egg-info/SOURCES.txt +amqp.egg-info/dependency_links.txt +amqp.egg-info/not-zip-safe +amqp.egg-info/top_level.txt +demo/amqp_clock.py +demo/demo_receive.py +demo/demo_send.py +docs/Makefile +docs/changelog.rst +docs/conf.py +docs/index.rst +docs/.static/.keep +docs/.templates/page.html +docs/.templates/sidebarintro.html +docs/.templates/sidebarlogo.html +docs/_ext/applyxrefs.py +docs/_ext/literals_to_xrefs.py +docs/_theme/celery/theme.conf +docs/_theme/celery/static/celery.css_t +docs/includes/intro.txt +docs/reference/amqp.abstract_channel.rst +docs/reference/amqp.basic_message.rst +docs/reference/amqp.channel.rst +docs/reference/amqp.connection.rst +docs/reference/amqp.exceptions.rst +docs/reference/amqp.five.rst +docs/reference/amqp.method_framing.rst +docs/reference/amqp.protocol.rst +docs/reference/amqp.serialization.rst +docs/reference/amqp.transport.rst +docs/reference/amqp.utils.rst +docs/reference/index.rst +docs/templates/readme.txt +extra/README +extra/generate_skeleton_0_8.py +extra/update_comments_from_spec.py +extra/release/bump_version.py +extra/release/sphinx-to-rst.py +funtests/run_all.py +funtests/settings.py +funtests/test_basic_message.py +funtests/test_channel.py +funtests/test_connection.py +funtests/test_exceptions.py +funtests/test_serialization.py +funtests/test_with.py +requirements/docs.txt +requirements/pkgutils.txt +requirements/test.txt \ No newline at end of file diff --git a/amqp.egg-info/dependency_links.txt b/amqp.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/amqp.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/amqp.egg-info/not-zip-safe b/amqp.egg-info/not-zip-safe new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/amqp.egg-info/not-zip-safe @@ -0,0 +1 @@ + diff --git a/amqp.egg-info/top_level.txt b/amqp.egg-info/top_level.txt new file mode 100644 index 0000000..5e610d3 --- /dev/null +++ b/amqp.egg-info/top_level.txt @@ -0,0 +1 @@ +amqp diff --git a/amqp/__init__.py b/amqp/__init__.py new file mode 100644 index 0000000..3bd57c9 --- /dev/null +++ b/amqp/__init__.py @@ -0,0 +1,70 @@ +"""Low-level AMQP client for Python (fork of amqplib)""" +# Copyright (C) 2007-2008 Barry Pederson +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 +from __future__ import absolute_import + +VERSION = (1, 4, 5) +__version__ = '.'.join(map(str, VERSION[0:3])) + ''.join(VERSION[3:]) +__author__ = 'Barry Pederson' +__maintainer__ = 'Ask Solem' +__contact__ = 'pyamqp@celeryproject.org' +__homepage__ = 'http://github.com/celery/py-amqp' +__docformat__ = 'restructuredtext' + +# -eof meta- + +# +# Pull in the public items from the various sub-modules +# +from .basic_message import Message # noqa +from .channel import Channel # noqa +from .connection import Connection # noqa +from .exceptions import ( # noqa + AMQPError, + ConnectionError, + RecoverableConnectionError, + IrrecoverableConnectionError, + ChannelError, + RecoverableChannelError, + IrrecoverableChannelError, + ConsumerCancelled, + ContentTooLarge, + NoConsumers, + ConnectionForced, + InvalidPath, + AccessRefused, + NotFound, + ResourceLocked, + PreconditionFailed, + FrameError, + FrameSyntaxError, + InvalidCommand, + ChannelNotOpen, + UnexpectedFrame, + ResourceError, + NotAllowed, + AMQPNotImplementedError, + InternalError, + error_for_code, + __all__ as _all_exceptions, +) +from .utils import promise # noqa + +__all__ = [ + 'Connection', + 'Channel', + 'Message', +] + _all_exceptions diff --git a/amqp/abstract_channel.py b/amqp/abstract_channel.py new file mode 100644 index 0000000..28cfe13 --- /dev/null +++ b/amqp/abstract_channel.py @@ -0,0 +1,93 @@ +"""Code common to Connection and Channel objects.""" +# Copyright (C) 2007-2008 Barry Pederson ) +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 +from __future__ import absolute_import + +from .exceptions import AMQPNotImplementedError, RecoverableConnectionError +from .serialization import AMQPWriter + +__all__ = ['AbstractChannel'] + + +class AbstractChannel(object): + """Superclass for both the Connection, which is treated + as channel 0, and other user-created Channel objects. + + The subclasses must have a _METHOD_MAP class property, mapping + between AMQP method signatures and Python methods. + + """ + def __init__(self, connection, channel_id): + self.connection = connection + self.channel_id = channel_id + connection.channels[channel_id] = self + self.method_queue = [] # Higher level queue for methods + self.auto_decode = False + + def __enter__(self): + return self + + def __exit__(self, *exc_info): + self.close() + + def _send_method(self, method_sig, args=bytes(), content=None): + """Send a method for our channel.""" + conn = self.connection + if conn is None: + raise RecoverableConnectionError('connection already closed') + + if isinstance(args, AMQPWriter): + args = args.getvalue() + + conn.method_writer.write_method( + self.channel_id, method_sig, args, content, + ) + + def close(self): + """Close this Channel or Connection""" + raise NotImplementedError('Must be overriden in subclass') + + def wait(self, allowed_methods=None): + """Wait for a method that matches our allowed_methods parameter (the + default value of None means match any method), and dispatch to it.""" + method_sig, args, content = self.connection._wait_method( + self.channel_id, allowed_methods) + + return self.dispatch_method(method_sig, args, content) + + def dispatch_method(self, method_sig, args, content): + if content and \ + self.auto_decode and \ + hasattr(content, 'content_encoding'): + try: + content.body = content.body.decode(content.content_encoding) + except Exception: + pass + + try: + amqp_method = self._METHOD_MAP[method_sig] + except KeyError: + raise AMQPNotImplementedError( + 'Unknown AMQP method {0!r}'.format(method_sig)) + + if content is None: + return amqp_method(self, args) + else: + return amqp_method(self, args, content) + + #: Placeholder, the concrete implementations will have to + #: supply their own versions of _METHOD_MAP + _METHOD_MAP = {} diff --git a/amqp/basic_message.py b/amqp/basic_message.py new file mode 100644 index 0000000..192ede9 --- /dev/null +++ b/amqp/basic_message.py @@ -0,0 +1,124 @@ +"""Messages for AMQP""" +# Copyright (C) 2007-2008 Barry Pederson +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 +from __future__ import absolute_import + +from .serialization import GenericContent + +__all__ = ['Message'] + + +class Message(GenericContent): + """A Message for use with the Channnel.basic_* methods.""" + + #: Instances of this class have these attributes, which + #: are passed back and forth as message properties between + #: client and server + PROPERTIES = [ + ('content_type', 'shortstr'), + ('content_encoding', 'shortstr'), + ('application_headers', 'table'), + ('delivery_mode', 'octet'), + ('priority', 'octet'), + ('correlation_id', 'shortstr'), + ('reply_to', 'shortstr'), + ('expiration', 'shortstr'), + ('message_id', 'shortstr'), + ('timestamp', 'timestamp'), + ('type', 'shortstr'), + ('user_id', 'shortstr'), + ('app_id', 'shortstr'), + ('cluster_id', 'shortstr') + ] + + def __init__(self, body='', children=None, channel=None, **properties): + """Expected arg types + + body: string + children: (not supported) + + Keyword properties may include: + + content_type: shortstr + MIME content type + + content_encoding: shortstr + MIME content encoding + + application_headers: table + Message header field table, a dict with string keys, + and string | int | Decimal | datetime | dict values. + + delivery_mode: octet + Non-persistent (1) or persistent (2) + + priority: octet + The message priority, 0 to 9 + + correlation_id: shortstr + The application correlation identifier + + reply_to: shortstr + The destination to reply to + + expiration: shortstr + Message expiration specification + + message_id: shortstr + The application message identifier + + timestamp: datetime.datetime + The message timestamp + + type: shortstr + The message type name + + user_id: shortstr + The creating user id + + app_id: shortstr + The creating application id + + cluster_id: shortstr + Intra-cluster routing identifier + + Unicode bodies are encoded according to the 'content_encoding' + argument. If that's None, it's set to 'UTF-8' automatically. + + example:: + + msg = Message('hello world', + content_type='text/plain', + application_headers={'foo': 7}) + + """ + super(Message, self).__init__(**properties) + self.body = body + self.channel = channel + + def __eq__(self, other): + """Check if the properties and bodies of this Message and another + Message are the same. + + Received messages may contain a 'delivery_info' attribute, + which isn't compared. + + """ + try: + return (super(Message, self).__eq__(other) and + self.body == other.body) + except AttributeError: + return NotImplemented diff --git a/amqp/channel.py b/amqp/channel.py new file mode 100644 index 0000000..05eb09a --- /dev/null +++ b/amqp/channel.py @@ -0,0 +1,2537 @@ +"""AMQP Channels""" +# Copyright (C) 2007-2008 Barry Pederson +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 +from __future__ import absolute_import + +import logging + +from collections import defaultdict +from warnings import warn + +from .abstract_channel import AbstractChannel +from .exceptions import ChannelError, ConsumerCancelled, error_for_code +from .five import Queue +from .protocol import basic_return_t, queue_declare_ok_t +from .serialization import AMQPWriter + +__all__ = ['Channel'] + +AMQP_LOGGER = logging.getLogger('amqp') + +EXCHANGE_AUTODELETE_DEPRECATED = """\ +The auto_delete flag for exchanges has been deprecated and will be removed +from py-amqp v1.5.0.\ +""" + + +class VDeprecationWarning(DeprecationWarning): + pass + + +class Channel(AbstractChannel): + """Work with channels + + The channel class provides methods for a client to establish a + virtual connection - a channel - to a server and for both peers to + operate the virtual connection thereafter. + + GRAMMAR:: + + channel = open-channel *use-channel close-channel + open-channel = C:OPEN S:OPEN-OK + use-channel = C:FLOW S:FLOW-OK + / S:FLOW C:FLOW-OK + / functional-class + close-channel = C:CLOSE S:CLOSE-OK + / S:CLOSE C:CLOSE-OK + + """ + + def __init__(self, connection, channel_id=None, auto_decode=True): + """Create a channel bound to a connection and using the specified + numeric channel_id, and open on the server. + + The 'auto_decode' parameter (defaults to True), indicates + whether the library should attempt to decode the body + of Messages to a Unicode string if there's a 'content_encoding' + property for the message. If there's no 'content_encoding' + property, or the decode raises an Exception, the message body + is left as plain bytes. + + """ + if channel_id: + connection._claim_channel_id(channel_id) + else: + channel_id = connection._get_free_channel_id() + + AMQP_LOGGER.debug('using channel_id: %d', channel_id) + + super(Channel, self).__init__(connection, channel_id) + + self.is_open = False + self.active = True # Flow control + self.returned_messages = Queue() + self.callbacks = {} + self.cancel_callbacks = {} + self.auto_decode = auto_decode + self.events = defaultdict(set) + self.no_ack_consumers = set() + + # set first time basic_publish_confirm is called + # and publisher confirms are enabled for this channel. + self._confirm_selected = False + if self.connection.confirm_publish: + self.basic_publish = self.basic_publish_confirm + + self._x_open() + + def _do_close(self): + """Tear down this object, after we've agreed to close + with the server.""" + AMQP_LOGGER.debug('Closed channel #%d', self.channel_id) + self.is_open = False + channel_id, self.channel_id = self.channel_id, None + connection, self.connection = self.connection, None + if connection: + connection.channels.pop(channel_id, None) + connection._avail_channel_ids.append(channel_id) + self.callbacks.clear() + self.cancel_callbacks.clear() + self.events.clear() + self.no_ack_consumers.clear() + + def _do_revive(self): + self.is_open = False + self._x_open() + + def close(self, reply_code=0, reply_text='', method_sig=(0, 0)): + """Request a channel close + + This method indicates that the sender wants to close the + channel. This may be due to internal conditions (e.g. a forced + shut-down) or due to an error handling a specific method, i.e. + an exception. When a close is due to an exception, the sender + provides the class and method id of the method which caused + the exception. + + RULE: + + After sending this method any received method except + Channel.Close-OK MUST be discarded. + + RULE: + + The peer sending this method MAY use a counter or timeout + to detect failure of the other peer to respond correctly + with Channel.Close-OK.. + + PARAMETERS: + reply_code: short + + The reply code. The AMQ reply codes are defined in AMQ + RFC 011. + + reply_text: shortstr + + The localised reply text. This text can be logged as an + aid to resolving issues. + + class_id: short + + failing method class + + When the close is provoked by a method exception, this + is the class of the method. + + method_id: short + + failing method ID + + When the close is provoked by a method exception, this + is the ID of the method. + + """ + try: + if not self.is_open or self.connection is None: + return + + args = AMQPWriter() + args.write_short(reply_code) + args.write_shortstr(reply_text) + args.write_short(method_sig[0]) # class_id + args.write_short(method_sig[1]) # method_id + self._send_method((20, 40), args) + return self.wait(allowed_methods=[ + (20, 40), # Channel.close + (20, 41), # Channel.close_ok + ]) + finally: + self.connection = None + + def _close(self, args): + """Request a channel close + + This method indicates that the sender wants to close the + channel. This may be due to internal conditions (e.g. a forced + shut-down) or due to an error handling a specific method, i.e. + an exception. When a close is due to an exception, the sender + provides the class and method id of the method which caused + the exception. + + RULE: + + After sending this method any received method except + Channel.Close-OK MUST be discarded. + + RULE: + + The peer sending this method MAY use a counter or timeout + to detect failure of the other peer to respond correctly + with Channel.Close-OK.. + + PARAMETERS: + reply_code: short + + The reply code. The AMQ reply codes are defined in AMQ + RFC 011. + + reply_text: shortstr + + The localised reply text. This text can be logged as an + aid to resolving issues. + + class_id: short + + failing method class + + When the close is provoked by a method exception, this + is the class of the method. + + method_id: short + + failing method ID + + When the close is provoked by a method exception, this + is the ID of the method. + + """ + + reply_code = args.read_short() + reply_text = args.read_shortstr() + class_id = args.read_short() + method_id = args.read_short() + + self._send_method((20, 41)) + self._do_revive() + + raise error_for_code( + reply_code, reply_text, (class_id, method_id), ChannelError, + ) + + def _close_ok(self, args): + """Confirm a channel close + + This method confirms a Channel.Close method and tells the + recipient that it is safe to release resources for the channel + and close the socket. + + RULE: + + A peer that detects a socket closure without having + received a Channel.Close-Ok handshake method SHOULD log + the error. + + """ + self._do_close() + + def flow(self, active): + """Enable/disable flow from peer + + This method asks the peer to pause or restart the flow of + content data. This is a simple flow-control mechanism that a + peer can use to avoid oveflowing its queues or otherwise + finding itself receiving more messages than it can process. + Note that this method is not intended for window control. The + peer that receives a request to stop sending content should + finish sending the current content, if any, and then wait + until it receives a Flow restart method. + + RULE: + + When a new channel is opened, it is active. Some + applications assume that channels are inactive until + started. To emulate this behaviour a client MAY open the + channel, then pause it. + + RULE: + + When sending content data in multiple frames, a peer + SHOULD monitor the channel for incoming methods and + respond to a Channel.Flow as rapidly as possible. + + RULE: + + A peer MAY use the Channel.Flow method to throttle + incoming content data for internal reasons, for example, + when exchangeing data over a slower connection. + + RULE: + + The peer that requests a Channel.Flow method MAY + disconnect and/or ban a peer that does not respect the + request. + + PARAMETERS: + active: boolean + + start/stop content frames + + If True, the peer starts sending content frames. If + False, the peer stops sending content frames. + + """ + args = AMQPWriter() + args.write_bit(active) + self._send_method((20, 20), args) + return self.wait(allowed_methods=[ + (20, 21), # Channel.flow_ok + ]) + + def _flow(self, args): + """Enable/disable flow from peer + + This method asks the peer to pause or restart the flow of + content data. This is a simple flow-control mechanism that a + peer can use to avoid oveflowing its queues or otherwise + finding itself receiving more messages than it can process. + Note that this method is not intended for window control. The + peer that receives a request to stop sending content should + finish sending the current content, if any, and then wait + until it receives a Flow restart method. + + RULE: + + When a new channel is opened, it is active. Some + applications assume that channels are inactive until + started. To emulate this behaviour a client MAY open the + channel, then pause it. + + RULE: + + When sending content data in multiple frames, a peer + SHOULD monitor the channel for incoming methods and + respond to a Channel.Flow as rapidly as possible. + + RULE: + + A peer MAY use the Channel.Flow method to throttle + incoming content data for internal reasons, for example, + when exchangeing data over a slower connection. + + RULE: + + The peer that requests a Channel.Flow method MAY + disconnect and/or ban a peer that does not respect the + request. + + PARAMETERS: + active: boolean + + start/stop content frames + + If True, the peer starts sending content frames. If + False, the peer stops sending content frames. + + """ + self.active = args.read_bit() + self._x_flow_ok(self.active) + + def _x_flow_ok(self, active): + """Confirm a flow method + + Confirms to the peer that a flow command was received and + processed. + + PARAMETERS: + active: boolean + + current flow setting + + Confirms the setting of the processed flow method: + True means the peer will start sending or continue + to send content frames; False means it will not. + + """ + args = AMQPWriter() + args.write_bit(active) + self._send_method((20, 21), args) + + def _flow_ok(self, args): + """Confirm a flow method + + Confirms to the peer that a flow command was received and + processed. + + PARAMETERS: + active: boolean + + current flow setting + + Confirms the setting of the processed flow method: + True means the peer will start sending or continue + to send content frames; False means it will not. + + """ + return args.read_bit() + + def _x_open(self): + """Open a channel for use + + This method opens a virtual connection (a channel). + + RULE: + + This method MUST NOT be called when the channel is already + open. + + PARAMETERS: + out_of_band: shortstr (DEPRECATED) + + out-of-band settings + + Configures out-of-band transfers on this channel. The + syntax and meaning of this field will be formally + defined at a later date. + + """ + if self.is_open: + return + + args = AMQPWriter() + args.write_shortstr('') # out_of_band: deprecated + self._send_method((20, 10), args) + return self.wait(allowed_methods=[ + (20, 11), # Channel.open_ok + ]) + + def _open_ok(self, args): + """Signal that the channel is ready + + This method signals to the client that the channel is ready + for use. + + """ + self.is_open = True + AMQP_LOGGER.debug('Channel open') + + ############# + # + # Exchange + # + # + # work with exchanges + # + # Exchanges match and distribute messages across queues. + # Exchanges can be configured in the server or created at runtime. + # + # GRAMMAR:: + # + # exchange = C:DECLARE S:DECLARE-OK + # / C:DELETE S:DELETE-OK + # + # RULE: + # + # The server MUST implement the direct and fanout exchange + # types, and predeclare the corresponding exchanges named + # amq.direct and amq.fanout in each virtual host. The server + # MUST also predeclare a direct exchange to act as the default + # exchange for content Publish methods and for default queue + # bindings. + # + # RULE: + # + # The server SHOULD implement the topic exchange type, and + # predeclare the corresponding exchange named amq.topic in + # each virtual host. + # + # RULE: + # + # The server MAY implement the system exchange type, and + # predeclare the corresponding exchanges named amq.system in + # each virtual host. If the client attempts to bind a queue to + # the system exchange, the server MUST raise a connection + # exception with reply code 507 (not allowed). + # + + def exchange_declare(self, exchange, type, passive=False, durable=False, + auto_delete=True, nowait=False, arguments=None): + """Declare exchange, create if needed + + This method creates an exchange if it does not already exist, + and if the exchange exists, verifies that it is of the correct + and expected class. + + RULE: + + The server SHOULD support a minimum of 16 exchanges per + virtual host and ideally, impose no limit except as + defined by available resources. + + PARAMETERS: + exchange: shortstr + + RULE: + + Exchange names starting with "amq." are reserved + for predeclared and standardised exchanges. If + the client attempts to create an exchange starting + with "amq.", the server MUST raise a channel + exception with reply code 403 (access refused). + + type: shortstr + + exchange type + + Each exchange belongs to one of a set of exchange + types implemented by the server. The exchange types + define the functionality of the exchange - i.e. how + messages are routed through it. It is not valid or + meaningful to attempt to change the type of an + existing exchange. + + RULE: + + If the exchange already exists with a different + type, the server MUST raise a connection exception + with a reply code 507 (not allowed). + + RULE: + + If the server does not support the requested + exchange type it MUST raise a connection exception + with a reply code 503 (command invalid). + + passive: boolean + + do not create exchange + + If set, the server will not create the exchange. The + client can use this to check whether an exchange + exists without modifying the server state. + + RULE: + + If set, and the exchange does not already exist, + the server MUST raise a channel exception with + reply code 404 (not found). + + durable: boolean + + request a durable exchange + + If set when creating a new exchange, the exchange will + be marked as durable. Durable exchanges remain active + when a server restarts. Non-durable exchanges + (transient exchanges) are purged if/when a server + restarts. + + RULE: + + The server MUST support both durable and transient + exchanges. + + RULE: + + The server MUST ignore the durable field if the + exchange already exists. + + auto_delete: boolean + + auto-delete when unused + + If set, the exchange is deleted when all queues have + finished using it. + + RULE: + + The server SHOULD allow for a reasonable delay + between the point when it determines that an + exchange is not being used (or no longer used), + and the point when it deletes the exchange. At + the least it must allow a client to create an + exchange and then bind a queue to it, with a small + but non-zero delay between these two actions. + + RULE: + + The server MUST ignore the auto-delete field if + the exchange already exists. + + nowait: boolean + + do not send a reply method + + If set, the server will not respond to the method. The + client should not wait for a reply method. If the + server could not complete the method it will raise a + channel or connection exception. + + arguments: table + + arguments for declaration + + A set of arguments for the declaration. The syntax and + semantics of these arguments depends on the server + implementation. This field is ignored if passive is + True. + + """ + arguments = {} if arguments is None else arguments + args = AMQPWriter() + args.write_short(0) + args.write_shortstr(exchange) + args.write_shortstr(type) + args.write_bit(passive) + args.write_bit(durable) + args.write_bit(auto_delete) + args.write_bit(False) # internal: deprecated + args.write_bit(nowait) + args.write_table(arguments) + self._send_method((40, 10), args) + + if auto_delete: + warn(VDeprecationWarning(EXCHANGE_AUTODELETE_DEPRECATED)) + + if not nowait: + return self.wait(allowed_methods=[ + (40, 11), # Channel.exchange_declare_ok + ]) + + def _exchange_declare_ok(self, args): + """Confirms an exchange declaration + + This method confirms a Declare method and confirms the name of + the exchange, essential for automatically-named exchanges. + + """ + pass + + def exchange_delete(self, exchange, if_unused=False, nowait=False): + """Delete an exchange + + This method deletes an exchange. When an exchange is deleted + all queue bindings on the exchange are cancelled. + + PARAMETERS: + exchange: shortstr + + RULE: + + The exchange MUST exist. Attempting to delete a + non-existing exchange causes a channel exception. + + if_unused: boolean + + delete only if unused + + If set, the server will only delete the exchange if it + has no queue bindings. If the exchange has queue + bindings the server does not delete it but raises a + channel exception instead. + + RULE: + + If set, the server SHOULD delete the exchange but + only if it has no queue bindings. + + RULE: + + If set, the server SHOULD raise a channel + exception if the exchange is in use. + + nowait: boolean + + do not send a reply method + + If set, the server will not respond to the method. The + client should not wait for a reply method. If the + server could not complete the method it will raise a + channel or connection exception. + + """ + args = AMQPWriter() + args.write_short(0) + args.write_shortstr(exchange) + args.write_bit(if_unused) + args.write_bit(nowait) + self._send_method((40, 20), args) + + if not nowait: + return self.wait(allowed_methods=[ + (40, 21), # Channel.exchange_delete_ok + ]) + + def _exchange_delete_ok(self, args): + """Confirm deletion of an exchange + + This method confirms the deletion of an exchange. + + """ + pass + + def exchange_bind(self, destination, source='', routing_key='', + nowait=False, arguments=None): + """This method binds an exchange to an exchange. + + RULE: + + A server MUST allow and ignore duplicate bindings - that + is, two or more bind methods for a specific exchanges, + with identical arguments - without treating these as an + error. + + RULE: + + A server MUST allow cycles of exchange bindings to be + created including allowing an exchange to be bound to + itself. + + RULE: + + A server MUST not deliver the same message more than once + to a destination exchange, even if the topology of + exchanges and bindings results in multiple (even infinite) + routes to that exchange. + + PARAMETERS: + reserved-1: short + + destination: shortstr + + Specifies the name of the destination exchange to + bind. + + RULE: + + A client MUST NOT be allowed to bind a non- + existent destination exchange. + + RULE: + + The server MUST accept a blank exchange name to + mean the default exchange. + + source: shortstr + + Specifies the name of the source exchange to bind. + + RULE: + + A client MUST NOT be allowed to bind a non- + existent source exchange. + + RULE: + + The server MUST accept a blank exchange name to + mean the default exchange. + + routing-key: shortstr + + Specifies the routing key for the binding. The routing + key is used for routing messages depending on the + exchange configuration. Not all exchanges use a + routing key - refer to the specific exchange + documentation. + + no-wait: bit + + arguments: table + + A set of arguments for the binding. The syntax and + semantics of these arguments depends on the exchange + class. + + """ + arguments = {} if arguments is None else arguments + args = AMQPWriter() + args.write_short(0) + args.write_shortstr(destination) + args.write_shortstr(source) + args.write_shortstr(routing_key) + args.write_bit(nowait) + args.write_table(arguments) + self._send_method((40, 30), args) + + if not nowait: + return self.wait(allowed_methods=[ + (40, 31), # Channel.exchange_bind_ok + ]) + + def exchange_unbind(self, destination, source='', routing_key='', + nowait=False, arguments=None): + """This method unbinds an exchange from an exchange. + + RULE: + + If a unbind fails, the server MUST raise a connection + exception. + + PARAMETERS: + reserved-1: short + + destination: shortstr + + Specifies the name of the destination exchange to + unbind. + + RULE: + + The client MUST NOT attempt to unbind an exchange + that does not exist from an exchange. + + RULE: + + The server MUST accept a blank exchange name to + mean the default exchange. + + source: shortstr + + Specifies the name of the source exchange to unbind. + + RULE: + + The client MUST NOT attempt to unbind an exchange + from an exchange that does not exist. + + RULE: + + The server MUST accept a blank exchange name to + mean the default exchange. + + routing-key: shortstr + + Specifies the routing key of the binding to unbind. + + no-wait: bit + + arguments: table + + Specifies the arguments of the binding to unbind. + + """ + arguments = {} if arguments is None else arguments + args = AMQPWriter() + args.write_short(0) + args.write_shortstr(destination) + args.write_shortstr(source) + args.write_shortstr(routing_key) + args.write_bit(nowait) + args.write_table(arguments) + self._send_method((40, 40), args) + + if not nowait: + return self.wait(allowed_methods=[ + (40, 51), # Channel.exchange_unbind_ok + ]) + + def _exchange_bind_ok(self, args): + """Confirm bind successful + + This method confirms that the bind was successful. + + """ + pass + + def _exchange_unbind_ok(self, args): + """Confirm unbind successful + + This method confirms that the unbind was successful. + + """ + pass + + ############# + # + # Queue + # + # + # work with queues + # + # Queues store and forward messages. Queues can be configured in + # the server or created at runtime. Queues must be attached to at + # least one exchange in order to receive messages from publishers. + # + # GRAMMAR:: + # + # queue = C:DECLARE S:DECLARE-OK + # / C:BIND S:BIND-OK + # / C:PURGE S:PURGE-OK + # / C:DELETE S:DELETE-OK + # + # RULE: + # + # A server MUST allow any content class to be sent to any + # queue, in any mix, and queue and delivery these content + # classes independently. Note that all methods that fetch + # content off queues are specific to a given content class. + # + + def queue_bind(self, queue, exchange='', routing_key='', + nowait=False, arguments=None): + """Bind queue to an exchange + + This method binds a queue to an exchange. Until a queue is + bound it will not receive any messages. In a classic + messaging model, store-and-forward queues are bound to a dest + exchange and subscription queues are bound to a dest_wild + exchange. + + RULE: + + A server MUST allow ignore duplicate bindings - that is, + two or more bind methods for a specific queue, with + identical arguments - without treating these as an error. + + RULE: + + If a bind fails, the server MUST raise a connection + exception. + + RULE: + + The server MUST NOT allow a durable queue to bind to a + transient exchange. If the client attempts this the server + MUST raise a channel exception. + + RULE: + + Bindings for durable queues are automatically durable and + the server SHOULD restore such bindings after a server + restart. + + RULE: + + The server SHOULD support at least 4 bindings per queue, + and ideally, impose no limit except as defined by + available resources. + + PARAMETERS: + queue: shortstr + + Specifies the name of the queue to bind. If the queue + name is empty, refers to the current queue for the + channel, which is the last declared queue. + + RULE: + + If the client did not previously declare a queue, + and the queue name in this method is empty, the + server MUST raise a connection exception with + reply code 530 (not allowed). + + RULE: + + If the queue does not exist the server MUST raise + a channel exception with reply code 404 (not + found). + + exchange: shortstr + + The name of the exchange to bind to. + + RULE: + + If the exchange does not exist the server MUST + raise a channel exception with reply code 404 (not + found). + + routing_key: shortstr + + message routing key + + Specifies the routing key for the binding. The + routing key is used for routing messages depending on + the exchange configuration. Not all exchanges use a + routing key - refer to the specific exchange + documentation. If the routing key is empty and the + queue name is empty, the routing key will be the + current queue for the channel, which is the last + declared queue. + + nowait: boolean + + do not send a reply method + + If set, the server will not respond to the method. The + client should not wait for a reply method. If the + server could not complete the method it will raise a + channel or connection exception. + + arguments: table + + arguments for binding + + A set of arguments for the binding. The syntax and + semantics of these arguments depends on the exchange + class. + """ + arguments = {} if arguments is None else arguments + args = AMQPWriter() + args.write_short(0) + args.write_shortstr(queue) + args.write_shortstr(exchange) + args.write_shortstr(routing_key) + args.write_bit(nowait) + args.write_table(arguments) + self._send_method((50, 20), args) + + if not nowait: + return self.wait(allowed_methods=[ + (50, 21), # Channel.queue_bind_ok + ]) + + def _queue_bind_ok(self, args): + """Confirm bind successful + + This method confirms that the bind was successful. + + """ + pass + + def queue_unbind(self, queue, exchange, routing_key='', + nowait=False, arguments=None): + """Unbind a queue from an exchange + + This method unbinds a queue from an exchange. + + RULE: + + If a unbind fails, the server MUST raise a connection exception. + + PARAMETERS: + queue: shortstr + + Specifies the name of the queue to unbind. + + RULE: + + The client MUST either specify a queue name or have + previously declared a queue on the same channel + + RULE: + + The client MUST NOT attempt to unbind a queue that + does not exist. + + exchange: shortstr + + The name of the exchange to unbind from. + + RULE: + + The client MUST NOT attempt to unbind a queue from an + exchange that does not exist. + + RULE: + + The server MUST accept a blank exchange name to mean + the default exchange. + + routing_key: shortstr + + routing key of binding + + Specifies the routing key of the binding to unbind. + + arguments: table + + arguments of binding + + Specifies the arguments of the binding to unbind. + + """ + arguments = {} if arguments is None else arguments + args = AMQPWriter() + args.write_short(0) + args.write_shortstr(queue) + args.write_shortstr(exchange) + args.write_shortstr(routing_key) + #args.write_bit(nowait) + args.write_table(arguments) + self._send_method((50, 50), args) + + if not nowait: + return self.wait(allowed_methods=[ + (50, 51), # Channel.queue_unbind_ok + ]) + + def _queue_unbind_ok(self, args): + """Confirm unbind successful + + This method confirms that the unbind was successful. + + """ + pass + + def queue_declare(self, queue='', passive=False, durable=False, + exclusive=False, auto_delete=True, nowait=False, + arguments=None): + """Declare queue, create if needed + + This method creates or checks a queue. When creating a new + queue the client can specify various properties that control + the durability of the queue and its contents, and the level of + sharing for the queue. + + RULE: + + The server MUST create a default binding for a newly- + created queue to the default exchange, which is an + exchange of type 'direct'. + + RULE: + + The server SHOULD support a minimum of 256 queues per + virtual host and ideally, impose no limit except as + defined by available resources. + + PARAMETERS: + queue: shortstr + + RULE: + + The queue name MAY be empty, in which case the + server MUST create a new queue with a unique + generated name and return this to the client in + the Declare-Ok method. + + RULE: + + Queue names starting with "amq." are reserved for + predeclared and standardised server queues. If + the queue name starts with "amq." and the passive + option is False, the server MUST raise a connection + exception with reply code 403 (access refused). + + passive: boolean + + do not create queue + + If set, the server will not create the queue. The + client can use this to check whether a queue exists + without modifying the server state. + + RULE: + + If set, and the queue does not already exist, the + server MUST respond with a reply code 404 (not + found) and raise a channel exception. + + durable: boolean + + request a durable queue + + If set when creating a new queue, the queue will be + marked as durable. Durable queues remain active when + a server restarts. Non-durable queues (transient + queues) are purged if/when a server restarts. Note + that durable queues do not necessarily hold persistent + messages, although it does not make sense to send + persistent messages to a transient queue. + + RULE: + + The server MUST recreate the durable queue after a + restart. + + RULE: + + The server MUST support both durable and transient + queues. + + RULE: + + The server MUST ignore the durable field if the + queue already exists. + + exclusive: boolean + + request an exclusive queue + + Exclusive queues may only be consumed from by the + current connection. Setting the 'exclusive' flag + always implies 'auto-delete'. + + RULE: + + The server MUST support both exclusive (private) + and non-exclusive (shared) queues. + + RULE: + + The server MUST raise a channel exception if + 'exclusive' is specified and the queue already + exists and is owned by a different connection. + + auto_delete: boolean + + auto-delete queue when unused + + If set, the queue is deleted when all consumers have + finished using it. Last consumer can be cancelled + either explicitly or because its channel is closed. If + there was no consumer ever on the queue, it won't be + deleted. + + RULE: + + The server SHOULD allow for a reasonable delay + between the point when it determines that a queue + is not being used (or no longer used), and the + point when it deletes the queue. At the least it + must allow a client to create a queue and then + create a consumer to read from it, with a small + but non-zero delay between these two actions. The + server should equally allow for clients that may + be disconnected prematurely, and wish to re- + consume from the same queue without losing + messages. We would recommend a configurable + timeout, with a suitable default value being one + minute. + + RULE: + + The server MUST ignore the auto-delete field if + the queue already exists. + + nowait: boolean + + do not send a reply method + + If set, the server will not respond to the method. The + client should not wait for a reply method. If the + server could not complete the method it will raise a + channel or connection exception. + + arguments: table + + arguments for declaration + + A set of arguments for the declaration. The syntax and + semantics of these arguments depends on the server + implementation. This field is ignored if passive is + True. + + Returns a tuple containing 3 items: + the name of the queue (essential for automatically-named queues) + message count + consumer count + + """ + arguments = {} if arguments is None else arguments + args = AMQPWriter() + args.write_short(0) + args.write_shortstr(queue) + args.write_bit(passive) + args.write_bit(durable) + args.write_bit(exclusive) + args.write_bit(auto_delete) + args.write_bit(nowait) + args.write_table(arguments) + self._send_method((50, 10), args) + + if not nowait: + return self.wait(allowed_methods=[ + (50, 11), # Channel.queue_declare_ok + ]) + + def _queue_declare_ok(self, args): + """Confirms a queue definition + + This method confirms a Declare method and confirms the name of + the queue, essential for automatically-named queues. + + PARAMETERS: + queue: shortstr + + Reports the name of the queue. If the server generated + a queue name, this field contains that name. + + message_count: long + + number of messages in queue + + Reports the number of messages in the queue, which + will be zero for newly-created queues. + + consumer_count: long + + number of consumers + + Reports the number of active consumers for the queue. + Note that consumers can suspend activity + (Channel.Flow) in which case they do not appear in + this count. + + """ + return queue_declare_ok_t( + args.read_shortstr(), + args.read_long(), + args.read_long(), + ) + + def queue_delete(self, queue='', + if_unused=False, if_empty=False, nowait=False): + """Delete a queue + + This method deletes a queue. When a queue is deleted any + pending messages are sent to a dead-letter queue if this is + defined in the server configuration, and all consumers on the + queue are cancelled. + + RULE: + + The server SHOULD use a dead-letter queue to hold messages + that were pending on a deleted queue, and MAY provide + facilities for a system administrator to move these + messages back to an active queue. + + PARAMETERS: + queue: shortstr + + Specifies the name of the queue to delete. If the + queue name is empty, refers to the current queue for + the channel, which is the last declared queue. + + RULE: + + If the client did not previously declare a queue, + and the queue name in this method is empty, the + server MUST raise a connection exception with + reply code 530 (not allowed). + + RULE: + + The queue must exist. Attempting to delete a non- + existing queue causes a channel exception. + + if_unused: boolean + + delete only if unused + + If set, the server will only delete the queue if it + has no consumers. If the queue has consumers the + server does does not delete it but raises a channel + exception instead. + + RULE: + + The server MUST respect the if-unused flag when + deleting a queue. + + if_empty: boolean + + delete only if empty + + If set, the server will only delete the queue if it + has no messages. If the queue is not empty the server + raises a channel exception. + + nowait: boolean + + do not send a reply method + + If set, the server will not respond to the method. The + client should not wait for a reply method. If the + server could not complete the method it will raise a + channel or connection exception. + + """ + args = AMQPWriter() + args.write_short(0) + args.write_shortstr(queue) + args.write_bit(if_unused) + args.write_bit(if_empty) + args.write_bit(nowait) + self._send_method((50, 40), args) + + if not nowait: + return self.wait(allowed_methods=[ + (50, 41), # Channel.queue_delete_ok + ]) + + def _queue_delete_ok(self, args): + """Confirm deletion of a queue + + This method confirms the deletion of a queue. + + PARAMETERS: + message_count: long + + number of messages purged + + Reports the number of messages purged. + + """ + return args.read_long() + + def queue_purge(self, queue='', nowait=False): + """Purge a queue + + This method removes all messages from a queue. It does not + cancel consumers. Purged messages are deleted without any + formal "undo" mechanism. + + RULE: + + A call to purge MUST result in an empty queue. + + RULE: + + On transacted channels the server MUST not purge messages + that have already been sent to a client but not yet + acknowledged. + + RULE: + + The server MAY implement a purge queue or log that allows + system administrators to recover accidentally-purged + messages. The server SHOULD NOT keep purged messages in + the same storage spaces as the live messages since the + volumes of purged messages may get very large. + + PARAMETERS: + queue: shortstr + + Specifies the name of the queue to purge. If the + queue name is empty, refers to the current queue for + the channel, which is the last declared queue. + + RULE: + + If the client did not previously declare a queue, + and the queue name in this method is empty, the + server MUST raise a connection exception with + reply code 530 (not allowed). + + RULE: + + The queue must exist. Attempting to purge a non- + existing queue causes a channel exception. + + nowait: boolean + + do not send a reply method + + If set, the server will not respond to the method. The + client should not wait for a reply method. If the + server could not complete the method it will raise a + channel or connection exception. + + if nowait is False, returns a message_count + + """ + args = AMQPWriter() + args.write_short(0) + args.write_shortstr(queue) + args.write_bit(nowait) + self._send_method((50, 30), args) + + if not nowait: + return self.wait(allowed_methods=[ + (50, 31), # Channel.queue_purge_ok + ]) + + def _queue_purge_ok(self, args): + """Confirms a queue purge + + This method confirms the purge of a queue. + + PARAMETERS: + message_count: long + + number of messages purged + + Reports the number of messages purged. + + """ + return args.read_long() + + ############# + # + # Basic + # + # + # work with basic content + # + # The Basic class provides methods that support an industry- + # standard messaging model. + # + # GRAMMAR:: + # + # basic = C:QOS S:QOS-OK + # / C:CONSUME S:CONSUME-OK + # / C:CANCEL S:CANCEL-OK + # / C:PUBLISH content + # / S:RETURN content + # / S:DELIVER content + # / C:GET ( S:GET-OK content / S:GET-EMPTY ) + # / C:ACK + # / C:REJECT + # + # RULE: + # + # The server SHOULD respect the persistent property of basic + # messages and SHOULD make a best-effort to hold persistent + # basic messages on a reliable storage mechanism. + # + # RULE: + # + # The server MUST NOT discard a persistent basic message in + # case of a queue overflow. The server MAY use the + # Channel.Flow method to slow or stop a basic message + # publisher when necessary. + # + # RULE: + # + # The server MAY overflow non-persistent basic messages to + # persistent storage and MAY discard or dead-letter non- + # persistent basic messages on a priority basis if the queue + # size exceeds some configured limit. + # + # RULE: + # + # The server MUST implement at least 2 priority levels for + # basic messages, where priorities 0-4 and 5-9 are treated as + # two distinct levels. The server MAY implement up to 10 + # priority levels. + # + # RULE: + # + # The server MUST deliver messages of the same priority in + # order irrespective of their individual persistence. + # + # RULE: + # + # The server MUST support both automatic and explicit + # acknowledgements on Basic content. + # + + def basic_ack(self, delivery_tag, multiple=False): + """Acknowledge one or more messages + + This method acknowledges one or more messages delivered via + the Deliver or Get-Ok methods. The client can ask to confirm + a single message or a set of messages up to and including a + specific message. + + PARAMETERS: + delivery_tag: longlong + + server-assigned delivery tag + + The server-assigned and channel-specific delivery tag + + RULE: + + The delivery tag is valid only within the channel + from which the message was received. I.e. a client + MUST NOT receive a message on one channel and then + acknowledge it on another. + + RULE: + + The server MUST NOT use a zero value for delivery + tags. Zero is reserved for client use, meaning "all + messages so far received". + + multiple: boolean + + acknowledge multiple messages + + If set to True, the delivery tag is treated as "up to + and including", so that the client can acknowledge + multiple messages with a single method. If set to + False, the delivery tag refers to a single message. + If the multiple field is True, and the delivery tag + is zero, tells the server to acknowledge all + outstanding mesages. + + RULE: + + The server MUST validate that a non-zero delivery- + tag refers to an delivered message, and raise a + channel exception if this is not the case. + + """ + args = AMQPWriter() + args.write_longlong(delivery_tag) + args.write_bit(multiple) + self._send_method((60, 80), args) + + def basic_cancel(self, consumer_tag, nowait=False): + """End a queue consumer + + This method cancels a consumer. This does not affect already + delivered messages, but it does mean the server will not send + any more messages for that consumer. The client may receive + an abitrary number of messages in between sending the cancel + method and receiving the cancel-ok reply. + + RULE: + + If the queue no longer exists when the client sends a + cancel command, or the consumer has been cancelled for + other reasons, this command has no effect. + + PARAMETERS: + consumer_tag: shortstr + + consumer tag + + Identifier for the consumer, valid within the current + connection. + + RULE: + + The consumer tag is valid only within the channel + from which the consumer was created. I.e. a client + MUST NOT create a consumer in one channel and then + use it in another. + + nowait: boolean + + do not send a reply method + + If set, the server will not respond to the method. The + client should not wait for a reply method. If the + server could not complete the method it will raise a + channel or connection exception. + + """ + if self.connection is not None: + self.no_ack_consumers.discard(consumer_tag) + args = AMQPWriter() + args.write_shortstr(consumer_tag) + args.write_bit(nowait) + self._send_method((60, 30), args) + return self.wait(allowed_methods=[ + (60, 31), # Channel.basic_cancel_ok + ]) + + def _basic_cancel_notify(self, args): + """Consumer cancelled by server. + + Most likely the queue was deleted. + + """ + consumer_tag = args.read_shortstr() + callback = self._on_cancel(consumer_tag) + if callback: + callback(consumer_tag) + else: + raise ConsumerCancelled(consumer_tag, (60, 30)) + + def _basic_cancel_ok(self, args): + """Confirm a cancelled consumer + + This method confirms that the cancellation was completed. + + PARAMETERS: + consumer_tag: shortstr + + consumer tag + + Identifier for the consumer, valid within the current + connection. + + RULE: + + The consumer tag is valid only within the channel + from which the consumer was created. I.e. a client + MUST NOT create a consumer in one channel and then + use it in another. + + """ + consumer_tag = args.read_shortstr() + self._on_cancel(consumer_tag) + + def _on_cancel(self, consumer_tag): + self.callbacks.pop(consumer_tag, None) + return self.cancel_callbacks.pop(consumer_tag, None) + + def basic_consume(self, queue='', consumer_tag='', no_local=False, + no_ack=False, exclusive=False, nowait=False, + callback=None, arguments=None, on_cancel=None): + """Start a queue consumer + + This method asks the server to start a "consumer", which is a + transient request for messages from a specific queue. + Consumers last as long as the channel they were created on, or + until the client cancels them. + + RULE: + + The server SHOULD support at least 16 consumers per queue, + unless the queue was declared as private, and ideally, + impose no limit except as defined by available resources. + + PARAMETERS: + queue: shortstr + + Specifies the name of the queue to consume from. If + the queue name is null, refers to the current queue + for the channel, which is the last declared queue. + + RULE: + + If the client did not previously declare a queue, + and the queue name in this method is empty, the + server MUST raise a connection exception with + reply code 530 (not allowed). + + consumer_tag: shortstr + + Specifies the identifier for the consumer. The + consumer tag is local to a connection, so two clients + can use the same consumer tags. If this field is empty + the server will generate a unique tag. + + RULE: + + The tag MUST NOT refer to an existing consumer. If + the client attempts to create two consumers with + the same non-empty tag the server MUST raise a + connection exception with reply code 530 (not + allowed). + + no_local: boolean + + do not deliver own messages + + If the no-local field is set the server will not send + messages to the client that published them. + + no_ack: boolean + + no acknowledgement needed + + If this field is set the server does not expect + acknowledgments for messages. That is, when a message + is delivered to the client the server automatically and + silently acknowledges it on behalf of the client. This + functionality increases performance but at the cost of + reliability. Messages can get lost if a client dies + before it can deliver them to the application. + + exclusive: boolean + + request exclusive access + + Request exclusive consumer access, meaning only this + consumer can access the queue. + + RULE: + + If the server cannot grant exclusive access to the + queue when asked, - because there are other + consumers active - it MUST raise a channel + exception with return code 403 (access refused). + + nowait: boolean + + do not send a reply method + + If set, the server will not respond to the method. The + client should not wait for a reply method. If the + server could not complete the method it will raise a + channel or connection exception. + + callback: Python callable + + function/method called with each delivered message + + For each message delivered by the broker, the + callable will be called with a Message object + as the single argument. If no callable is specified, + messages are quietly discarded, no_ack should probably + be set to True in that case. + + """ + args = AMQPWriter() + args.write_short(0) + args.write_shortstr(queue) + args.write_shortstr(consumer_tag) + args.write_bit(no_local) + args.write_bit(no_ack) + args.write_bit(exclusive) + args.write_bit(nowait) + args.write_table(arguments or {}) + self._send_method((60, 20), args) + + if not nowait: + consumer_tag = self.wait(allowed_methods=[ + (60, 21), # Channel.basic_consume_ok + ]) + + self.callbacks[consumer_tag] = callback + + if on_cancel: + self.cancel_callbacks[consumer_tag] = on_cancel + if no_ack: + self.no_ack_consumers.add(consumer_tag) + + return consumer_tag + + def _basic_consume_ok(self, args): + """Confirm a new consumer + + The server provides the client with a consumer tag, which is + used by the client for methods called on the consumer at a + later stage. + + PARAMETERS: + consumer_tag: shortstr + + Holds the consumer tag specified by the client or + provided by the server. + + """ + return args.read_shortstr() + + def _basic_deliver(self, args, msg): + """Notify the client of a consumer message + + This method delivers a message to the client, via a consumer. + In the asynchronous message delivery model, the client starts + a consumer using the Consume method, then the server responds + with Deliver methods as and when messages arrive for that + consumer. + + RULE: + + The server SHOULD track the number of times a message has + been delivered to clients and when a message is + redelivered a certain number of times - e.g. 5 times - + without being acknowledged, the server SHOULD consider the + message to be unprocessable (possibly causing client + applications to abort), and move the message to a dead + letter queue. + + PARAMETERS: + consumer_tag: shortstr + + consumer tag + + Identifier for the consumer, valid within the current + connection. + + RULE: + + The consumer tag is valid only within the channel + from which the consumer was created. I.e. a client + MUST NOT create a consumer in one channel and then + use it in another. + + delivery_tag: longlong + + server-assigned delivery tag + + The server-assigned and channel-specific delivery tag + + RULE: + + The delivery tag is valid only within the channel + from which the message was received. I.e. a client + MUST NOT receive a message on one channel and then + acknowledge it on another. + + RULE: + + The server MUST NOT use a zero value for delivery + tags. Zero is reserved for client use, meaning "all + messages so far received". + + redelivered: boolean + + message is being redelivered + + This indicates that the message has been previously + delivered to this or another client. + + exchange: shortstr + + Specifies the name of the exchange that the message + was originally published to. + + routing_key: shortstr + + Message routing key + + Specifies the routing key name specified when the + message was published. + + """ + consumer_tag = args.read_shortstr() + delivery_tag = args.read_longlong() + redelivered = args.read_bit() + exchange = args.read_shortstr() + routing_key = args.read_shortstr() + + msg.channel = self + msg.delivery_info = { + 'consumer_tag': consumer_tag, + 'delivery_tag': delivery_tag, + 'redelivered': redelivered, + 'exchange': exchange, + 'routing_key': routing_key, + } + + try: + fun = self.callbacks[consumer_tag] + except KeyError: + pass + else: + fun(msg) + + def basic_get(self, queue='', no_ack=False): + """Direct access to a queue + + This method provides a direct access to the messages in a + queue using a synchronous dialogue that is designed for + specific types of application where synchronous functionality + is more important than performance. + + PARAMETERS: + queue: shortstr + + Specifies the name of the queue to consume from. If + the queue name is null, refers to the current queue + for the channel, which is the last declared queue. + + RULE: + + If the client did not previously declare a queue, + and the queue name in this method is empty, the + server MUST raise a connection exception with + reply code 530 (not allowed). + + no_ack: boolean + + no acknowledgement needed + + If this field is set the server does not expect + acknowledgments for messages. That is, when a message + is delivered to the client the server automatically and + silently acknowledges it on behalf of the client. This + functionality increases performance but at the cost of + reliability. Messages can get lost if a client dies + before it can deliver them to the application. + + Non-blocking, returns a message object, or None. + + """ + args = AMQPWriter() + args.write_short(0) + args.write_shortstr(queue) + args.write_bit(no_ack) + self._send_method((60, 70), args) + return self.wait(allowed_methods=[ + (60, 71), # Channel.basic_get_ok + (60, 72), # Channel.basic_get_empty + ]) + + def _basic_get_empty(self, args): + """Indicate no messages available + + This method tells the client that the queue has no messages + available for the client. + + PARAMETERS: + cluster_id: shortstr + + Cluster id + + For use by cluster applications, should not be used by + client applications. + + """ + cluster_id = args.read_shortstr() # noqa + + def _basic_get_ok(self, args, msg): + """Provide client with a message + + This method delivers a message to the client following a get + method. A message delivered by 'get-ok' must be acknowledged + unless the no-ack option was set in the get method. + + PARAMETERS: + delivery_tag: longlong + + server-assigned delivery tag + + The server-assigned and channel-specific delivery tag + + RULE: + + The delivery tag is valid only within the channel + from which the message was received. I.e. a client + MUST NOT receive a message on one channel and then + acknowledge it on another. + + RULE: + + The server MUST NOT use a zero value for delivery + tags. Zero is reserved for client use, meaning "all + messages so far received". + + redelivered: boolean + + message is being redelivered + + This indicates that the message has been previously + delivered to this or another client. + + exchange: shortstr + + Specifies the name of the exchange that the message + was originally published to. If empty, the message + was published to the default exchange. + + routing_key: shortstr + + Message routing key + + Specifies the routing key name specified when the + message was published. + + message_count: long + + number of messages pending + + This field reports the number of messages pending on + the queue, excluding the message being delivered. + Note that this figure is indicative, not reliable, and + can change arbitrarily as messages are added to the + queue and removed by other clients. + + """ + delivery_tag = args.read_longlong() + redelivered = args.read_bit() + exchange = args.read_shortstr() + routing_key = args.read_shortstr() + message_count = args.read_long() + + msg.channel = self + msg.delivery_info = { + 'delivery_tag': delivery_tag, + 'redelivered': redelivered, + 'exchange': exchange, + 'routing_key': routing_key, + 'message_count': message_count + } + return msg + + def _basic_publish(self, msg, exchange='', routing_key='', + mandatory=False, immediate=False): + """Publish a message + + This method publishes a message to a specific exchange. The + message will be routed to queues as defined by the exchange + configuration and distributed to any active consumers when the + transaction, if any, is committed. + + PARAMETERS: + exchange: shortstr + + Specifies the name of the exchange to publish to. The + exchange name can be empty, meaning the default + exchange. If the exchange name is specified, and that + exchange does not exist, the server will raise a + channel exception. + + RULE: + + The server MUST accept a blank exchange name to + mean the default exchange. + + RULE: + + The exchange MAY refuse basic content in which + case it MUST raise a channel exception with reply + code 540 (not implemented). + + routing_key: shortstr + + Message routing key + + Specifies the routing key for the message. The + routing key is used for routing messages depending on + the exchange configuration. + + mandatory: boolean + + indicate mandatory routing + + This flag tells the server how to react if the message + cannot be routed to a queue. If this flag is True, the + server will return an unroutable message with a Return + method. If this flag is False, the server silently + drops the message. + + RULE: + + The server SHOULD implement the mandatory flag. + + immediate: boolean + + request immediate delivery + + This flag tells the server how to react if the message + cannot be routed to a queue consumer immediately. If + this flag is set, the server will return an + undeliverable message with a Return method. If this + flag is zero, the server will queue the message, but + with no guarantee that it will ever be consumed. + + RULE: + + The server SHOULD implement the immediate flag. + + """ + args = AMQPWriter() + args.write_short(0) + args.write_shortstr(exchange) + args.write_shortstr(routing_key) + args.write_bit(mandatory) + args.write_bit(immediate) + + self._send_method((60, 40), args, msg) + basic_publish = _basic_publish + + def basic_publish_confirm(self, *args, **kwargs): + if not self._confirm_selected: + self._confirm_selected = True + self.confirm_select() + ret = self._basic_publish(*args, **kwargs) + self.wait([(60, 80)]) + return ret + + def basic_qos(self, prefetch_size, prefetch_count, a_global): + """Specify quality of service + + This method requests a specific quality of service. The QoS + can be specified for the current channel or for all channels + on the connection. The particular properties and semantics of + a qos method always depend on the content class semantics. + Though the qos method could in principle apply to both peers, + it is currently meaningful only for the server. + + PARAMETERS: + prefetch_size: long + + prefetch window in octets + + The client can request that messages be sent in + advance so that when the client finishes processing a + message, the following message is already held + locally, rather than needing to be sent down the + channel. Prefetching gives a performance improvement. + This field specifies the prefetch window size in + octets. The server will send a message in advance if + it is equal to or smaller in size than the available + prefetch size (and also falls into other prefetch + limits). May be set to zero, meaning "no specific + limit", although other prefetch limits may still + apply. The prefetch-size is ignored if the no-ack + option is set. + + RULE: + + The server MUST ignore this setting when the + client is not processing any messages - i.e. the + prefetch size does not limit the transfer of + single messages to a client, only the sending in + advance of more messages while the client still + has one or more unacknowledged messages. + + prefetch_count: short + + prefetch window in messages + + Specifies a prefetch window in terms of whole + messages. This field may be used in combination with + the prefetch-size field; a message will only be sent + in advance if both prefetch windows (and those at the + channel and connection level) allow it. The prefetch- + count is ignored if the no-ack option is set. + + RULE: + + The server MAY send less data in advance than + allowed by the client's specified prefetch windows + but it MUST NOT send more. + + a_global: boolean + + apply to entire connection + + By default the QoS settings apply to the current + channel only. If this field is set, they are applied + to the entire connection. + + """ + args = AMQPWriter() + args.write_long(prefetch_size) + args.write_short(prefetch_count) + args.write_bit(a_global) + self._send_method((60, 10), args) + return self.wait(allowed_methods=[ + (60, 11), # Channel.basic_qos_ok + ]) + + def _basic_qos_ok(self, args): + """Confirm the requested qos + + This method tells the client that the requested QoS levels + could be handled by the server. The requested QoS applies to + all active consumers until a new QoS is defined. + + """ + pass + + def basic_recover(self, requeue=False): + """Redeliver unacknowledged messages + + This method asks the broker to redeliver all unacknowledged + messages on a specified channel. Zero or more messages may be + redelivered. This method is only allowed on non-transacted + channels. + + RULE: + + The server MUST set the redelivered flag on all messages + that are resent. + + RULE: + + The server MUST raise a channel exception if this is + called on a transacted channel. + + PARAMETERS: + requeue: boolean + + requeue the message + + If this field is False, the message will be redelivered + to the original recipient. If this field is True, the + server will attempt to requeue the message, + potentially then delivering it to an alternative + subscriber. + + """ + args = AMQPWriter() + args.write_bit(requeue) + self._send_method((60, 110), args) + + def basic_recover_async(self, requeue=False): + args = AMQPWriter() + args.write_bit(requeue) + self._send_method((60, 100), args) + + def _basic_recover_ok(self, args): + """In 0-9-1 the deprecated recover solicits a response.""" + pass + + def basic_reject(self, delivery_tag, requeue): + """Reject an incoming message + + This method allows a client to reject a message. It can be + used to interrupt and cancel large incoming messages, or + return untreatable messages to their original queue. + + RULE: + + The server SHOULD be capable of accepting and process the + Reject method while sending message content with a Deliver + or Get-Ok method. I.e. the server should read and process + incoming methods while sending output frames. To cancel a + partially-send content, the server sends a content body + frame of size 1 (i.e. with no data except the frame-end + octet). + + RULE: + + The server SHOULD interpret this method as meaning that + the client is unable to process the message at this time. + + RULE: + + A client MUST NOT use this method as a means of selecting + messages to process. A rejected message MAY be discarded + or dead-lettered, not necessarily passed to another + client. + + PARAMETERS: + delivery_tag: longlong + + server-assigned delivery tag + + The server-assigned and channel-specific delivery tag + + RULE: + + The delivery tag is valid only within the channel + from which the message was received. I.e. a client + MUST NOT receive a message on one channel and then + acknowledge it on another. + + RULE: + + The server MUST NOT use a zero value for delivery + tags. Zero is reserved for client use, meaning "all + messages so far received". + + requeue: boolean + + requeue the message + + If this field is False, the message will be discarded. + If this field is True, the server will attempt to + requeue the message. + + RULE: + + The server MUST NOT deliver the message to the + same client within the context of the current + channel. The recommended strategy is to attempt + to deliver the message to an alternative consumer, + and if that is not possible, to move the message + to a dead-letter queue. The server MAY use more + sophisticated tracking to hold the message on the + queue and redeliver it to the same client at a + later stage. + + """ + args = AMQPWriter() + args.write_longlong(delivery_tag) + args.write_bit(requeue) + self._send_method((60, 90), args) + + def _basic_return(self, args, msg): + """Return a failed message + + This method returns an undeliverable message that was + published with the "immediate" flag set, or an unroutable + message published with the "mandatory" flag set. The reply + code and text provide information about the reason that the + message was undeliverable. + + PARAMETERS: + reply_code: short + + The reply code. The AMQ reply codes are defined in AMQ + RFC 011. + + reply_text: shortstr + + The localised reply text. This text can be logged as an + aid to resolving issues. + + exchange: shortstr + + Specifies the name of the exchange that the message + was originally published to. + + routing_key: shortstr + + Message routing key + + Specifies the routing key name specified when the + message was published. + + """ + self.returned_messages.put(basic_return_t( + args.read_short(), + args.read_shortstr(), + args.read_shortstr(), + args.read_shortstr(), + msg, + )) + + ############# + # + # Tx + # + # + # work with standard transactions + # + # Standard transactions provide so-called "1.5 phase commit". We + # can ensure that work is never lost, but there is a chance of + # confirmations being lost, so that messages may be resent. + # Applications that use standard transactions must be able to + # detect and ignore duplicate messages. + # + # GRAMMAR:: + # + # tx = C:SELECT S:SELECT-OK + # / C:COMMIT S:COMMIT-OK + # / C:ROLLBACK S:ROLLBACK-OK + # + # RULE: + # + # An client using standard transactions SHOULD be able to + # track all messages received within a reasonable period, and + # thus detect and reject duplicates of the same message. It + # SHOULD NOT pass these to the application layer. + # + # + + def tx_commit(self): + """Commit the current transaction + + This method commits all messages published and acknowledged in + the current transaction. A new transaction starts immediately + after a commit. + + """ + self._send_method((90, 20)) + return self.wait(allowed_methods=[ + (90, 21), # Channel.tx_commit_ok + ]) + + def _tx_commit_ok(self, args): + """Confirm a successful commit + + This method confirms to the client that the commit succeeded. + Note that if a commit fails, the server raises a channel + exception. + + """ + pass + + def tx_rollback(self): + """Abandon the current transaction + + This method abandons all messages published and acknowledged + in the current transaction. A new transaction starts + immediately after a rollback. + + """ + self._send_method((90, 30)) + return self.wait(allowed_methods=[ + (90, 31), # Channel.tx_rollback_ok + ]) + + def _tx_rollback_ok(self, args): + """Confirm a successful rollback + + This method confirms to the client that the rollback + succeeded. Note that if an rollback fails, the server raises a + channel exception. + + """ + pass + + def tx_select(self): + """Select standard transaction mode + + This method sets the channel to use standard transactions. + The client must use this method at least once on a channel + before using the Commit or Rollback methods. + + """ + self._send_method((90, 10)) + return self.wait(allowed_methods=[ + (90, 11), # Channel.tx_select_ok + ]) + + def _tx_select_ok(self, args): + """Confirm transaction mode + + This method confirms to the client that the channel was + successfully set to use standard transactions. + + """ + pass + + def confirm_select(self, nowait=False): + """Enables publisher confirms for this channel (an RabbitMQ + extension). + + Can now be used if the channel is in transactional mode. + + :param nowait: + If set, the server will not respond to the method. + The client should not wait for a reply method. If the + server could not complete the method it will raise a channel + or connection exception. + + """ + args = AMQPWriter() + args.write_bit(nowait) + + self._send_method((85, 10), args) + if not nowait: + self.wait(allowed_methods=[ + (85, 11), # Confirm.select_ok + ]) + + def _confirm_select_ok(self, args): + """With this method the broker confirms to the client that + the channel is now using publisher confirms.""" + pass + + def _basic_ack_recv(self, args): + delivery_tag = args.read_longlong() + multiple = args.read_bit() + for callback in self.events['basic_ack']: + callback(delivery_tag, multiple) + + _METHOD_MAP = { + (20, 11): _open_ok, + (20, 20): _flow, + (20, 21): _flow_ok, + (20, 40): _close, + (20, 41): _close_ok, + (40, 11): _exchange_declare_ok, + (40, 21): _exchange_delete_ok, + (40, 31): _exchange_bind_ok, + (40, 51): _exchange_unbind_ok, + (50, 11): _queue_declare_ok, + (50, 21): _queue_bind_ok, + (50, 31): _queue_purge_ok, + (50, 41): _queue_delete_ok, + (50, 51): _queue_unbind_ok, + (60, 11): _basic_qos_ok, + (60, 21): _basic_consume_ok, + (60, 30): _basic_cancel_notify, + (60, 31): _basic_cancel_ok, + (60, 50): _basic_return, + (60, 60): _basic_deliver, + (60, 71): _basic_get_ok, + (60, 72): _basic_get_empty, + (60, 80): _basic_ack_recv, + (60, 111): _basic_recover_ok, + (85, 11): _confirm_select_ok, + (90, 11): _tx_select_ok, + (90, 21): _tx_commit_ok, + (90, 31): _tx_rollback_ok, + } + + _IMMEDIATE_METHODS = [ + (60, 50), # basic_return + ] diff --git a/amqp/connection.py b/amqp/connection.py new file mode 100644 index 0000000..c93d91f --- /dev/null +++ b/amqp/connection.py @@ -0,0 +1,1004 @@ +"""AMQP Connections""" +# Copyright (C) 2007-2008 Barry Pederson +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 +from __future__ import absolute_import + +import logging +import socket + +from array import array +try: + from ssl import SSLError +except ImportError: + class SSLError(Exception): # noqa + pass + +from . import __version__ +from .abstract_channel import AbstractChannel +from .channel import Channel +from .exceptions import ( + AMQPNotImplementedError, ChannelError, ResourceError, + ConnectionForced, ConnectionError, error_for_code, + RecoverableConnectionError, RecoverableChannelError, +) +from .five import items, range, values, monotonic +from .method_framing import MethodReader, MethodWriter +from .serialization import AMQPWriter +from .transport import create_transport + +HAS_MSG_PEEK = hasattr(socket, 'MSG_PEEK') + +START_DEBUG_FMT = """ +Start from server, version: %d.%d, properties: %s, mechanisms: %s, locales: %s +""".strip() + +__all__ = ['Connection'] + +# +# Client property info that gets sent to the server on connection startup +# +LIBRARY_PROPERTIES = { + 'product': 'py-amqp', + 'product_version': __version__, + 'capabilities': {}, +} + +AMQP_LOGGER = logging.getLogger('amqp') + + +class Connection(AbstractChannel): + """The connection class provides methods for a client to establish a + network connection to a server, and for both peers to operate the + connection thereafter. + + GRAMMAR:: + + connection = open-connection *use-connection close-connection + open-connection = C:protocol-header + S:START C:START-OK + *challenge + S:TUNE C:TUNE-OK + C:OPEN S:OPEN-OK + challenge = S:SECURE C:SECURE-OK + use-connection = *channel + close-connection = C:CLOSE S:CLOSE-OK + / S:CLOSE C:CLOSE-OK + + """ + Channel = Channel + + #: Final heartbeat interval value (in float seconds) after negotiation + heartbeat = None + + #: Original heartbeat interval value proposed by client. + client_heartbeat = None + + #: Original heartbeat interval proposed by server. + server_heartbeat = None + + #: Time of last heartbeat sent (in monotonic time, if available). + last_heartbeat_sent = 0 + + #: Time of last heartbeat received (in monotonic time, if available). + last_heartbeat_received = 0 + + #: Number of bytes sent to socket at the last heartbeat check. + prev_sent = None + + #: Number of bytes received from socket at the last heartbeat check. + prev_recv = None + + def __init__(self, host='localhost', userid='guest', password='guest', + login_method='AMQPLAIN', login_response=None, + virtual_host='/', locale='en_US', client_properties=None, + ssl=False, connect_timeout=None, channel_max=None, + frame_max=None, heartbeat=0, on_blocked=None, + on_unblocked=None, confirm_publish=False, **kwargs): + """Create a connection to the specified host, which should be + a 'host[:port]', such as 'localhost', or '1.2.3.4:5672' + (defaults to 'localhost', if a port is not specified then + 5672 is used) + + If login_response is not specified, one is built up for you from + userid and password if they are present. + + The 'ssl' parameter may be simply True/False, or for Python >= 2.6 + a dictionary of options to pass to ssl.wrap_socket() such as + requiring certain certificates. + + """ + channel_max = channel_max or 65535 + frame_max = frame_max or 131072 + if (login_response is None) \ + and (userid is not None) \ + and (password is not None): + login_response = AMQPWriter() + login_response.write_table({'LOGIN': userid, 'PASSWORD': password}) + login_response = login_response.getvalue()[4:] # Skip the length + # at the beginning + + d = dict(LIBRARY_PROPERTIES, **client_properties or {}) + self._method_override = {(60, 50): self._dispatch_basic_return} + + self.channels = {} + # The connection object itself is treated as channel 0 + super(Connection, self).__init__(self, 0) + + self.transport = None + + # Properties set in the Tune method + self.channel_max = channel_max + self.frame_max = frame_max + self.client_heartbeat = heartbeat + + self.confirm_publish = confirm_publish + + # Callbacks + self.on_blocked = on_blocked + self.on_unblocked = on_unblocked + + self._avail_channel_ids = array('H', range(self.channel_max, 0, -1)) + + # Properties set in the Start method + self.version_major = 0 + self.version_minor = 0 + self.server_properties = {} + self.mechanisms = [] + self.locales = [] + + # Let the transport.py module setup the actual + # socket connection to the broker. + # + self.transport = create_transport(host, connect_timeout, ssl) + + self.method_reader = MethodReader(self.transport) + self.method_writer = MethodWriter(self.transport, self.frame_max) + + self.wait(allowed_methods=[ + (10, 10), # start + ]) + + self._x_start_ok(d, login_method, login_response, locale) + + self._wait_tune_ok = True + while self._wait_tune_ok: + self.wait(allowed_methods=[ + (10, 20), # secure + (10, 30), # tune + ]) + + return self._x_open(virtual_host) + + @property + def connected(self): + return self.transport and self.transport.connected + + def _do_close(self): + try: + self.transport.close() + + temp_list = [x for x in values(self.channels) if x is not self] + for ch in temp_list: + ch._do_close() + except socket.error: + pass # connection already closed on the other end + finally: + self.transport = self.connection = self.channels = None + + def _get_free_channel_id(self): + try: + return self._avail_channel_ids.pop() + except IndexError: + raise ResourceError( + 'No free channel ids, current={0}, channel_max={1}'.format( + len(self.channels), self.channel_max), (20, 10)) + + def _claim_channel_id(self, channel_id): + try: + return self._avail_channel_ids.remove(channel_id) + except ValueError: + raise ConnectionError( + 'Channel %r already open' % (channel_id, )) + + def _wait_method(self, channel_id, allowed_methods): + """Wait for a method from the server destined for + a particular channel.""" + # + # Check the channel's deferred methods + # + method_queue = self.channels[channel_id].method_queue + + for queued_method in method_queue: + method_sig = queued_method[0] + if (allowed_methods is None) \ + or (method_sig in allowed_methods) \ + or (method_sig == (20, 40)): + method_queue.remove(queued_method) + return queued_method + + # + # Nothing queued, need to wait for a method from the peer + # + while 1: + channel, method_sig, args, content = \ + self.method_reader.read_method() + + if channel == channel_id and ( + allowed_methods is None or + method_sig in allowed_methods or + method_sig == (20, 40)): + return method_sig, args, content + + # + # Certain methods like basic_return should be dispatched + # immediately rather than being queued, even if they're not + # one of the 'allowed_methods' we're looking for. + # + if channel and method_sig in self.Channel._IMMEDIATE_METHODS: + self.channels[channel].dispatch_method( + method_sig, args, content, + ) + continue + + # + # Not the channel and/or method we were looking for. Queue + # this method for later + # + self.channels[channel].method_queue.append( + (method_sig, args, content), + ) + + # + # If we just queued up a method for channel 0 (the Connection + # itself) it's probably a close method in reaction to some + # error, so deal with it right away. + # + if not channel: + self.wait() + + def channel(self, channel_id=None): + """Fetch a Channel object identified by the numeric channel_id, or + create that object if it doesn't already exist.""" + try: + return self.channels[channel_id] + except KeyError: + return self.Channel(self, channel_id) + + def is_alive(self): + if HAS_MSG_PEEK: + sock = self.sock + prev = sock.gettimeout() + sock.settimeout(0.0001) + try: + sock.recv(1, socket.MSG_PEEK) + except socket.timeout: + pass + except socket.error: + return False + finally: + sock.settimeout(prev) + return True + + def drain_events(self, timeout=None): + """Wait for an event on a channel.""" + chanmap = self.channels + chanid, method_sig, args, content = self._wait_multiple( + chanmap, None, timeout=timeout, + ) + + channel = chanmap[chanid] + + if (content and + channel.auto_decode and + hasattr(content, 'content_encoding')): + try: + content.body = content.body.decode(content.content_encoding) + except Exception: + pass + + amqp_method = (self._method_override.get(method_sig) or + channel._METHOD_MAP.get(method_sig, None)) + + if amqp_method is None: + raise AMQPNotImplementedError( + 'Unknown AMQP method {0!r}'.format(method_sig)) + + if content is None: + return amqp_method(channel, args) + else: + return amqp_method(channel, args, content) + + def read_timeout(self, timeout=None): + if timeout is None: + return self.method_reader.read_method() + sock = self.sock + prev = sock.gettimeout() + if prev != timeout: + sock.settimeout(timeout) + try: + try: + return self.method_reader.read_method() + except SSLError as exc: + # http://bugs.python.org/issue10272 + if 'timed out' in str(exc): + raise socket.timeout() + # Non-blocking SSL sockets can throw SSLError + if 'The operation did not complete' in str(exc): + raise socket.timeout() + raise + finally: + if prev != timeout: + sock.settimeout(prev) + + def _wait_multiple(self, channels, allowed_methods, timeout=None): + for channel_id, channel in items(channels): + method_queue = channel.method_queue + for queued_method in method_queue: + method_sig = queued_method[0] + if (allowed_methods is None or + method_sig in allowed_methods or + method_sig == (20, 40)): + method_queue.remove(queued_method) + method_sig, args, content = queued_method + return channel_id, method_sig, args, content + + # Nothing queued, need to wait for a method from the peer + read_timeout = self.read_timeout + wait = self.wait + while 1: + channel, method_sig, args, content = read_timeout(timeout) + + if channel in channels and ( + allowed_methods is None or + method_sig in allowed_methods or + method_sig == (20, 40)): + return channel, method_sig, args, content + + # Not the channel and/or method we were looking for. Queue + # this method for later + channels[channel].method_queue.append((method_sig, args, content)) + + # + # If we just queued up a method for channel 0 (the Connection + # itself) it's probably a close method in reaction to some + # error, so deal with it right away. + # + if channel == 0: + wait() + + def _dispatch_basic_return(self, channel, args, msg): + reply_code = args.read_short() + reply_text = args.read_shortstr() + exchange = args.read_shortstr() + routing_key = args.read_shortstr() + + exc = error_for_code(reply_code, reply_text, (50, 60), ChannelError) + handlers = channel.events.get('basic_return') + if not handlers: + raise exc + for callback in handlers: + callback(exc, exchange, routing_key, msg) + + def close(self, reply_code=0, reply_text='', method_sig=(0, 0)): + """Request a connection close + + This method indicates that the sender wants to close the + connection. This may be due to internal conditions (e.g. a + forced shut-down) or due to an error handling a specific + method, i.e. an exception. When a close is due to an + exception, the sender provides the class and method id of the + method which caused the exception. + + RULE: + + After sending this method any received method except the + Close-OK method MUST be discarded. + + RULE: + + The peer sending this method MAY use a counter or timeout + to detect failure of the other peer to respond correctly + with the Close-OK method. + + RULE: + + When a server receives the Close method from a client it + MUST delete all server-side resources associated with the + client's context. A client CANNOT reconnect to a context + after sending or receiving a Close method. + + PARAMETERS: + reply_code: short + + The reply code. The AMQ reply codes are defined in AMQ + RFC 011. + + reply_text: shortstr + + The localised reply text. This text can be logged as an + aid to resolving issues. + + class_id: short + + failing method class + + When the close is provoked by a method exception, this + is the class of the method. + + method_id: short + + failing method ID + + When the close is provoked by a method exception, this + is the ID of the method. + + """ + if self.transport is None: + # already closed + return + + args = AMQPWriter() + args.write_short(reply_code) + args.write_shortstr(reply_text) + args.write_short(method_sig[0]) # class_id + args.write_short(method_sig[1]) # method_id + self._send_method((10, 50), args) + return self.wait(allowed_methods=[ + (10, 50), # Connection.close + (10, 51), # Connection.close_ok + ]) + + def _close(self, args): + """Request a connection close + + This method indicates that the sender wants to close the + connection. This may be due to internal conditions (e.g. a + forced shut-down) or due to an error handling a specific + method, i.e. an exception. When a close is due to an + exception, the sender provides the class and method id of the + method which caused the exception. + + RULE: + + After sending this method any received method except the + Close-OK method MUST be discarded. + + RULE: + + The peer sending this method MAY use a counter or timeout + to detect failure of the other peer to respond correctly + with the Close-OK method. + + RULE: + + When a server receives the Close method from a client it + MUST delete all server-side resources associated with the + client's context. A client CANNOT reconnect to a context + after sending or receiving a Close method. + + PARAMETERS: + reply_code: short + + The reply code. The AMQ reply codes are defined in AMQ + RFC 011. + + reply_text: shortstr + + The localised reply text. This text can be logged as an + aid to resolving issues. + + class_id: short + + failing method class + + When the close is provoked by a method exception, this + is the class of the method. + + method_id: short + + failing method ID + + When the close is provoked by a method exception, this + is the ID of the method. + + """ + reply_code = args.read_short() + reply_text = args.read_shortstr() + class_id = args.read_short() + method_id = args.read_short() + + self._x_close_ok() + + raise error_for_code(reply_code, reply_text, + (class_id, method_id), ConnectionError) + + def _blocked(self, args): + """RabbitMQ Extension.""" + reason = args.read_shortstr() + if self.on_blocked: + return self.on_blocked(reason) + + def _unblocked(self, *args): + if self.on_unblocked: + return self.on_unblocked() + + def _x_close_ok(self): + """Confirm a connection close + + This method confirms a Connection.Close method and tells the + recipient that it is safe to release resources for the + connection and close the socket. + + RULE: + + A peer that detects a socket closure without having + received a Close-Ok handshake method SHOULD log the error. + + """ + self._send_method((10, 51)) + self._do_close() + + def _close_ok(self, args): + """Confirm a connection close + + This method confirms a Connection.Close method and tells the + recipient that it is safe to release resources for the + connection and close the socket. + + RULE: + + A peer that detects a socket closure without having + received a Close-Ok handshake method SHOULD log the error. + + """ + self._do_close() + + def _x_open(self, virtual_host, capabilities=''): + """Open connection to virtual host + + This method opens a connection to a virtual host, which is a + collection of resources, and acts to separate multiple + application domains within a server. + + RULE: + + The client MUST open the context before doing any work on + the connection. + + PARAMETERS: + virtual_host: shortstr + + virtual host name + + The name of the virtual host to work with. + + RULE: + + If the server supports multiple virtual hosts, it + MUST enforce a full separation of exchanges, + queues, and all associated entities per virtual + host. An application, connected to a specific + virtual host, MUST NOT be able to access resources + of another virtual host. + + RULE: + + The server SHOULD verify that the client has + permission to access the specified virtual host. + + RULE: + + The server MAY configure arbitrary limits per + virtual host, such as the number of each type of + entity that may be used, per connection and/or in + total. + + capabilities: shortstr + + required capabilities + + The client may specify a number of capability names, + delimited by spaces. The server can use this string + to how to process the client's connection request. + + """ + args = AMQPWriter() + args.write_shortstr(virtual_host) + args.write_shortstr(capabilities) + args.write_bit(False) + self._send_method((10, 40), args) + return self.wait(allowed_methods=[ + (10, 41), # Connection.open_ok + ]) + + def _open_ok(self, args): + """Signal that the connection is ready + + This method signals to the client that the connection is ready + for use. + + PARAMETERS: + known_hosts: shortstr (deprecated) + + """ + AMQP_LOGGER.debug('Open OK!') + + def _secure(self, args): + """Security mechanism challenge + + The SASL protocol works by exchanging challenges and responses + until both peers have received sufficient information to + authenticate each other. This method challenges the client to + provide more information. + + PARAMETERS: + challenge: longstr + + security challenge data + + Challenge information, a block of opaque binary data + passed to the security mechanism. + + """ + challenge = args.read_longstr() # noqa + + def _x_secure_ok(self, response): + """Security mechanism response + + This method attempts to authenticate, passing a block of SASL + data for the security mechanism at the server side. + + PARAMETERS: + response: longstr + + security response data + + A block of opaque data passed to the security + mechanism. The contents of this data are defined by + the SASL security mechanism. + + """ + args = AMQPWriter() + args.write_longstr(response) + self._send_method((10, 21), args) + + def _start(self, args): + """Start connection negotiation + + This method starts the connection negotiation process by + telling the client the protocol version that the server + proposes, along with a list of security mechanisms which the + client can use for authentication. + + RULE: + + If the client cannot handle the protocol version suggested + by the server it MUST close the socket connection. + + RULE: + + The server MUST provide a protocol version that is lower + than or equal to that requested by the client in the + protocol header. If the server cannot support the + specified protocol it MUST NOT send this method, but MUST + close the socket connection. + + PARAMETERS: + version_major: octet + + protocol major version + + The protocol major version that the server agrees to + use, which cannot be higher than the client's major + version. + + version_minor: octet + + protocol major version + + The protocol minor version that the server agrees to + use, which cannot be higher than the client's minor + version. + + server_properties: table + + server properties + + mechanisms: longstr + + available security mechanisms + + A list of the security mechanisms that the server + supports, delimited by spaces. Currently ASL supports + these mechanisms: PLAIN. + + locales: longstr + + available message locales + + A list of the message locales that the server + supports, delimited by spaces. The locale defines the + language in which the server will send reply texts. + + RULE: + + All servers MUST support at least the en_US + locale. + + """ + self.version_major = args.read_octet() + self.version_minor = args.read_octet() + self.server_properties = args.read_table() + self.mechanisms = args.read_longstr().split(' ') + self.locales = args.read_longstr().split(' ') + + AMQP_LOGGER.debug( + START_DEBUG_FMT, + self.version_major, self.version_minor, + self.server_properties, self.mechanisms, self.locales, + ) + + def _x_start_ok(self, client_properties, mechanism, response, locale): + """Select security mechanism and locale + + This method selects a SASL security mechanism. ASL uses SASL + (RFC2222) to negotiate authentication and encryption. + + PARAMETERS: + client_properties: table + + client properties + + mechanism: shortstr + + selected security mechanism + + A single security mechanisms selected by the client, + which must be one of those specified by the server. + + RULE: + + The client SHOULD authenticate using the highest- + level security profile it can handle from the list + provided by the server. + + RULE: + + The mechanism field MUST contain one of the + security mechanisms proposed by the server in the + Start method. If it doesn't, the server MUST close + the socket. + + response: longstr + + security response data + + A block of opaque data passed to the security + mechanism. The contents of this data are defined by + the SASL security mechanism. For the PLAIN security + mechanism this is defined as a field table holding two + fields, LOGIN and PASSWORD. + + locale: shortstr + + selected message locale + + A single message local selected by the client, which + must be one of those specified by the server. + + """ + if self.server_capabilities.get('consumer_cancel_notify'): + if 'capabilities' not in client_properties: + client_properties['capabilities'] = {} + client_properties['capabilities']['consumer_cancel_notify'] = True + if self.server_capabilities.get('connection.blocked'): + if 'capabilities' not in client_properties: + client_properties['capabilities'] = {} + client_properties['capabilities']['connection.blocked'] = True + args = AMQPWriter() + args.write_table(client_properties) + args.write_shortstr(mechanism) + args.write_longstr(response) + args.write_shortstr(locale) + self._send_method((10, 11), args) + + def _tune(self, args): + """Propose connection tuning parameters + + This method proposes a set of connection configuration values + to the client. The client can accept and/or adjust these. + + PARAMETERS: + channel_max: short + + proposed maximum channels + + The maximum total number of channels that the server + allows per connection. Zero means that the server does + not impose a fixed limit, but the number of allowed + channels may be limited by available server resources. + + frame_max: long + + proposed maximum frame size + + The largest frame size that the server proposes for + the connection. The client can negotiate a lower + value. Zero means that the server does not impose any + specific limit but may reject very large frames if it + cannot allocate resources for them. + + RULE: + + Until the frame-max has been negotiated, both + peers MUST accept frames of up to 4096 octets + large. The minimum non-zero value for the frame- + max field is 4096. + + heartbeat: short + + desired heartbeat delay + + The delay, in seconds, of the connection heartbeat + that the server wants. Zero means the server does not + want a heartbeat. + + """ + client_heartbeat = self.client_heartbeat or 0 + self.channel_max = args.read_short() or self.channel_max + self.frame_max = args.read_long() or self.frame_max + self.method_writer.frame_max = self.frame_max + self.server_heartbeat = args.read_short() or 0 + + # negotiate the heartbeat interval to the smaller of the + # specified values + if self.server_heartbeat == 0 or client_heartbeat == 0: + self.heartbeat = max(self.server_heartbeat, client_heartbeat) + else: + self.heartbeat = min(self.server_heartbeat, client_heartbeat) + + # Ignore server heartbeat if client_heartbeat is disabled + if not self.client_heartbeat: + self.heartbeat = 0 + + self._x_tune_ok(self.channel_max, self.frame_max, self.heartbeat) + + def send_heartbeat(self): + self.transport.write_frame(8, 0, bytes()) + + def heartbeat_tick(self, rate=2): + """Send heartbeat packets, if necessary, and fail if none have been + received recently. This should be called frequently, on the order of + once per second. + + :keyword rate: Ignored + """ + if not self.heartbeat: + return + + # treat actual data exchange in either direction as a heartbeat + sent_now = self.method_writer.bytes_sent + recv_now = self.method_reader.bytes_recv + if self.prev_sent is None or self.prev_sent != sent_now: + self.last_heartbeat_sent = monotonic() + if self.prev_recv is None or self.prev_recv != recv_now: + self.last_heartbeat_received = monotonic() + self.prev_sent, self.prev_recv = sent_now, recv_now + + # send a heartbeat if it's time to do so + if monotonic() > self.last_heartbeat_sent + self.heartbeat: + self.send_heartbeat() + self.last_heartbeat_sent = monotonic() + + # if we've missed two intervals' heartbeats, fail; this gives the + # server enough time to send heartbeats a little late + if (self.last_heartbeat_received and + self.last_heartbeat_received + 2 * + self.heartbeat < monotonic()): + raise ConnectionForced('Too many heartbeats missed') + + def _x_tune_ok(self, channel_max, frame_max, heartbeat): + """Negotiate connection tuning parameters + + This method sends the client's connection tuning parameters to + the server. Certain fields are negotiated, others provide + capability information. + + PARAMETERS: + channel_max: short + + negotiated maximum channels + + The maximum total number of channels that the client + will use per connection. May not be higher than the + value specified by the server. + + RULE: + + The server MAY ignore the channel-max value or MAY + use it for tuning its resource allocation. + + frame_max: long + + negotiated maximum frame size + + The largest frame size that the client and server will + use for the connection. Zero means that the client + does not impose any specific limit but may reject very + large frames if it cannot allocate resources for them. + Note that the frame-max limit applies principally to + content frames, where large contents can be broken + into frames of arbitrary size. + + RULE: + + Until the frame-max has been negotiated, both + peers must accept frames of up to 4096 octets + large. The minimum non-zero value for the frame- + max field is 4096. + + heartbeat: short + + desired heartbeat delay + + The delay, in seconds, of the connection heartbeat + that the client wants. Zero means the client does not + want a heartbeat. + + """ + args = AMQPWriter() + args.write_short(channel_max) + args.write_long(frame_max) + args.write_short(heartbeat or 0) + self._send_method((10, 31), args) + self._wait_tune_ok = False + + @property + def sock(self): + return self.transport.sock + + @property + def server_capabilities(self): + return self.server_properties.get('capabilities') or {} + + _METHOD_MAP = { + (10, 10): _start, + (10, 20): _secure, + (10, 30): _tune, + (10, 41): _open_ok, + (10, 50): _close, + (10, 51): _close_ok, + (10, 60): _blocked, + (10, 61): _unblocked, + } + + _IMMEDIATE_METHODS = [] + connection_errors = ( + ConnectionError, + socket.error, + IOError, + OSError, + ) + channel_errors = (ChannelError, ) + recoverable_connection_errors = ( + RecoverableConnectionError, + socket.error, + IOError, + OSError, + ) + recoverable_channel_errors = ( + RecoverableChannelError, + ) diff --git a/amqp/exceptions.py b/amqp/exceptions.py new file mode 100644 index 0000000..e3e144a --- /dev/null +++ b/amqp/exceptions.py @@ -0,0 +1,258 @@ +"""Exceptions used by amqp""" +# Copyright (C) 2007-2008 Barry Pederson +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 +from __future__ import absolute_import + +from struct import pack, unpack + +__all__ = [ + 'AMQPError', + 'ConnectionError', 'ChannelError', + 'RecoverableConnectionError', 'IrrecoverableConnectionError', + 'RecoverableChannelError', 'IrrecoverableChannelError', + 'ConsumerCancelled', 'ContentTooLarge', 'NoConsumers', + 'ConnectionForced', 'InvalidPath', 'AccessRefused', 'NotFound', + 'ResourceLocked', 'PreconditionFailed', 'FrameError', 'FrameSyntaxError', + 'InvalidCommand', 'ChannelNotOpen', 'UnexpectedFrame', 'ResourceError', + 'NotAllowed', 'AMQPNotImplementedError', 'InternalError', +] + + +class AMQPError(Exception): + code = 0 + + def __init__(self, reply_text=None, method_sig=None, + method_name=None, reply_code=None): + self.message = reply_text + self.reply_code = reply_code or self.code + self.reply_text = reply_text + self.method_sig = method_sig + self.method_name = method_name or '' + if method_sig and not self.method_name: + self.method_name = METHOD_NAME_MAP.get(method_sig, '') + Exception.__init__(self, reply_code, + reply_text, method_sig, self.method_name) + + def __str__(self): + if self.method: + return '{0.method}: ({0.reply_code}) {0.reply_text}'.format(self) + return self.reply_text or '' + + @property + def method(self): + return self.method_name or self.method_sig + + +class ConnectionError(AMQPError): + pass + + +class ChannelError(AMQPError): + pass + + +class RecoverableChannelError(ChannelError): + pass + + +class IrrecoverableChannelError(ChannelError): + pass + + +class RecoverableConnectionError(ConnectionError): + pass + + +class IrrecoverableConnectionError(ConnectionError): + pass + + +class Blocked(RecoverableConnectionError): + pass + + +class ConsumerCancelled(RecoverableConnectionError): + pass + + +class ContentTooLarge(RecoverableChannelError): + code = 311 + + +class NoConsumers(RecoverableChannelError): + code = 313 + + +class ConnectionForced(RecoverableConnectionError): + code = 320 + + +class InvalidPath(IrrecoverableConnectionError): + code = 402 + + +class AccessRefused(IrrecoverableChannelError): + code = 403 + + +class NotFound(IrrecoverableChannelError): + code = 404 + + +class ResourceLocked(RecoverableChannelError): + code = 405 + + +class PreconditionFailed(IrrecoverableChannelError): + code = 406 + + +class FrameError(IrrecoverableConnectionError): + code = 501 + + +class FrameSyntaxError(IrrecoverableConnectionError): + code = 502 + + +class InvalidCommand(IrrecoverableConnectionError): + code = 503 + + +class ChannelNotOpen(IrrecoverableConnectionError): + code = 504 + + +class UnexpectedFrame(IrrecoverableConnectionError): + code = 505 + + +class ResourceError(RecoverableConnectionError): + code = 506 + + +class NotAllowed(IrrecoverableConnectionError): + code = 530 + + +class AMQPNotImplementedError(IrrecoverableConnectionError): + code = 540 + + +class InternalError(IrrecoverableConnectionError): + code = 541 + + +ERROR_MAP = { + 311: ContentTooLarge, + 313: NoConsumers, + 320: ConnectionForced, + 402: InvalidPath, + 403: AccessRefused, + 404: NotFound, + 405: ResourceLocked, + 406: PreconditionFailed, + 501: FrameError, + 502: FrameSyntaxError, + 503: InvalidCommand, + 504: ChannelNotOpen, + 505: UnexpectedFrame, + 506: ResourceError, + 530: NotAllowed, + 540: AMQPNotImplementedError, + 541: InternalError, +} + + +def error_for_code(code, text, method, default): + try: + return ERROR_MAP[code](text, method, reply_code=code) + except KeyError: + return default(text, method, reply_code=code) + + +def raise_for_code(code, text, method, default): + raise error_for_code(code, text, method, default) + + +METHOD_NAME_MAP = { + (10, 10): 'Connection.start', + (10, 11): 'Connection.start_ok', + (10, 20): 'Connection.secure', + (10, 21): 'Connection.secure_ok', + (10, 30): 'Connection.tune', + (10, 31): 'Connection.tune_ok', + (10, 40): 'Connection.open', + (10, 41): 'Connection.open_ok', + (10, 50): 'Connection.close', + (10, 51): 'Connection.close_ok', + (20, 10): 'Channel.open', + (20, 11): 'Channel.open_ok', + (20, 20): 'Channel.flow', + (20, 21): 'Channel.flow_ok', + (20, 40): 'Channel.close', + (20, 41): 'Channel.close_ok', + (30, 10): 'Access.request', + (30, 11): 'Access.request_ok', + (40, 10): 'Exchange.declare', + (40, 11): 'Exchange.declare_ok', + (40, 20): 'Exchange.delete', + (40, 21): 'Exchange.delete_ok', + (40, 30): 'Exchange.bind', + (40, 31): 'Exchange.bind_ok', + (40, 40): 'Exchange.unbind', + (40, 41): 'Exchange.unbind_ok', + (50, 10): 'Queue.declare', + (50, 11): 'Queue.declare_ok', + (50, 20): 'Queue.bind', + (50, 21): 'Queue.bind_ok', + (50, 30): 'Queue.purge', + (50, 31): 'Queue.purge_ok', + (50, 40): 'Queue.delete', + (50, 41): 'Queue.delete_ok', + (50, 50): 'Queue.unbind', + (50, 51): 'Queue.unbind_ok', + (60, 10): 'Basic.qos', + (60, 11): 'Basic.qos_ok', + (60, 20): 'Basic.consume', + (60, 21): 'Basic.consume_ok', + (60, 30): 'Basic.cancel', + (60, 31): 'Basic.cancel_ok', + (60, 40): 'Basic.publish', + (60, 50): 'Basic.return', + (60, 60): 'Basic.deliver', + (60, 70): 'Basic.get', + (60, 71): 'Basic.get_ok', + (60, 72): 'Basic.get_empty', + (60, 80): 'Basic.ack', + (60, 90): 'Basic.reject', + (60, 100): 'Basic.recover_async', + (60, 110): 'Basic.recover', + (60, 111): 'Basic.recover_ok', + (60, 120): 'Basic.nack', + (90, 10): 'Tx.select', + (90, 11): 'Tx.select_ok', + (90, 20): 'Tx.commit', + (90, 21): 'Tx.commit_ok', + (90, 30): 'Tx.rollback', + (90, 31): 'Tx.rollback_ok', + (85, 10): 'Confirm.select', + (85, 11): 'Confirm.select_ok', +} + + +for _method_id, _method_name in list(METHOD_NAME_MAP.items()): + METHOD_NAME_MAP[unpack('>I', pack('>HH', *_method_id))[0]] = _method_name diff --git a/amqp/five.py b/amqp/five.py new file mode 100644 index 0000000..5157df5 --- /dev/null +++ b/amqp/five.py @@ -0,0 +1,188 @@ +# -*- coding: utf-8 -*- +""" + celery.five + ~~~~~~~~~~~ + + Compatibility implementations of features + only available in newer Python versions. + + +""" +from __future__ import absolute_import + +############## py3k ######################################################### +import sys +PY3 = sys.version_info[0] == 3 + +try: + reload = reload # noqa +except NameError: # pragma: no cover + from imp import reload # noqa + +try: + from UserList import UserList # noqa +except ImportError: # pragma: no cover + from collections import UserList # noqa + +try: + from UserDict import UserDict # noqa +except ImportError: # pragma: no cover + from collections import UserDict # noqa + + +if PY3: + import builtins + + from queue import Queue, Empty + from itertools import zip_longest + from io import StringIO, BytesIO + + map = map + string = str + string_t = str + long_t = int + text_t = str + range = range + int_types = (int, ) + + open_fqdn = 'builtins.open' + + def items(d): + return d.items() + + def keys(d): + return d.keys() + + def values(d): + return d.values() + + def nextfun(it): + return it.__next__ + + exec_ = getattr(builtins, 'exec') + + def reraise(tp, value, tb=None): + if value.__traceback__ is not tb: + raise value.with_traceback(tb) + raise value + + class WhateverIO(StringIO): + + def write(self, data): + if isinstance(data, bytes): + data = data.encode() + StringIO.write(self, data) + +else: + import __builtin__ as builtins # noqa + from Queue import Queue, Empty # noqa + from itertools import imap as map, izip_longest as zip_longest # noqa + from StringIO import StringIO # noqa + string = unicode # noqa + string_t = basestring # noqa + text_t = unicode + long_t = long # noqa + range = xrange + int_types = (int, long) + + open_fqdn = '__builtin__.open' + + def items(d): # noqa + return d.iteritems() + + def keys(d): # noqa + return d.iterkeys() + + def values(d): # noqa + return d.itervalues() + + def nextfun(it): # noqa + return it.next + + def exec_(code, globs=None, locs=None): + """Execute code in a namespace.""" + if globs is None: + frame = sys._getframe(1) + globs = frame.f_globals + if locs is None: + locs = frame.f_locals + del frame + elif locs is None: + locs = globs + exec("""exec code in globs, locs""") + + exec_("""def reraise(tp, value, tb=None): raise tp, value, tb""") + + BytesIO = WhateverIO = StringIO # noqa + + +def with_metaclass(Type, skip_attrs=set(['__dict__', '__weakref__'])): + """Class decorator to set metaclass. + + Works with both Python 3 and Python 3 and it does not add + an extra class in the lookup order like ``six.with_metaclass`` does + (that is -- it copies the original class instead of using inheritance). + + """ + + def _clone_with_metaclass(Class): + attrs = dict((key, value) for key, value in items(vars(Class)) + if key not in skip_attrs) + return Type(Class.__name__, Class.__bases__, attrs) + + return _clone_with_metaclass + +############## time.monotonic ################################################ + +if sys.version_info < (3, 3): + + import platform + SYSTEM = platform.system() + + if SYSTEM == 'Darwin': + import ctypes + from ctypes.util import find_library + libSystem = ctypes.CDLL('libSystem.dylib') + CoreServices = ctypes.CDLL(find_library('CoreServices'), + use_errno=True) + mach_absolute_time = libSystem.mach_absolute_time + mach_absolute_time.restype = ctypes.c_uint64 + absolute_to_nanoseconds = CoreServices.AbsoluteToNanoseconds + absolute_to_nanoseconds.restype = ctypes.c_uint64 + absolute_to_nanoseconds.argtypes = [ctypes.c_uint64] + + def _monotonic(): + return absolute_to_nanoseconds(mach_absolute_time()) * 1e-9 + + elif SYSTEM == 'Linux': + # from stackoverflow: + # questions/1205722/how-do-i-get-monotonic-time-durations-in-python + import ctypes + import os + + CLOCK_MONOTONIC = 1 # see + + class timespec(ctypes.Structure): + _fields_ = [ + ('tv_sec', ctypes.c_long), + ('tv_nsec', ctypes.c_long), + ] + + librt = ctypes.CDLL('librt.so.1', use_errno=True) + clock_gettime = librt.clock_gettime + clock_gettime.argtypes = [ + ctypes.c_int, ctypes.POINTER(timespec), + ] + + def _monotonic(): # noqa + t = timespec() + if clock_gettime(CLOCK_MONOTONIC, ctypes.pointer(t)) != 0: + errno_ = ctypes.get_errno() + raise OSError(errno_, os.strerror(errno_)) + return t.tv_sec + t.tv_nsec * 1e-9 + else: + from time import time as _monotonic +try: + from time import monotonic +except ImportError: + monotonic = _monotonic # noqa diff --git a/amqp/method_framing.py b/amqp/method_framing.py new file mode 100644 index 0000000..b454524 --- /dev/null +++ b/amqp/method_framing.py @@ -0,0 +1,231 @@ +"""Convert between frames and higher-level AMQP methods""" +# Copyright (C) 2007-2008 Barry Pederson +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 +from __future__ import absolute_import + +from collections import defaultdict, deque +from struct import pack, unpack + +from .basic_message import Message +from .exceptions import AMQPError, UnexpectedFrame +from .five import range, string +from .serialization import AMQPReader + +__all__ = ['MethodReader'] + +# +# MethodReader needs to know which methods are supposed +# to be followed by content headers and bodies. +# +_CONTENT_METHODS = [ + (60, 50), # Basic.return + (60, 60), # Basic.deliver + (60, 71), # Basic.get_ok +] + + +class _PartialMessage(object): + """Helper class to build up a multi-frame method.""" + + def __init__(self, method_sig, args, channel): + self.method_sig = method_sig + self.args = args + self.msg = Message() + self.body_parts = [] + self.body_received = 0 + self.body_size = None + self.complete = False + + def add_header(self, payload): + class_id, weight, self.body_size = unpack('>HHQ', payload[:12]) + self.msg._load_properties(payload[12:]) + self.complete = (self.body_size == 0) + + def add_payload(self, payload): + parts = self.body_parts + self.body_received += len(payload) + if self.body_received == self.body_size: + if parts: + parts.append(payload) + self.msg.body = bytes().join(parts) + else: + self.msg.body = payload + self.complete = True + else: + parts.append(payload) + + +class MethodReader(object): + """Helper class to receive frames from the broker, combine them if + necessary with content-headers and content-bodies into complete methods. + + Normally a method is represented as a tuple containing + (channel, method_sig, args, content). + + In the case of a framing error, an :exc:`ConnectionError` is placed + in the queue. + + In the case of unexpected frames, a tuple made up of + ``(channel, ChannelError)`` is placed in the queue. + + """ + + def __init__(self, source): + self.source = source + self.queue = deque() + self.running = False + self.partial_messages = {} + self.heartbeats = 0 + # For each channel, which type is expected next + self.expected_types = defaultdict(lambda: 1) + # not an actual byte count, just incremented whenever we receive + self.bytes_recv = 0 + self._quick_put = self.queue.append + self._quick_get = self.queue.popleft + + def _next_method(self): + """Read the next method from the source, once one complete method has + been assembled it is placed in the internal queue.""" + queue = self.queue + put = self._quick_put + read_frame = self.source.read_frame + while not queue: + try: + frame_type, channel, payload = read_frame() + except Exception as exc: + # + # Connection was closed? Framing Error? + # + put(exc) + break + + self.bytes_recv += 1 + + if frame_type not in (self.expected_types[channel], 8): + put(( + channel, + UnexpectedFrame( + 'Received frame {0} while expecting type: {1}'.format( + frame_type, self.expected_types[channel])))) + elif frame_type == 1: + self._process_method_frame(channel, payload) + elif frame_type == 2: + self._process_content_header(channel, payload) + elif frame_type == 3: + self._process_content_body(channel, payload) + elif frame_type == 8: + self._process_heartbeat(channel, payload) + + def _process_heartbeat(self, channel, payload): + self.heartbeats += 1 + + def _process_method_frame(self, channel, payload): + """Process Method frames""" + method_sig = unpack('>HH', payload[:4]) + args = AMQPReader(payload[4:]) + + if method_sig in _CONTENT_METHODS: + # + # Save what we've got so far and wait for the content-header + # + self.partial_messages[channel] = _PartialMessage( + method_sig, args, channel, + ) + self.expected_types[channel] = 2 + else: + self._quick_put((channel, method_sig, args, None)) + + def _process_content_header(self, channel, payload): + """Process Content Header frames""" + partial = self.partial_messages[channel] + partial.add_header(payload) + + if partial.complete: + # + # a bodyless message, we're done + # + self._quick_put((channel, partial.method_sig, + partial.args, partial.msg)) + self.partial_messages.pop(channel, None) + self.expected_types[channel] = 1 + else: + # + # wait for the content-body + # + self.expected_types[channel] = 3 + + def _process_content_body(self, channel, payload): + """Process Content Body frames""" + partial = self.partial_messages[channel] + partial.add_payload(payload) + if partial.complete: + # + # Stick the message in the queue and go back to + # waiting for method frames + # + self._quick_put((channel, partial.method_sig, + partial.args, partial.msg)) + self.partial_messages.pop(channel, None) + self.expected_types[channel] = 1 + + def read_method(self): + """Read a method from the peer.""" + self._next_method() + m = self._quick_get() + if isinstance(m, Exception): + raise m + if isinstance(m, tuple) and isinstance(m[1], AMQPError): + raise m[1] + return m + + +class MethodWriter(object): + """Convert AMQP methods into AMQP frames and send them out + to the peer.""" + + def __init__(self, dest, frame_max): + self.dest = dest + self.frame_max = frame_max + self.bytes_sent = 0 + + def write_method(self, channel, method_sig, args, content=None): + write_frame = self.dest.write_frame + payload = pack('>HH', method_sig[0], method_sig[1]) + args + + if content: + # do this early, so we can raise an exception if there's a + # problem with the content properties, before sending the + # first frame + body = content.body + if isinstance(body, string): + coding = content.properties.get('content_encoding', None) + if coding is None: + coding = content.properties['content_encoding'] = 'UTF-8' + + body = body.encode(coding) + properties = content._serialize_properties() + + write_frame(1, channel, payload) + + if content: + payload = pack('>HHQ', method_sig[0], 0, len(body)) + properties + + write_frame(2, channel, payload) + + chunk_size = self.frame_max - 8 + for i in range(0, len(body), chunk_size): + write_frame(3, channel, body[i:i + chunk_size]) + self.bytes_sent += 1 diff --git a/amqp/protocol.py b/amqp/protocol.py new file mode 100644 index 0000000..0856eb4 --- /dev/null +++ b/amqp/protocol.py @@ -0,0 +1,13 @@ +from __future__ import absolute_import + +from collections import namedtuple + + +queue_declare_ok_t = namedtuple( + 'queue_declare_ok_t', ('queue', 'message_count', 'consumer_count'), +) + +basic_return_t = namedtuple( + 'basic_return_t', + ('reply_code', 'reply_text', 'exchange', 'routing_key', 'message'), +) diff --git a/amqp/serialization.py b/amqp/serialization.py new file mode 100644 index 0000000..4ad1b06 --- /dev/null +++ b/amqp/serialization.py @@ -0,0 +1,510 @@ +""" +Convert between bytestreams and higher-level AMQP types. + +2007-11-05 Barry Pederson + +""" +# Copyright (C) 2007 Barry Pederson +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 +from __future__ import absolute_import + +import sys + +from datetime import datetime +from decimal import Decimal +from io import BytesIO +from struct import pack, unpack +from time import mktime + +from .exceptions import FrameSyntaxError +from .five import int_types, long_t, string, string_t, items + +IS_PY3K = sys.version_info[0] >= 3 + +if IS_PY3K: + def byte(n): + return bytes([n]) +else: + byte = chr + + +ILLEGAL_TABLE_TYPE_WITH_KEY = """\ +Table type {0!r} for key {1!r} not handled by amqp. [value: {2!r}] +""" + +ILLEGAL_TABLE_TYPE = """\ + Table type {0!r} not handled by amqp. [value: {1!r}] +""" + + +class AMQPReader(object): + """Read higher-level AMQP types from a bytestream.""" + def __init__(self, source): + """Source should be either a file-like object with a read() method, or + a plain (non-unicode) string.""" + if isinstance(source, bytes): + self.input = BytesIO(source) + elif hasattr(source, 'read'): + self.input = source + else: + raise ValueError( + 'AMQPReader needs a file-like object or plain string') + + self.bitcount = self.bits = 0 + + def close(self): + self.input.close() + + def read(self, n): + """Read n bytes.""" + self.bitcount = self.bits = 0 + return self.input.read(n) + + def read_bit(self): + """Read a single boolean value.""" + if not self.bitcount: + self.bits = ord(self.input.read(1)) + self.bitcount = 8 + result = (self.bits & 1) == 1 + self.bits >>= 1 + self.bitcount -= 1 + return result + + def read_octet(self): + """Read one byte, return as an integer""" + self.bitcount = self.bits = 0 + return unpack('B', self.input.read(1))[0] + + def read_short(self): + """Read an unsigned 16-bit integer""" + self.bitcount = self.bits = 0 + return unpack('>H', self.input.read(2))[0] + + def read_long(self): + """Read an unsigned 32-bit integer""" + self.bitcount = self.bits = 0 + return unpack('>I', self.input.read(4))[0] + + def read_longlong(self): + """Read an unsigned 64-bit integer""" + self.bitcount = self.bits = 0 + return unpack('>Q', self.input.read(8))[0] + + def read_float(self): + """Read float value.""" + self.bitcount = self.bits = 0 + return unpack('>d', self.input.read(8))[0] + + def read_shortstr(self): + """Read a short string that's stored in up to 255 bytes. + + The encoding isn't specified in the AMQP spec, so + assume it's utf-8 + + """ + self.bitcount = self.bits = 0 + slen = unpack('B', self.input.read(1))[0] + return self.input.read(slen).decode('utf-8') + + def read_longstr(self): + """Read a string that's up to 2**32 bytes. + + The encoding isn't specified in the AMQP spec, so + assume it's utf-8 + + """ + self.bitcount = self.bits = 0 + slen = unpack('>I', self.input.read(4))[0] + return self.input.read(slen).decode('utf-8') + + def read_table(self): + """Read an AMQP table, and return as a Python dictionary.""" + self.bitcount = self.bits = 0 + tlen = unpack('>I', self.input.read(4))[0] + table_data = AMQPReader(self.input.read(tlen)) + result = {} + while table_data.input.tell() < tlen: + name = table_data.read_shortstr() + val = table_data.read_item() + result[name] = val + return result + + def read_item(self): + ftype = ord(self.input.read(1)) + + # 'S': long string + if ftype == 83: + val = self.read_longstr() + # 's': short string + elif ftype == 115: + val = self.read_shortstr() + # 'b': short-short int + elif ftype == 98: + val, = unpack('>B', self.input.read(1)) + # 'B': short-short unsigned int + elif ftype == 66: + val, = unpack('>b', self.input.read(1)) + # 'U': short int + elif ftype == 85: + val, = unpack('>h', self.input.read(2)) + # 'u': short unsigned int + elif ftype == 117: + val, = unpack('>H', self.input.read(2)) + # 'I': long int + elif ftype == 73: + val, = unpack('>i', self.input.read(4)) + # 'i': long unsigned int + elif ftype == 105: # 'l' + val, = unpack('>I', self.input.read(4)) + # 'L': long long int + elif ftype == 76: + val, = unpack('>q', self.input.read(8)) + # 'l': long long unsigned int + elif ftype == 108: + val, = unpack('>Q', self.input.read(8)) + # 'f': float + elif ftype == 102: + val, = unpack('>f', self.input.read(4)) + # 'd': double + elif ftype == 100: + val = self.read_float() + # 'D': decimal + elif ftype == 68: + d = self.read_octet() + n, = unpack('>i', self.input.read(4)) + val = Decimal(n) / Decimal(10 ** d) + # 'F': table + elif ftype == 70: + val = self.read_table() # recurse + # 'A': array + elif ftype == 65: + val = self.read_array() + # 't' (bool) + elif ftype == 116: + val = self.read_bit() + # 'T': timestamp + elif ftype == 84: + val = self.read_timestamp() + # 'V': void + elif ftype == 86: + val = None + else: + raise FrameSyntaxError( + 'Unknown value in table: {0!r} ({1!r})'.format( + ftype, type(ftype))) + return val + + def read_array(self): + array_length = unpack('>I', self.input.read(4))[0] + array_data = AMQPReader(self.input.read(array_length)) + result = [] + while array_data.input.tell() < array_length: + val = array_data.read_item() + result.append(val) + return result + + def read_timestamp(self): + """Read and AMQP timestamp, which is a 64-bit integer representing + seconds since the Unix epoch in 1-second resolution. + + Return as a Python datetime.datetime object, + expressed as localtime. + + """ + return datetime.fromtimestamp(self.read_longlong()) + + +class AMQPWriter(object): + """Convert higher-level AMQP types to bytestreams.""" + + def __init__(self, dest=None): + """dest may be a file-type object (with a write() method). If None + then a BytesIO is created, and the contents can be accessed with + this class's getvalue() method.""" + self.out = BytesIO() if dest is None else dest + self.bits = [] + self.bitcount = 0 + + def _flushbits(self): + if self.bits: + out = self.out + for b in self.bits: + out.write(pack('B', b)) + self.bits = [] + self.bitcount = 0 + + def close(self): + """Pass through if possible to any file-like destinations.""" + try: + self.out.close() + except AttributeError: + pass + + def flush(self): + """Pass through if possible to any file-like destinations.""" + try: + self.out.flush() + except AttributeError: + pass + + def getvalue(self): + """Get what's been encoded so far if we're working with a BytesIO.""" + self._flushbits() + return self.out.getvalue() + + def write(self, s): + """Write a plain Python string with no special encoding in Python 2.x, + or bytes in Python 3.x""" + self._flushbits() + self.out.write(s) + + def write_bit(self, b): + """Write a boolean value.""" + b = 1 if b else 0 + shift = self.bitcount % 8 + if shift == 0: + self.bits.append(0) + self.bits[-1] |= (b << shift) + self.bitcount += 1 + + def write_octet(self, n): + """Write an integer as an unsigned 8-bit value.""" + if n < 0 or n > 255: + raise FrameSyntaxError( + 'Octet {0!r} out of range 0..255'.format(n)) + self._flushbits() + self.out.write(pack('B', n)) + + def write_short(self, n): + """Write an integer as an unsigned 16-bit value.""" + if n < 0 or n > 65535: + raise FrameSyntaxError( + 'Octet {0!r} out of range 0..65535'.format(n)) + self._flushbits() + self.out.write(pack('>H', int(n))) + + def write_long(self, n): + """Write an integer as an unsigned2 32-bit value.""" + if n < 0 or n >= 4294967296: + raise FrameSyntaxError( + 'Octet {0!r} out of range 0..2**31-1'.format(n)) + self._flushbits() + self.out.write(pack('>I', n)) + + def write_longlong(self, n): + """Write an integer as an unsigned 64-bit value.""" + if n < 0 or n >= 18446744073709551616: + raise FrameSyntaxError( + 'Octet {0!r} out of range 0..2**64-1'.format(n)) + self._flushbits() + self.out.write(pack('>Q', n)) + + def write_shortstr(self, s): + """Write a string up to 255 bytes long (after any encoding). + + If passed a unicode string, encode with UTF-8. + + """ + self._flushbits() + if isinstance(s, string): + s = s.encode('utf-8') + if len(s) > 255: + raise FrameSyntaxError( + 'Shortstring overflow ({0} > 255)'.format(len(s))) + self.write_octet(len(s)) + self.out.write(s) + + def write_longstr(self, s): + """Write a string up to 2**32 bytes long after encoding. + + If passed a unicode string, encode as UTF-8. + + """ + self._flushbits() + if isinstance(s, string): + s = s.encode('utf-8') + self.write_long(len(s)) + self.out.write(s) + + def write_table(self, d): + """Write out a Python dictionary made of up string keys, and values + that are strings, signed integers, Decimal, datetime.datetime, or + sub-dictionaries following the same constraints.""" + self._flushbits() + table_data = AMQPWriter() + for k, v in items(d): + table_data.write_shortstr(k) + table_data.write_item(v, k) + table_data = table_data.getvalue() + self.write_long(len(table_data)) + self.out.write(table_data) + + def write_item(self, v, k=None): + if isinstance(v, (string_t, bytes)): + if isinstance(v, string): + v = v.encode('utf-8') + self.write(b'S') + self.write_longstr(v) + elif isinstance(v, bool): + self.write(pack('>cB', b't', int(v))) + elif isinstance(v, float): + self.write(pack('>cd', b'd', v)) + elif isinstance(v, int_types): + self.write(pack('>ci', b'I', v)) + elif isinstance(v, Decimal): + self.write(b'D') + sign, digits, exponent = v.as_tuple() + v = 0 + for d in digits: + v = (v * 10) + d + if sign: + v = -v + self.write_octet(-exponent) + self.write(pack('>i', v)) + elif isinstance(v, datetime): + self.write(b'T') + self.write_timestamp(v) + ## FIXME: timezone ? + elif isinstance(v, dict): + self.write(b'F') + self.write_table(v) + elif isinstance(v, (list, tuple)): + self.write(b'A') + self.write_array(v) + elif v is None: + self.write(b'V') + else: + err = (ILLEGAL_TABLE_TYPE_WITH_KEY.format(type(v), k, v) if k + else ILLEGAL_TABLE_TYPE.format(type(v), v)) + raise FrameSyntaxError(err) + + def write_array(self, a): + array_data = AMQPWriter() + for v in a: + array_data.write_item(v) + array_data = array_data.getvalue() + self.write_long(len(array_data)) + self.out.write(array_data) + + def write_timestamp(self, v): + """Write out a Python datetime.datetime object as a 64-bit integer + representing seconds since the Unix epoch.""" + self.out.write(pack('>q', long_t(mktime(v.timetuple())))) + + +class GenericContent(object): + """Abstract base class for AMQP content. + + Subclasses should override the PROPERTIES attribute. + + """ + PROPERTIES = [('dummy', 'shortstr')] + + def __init__(self, **props): + """Save the properties appropriate to this AMQP content type + in a 'properties' dictionary.""" + d = {} + for propname, _ in self.PROPERTIES: + if propname in props: + d[propname] = props[propname] + # FIXME: should we ignore unknown properties? + + self.properties = d + + def __eq__(self, other): + """Check if this object has the same properties as another + content object.""" + try: + return self.properties == other.properties + except AttributeError: + return NotImplemented + + def __getattr__(self, name): + """Look for additional properties in the 'properties' + dictionary, and if present - the 'delivery_info' + dictionary.""" + if name == '__setstate__': + # Allows pickling/unpickling to work + raise AttributeError('__setstate__') + + if name in self.properties: + return self.properties[name] + + if 'delivery_info' in self.__dict__ \ + and name in self.delivery_info: + return self.delivery_info[name] + + raise AttributeError(name) + + def _load_properties(self, raw_bytes): + """Given the raw bytes containing the property-flags and property-list + from a content-frame-header, parse and insert into a dictionary + stored in this object as an attribute named 'properties'.""" + r = AMQPReader(raw_bytes) + + # + # Read 16-bit shorts until we get one with a low bit set to zero + # + flags = [] + while 1: + flag_bits = r.read_short() + flags.append(flag_bits) + if flag_bits & 1 == 0: + break + + shift = 0 + d = {} + for key, proptype in self.PROPERTIES: + if shift == 0: + if not flags: + break + flag_bits, flags = flags[0], flags[1:] + shift = 15 + if flag_bits & (1 << shift): + d[key] = getattr(r, 'read_' + proptype)() + shift -= 1 + + self.properties = d + + def _serialize_properties(self): + """serialize the 'properties' attribute (a dictionary) into + the raw bytes making up a set of property flags and a + property list, suitable for putting into a content frame header.""" + shift = 15 + flag_bits = 0 + flags = [] + raw_bytes = AMQPWriter() + for key, proptype in self.PROPERTIES: + val = self.properties.get(key, None) + if val is not None: + if shift == 0: + flags.append(flag_bits) + flag_bits = 0 + shift = 15 + + flag_bits |= (1 << shift) + if proptype != 'bit': + getattr(raw_bytes, 'write_' + proptype)(val) + + shift -= 1 + + flags.append(flag_bits) + result = AMQPWriter() + for flag_bits in flags: + result.write_short(flag_bits) + result.write(raw_bytes.getvalue()) + + return result.getvalue() diff --git a/amqp/transport.py b/amqp/transport.py new file mode 100644 index 0000000..eac3dbc --- /dev/null +++ b/amqp/transport.py @@ -0,0 +1,294 @@ +# Copyright (C) 2009 Barry Pederson +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 +from __future__ import absolute_import + +import errno +import re +import socket +import ssl + +# Jython does not have this attribute +try: + from socket import SOL_TCP +except ImportError: # pragma: no cover + from socket import IPPROTO_TCP as SOL_TCP # noqa + +try: + from ssl import SSLError +except ImportError: + class SSLError(Exception): # noqa + pass + +from struct import pack, unpack + +from .exceptions import UnexpectedFrame +from .utils import get_errno, set_cloexec + +_UNAVAIL = errno.EAGAIN, errno.EINTR, errno.ENOENT + +AMQP_PORT = 5672 + +EMPTY_BUFFER = bytes() + +# Yes, Advanced Message Queuing Protocol Protocol is redundant +AMQP_PROTOCOL_HEADER = 'AMQP\x01\x01\x00\x09'.encode('latin_1') + +# Match things like: [fe80::1]:5432, from RFC 2732 +IPV6_LITERAL = re.compile(r'\[([\.0-9a-f:]+)\](?::(\d+))?') + + +class _AbstractTransport(object): + """Common superclass for TCP and SSL transports""" + connected = False + + def __init__(self, host, connect_timeout): + self.connected = True + msg = None + port = AMQP_PORT + + m = IPV6_LITERAL.match(host) + if m: + host = m.group(1) + if m.group(2): + port = int(m.group(2)) + else: + if ':' in host: + host, port = host.rsplit(':', 1) + port = int(port) + + self.sock = None + last_err = None + for res in socket.getaddrinfo(host, port, 0, + socket.SOCK_STREAM, SOL_TCP): + af, socktype, proto, canonname, sa = res + try: + self.sock = socket.socket(af, socktype, proto) + try: + set_cloexec(self.sock, True) + except NotImplementedError: + pass + self.sock.settimeout(connect_timeout) + self.sock.connect(sa) + except socket.error as exc: + msg = exc + self.sock.close() + self.sock = None + last_err = msg + continue + break + + if not self.sock: + # Didn't connect, return the most recent error message + raise socket.error(last_err) + + try: + self.sock.settimeout(None) + self.sock.setsockopt(SOL_TCP, socket.TCP_NODELAY, 1) + self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) + + self._setup_transport() + + self._write(AMQP_PROTOCOL_HEADER) + except (OSError, IOError, socket.error) as exc: + if get_errno(exc) not in _UNAVAIL: + self.connected = False + raise + + def __del__(self): + try: + # socket module may have been collected by gc + # if this is called by a thread at shutdown. + if socket is not None: + try: + self.close() + except socket.error: + pass + finally: + self.sock = None + + def _read(self, n, initial=False): + """Read exactly n bytes from the peer""" + raise NotImplementedError('Must be overriden in subclass') + + def _setup_transport(self): + """Do any additional initialization of the class (used + by the subclasses).""" + pass + + def _shutdown_transport(self): + """Do any preliminary work in shutting down the connection.""" + pass + + def _write(self, s): + """Completely write a string to the peer.""" + raise NotImplementedError('Must be overriden in subclass') + + def close(self): + if self.sock is not None: + self._shutdown_transport() + # Call shutdown first to make sure that pending messages + # reach the AMQP broker if the program exits after + # calling this method. + self.sock.shutdown(socket.SHUT_RDWR) + self.sock.close() + self.sock = None + self.connected = False + + def read_frame(self, unpack=unpack): + read = self._read + try: + frame_type, channel, size = unpack('>BHI', read(7, True)) + payload = read(size) + ch = ord(read(1)) + except socket.timeout: + raise + except (OSError, IOError, socket.error) as exc: + # Don't disconnect for ssl read time outs + # http://bugs.python.org/issue10272 + if isinstance(exc, SSLError) and 'timed out' in str(exc): + raise socket.timeout() + if get_errno(exc) not in _UNAVAIL: + self.connected = False + raise + if ch == 206: # '\xce' + return frame_type, channel, payload + else: + raise UnexpectedFrame( + 'Received 0x{0:02x} while expecting 0xce'.format(ch)) + + def write_frame(self, frame_type, channel, payload): + size = len(payload) + try: + self._write(pack( + '>BHI%dsB' % size, + frame_type, channel, size, payload, 0xce, + )) + except socket.timeout: + raise + except (OSError, IOError, socket.error) as exc: + if get_errno(exc) not in _UNAVAIL: + self.connected = False + raise + + +class SSLTransport(_AbstractTransport): + """Transport that works over SSL""" + + def __init__(self, host, connect_timeout, ssl): + if isinstance(ssl, dict): + self.sslopts = ssl + self._read_buffer = EMPTY_BUFFER + super(SSLTransport, self).__init__(host, connect_timeout) + + def _setup_transport(self): + """Wrap the socket in an SSL object.""" + if hasattr(self, 'sslopts'): + self.sock = ssl.wrap_socket(self.sock, **self.sslopts) + else: + self.sock = ssl.wrap_socket(self.sock) + self.sock.do_handshake() + self._quick_recv = self.sock.read + + def _shutdown_transport(self): + """Unwrap a Python 2.6 SSL socket, so we can call shutdown()""" + if self.sock is not None: + try: + unwrap = self.sock.unwrap + except AttributeError: + return + self.sock = unwrap() + + def _read(self, n, initial=False, + _errnos=(errno.ENOENT, errno.EAGAIN, errno.EINTR)): + # According to SSL_read(3), it can at most return 16kb of data. + # Thus, we use an internal read buffer like TCPTransport._read + # to get the exact number of bytes wanted. + recv = self._quick_recv + rbuf = self._read_buffer + try: + while len(rbuf) < n: + try: + s = recv(n - len(rbuf)) # see note above + except socket.error as exc: + # ssl.sock.read may cause ENOENT if the + # operation couldn't be performed (Issue celery#1414). + if not initial and exc.errno in _errnos: + continue + raise + if not s: + raise IOError('Socket closed') + rbuf += s + except: + self._read_buffer = rbuf + raise + result, self._read_buffer = rbuf[:n], rbuf[n:] + return result + + def _write(self, s): + """Write a string out to the SSL socket fully.""" + try: + write = self.sock.write + except AttributeError: + # Works around a bug in python socket library + raise IOError('Socket closed') + else: + while s: + n = write(s) + if not n: + raise IOError('Socket closed') + s = s[n:] + + +class TCPTransport(_AbstractTransport): + """Transport that deals directly with TCP socket.""" + + def _setup_transport(self): + """Setup to _write() directly to the socket, and + do our own buffered reads.""" + self._write = self.sock.sendall + self._read_buffer = EMPTY_BUFFER + self._quick_recv = self.sock.recv + + def _read(self, n, initial=False, _errnos=(errno.EAGAIN, errno.EINTR)): + """Read exactly n bytes from the socket""" + recv = self._quick_recv + rbuf = self._read_buffer + try: + while len(rbuf) < n: + try: + s = recv(n - len(rbuf)) + except socket.error as exc: + if not initial and exc.errno in _errnos: + continue + raise + if not s: + raise IOError('Socket closed') + rbuf += s + except: + self._read_buffer = rbuf + raise + + result, self._read_buffer = rbuf[:n], rbuf[n:] + return result + + +def create_transport(host, connect_timeout, ssl=False): + """Given a few parameters from the Connection constructor, + select and create a subclass of _AbstractTransport.""" + if ssl: + return SSLTransport(host, connect_timeout, ssl) + else: + return TCPTransport(host, connect_timeout) diff --git a/amqp/utils.py b/amqp/utils.py new file mode 100644 index 0000000..900d2aa --- /dev/null +++ b/amqp/utils.py @@ -0,0 +1,102 @@ +from __future__ import absolute_import + +import sys + +try: + import fcntl +except ImportError: + fcntl = None # noqa + + +class promise(object): + if not hasattr(sys, 'pypy_version_info'): + __slots__ = tuple( + 'fun args kwargs value ready failed ' + ' on_success on_error calls'.split() + ) + + def __init__(self, fun, args=(), kwargs=(), + on_success=None, on_error=None): + self.fun = fun + self.args = args + self.kwargs = kwargs + self.ready = False + self.failed = False + self.on_success = on_success + self.on_error = on_error + self.value = None + self.calls = 0 + + def __repr__(self): + return '<$: {0.fun.__name__}(*{0.args!r}, **{0.kwargs!r})'.format( + self, + ) + + def __call__(self, *args, **kwargs): + try: + self.value = self.fun( + *self.args + args if self.args else args, + **dict(self.kwargs, **kwargs) if self.kwargs else kwargs + ) + except Exception as exc: + self.set_error_state(exc) + else: + if self.on_success: + self.on_success(self.value) + finally: + self.ready = True + self.calls += 1 + + def then(self, callback=None, on_error=None): + self.on_success = callback + self.on_error = on_error + return callback + + def set_error_state(self, exc): + self.failed = True + if self.on_error is None: + raise + self.on_error(exc) + + def throw(self, exc): + try: + raise exc + except exc.__class__ as with_cause: + self.set_error_state(with_cause) + + +def noop(): + return promise(lambda *a, **k: None) + + +try: + from os import set_cloexec # Python 3.4? +except ImportError: + def set_cloexec(fd, cloexec): # noqa + try: + FD_CLOEXEC = fcntl.FD_CLOEXEC + except AttributeError: + raise NotImplementedError( + 'close-on-exec flag not supported on this platform', + ) + flags = fcntl.fcntl(fd, fcntl.F_GETFD) + if cloexec: + flags |= FD_CLOEXEC + else: + flags &= ~FD_CLOEXEC + return fcntl.fcntl(fd, fcntl.F_SETFD, flags) + + +def get_errno(exc): + """:exc:`socket.error` and :exc:`IOError` first got + the ``.errno`` attribute in Py2.7""" + try: + return exc.errno + except AttributeError: + try: + # e.args = (errno, reason) + if isinstance(exc.args, tuple) and len(exc.args) == 2: + return exc.args[0] + except AttributeError: + pass + return 0 diff --git a/demo/amqp_clock.py b/demo/amqp_clock.py new file mode 100755 index 0000000..c718266 --- /dev/null +++ b/demo/amqp_clock.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python +""" +AMQP Clock + +Fires off simple messages at one-minute intervals to a topic +exchange named 'clock', with the topic of the message being +the local time as 'year.month.date.dow.hour.minute', +for example: '2007.11.26.1.12.33', where the dow (day of week) +is 0 for Sunday, 1 for Monday, and so on (similar to Unix crontab). + +A consumer could then bind a queue to the routing key '#.0' +for example to get a message at the beginning of each hour. + +2007-11-26 Barry Pederson + +""" +from datetime import datetime +from optparse import OptionParser +from time import sleep + +import amqp +Message = amqp.Message + +EXCHANGE_NAME = 'clock' +TOPIC_PATTERN = '%Y.%m.%d.%w.%H.%M' # Python datetime.strftime() pattern + + +def main(): + parser = OptionParser() + parser.add_option( + '--host', dest='host', + help='AMQP server to connect to (default: %default)', + default='localhost', + ) + parser.add_option( + '-u', '--userid', dest='userid', + help='AMQP userid to authenticate as (default: %default)', + default='guest', + ) + parser.add_option( + '-p', '--password', dest='password', + help='AMQP password to authenticate with (default: %default)', + default='guest', + ) + parser.add_option( + '--ssl', dest='ssl', action='store_true', + help='Enable SSL with AMQP server (default: not enabled)', + default=False, + ) + + options, args = parser.parse_args() + + conn = amqp.Connection(options.host, options.userid, options.password) + ch = conn.channel() + ch.exchange_declare(EXCHANGE_NAME, type='topic') + + # Make sure our first message is close to the beginning + # of a minute + now = datetime.now() + if now.second > 0: + sleep(60 - now.second) + + while True: + now = datetime.now() + msg = Message(timestamp=now) + msg_topic = now.strftime(TOPIC_PATTERN) + ch.basic_publish(msg, EXCHANGE_NAME, routing_key=msg_topic) + + # Don't know how long the basic_publish took, so + # grab the time again. + now = datetime.now() + sleep(60 - now.second) + + ch.close() + conn.close() + +if __name__ == '__main__': + main() diff --git a/demo/demo_receive.py b/demo/demo_receive.py new file mode 100755 index 0000000..bfda624 --- /dev/null +++ b/demo/demo_receive.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python +""" +Test AMQP library. + +Repeatedly receive messages from the demo_send.py +script, until it receives a message with 'quit' as the body. + +2007-11-11 Barry Pederson + +""" +from optparse import OptionParser +from functools import partial + +import amqp + + +def callback(channel, msg): + for key, val in msg.properties.items(): + print('%s: %s' % (key, str(val))) + for key, val in msg.delivery_info.items(): + print('> %s: %s' % (key, str(val))) + + print('') + print(msg.body) + print('-------') + print(msg.delivery_tag) + channel.basic_ack(msg.delivery_tag) + + # + # Cancel this callback + # + if msg.body == 'quit': + channel.basic_cancel(msg.consumer_tag) + + +def main(): + parser = OptionParser() + parser.add_option( + '--host', dest='host', + help='AMQP server to connect to (default: %default)', + default='localhost', + ) + parser.add_option( + '-u', '--userid', dest='userid', + help='userid to authenticate as (default: %default)', + default='guest', + ) + parser.add_option( + '-p', '--password', dest='password', + help='password to authenticate with (default: %default)', + default='guest', + ) + parser.add_option( + '--ssl', dest='ssl', action='store_true', + help='Enable SSL (default: not enabled)', + default=False, + ) + + options, args = parser.parse_args() + + conn = amqp.Connection(options.host, userid=options.userid, + password=options.password, ssl=options.ssl) + + ch = conn.channel() + + ch.exchange_declare('myfan', 'fanout') + qname, _, _ = ch.queue_declare() + ch.queue_bind(qname, 'myfan') + ch.basic_consume(qname, callback=partial(callback, ch)) + + #pyamqp:// + + # + # Loop as long as the channel has callbacks registered + # + while ch.callbacks: + ch.wait() + + ch.close() + conn.close() + +if __name__ == '__main__': + main() diff --git a/demo/demo_send.py b/demo/demo_send.py new file mode 100755 index 0000000..27bb1b1 --- /dev/null +++ b/demo/demo_send.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python +""" +Test AMQP library. + +Send a message to the corresponding demo_receive.py script, any +arguments to this program are joined together and sent as a message +body. + +2007-11-11 Barry Pederson + +""" +import sys +from optparse import OptionParser + +import amqp + + +def main(): + parser = OptionParser( + usage='usage: %prog [options] message\nexample: %prog hello world', + ) + parser.add_option( + '--host', dest='host', + help='AMQP server to connect to (default: %default)', + default='localhost', + ) + parser.add_option( + '-u', '--userid', dest='userid', + help='userid to authenticate as (default: %default)', + default='guest', + ) + parser.add_option( + '-p', '--password', dest='password', + help='password to authenticate with (default: %default)', + default='guest', + ) + parser.add_option( + '--ssl', dest='ssl', action='store_true', + help='Enable SSL (default: not enabled)', + default=False, + ) + + options, args = parser.parse_args() + + if not args: + parser.print_help() + sys.exit(1) + + msg_body = ' '.join(args) + + conn = amqp.Connection(options.host, userid=options.userid, + password=options.password, ssl=options.ssl) + + ch = conn.channel() + ch.exchange_declare('myfan', 'fanout') + + msg = amqp.Message(msg_body, content_type='text/plain', + application_headers={'foo': 7, 'bar': 'baz'}) + + ch.basic_publish(msg, 'myfan') + + ch.close() + conn.close() + +if __name__ == '__main__': + main() diff --git a/docs/.static/.keep b/docs/.static/.keep new file mode 100644 index 0000000..e69de29 diff --git a/docs/.templates/page.html b/docs/.templates/page.html new file mode 100644 index 0000000..f01b9d3 --- /dev/null +++ b/docs/.templates/page.html @@ -0,0 +1,4 @@ +{% extends "layout.html" %} +{% block body %} + {{ body }} +{% endblock %} diff --git a/docs/.templates/sidebarintro.html b/docs/.templates/sidebarintro.html new file mode 100644 index 0000000..fbab09d --- /dev/null +++ b/docs/.templates/sidebarintro.html @@ -0,0 +1,4 @@ + + diff --git a/docs/.templates/sidebarlogo.html b/docs/.templates/sidebarlogo.html new file mode 100644 index 0000000..0e2da8c --- /dev/null +++ b/docs/.templates/sidebarlogo.html @@ -0,0 +1,3 @@ + diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..1ea219c --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,81 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d .build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help clean html web pickle htmlhelp latex changes linkcheck + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " changes to make an overview over all changed/added/deprecated items" + @echo " linkcheck to check all external links for integrity" + +clean: + -rm -rf .build/* + +html: + mkdir -p .build/html .build/doctrees + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) .build/html + @echo + @echo "Build finished. The HTML pages are in .build/html." + +coverage: + mkdir -p .build/coverage .build/doctrees + $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) .build/coverage + @echo + @echo "Build finished." + +pickle: + mkdir -p .build/pickle .build/doctrees + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) .build/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +web: pickle + +json: + mkdir -p .build/json .build/doctrees + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) .build/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + mkdir -p .build/htmlhelp .build/doctrees + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) .build/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in .build/htmlhelp." + +latex: + mkdir -p .build/latex .build/doctrees + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) .build/latex + @echo + @echo "Build finished; the LaTeX files are in .build/latex." + @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ + "run these through (pdf)latex." + +changes: + mkdir -p .build/changes .build/doctrees + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) .build/changes + @echo + @echo "The overview file is in .build/changes." + +linkcheck: + mkdir -p .build/linkcheck .build/doctrees + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) .build/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in .build/linkcheck/output.txt." diff --git a/docs/_ext/applyxrefs.py b/docs/_ext/applyxrefs.py new file mode 100644 index 0000000..deed5d9 --- /dev/null +++ b/docs/_ext/applyxrefs.py @@ -0,0 +1,92 @@ +"""Adds xref targets to the top of files.""" + +import sys +import os + +testing = False + +DONT_TOUCH = ( + './index.txt', +) + + +def target_name(fn): + if fn.endswith('.txt'): + fn = fn[:-4] + return '_' + fn.lstrip('./').replace('/', '-') + + +def process_file(fn, lines): + lines.insert(0, '\n') + lines.insert(0, '.. %s:\n' % target_name(fn)) + try: + f = open(fn, 'w') + except IOError: + print("Can't open %s for writing. Not touching it." % fn) + return + try: + f.writelines(lines) + except IOError: + print("Can't write to %s. Not touching it." % fn) + finally: + f.close() + + +def has_target(fn): + try: + f = open(fn, 'r') + except IOError: + print("Can't open %s. Not touching it." % fn) + return (True, None) + readok = True + try: + lines = f.readlines() + except IOError: + print("Can't read %s. Not touching it." % fn) + readok = False + finally: + f.close() + if not readok: + return (True, None) + + #print fn, len(lines) + if len(lines) < 1: + print("Not touching empty file %s." % fn) + return (True, None) + if lines[0].startswith('.. _'): + return (True, None) + return (False, lines) + + +def main(argv=None): + if argv is None: + argv = sys.argv + + if len(argv) == 1: + argv.extend('.') + + files = [] + for root in argv[1:]: + for (dirpath, dirnames, filenames) in os.walk(root): + files.extend([(dirpath, f) for f in filenames]) + files.sort() + files = [os.path.join(p, fn) for p, fn in files if fn.endswith('.txt')] + #print files + + for fn in files: + if fn in DONT_TOUCH: + print("Skipping blacklisted file %s." % fn) + continue + + target_found, lines = has_target(fn) + if not target_found: + if testing: + print '%s: %s' % (fn, lines[0]), + else: + print "Adding xref to %s" % fn + process_file(fn, lines) + else: + print "Skipping %s: already has a xref" % fn + +if __name__ == '__main__': + sys.exit(main()) diff --git a/docs/_ext/literals_to_xrefs.py b/docs/_ext/literals_to_xrefs.py new file mode 100644 index 0000000..41aa616 --- /dev/null +++ b/docs/_ext/literals_to_xrefs.py @@ -0,0 +1,173 @@ +""" +Runs through a reST file looking for old-style literals, and helps replace them +with new-style references. +""" + +import re +import sys +import shelve + +refre = re.compile(r'``([^`\s]+?)``') + +ROLES = ( + 'attr', + 'class', + "djadmin", + 'data', + 'exc', + 'file', + 'func', + 'lookup', + 'meth', + 'mod', + "djadminopt", + "ref", + "setting", + "term", + "tfilter", + "ttag", + + # special + "skip", +) + +ALWAYS_SKIP = [ + "NULL", + "True", + "False", +] + + +def fixliterals(fname): + data = open(fname).read() + + last = 0 + new = [] + storage = shelve.open("/tmp/literals_to_xref.shelve") + lastvalues = storage.get("lastvalues", {}) + + for m in refre.finditer(data): + + new.append(data[last:m.start()]) + last = m.end() + + line_start = data.rfind("\n", 0, m.start()) + line_end = data.find("\n", m.end()) + prev_start = data.rfind("\n", 0, line_start) + next_end = data.find("\n", line_end + 1) + + # Skip always-skip stuff + if m.group(1) in ALWAYS_SKIP: + new.append(m.group(0)) + continue + + # skip when the next line is a title + next_line = data[m.end():next_end].strip() + if next_line[0] in "!-/:-@[-`{-~" and \ + all(c == next_line[0] for c in next_line): + new.append(m.group(0)) + continue + + sys.stdout.write("\n" + "-" * 80 + "\n") + sys.stdout.write(data[prev_start + 1:m.start()]) + sys.stdout.write(colorize(m.group(0), fg="red")) + sys.stdout.write(data[m.end():next_end]) + sys.stdout.write("\n\n") + + replace_type = None + while replace_type is None: + replace_type = raw_input( + colorize("Replace role: ", fg="yellow")).strip().lower() + if replace_type and replace_type not in ROLES: + replace_type = None + + if replace_type == "": + new.append(m.group(0)) + continue + + if replace_type == "skip": + new.append(m.group(0)) + ALWAYS_SKIP.append(m.group(1)) + continue + + default = lastvalues.get(m.group(1), m.group(1)) + if default.endswith("()") and \ + replace_type in ("class", "func", "meth"): + default = default[:-2] + replace_value = raw_input( + colorize("Text [", fg="yellow") + default + + colorize("]: ", fg="yellow")).strip() + if not replace_value: + replace_value = default + new.append(":%s:`%s`" % (replace_type, replace_value)) + lastvalues[m.group(1)] = replace_value + + new.append(data[last:]) + open(fname, "w").write("".join(new)) + + storage["lastvalues"] = lastvalues + storage.close() + + +def colorize(text='', opts=(), **kwargs): + """ + Returns your text, enclosed in ANSI graphics codes. + + Depends on the keyword arguments 'fg' and 'bg', and the contents of + the opts tuple/list. + + Returns the RESET code if no parameters are given. + + Valid colors: + 'black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white' + + Valid options: + 'bold' + 'underscore' + 'blink' + 'reverse' + 'conceal' + 'noreset' - string will not be auto-terminated with the RESET code + + Examples: + colorize('hello', fg='red', bg='blue', opts=('blink',)) + colorize() + colorize('goodbye', opts=('underscore',)) + print colorize('first line', fg='red', opts=('noreset',)) + print 'this should be red too' + print colorize('and so should this') + print 'this should not be red' + """ + color_names = ('black', 'red', 'green', 'yellow', + 'blue', 'magenta', 'cyan', 'white') + foreground = dict([(color_names[x], '3%s' % x) for x in range(8)]) + background = dict([(color_names[x], '4%s' % x) for x in range(8)]) + + RESET = '0' + opt_dict = {'bold': '1', + 'underscore': '4', + 'blink': '5', + 'reverse': '7', + 'conceal': '8'} + + text = str(text) + code_list = [] + if text == '' and len(opts) == 1 and opts[0] == 'reset': + return '\x1b[%sm' % RESET + for k, v in kwargs.iteritems(): + if k == 'fg': + code_list.append(foreground[v]) + elif k == 'bg': + code_list.append(background[v]) + for o in opts: + if o in opt_dict: + code_list.append(opt_dict[o]) + if 'noreset' not in opts: + text = text + '\x1b[%sm' % RESET + return ('\x1b[%sm' % ';'.join(code_list)) + text + +if __name__ == '__main__': + try: + fixliterals(sys.argv[1]) + except (KeyboardInterrupt, SystemExit): + print diff --git a/docs/_theme/celery/static/celery.css_t b/docs/_theme/celery/static/celery.css_t new file mode 100644 index 0000000..807081a --- /dev/null +++ b/docs/_theme/celery/static/celery.css_t @@ -0,0 +1,401 @@ +/* + * celery.css_t + * ~~~~~~~~~~~~ + * + * :copyright: Copyright 2010 by Armin Ronacher. + * :license: BSD, see LICENSE for details. + */ + +{% set page_width = 940 %} +{% set sidebar_width = 220 %} +{% set body_font_stack = 'Optima, Segoe, "Segoe UI", Candara, Calibri, Arial, sans-serif' %} +{% set headline_font_stack = 'Futura, "Trebuchet MS", Arial, sans-serif' %} +{% set code_font_stack = "'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace" %} + +@import url("basic.css"); + +/* -- page layout ----------------------------------------------------------- */ + +body { + font-family: {{ body_font_stack }}; + font-size: 17px; + background-color: white; + color: #000; + margin: 30px 0 0 0; + padding: 0; +} + +div.document { + width: {{ page_width }}px; + margin: 0 auto; +} + +div.deck { + font-size: 18px; +} + +p.developmentversion { + color: red; +} + +div.related { + width: {{ page_width - 20 }}px; + padding: 5px 10px; + background: #F2FCEE; + margin: 15px auto 15px auto; +} + +div.documentwrapper { + float: left; + width: 100%; +} + +div.bodywrapper { + margin: 0 0 0 {{ sidebar_width }}px; +} + +div.sphinxsidebar { + width: {{ sidebar_width }}px; +} + +hr { + border: 1px solid #B1B4B6; +} + +div.body { + background-color: #ffffff; + color: #3E4349; + padding: 0 30px 0 30px; +} + +img.celerylogo { + padding: 0 0 10px 10px; + float: right; +} + +div.footer { + width: {{ page_width - 15 }}px; + margin: 10px auto 30px auto; + padding-right: 15px; + font-size: 14px; + color: #888; + text-align: right; +} + +div.footer a { + color: #888; +} + +div.sphinxsidebar a { + color: #444; + text-decoration: none; + border-bottom: 1px dashed #DCF0D5; +} + +div.sphinxsidebar a:hover { + border-bottom: 1px solid #999; +} + +div.sphinxsidebar { + font-size: 14px; + line-height: 1.5; +} + +div.sphinxsidebarwrapper { + padding: 7px 10px; +} + +div.sphinxsidebarwrapper p.logo { + padding: 0 0 20px 0; + margin: 0; +} + +div.sphinxsidebar h3, +div.sphinxsidebar h4 { + font-family: {{ headline_font_stack }}; + color: #444; + font-size: 24px; + font-weight: normal; + margin: 0 0 5px 0; + padding: 0; +} + +div.sphinxsidebar h4 { + font-size: 20px; +} + +div.sphinxsidebar h3 a { + color: #444; +} + +div.sphinxsidebar p.logo a, +div.sphinxsidebar h3 a, +div.sphinxsidebar p.logo a:hover, +div.sphinxsidebar h3 a:hover { + border: none; +} + +div.sphinxsidebar p { + color: #555; + margin: 10px 0; +} + +div.sphinxsidebar ul { + margin: 10px 0; + padding: 0; + color: #000; +} + +div.sphinxsidebar input { + border: 1px solid #ccc; + font-family: {{ body_font_stack }}; + font-size: 1em; +} + +/* -- body styles ----------------------------------------------------------- */ + +a { + color: #348613; + text-decoration: underline; +} + +a:hover { + color: #59B833; + text-decoration: underline; +} + +div.body h1, +div.body h2, +div.body h3, +div.body h4, +div.body h5, +div.body h6 { + font-family: {{ headline_font_stack }}; + font-weight: normal; + margin: 30px 0px 10px 0px; + padding: 0; +} + +div.body h1 { margin-top: 0; padding-top: 0; font-size: 200%; } +div.body h2 { font-size: 180%; } +div.body h3 { font-size: 150%; } +div.body h4 { font-size: 130%; } +div.body h5 { font-size: 100%; } +div.body h6 { font-size: 100%; } + +div.body h1 a.toc-backref, +div.body h2 a.toc-backref, +div.body h3 a.toc-backref, +div.body h4 a.toc-backref, +div.body h5 a.toc-backref, +div.body h6 a.toc-backref { + color: inherit!important; + text-decoration: none; +} + +a.headerlink { + color: #ddd; + padding: 0 4px; + text-decoration: none; +} + +a.headerlink:hover { + color: #444; + background: #eaeaea; +} + +div.body p, div.body dd, div.body li { + line-height: 1.4em; +} + +div.admonition { + background: #fafafa; + margin: 20px -30px; + padding: 10px 30px; + border-top: 1px solid #ccc; + border-bottom: 1px solid #ccc; +} + +div.admonition p.admonition-title { + font-family: {{ headline_font_stack }}; + font-weight: normal; + font-size: 24px; + margin: 0 0 10px 0; + padding: 0; + line-height: 1; +} + +div.admonition p.last { + margin-bottom: 0; +} + +div.highlight{ + background-color: white; +} + +dt:target, .highlight { + background: #FAF3E8; +} + +div.note { + background-color: #eee; + border: 1px solid #ccc; +} + +div.seealso { + background-color: #ffc; + border: 1px solid #ff6; +} + +div.topic { + background-color: #eee; +} + +div.warning { + background-color: #ffe4e4; + border: 1px solid #f66; +} + +p.admonition-title { + display: inline; +} + +p.admonition-title:after { + content: ":"; +} + +pre, tt { + font-family: {{ code_font_stack }}; + font-size: 0.9em; +} + +img.screenshot { +} + +tt.descname, tt.descclassname { + font-size: 0.95em; +} + +tt.descname { + padding-right: 0.08em; +} + +img.screenshot { + -moz-box-shadow: 2px 2px 4px #eee; + -webkit-box-shadow: 2px 2px 4px #eee; + box-shadow: 2px 2px 4px #eee; +} + +table.docutils { + border: 1px solid #888; + -moz-box-shadow: 2px 2px 4px #eee; + -webkit-box-shadow: 2px 2px 4px #eee; + box-shadow: 2px 2px 4px #eee; +} + +table.docutils td, table.docutils th { + border: 1px solid #888; + padding: 0.25em 0.7em; +} + +table.field-list, table.footnote { + border: none; + -moz-box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; +} + +table.footnote { + margin: 15px 0; + width: 100%; + border: 1px solid #eee; + background: #fdfdfd; + font-size: 0.9em; +} + +table.footnote + table.footnote { + margin-top: -15px; + border-top: none; +} + +table.field-list th { + padding: 0 0.8em 0 0; +} + +table.field-list td { + padding: 0; +} + +table.footnote td.label { + width: 0px; + padding: 0.3em 0 0.3em 0.5em; +} + +table.footnote td { + padding: 0.3em 0.5em; +} + +dl { + margin: 0; + padding: 0; +} + +dl dd { + margin-left: 30px; +} + +blockquote { + margin: 0 0 0 30px; + padding: 0; +} + +ul { + margin: 10px 0 10px 30px; + padding: 0; +} + +pre { + background: #F0FFEB; + padding: 7px 10px; + margin: 15px 0; + border: 1px solid #C7ECB8; + border-radius: 2px; + -moz-border-radius: 2px; + -webkit-border-radius: 2px; + line-height: 1.3em; +} + +tt { + background: #F0FFEB; + color: #222; + /* padding: 1px 2px; */ +} + +tt.xref, a tt { + background: #F0FFEB; + border-bottom: 1px solid white; +} + +a.reference { + text-decoration: none; + border-bottom: 1px dashed #DCF0D5; +} + +a.reference:hover { + border-bottom: 1px solid #6D4100; +} + +a.footnote-reference { + text-decoration: none; + font-size: 0.7em; + vertical-align: top; + border-bottom: 1px dashed #DCF0D5; +} + +a.footnote-reference:hover { + border-bottom: 1px solid #6D4100; +} + +a:hover tt { + background: #EEE; +} diff --git a/docs/_theme/celery/theme.conf b/docs/_theme/celery/theme.conf new file mode 100644 index 0000000..9ad052c --- /dev/null +++ b/docs/_theme/celery/theme.conf @@ -0,0 +1,5 @@ +[theme] +inherit = basic +stylesheet = celery.css + +[options] diff --git a/docs/changelog.rst b/docs/changelog.rst new file mode 100644 index 0000000..e036942 --- /dev/null +++ b/docs/changelog.rst @@ -0,0 +1,484 @@ +Changes +======= + +py-amqp is fork of amqplib used by Kombu containing additional features and improvements. +The previous amqplib changelog is here: +http://code.google.com/p/py-amqplib/source/browse/CHANGES + +.. _version-1.4.5: + +1.4.5 +===== +:release-date: 2014-04-15 09:00 P.M UTC +:release-by: Ask Solem + +- Can now deserialize more AMQP types. + + Now handles types ``short string``, ``short short int``, + ``short short unsigned int``, ``short int``, ``short unsigned int``, + ``long unsigned int``, ``long long int``, ``long long unsigned int`` + and ``float`` which for some reason was missing, even in the original + amqplib module. + +- SSL: Workaround for Python SSL bug. + + A bug in the python socket library causes ``ssl.read/write()`` + on a closed socket to raise :exc:`AttributeError` instead of + :exc:`IOError`. + + Fix contributed by Craig Jellick. + +- ``Transport.__del_`` now handles errors occurring at late interpreter + shutdown (Issue #36). + +.. _version-1.4.4: + +1.4.4 +===== +:release-date: 2014-03-03 04:00 P.M UTC +:release-by: Ask Solem + +- SSL transport accidentally disconnected after read timeout. + + Fix contributed by Craig Jellick. + +.. _version-1.4.3: + +1.4.3 +===== +:release-date: 2014-02-09 03:00 P.M UTC +:release-by: Ask Solem + +- Fixed bug where more data was requested from the socket + than was actually needed. + + Contributed by Ionel Cristian Mărieș. + +.. _version-1.4.2: + +1.4.2 +===== +:release-date: 2014-01-23 05:00 P.M UTC + +- Heartbeat negotiation would use heartbeat value from server even + if heartbeat disabled (Issue #31). + +.. _version-1.4.1: + +1.4.1 +===== +:release-date: 2014-01-14 09:30 P.M UTC +:release-by: Ask Solem + +- Fixed error occurring when heartbeats disabled. + +.. _version-1.4.0: + +1.4.0 +===== +:release-date: 2014-01-13 03:00 P.M UTC +:release-by: Ask Solem + +- Heartbeat implementation improved (Issue #6). + + The new heartbeat behavior is the same approach as taken by the + RabbitMQ java library. + + This also means that clients should preferably call the ``heartbeat_tick`` + method more frequently (like every second) instead of using the old + ``rate`` argument (which is now ignored). + + - Heartbeat interval is negotiated with the server. + - Some delay is allowed if the heartbeat is late. + - Monotonic time is used to keep track of the heartbeat + instead of relying on the caller to call the checking function + at the right time. + + Contributed by Dustin J. Mitchell. + +- NoneType is now supported in tables and arrays. + + Contributed by Dominik Fässler. + +- SSLTransport: Now handles ``ENOENT``. + + Fix contributed by Adrien Guinet. + +.. _version-1.3.3: + +1.3.3 +===== +:release-date: 2013-11-11 03:30 P.M UTC +:release-by: Ask Solem + +- SSLTransport: Now keeps read buffer if an exception is raised + (Issue #26). + + Fix contributed by Tommie Gannert. + +.. _version-1.3.2: + +1.3.2 +===== +:release-date: 2013-10-29 02:00 P.M UTC +:release-by: Ask Solem + +- Message.channel is now a channel object (not the channel id). + +- Bug in previous version caused the socket to be flagged as disconnected + at EAGAIN/EINTR. + +.. _version-1.3.1: + +1.3.1 +===== +:release-date: 2013-10-24 04:00 P.M UTC +:release-by: Ask Solem + +- Now implements Connection.connected (Issue #22). + +- Fixed bug where ``str(AMQPError)`` did not return string. + +.. _version-1.3.0: + +1.3.0 +===== +:release-date: 2013-09-04 02:39 P.M UTC +:release-by: Ask Solem + +- Now sets ``Message.channel`` on delivery (Issue #12) + + amqplib used to make the channel object available + as ``Message.delivery_info['channel']``, but this was removed + in py-amqp. librabbitmq sets ``Message.channel``, + which is a more reasonable solution in our opinion as that + keeps the delivery info intact. + +- New option to wait for publish confirmations (Issue #3) + + There is now a new Connection ``confirm_publish`` that will + force any ``basic_publish`` call to wait for confirmation. + + Enabling publisher confirms like this degrades performance + considerably, but can be suitable for some applications + and now it's possible by configuration. + +- ``queue_declare`` now returns named tuple of type + :class:`~amqp.protocol.basic_declare_ok_t`. + + Supporting fields: ``queue``, ``message_count``, and + ``consumer_count``. + +- Contents of ``Channel.returned_messages`` is now named tuples. + + Supporting fields: ``reply_code``, ``reply_text``, ``exchange``, + ``routing_key``, and ``message``. + +- Sockets now set to close on exec using the ``FD_CLOEXEC`` flag. + + Currently only supported on platforms supporting this flag, + which does not include Windows. + + Contributed by Tommie Gannert. + +.. _version-1.2.1: + +1.2.1 +===== +:release-date: 2013-08-16 05:30 P.M UTC +:release-by: Ask Solem + +- Adds promise type: :meth:`amqp.utils.promise` + +- Merges fixes from 1.0.x + +.. _version-1.2.0: + +1.2.0 +===== +:release-date: 2012-11-12 04:00 P.M UTC +:release-by: Ask Solem + +- New exception hierarchy: + + - :class:`~amqp.AMQPError` + - :class:`~amqp.ConnectionError` + - :class:`~amqp.RecoverableConnectionError` + - :class:`~amqp.ConsumerCancelled` + - :class:`~amqp.ConnectionForced` + - :class:`~amqp.ResourceError` + - :class:`~IrrecoverableConnectionError` + - :class:`~amqp.ChannelNotOpen` + - :class:`~amqp.FrameError` + - :class:`~amqp.FrameSyntaxError` + - :class:`~amqp.InvalidCommand` + - :class:`~amqp.InvalidPath` + - :class:`~amqp.NotAllowed` + - :class:`~amqp.UnexpectedFrame` + - :class:`~amqp.AMQPNotImplementedError` + - :class:`~amqp.InternalError` + - :class:`~amqp.ChannelError` + - :class:`~RecoverableChannelError` + - :class:`~amqp.ContentTooLarge` + - :class:`~amqp.NoConsumers` + - :class:`~amqp.ResourceLocked` + - :class:`~IrrecoverableChannelError` + - :class:`~amqp.AccessRefused` + - :class:`~amqp.NotFound` + - :class:`~amqp.PreconditionFailed` + + +.. _version-1.1.0: + +1.1.0 +===== +:release-date: 2012-11-08 10:36 P.M UTC +:release-by: Ask Solem + +- No longer supports Python 2.5 + +- Fixed receiving of float table values. + +- Now Supports Python 3 and Python 2.6+ in the same source code. + +- Python 3 related fixes. + +.. _version-1.0.13: + +1.0.13 +====== +:release-date: 2013-07-31 04:00 P.M BST +:release-by: Ask Solem + +- Fixed problems with the SSL transport (Issue #15). + + Fix contributed by Adrien Guinet. + +- Small optimizations + +.. _version-1.0.12: + +1.0.12 +====== +:release-date: 2013-06-25 02:00 P.M BST +:release-by: Ask Solem + +- Fixed another Python 3 compatibility problem. + +.. _version-1.0.11: + +1.0.11 +====== +:release-date: 2013-04-11 06:00 P.M BST +:release-by: Ask Solem + +- Fixed Python 3 incompatibility in ``amqp/transport.py``. + +.. _version-1.0.10: + +1.0.10 +====== +:release-date: 2013-03-21 03:30 P.M UTC +:release-by: Ask Solem + +- Fixed Python 3 incompatibility in ``amqp/serialization.py``. + (Issue #11). + +.. _version-1.0.9: + +1.0.9 +===== +:release-date: 2013-03-08 10:40 A.M UTC +:release-by: Ask Solem + +- Publisher ack callbacks should now work after typo fix (Issue #9). + +- ``channel(explicit_id)`` will now claim that id from the array + of unused channel ids. + +- Fixes Jython compatibility. + +.. _version-1.0.8: + +1.0.8 +===== +:release-date: 2013-02-08 01:00 P.M UTC +:release-by: Ask Solem + +- Fixed SyntaxError on Python 2.5 + +.. _version-1.0.7: + +1.0.7 +===== +:release-date: 2013-02-08 01:00 P.M UTC +:release-by: Ask Solem + +- Workaround for bug on some Python 2.5 installations where (2**32) is 0. + +- Can now serialize the ARRAY type. + + Contributed by Adam Wentz. + +- Fixed tuple format bug in exception (Issue #4). + +.. _version-1.0.6: + +1.0.6 +===== +:release-date: 2012-11-29 01:14 P.M UTC +:release-by: Ask Solem + +- ``Channel.close`` is now ignored if the connection attribute is None. + +.. _version-1.0.5: + +1.0.5 +===== +:release-date: 2012-11-21 04:00 P.M UTC +:release-by: Ask Solem + +- ``Channel.basic_cancel`` is now ignored if the channel was already closed. + +- ``Channel.events`` is now a dict of sets:: + + >>> channel.events['basic_return'].add(on_basic_return) + >>> channel.events['basic_return'].discard(on_basic_return) + +.. _version-1.0.4: + +1.0.4 +===== +:release-date: 2012-11-13 04:00 P.M UTC +:release-by: Ask Solem + +- Fixes Python 2.5 support + +.. _version-1.0.3: + +1.0.3 +===== +:release-date: 2012-11-12 04:00 P.M UTC +:release-by: Ask Solem + +- Now can also handle float in headers/tables when receiving messages. + +- Now uses :class:`array.array` to keep track of unused channel ids. + +- The :data:`~amqp.exceptions.METHOD_NAME_MAP` has been updated for + amqp/0.9.1 and Rabbit extensions. + +- Removed a bunch of accidentally included images. + +.. _version-1.0.2: + +1.0.2 +===== +:release-date: 2012-11-06 05:00 P.M UTC +:release-by: Ask Solem + +- Now supports float values in headers/tables. + +.. _version-1.0.1: + +1.0.1 +===== +:release-date: 2012-11-05 01:00 P.M UTC +:release-by: Ask Solem + +- Connection errors no longer includes :exc:`AttributeError`. + +- Fixed problem with using the SSL transport in a non-blocking context. + + Fix contributed by Mher Movsisyan. + + +.. _version-1.0.0: + +1.0.0 +===== +:release-date: 2012-11-05 01:00 P.M UTC +:release-by: Ask Solem + +- Channels are now restored on channel error, so that the connection does not + have to closed. + +.. _version-0.9.4: + +Version 0.9.4 +============= + +- Adds support for ``exchange_bind`` and ``exchange_unbind``. + + Contributed by Rumyana Neykova + +- Fixed bugs in funtests and demo scripts. + + Contributed by Rumyana Neykova + +.. _version-0.9.3: + +Version 0.9.3 +============= + +- Fixed bug that could cause the consumer to crash when reading + large message payloads asynchronously. + +- Serialization error messages now include the invalid value. + +.. _version-0.9.2: + +Version 0.9.2 +============= + +- Consumer cancel notification support was broken (Issue #1) + + Fix contributed by Andrew Grangaard + +.. _version-0.9.1: + +Version 0.9.1 +============= + +- Supports draining events from multiple channels (``Connection.drain_events``) +- Support for timeouts +- Support for heartbeats + - ``Connection.heartbeat_tick(rate=2)`` must called at regular intervals + (half of the heartbeat value if rate is 2). + - Or some other scheme by using ``Connection.send_heartbeat``. +- Supports RabbitMQ extensions: + - Consumer Cancel Notifications + - by default a cancel results in ``ChannelError`` being raised + - but not if a ``on_cancel`` callback is passed to ``basic_consume``. + - Publisher confirms + - ``Channel.confirm_select()`` enables publisher confirms. + - ``Channel.events['basic_ack'].append(my_callback)`` adds a callback + to be called when a message is confirmed. This callback is then + called with the signature ``(delivery_tag, multiple)``. +- Support for ``basic_return`` +- Uses AMQP 0-9-1 instead of 0-8. + - ``Channel.access_request`` and ``ticket`` arguments to methods + **removed**. + - Supports the ``arguments`` argument to ``basic_consume``. + - ``internal`` argument to ``exchange_declare`` removed. + - ``auto_delete`` argument to ``exchange_declare`` deprecated + - ``insist`` argument to ``Connection`` removed. + - ``Channel.alerts`` has been removed. + - Support for ``Channel.basic_recover_async``. + - ``Channel.basic_recover`` deprecated. +- Exceptions renamed to have idiomatic names: + - ``AMQPException`` -> ``AMQPError`` + - ``AMQPConnectionException`` -> ConnectionError`` + - ``AMQPChannelException`` -> ChannelError`` + - ``Connection.known_hosts`` removed. + - ``Connection`` no longer supports redirects. + - ``exchange`` argument to ``queue_bind`` can now be empty + to use the "default exchange". +- Adds ``Connection.is_alive`` that tries to detect + whether the connection can still be used. +- Adds ``Connection.connection_errors`` and ``.channel_errors``, + a list of recoverable errors. +- Exposes the underlying socket as ``Connection.sock``. +- Adds ``Channel.no_ack_consumers`` to keep track of consumer tags + that set the no_ack flag. +- Slightly better at error recovery diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..1191395 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,127 @@ +# -*- coding: utf-8 -*- + +import sys +import os + +this = os.path.dirname(os.path.abspath(__file__)) + +# If your extensions are in another directory, add it here. If the directory +# is relative to the documentation root, use os.path.abspath to make it +# absolute, like shown here. +sys.path.append(os.path.join(os.pardir, "tests")) +sys.path.append(os.path.join(this, "_ext")) +import amqp + +# General configuration +# --------------------- + +extensions = ['sphinx.ext.autodoc', + 'sphinx.ext.coverage', + 'sphinx.ext.pngmath', + 'sphinx.ext.intersphinx', + 'sphinxcontrib.issuetracker'] + +html_show_sphinx = False + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['.templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'py-amqp' +copyright = u'2009-2012, Ask Solem & Contributors' + +# 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 = ".".join(map(str, amqp.VERSION[0:2])) +# The full version, including alpha/beta/rc tags. +release = amqp.__version__ + +exclude_trees = ['.build'] + +# If true, '()' will be appended to :func: etc. cross-reference text. +add_function_parentheses = True + +intersphinx_mapping = { +} + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'trac' + +# 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'] + +html_use_smartypants = True + +# If false, no module index is generated. +html_use_modindex = True + +# If false, no index is generated. +html_use_index = True + +latex_documents = [ + ('index', 'py-amqp.tex', ur'py-amqp Documentation', + ur'Ask Solem & Contributors', 'manual'), +] + +html_theme = "celery" +html_theme_path = ["_theme"] +html_sidebars = { + 'index': ['sidebarintro.html', 'sourcelink.html', 'searchbox.html'], + '**': ['sidebarlogo.html', 'relations.html', + 'sourcelink.html', 'searchbox.html'], +} + +### Issuetracker + +if False: + issuetracker = "github" + issuetracker_project = "celery/py-amqp" + issuetracker_issue_pattern = r'[Ii]ssue #(\d+)' + +# -- Options for Epub output ------------------------------------------------ + +# Bibliographic Dublin Core info. +epub_title = 'py-amqp Manual, Version 1.0' +epub_author = 'Ask Solem' +epub_publisher = 'Celery Project' +epub_copyright = '2009-2012' + +# The language of the text. It defaults to the language option +# or en if the language is not set. +epub_language = 'en' + +# The scheme of the identifier. Typical schemes are ISBN or URL. +epub_scheme = 'ISBN' + +# The unique identifier of the text. This can be a ISBN number +# or the project homepage. +epub_identifier = 'celeryproject.org' + +# A unique identification for the text. +epub_uid = 'py-amqp Manual, Version 1.0' + +# HTML files that should be inserted before the pages created by sphinx. +# The format is a list of tuples containing the path and title. +#epub_pre_files = [] + +# HTML files shat should be inserted after the pages created by sphinx. +# The format is a list of tuples containing the path and title. +#epub_post_files = [] + +# A list of files that should not be packed into the epub file. +epub_exclude_files = ['search.html'] + + +# The depth of the table of contents in toc.ncx. +epub_tocdepth = 3 diff --git a/docs/includes/intro.txt b/docs/includes/intro.txt new file mode 100644 index 0000000..cc57ce1 --- /dev/null +++ b/docs/includes/intro.txt @@ -0,0 +1,96 @@ +:Version: 1.4.2 +:Web: http://amqp.readthedocs.org/ +:Download: http://pypi.python.org/pypi/amqp/ +:Source: http://github.com/celery/py-amqp/ +:Keywords: amqp, rabbitmq + +About +===== + +This is a fork of amqplib_ which was originally written by Barry Pederson. +It is maintained by the Celery_ project, and used by `kombu`_ as a pure python +alternative when `librabbitmq`_ is not available. + +This library should be API compatible with `librabbitmq`_. + +.. _amqplib: http://pypi.python.org/pypi/amqplib +.. _Celery: http://celeryproject.org/ +.. _kombu: http://kombu.readthedocs.org/ +.. _librabbitmq: http://pypi.python.org/pypi/librabbitmq + +Differences from `amqplib`_ +=========================== + +- Supports draining events from multiple channels (``Connection.drain_events``) +- Support for timeouts +- Channels are restored after channel error, instead of having to close the + connection. +- Support for heartbeats + + - ``Connection.heartbeat_tick(rate=2)`` must called at regular intervals + (half of the heartbeat value if rate is 2). + - Or some other scheme by using ``Connection.send_heartbeat``. +- Supports RabbitMQ extensions: + - Consumer Cancel Notifications + - by default a cancel results in ``ChannelError`` being raised + - but not if a ``on_cancel`` callback is passed to ``basic_consume``. + - Publisher confirms + - ``Channel.confirm_select()`` enables publisher confirms. + - ``Channel.events['basic_ack'].append(my_callback)`` adds a callback + to be called when a message is confirmed. This callback is then + called with the signature ``(delivery_tag, multiple)``. + - Exchange-to-exchange bindings: ``exchange_bind`` / ``exchange_unbind``. + - ``Channel.confirm_select()`` enables publisher confirms. + - ``Channel.events['basic_ack'].append(my_callback)`` adds a callback + to be called when a message is confirmed. This callback is then + called with the signature ``(delivery_tag, multiple)``. +- Support for ``basic_return`` +- Uses AMQP 0-9-1 instead of 0-8. + - ``Channel.access_request`` and ``ticket`` arguments to methods + **removed**. + - Supports the ``arguments`` argument to ``basic_consume``. + - ``internal`` argument to ``exchange_declare`` removed. + - ``auto_delete`` argument to ``exchange_declare`` deprecated + - ``insist`` argument to ``Connection`` removed. + - ``Channel.alerts`` has been removed. + - Support for ``Channel.basic_recover_async``. + - ``Channel.basic_recover`` deprecated. +- Exceptions renamed to have idiomatic names: + - ``AMQPException`` -> ``AMQPError`` + - ``AMQPConnectionException`` -> ConnectionError`` + - ``AMQPChannelException`` -> ChannelError`` + - ``Connection.known_hosts`` removed. + - ``Connection`` no longer supports redirects. + - ``exchange`` argument to ``queue_bind`` can now be empty + to use the "default exchange". +- Adds ``Connection.is_alive`` that tries to detect + whether the connection can still be used. +- Adds ``Connection.connection_errors`` and ``.channel_errors``, + a list of recoverable errors. +- Exposes the underlying socket as ``Connection.sock``. +- Adds ``Channel.no_ack_consumers`` to keep track of consumer tags + that set the no_ack flag. +- Slightly better at error recovery + +Further +======= + +- Differences between AMQP 0.8 and 0.9.1 + + http://www.rabbitmq.com/amqp-0-8-to-0-9-1.html + +- AMQP 0.9.1 Quick Reference + + http://www.rabbitmq.com/amqp-0-9-1-quickref.html + +- RabbitMQ Extensions + + http://www.rabbitmq.com/extensions.html + +- For more information about AMQP, visit + + http://www.amqp.org + +- For other Python client libraries see: + + http://www.rabbitmq.com/devtools.html#python-dev diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..46c8ddd --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,22 @@ +============================================= + amqp - Python AMQP low-level client library +============================================= + +.. include:: includes/intro.txt + +Contents +======== + +.. toctree:: + :maxdepth: 2 + + reference/index + changelog + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/docs/reference/amqp.abstract_channel.rst b/docs/reference/amqp.abstract_channel.rst new file mode 100644 index 0000000..022a4f2 --- /dev/null +++ b/docs/reference/amqp.abstract_channel.rst @@ -0,0 +1,11 @@ +===================================================== + amqp.abstract_channel +===================================================== + +.. contents:: + :local: +.. currentmodule:: amqp.abstract_channel + +.. automodule:: amqp.abstract_channel + :members: + :undoc-members: diff --git a/docs/reference/amqp.basic_message.rst b/docs/reference/amqp.basic_message.rst new file mode 100644 index 0000000..43ee7f7 --- /dev/null +++ b/docs/reference/amqp.basic_message.rst @@ -0,0 +1,11 @@ +===================================================== + amqp.basic_message +===================================================== + +.. contents:: + :local: +.. currentmodule:: amqp.basic_message + +.. automodule:: amqp.basic_message + :members: + :undoc-members: diff --git a/docs/reference/amqp.channel.rst b/docs/reference/amqp.channel.rst new file mode 100644 index 0000000..ff4b471 --- /dev/null +++ b/docs/reference/amqp.channel.rst @@ -0,0 +1,11 @@ +===================================================== + amqp.channel +===================================================== + +.. contents:: + :local: +.. currentmodule:: amqp.channel + +.. automodule:: amqp.channel + :members: + :undoc-members: diff --git a/docs/reference/amqp.connection.rst b/docs/reference/amqp.connection.rst new file mode 100644 index 0000000..4a2e68c --- /dev/null +++ b/docs/reference/amqp.connection.rst @@ -0,0 +1,11 @@ +===================================================== + amqp.connection +===================================================== + +.. contents:: + :local: +.. currentmodule:: amqp.connection + +.. automodule:: amqp.connection + :members: + :undoc-members: diff --git a/docs/reference/amqp.exceptions.rst b/docs/reference/amqp.exceptions.rst new file mode 100644 index 0000000..5975c26 --- /dev/null +++ b/docs/reference/amqp.exceptions.rst @@ -0,0 +1,11 @@ +===================================================== + amqp.exceptions +===================================================== + +.. contents:: + :local: +.. currentmodule:: amqp.exceptions + +.. automodule:: amqp.exceptions + :members: + :undoc-members: diff --git a/docs/reference/amqp.five.rst b/docs/reference/amqp.five.rst new file mode 100644 index 0000000..b4dc083 --- /dev/null +++ b/docs/reference/amqp.five.rst @@ -0,0 +1,11 @@ +===================================================== + amqp.five +===================================================== + +.. contents:: + :local: +.. currentmodule:: amqp.five + +.. automodule:: amqp.five + :members: + :undoc-members: diff --git a/docs/reference/amqp.method_framing.rst b/docs/reference/amqp.method_framing.rst new file mode 100644 index 0000000..8c45982 --- /dev/null +++ b/docs/reference/amqp.method_framing.rst @@ -0,0 +1,11 @@ +===================================================== + amqp.method_framing +===================================================== + +.. contents:: + :local: +.. currentmodule:: amqp.method_framing + +.. automodule:: amqp.method_framing + :members: + :undoc-members: diff --git a/docs/reference/amqp.protocol.rst b/docs/reference/amqp.protocol.rst new file mode 100644 index 0000000..e47a469 --- /dev/null +++ b/docs/reference/amqp.protocol.rst @@ -0,0 +1,11 @@ +===================================================== + amqp.protocol +===================================================== + +.. contents:: + :local: +.. currentmodule:: amqp.protocol + +.. automodule:: amqp.protocol + :members: + :undoc-members: diff --git a/docs/reference/amqp.serialization.rst b/docs/reference/amqp.serialization.rst new file mode 100644 index 0000000..858e297 --- /dev/null +++ b/docs/reference/amqp.serialization.rst @@ -0,0 +1,11 @@ +===================================================== + amqp.serialization +===================================================== + +.. contents:: + :local: +.. currentmodule:: amqp.serialization + +.. automodule:: amqp.serialization + :members: + :undoc-members: diff --git a/docs/reference/amqp.transport.rst b/docs/reference/amqp.transport.rst new file mode 100644 index 0000000..fae34b1 --- /dev/null +++ b/docs/reference/amqp.transport.rst @@ -0,0 +1,11 @@ +===================================================== + amqp.transport +===================================================== + +.. contents:: + :local: +.. currentmodule:: amqp.transport + +.. automodule:: amqp.transport + :members: + :undoc-members: diff --git a/docs/reference/amqp.utils.rst b/docs/reference/amqp.utils.rst new file mode 100644 index 0000000..1d590ae --- /dev/null +++ b/docs/reference/amqp.utils.rst @@ -0,0 +1,11 @@ +===================================================== + amqp.utils +===================================================== + +.. contents:: + :local: +.. currentmodule:: amqp.utils + +.. automodule:: amqp.utils + :members: + :undoc-members: diff --git a/docs/reference/index.rst b/docs/reference/index.rst new file mode 100644 index 0000000..3a19812 --- /dev/null +++ b/docs/reference/index.rst @@ -0,0 +1,23 @@ +.. _apiref: + +=============== + API Reference +=============== + +:Release: |version| +:Date: |today| + +.. toctree:: + :maxdepth: 1 + + amqp.connection + amqp.channel + amqp.basic_message + amqp.exceptions + amqp.abstract_channel + amqp.transport + amqp.method_framing + amqp.protocol + amqp.serialization + amqp.utils + amqp.five diff --git a/docs/templates/readme.txt b/docs/templates/readme.txt new file mode 100644 index 0000000..315cc1c --- /dev/null +++ b/docs/templates/readme.txt @@ -0,0 +1,5 @@ +===================================================================== + Python AMQP 0.9.1 client library +===================================================================== + +.. include:: ../includes/intro.txt diff --git a/extra/README b/extra/README new file mode 100644 index 0000000..6cc994c --- /dev/null +++ b/extra/README @@ -0,0 +1,10 @@ +generate_skeleton_0_8.py was used to create an initial Python +module from the AMQP 0.8 spec file. + +The 0-8 spec file is available from: + + https://svn.amqp.org/amqp/tags/amqp_spec_0.8/amqp.xml + +A skeleton module named 'myskeleton.py' is generated by running + + generate_skeleton_0_8.py amqp.xml myskeleton.py diff --git a/extra/generate_skeleton_0_8.py b/extra/generate_skeleton_0_8.py new file mode 100755 index 0000000..3bf4482 --- /dev/null +++ b/extra/generate_skeleton_0_8.py @@ -0,0 +1,377 @@ +#!/usr/bin/env python +""" +Utility for parsing an AMQP XML spec file +and generating a Python module skeleton. + +This is a fairly ugly program, but it's only intended +to be run once. + +2007-11-10 Barry Pederson + +""" +# Copyright (C) 2007 Barry Pederson +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301 USA + +import sys +import textwrap + +from xml.etree import ElementTree + + +######### +# +# Helper code inspired by +# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/498286 +# described in http://www.agapow.net/programming/python/the-etree-tail-quirk +# +def _textlist(self, _addtail=False): + '''Returns a list of text strings contained within + an element and its sub-elements. + + Helpful for extracting text from prose-oriented XML + (such as XHTML or DocBook). + ''' + result = [] + if (not _addtail) and (self.text is not None): + result.append(self.text) + for elem in self: + result.extend(elem.textlist(True)) + if _addtail and self.tail is not None: + result.append(self.tail) + return result + +# inject the new method into the ElementTree framework +ElementTree._Element.textlist = _textlist + +# +# +######### + +domains = {} +method_name_map = {} + + +def _fixup_method_name(class_element, method_element): + if class_element.attrib['name'] != class_element.attrib['handler']: + prefix = '%s_' % class_element.attrib['name'] + else: + prefix = '' + return ('%s%s' % (prefix, method_element.attrib['name'])).replace('-', '_') + + +def _fixup_field_name(field_element): + result = field_element.attrib['name'].replace(' ', '_') + if result == 'global': + result = 'a_global' + return result + + +def _field_type(field_element): + if 'type' in field_element.attrib: + return field_element.attrib['type'] + if 'domain' in field_element.attrib: + return domains[field_element.attrib['domain']] + + +def _reindent(s, indent, reformat=True): + """ + Remove the existing indentation from each line of a chunk of + text, s, and then prefix each line with a new indent string. + + Also removes trailing whitespace from each line, and leading and + trailing blank lines. + + """ + s = textwrap.dedent(s) + s = s.split('\n') + s = [x.rstrip() for x in s] + while s and (not s[0]): + s = s[1:] + while s and (not s[-1]): + s = s[:-1] + if reformat: + s = '\n'.join(s) + s = textwrap.wrap(s, initial_indent=indent, subsequent_indent=indent) + else: + s = [indent + x for x in s] + return '\n'.join(s) + '\n' + + +def generate_docstr(element, indent='', wrap=None): + print 'Generate objects' + """ + Generate a Python docstr for a given element in the AMQP + XML spec file. The element could be a class or method + + The 'wrap' parameter is an optional chunk of text that's + added to the beginning and end of the resulting docstring. + + """ + result = [] + + txt = element.text and element.text.rstrip() + if txt: + result.append(_reindent(txt, indent)) + result.append(indent) + extra_indent = '' + """ + rules = element.findall('rule') + if rules: + result.append(indent + 'RULES:') + for r in rules: + result.append(indent + 'RULE:') + result.append(indent) + extra_indent = ' ' + rule_docs = generate_docstr(r, indent + ' ') + if rule_docs: + result.append(extra_indent) + result.append(rule_docs) + result.append(indent) + """ + for d in element.findall('doc') + element.findall('rule'): + if d.tag == 'rule': + result.append(indent + 'RULE:') + result.append(indent) + extra_indent = ' ' + d = d.findall('doc')[0] + + docval = ''.join(d.textlist()).rstrip() + if not docval: + continue + reformat = True + if 'name' in d.attrib: + result.append(indent + d.attrib['name'].upper() + ':') + result.append(indent) + extra_indent = ' ' + if d.attrib['name'] == 'grammar': + reformat = False # Don't want re-indenting to mess this up + #else: + # extra_indent = '' + result.append(_reindent(docval, indent + extra_indent, reformat)) + result.append(indent) + + fields = element.findall('field') + if fields: + result.append(indent + 'PARAMETERS:') + for f in fields: + result.append(indent + ' ' + _fixup_field_name(f) + + ': ' + _field_type(f)) + field_docs = generate_docstr(f, indent + ' ') + if field_docs: + result.append(indent) + result.append(field_docs) + result.append(indent) + + if not result: + return None + + if wrap is not None: + result = [wrap] + result + [wrap] + + return '\n'.join(x.rstrip() for x in result) + '\n' + + +def generate_methods(class_element, out): + methods = class_element.findall('method') + methods.sort(key=lambda x: x.attrib['name']) + + for amqp_method in methods: + fields = amqp_method.findall('field') + fieldnames = [_fixup_field_name(x) for x in fields] + + # move any 'ticket' arguments to the end of the method declaration + # so that they can have a default value. + if 'ticket' in fieldnames: + fieldnames = [x for x in fieldnames if x != 'ticket'] + ['ticket'] + + chassis = [x.attrib['name'] for x in amqp_method.findall('chassis')] + if 'server' in chassis: + params = ['self'] + if 'content' in amqp_method.attrib: + params.append('msg') + + out.write(' def %s(%s):\n' % ( + _fixup_method_name(class_element, amqp_method), + ', '.join(params + fieldnames)), + ) + + s = generate_docstr(amqp_method, ' ', ' """') + if s: + out.write(s) + + if fields: + out.write(' args = AMQPWriter()\n') + smf_arg = ', args' + else: + smf_arg = '' + for f in fields: + out.write(' args.write_%s(%s)\n' % ( + _field_type(f), _fixup_field_name(f))) + + if class_element.attrib['name'] == 'connection': + smf_pattern = ' self.send_method_frame(0, (%s, %s)%s)\n' + else: + smf_pattern = ' self.send_method_frame((%s, %s)%s)\n' + + out.write(smf_pattern % (class_element.attrib['index'], + amqp_method.attrib['index'], smf_arg)) + + if 'synchronous' in amqp_method.attrib: + responses = [x.attrib['name'] + for x in amqp_method.findall('response')] + out.write(' return self.wait(allowed_methods=[\n') + for r in responses: + resp = method_name_map[(class_element.attrib['name'], r)] + out.write( + ' (%s, %s), # %s\n' % resp) + out.write(' ])\n') + + out.write('\n\n') + + if 'client' in chassis: + out.write(' def _%s(self, args):\n' % ( + _fixup_method_name(class_element, amqp_method), )) + s = generate_docstr(amqp_method, ' ', ' """') + if s: + out.write(s) + need_pass = True + for f in fields: + out.write(' %s = args.read_%s()\n' % ( + _fixup_field_name(f), _field_type(f))) + need_pass = False + if 'content' in amqp_method.attrib: + out.write(' msg = self.wait()\n') + need_pass = False + if need_pass: + out.write(' pass\n') + out.write('\n\n') + + +def generate_class(spec, class_element, out): + out.write('class %s(object):\n' % ( + class_element.attrib['name'].capitalize(), )) + s = generate_docstr(class_element, ' ', ' """') + if s: + out.write(s) + + generate_methods(class_element, out) + + # + # Generate methods for handled classes + # + for amqp_class in spec.findall('class'): + if (amqp_class.attrib['handler'] == class_element.attrib['name']) and \ + (amqp_class.attrib['name'] != class_element.attrib['name']): + out.write(' #############\n') + out.write(' #\n') + out.write(' # %s\n' % amqp_class.attrib['name'].capitalize()) + out.write(' #\n') + s = generate_docstr(amqp_class, ' # ', ' # ') + if s: + out.write(s) + out.write('\n') + + generate_methods(amqp_class, out) + + +def generate_module(spec, out): + """ + Given an AMQP spec parsed into an xml.etree.ElemenTree, + and a file-like 'out' object to write to, generate + the skeleton of a Python module. + + """ + # + # HACK THE SPEC so that 'access' is handled by + # 'channel' instead of 'connection' + # + for amqp_class in spec.findall('class'): + if amqp_class.attrib['name'] == 'access': + amqp_class.attrib['handler'] = 'channel' + + # + # Build up some helper dictionaries + # + for domain in spec.findall('domain'): + domains[domain.attrib['name']] = domain.attrib['type'] + + for amqp_class in spec.findall('class'): + for amqp_method in amqp_class.findall('method'): + method_name_map[(amqp_class.attrib['name'], + amqp_method.attrib['name'])] = \ + ( + amqp_class.attrib['index'], + amqp_method.attrib['index'], + (amqp_class.attrib['handler'].capitalize() + '.' + + _fixup_method_name(amqp_class, amqp_method)), + ) + + #### Actually generate output + + for amqp_class in spec.findall('class'): + if amqp_class.attrib['handler'] == amqp_class.attrib['name']: + generate_class(spec, amqp_class, out) + + out.write('_METHOD_MAP = {\n') + for amqp_class in spec.findall('class'): + print amqp_class.attrib +# for chassis in amqp_class.findall('chassis'): +# print ' ', chassis.attrib + for amqp_method in amqp_class.findall('method'): +# print ' ', amqp_method.attrib +# for chassis in amqp_method.findall('chassis'): +# print ' ', chassis.attrib + chassis = [x.attrib['name'] + for x in amqp_method.findall('chassis')] + if 'client' in chassis: + out.write(" (%s, %s): (%s, %s._%s),\n" % ( + amqp_class.attrib['index'], + amqp_method.attrib['index'], + amqp_class.attrib['handler'].capitalize(), + amqp_class.attrib['handler'].capitalize(), + _fixup_method_name(amqp_class, amqp_method))) + out.write('}\n\n') + + out.write('_METHOD_NAME_MAP = {\n') + for amqp_class in spec.findall('class'): + for amqp_method in amqp_class.findall('method'): + out.write(" (%s, %s): '%s.%s',\n" % ( + amqp_class.attrib['index'], + amqp_method.attrib['index'], + amqp_class.attrib['handler'].capitalize(), + _fixup_method_name(amqp_class, amqp_method))) + out.write('}\n') + + +def main(argv=None): + if argv is None: + argv = sys.argv + + if len(argv) < 2: + print('Usage: %s []' % argv[0]) + return 1 + + spec = ElementTree.parse(argv[1]) + if len(argv) < 3: + out = sys.stdout + else: + out = open(argv[2], 'w') + + generate_module(spec, out) + +if __name__ == '__main__': + sys.exit(main()) diff --git a/extra/release/bump_version.py b/extra/release/bump_version.py new file mode 100755 index 0000000..87bb6bd --- /dev/null +++ b/extra/release/bump_version.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python + +from __future__ import absolute_import + +import errno +import os +import re +import shlex +import subprocess +import sys + +from contextlib import contextmanager +from tempfile import NamedTemporaryFile + +rq = lambda s: s.strip("\"'") + + +def cmd(*args): + return subprocess.Popen(args, stdout=subprocess.PIPE).communicate()[0] + + +@contextmanager +def no_enoent(): + try: + yield + except OSError as exc: + if exc.errno != errno.ENOENT: + raise + + +class StringVersion(object): + + def decode(self, s): + s = rq(s) + text = "" + major, minor, release = s.split(".") + if not release.isdigit(): + pos = release.index(re.split("\d+", release)[1][0]) + release, text = release[:pos], release[pos:] + return int(major), int(minor), int(release), text + + def encode(self, v): + return ".".join(map(str, v[:3])) + v[3] +to_str = StringVersion().encode +from_str = StringVersion().decode + + +class TupleVersion(object): + + def decode(self, s): + v = list(map(rq, s.split(", "))) + return (tuple(map(int, v[0:3])) + + tuple(["".join(v[3:])])) + + def encode(self, v): + v = list(v) + + def quote(lit): + if isinstance(lit, basestring): + return '"{0}"'.format(lit) + return str(lit) + + if not v[-1]: + v.pop() + return ", ".join(map(quote, v)) + + +class VersionFile(object): + + def __init__(self, filename): + self.filename = filename + self._kept = None + + def _as_orig(self, version): + return self.wb.format(version=self.type.encode(version), + kept=self._kept) + + def write(self, version): + pattern = self.regex + with no_enoent(): + with NamedTemporaryFile() as dest: + with open(self.filename) as orig: + for line in orig: + if pattern.match(line): + dest.write(self._as_orig(version)) + else: + dest.write(line) + os.rename(dest.name, self.filename) + + def parse(self): + pattern = self.regex + gpos = 0 + with open(self.filename) as fh: + for line in fh: + m = pattern.match(line) + if m: + if "?P" in pattern.pattern: + self._kept, gpos = m.groupdict()["keep"], 1 + return self.type.decode(m.groups()[gpos]) + + +class PyVersion(VersionFile): + regex = re.compile(r'^VERSION\s*=\s*\((.+?)\)') + wb = "VERSION = ({version})\n" + type = TupleVersion() + + +class SphinxVersion(VersionFile): + regex = re.compile(r'^:[Vv]ersion:\s*(.+?)$') + wb = ':Version: {version}\n' + type = StringVersion() + + +class CPPVersion(VersionFile): + regex = re.compile(r'^\#\s*define\s*(?P\w*)VERSION\s+(.+)') + wb = '#define {kept}VERSION "{version}"\n' + type = StringVersion() + + +_filetype_to_type = {"py": PyVersion, + "rst": SphinxVersion, + "txt": SphinxVersion, + "c": CPPVersion, + "h": CPPVersion} + + +def filetype_to_type(filename): + _, _, suffix = filename.rpartition(".") + return _filetype_to_type[suffix](filename) + + +def bump(*files, **kwargs): + version = kwargs.get("version") + before_commit = kwargs.get("before_commit") + files = [filetype_to_type(f) for f in files] + versions = [v.parse() for v in files] + current = list(reversed(sorted(versions)))[0] # find highest + current = current.split()[0] # only first sentence + + if version: + next = from_str(version) + else: + major, minor, release, text = current + if text: + raise Exception("Can't bump alpha releases") + next = (major, minor, release + 1, text) + + print("Bump version from {0} -> {1}".format(to_str(current), to_str(next))) + + for v in files: + print(" writing {0.filename!r}...".format(v)) + v.write(next) + + if before_commit: + cmd(*shlex.split(before_commit)) + + print(cmd("git", "commit", "-m", "Bumps version to {0}".format( + to_str(next)), *[f.filename for f in files])) + print(cmd("git", "tag", "v{0}".format(to_str(next)))) + + +def main(argv=sys.argv, version=None, before_commit=None): + if not len(argv) > 1: + print("Usage: distdir [docfile] -- ") + sys.exit(0) + + args = [] + for arg in argv: + if arg.startswith("--before-commit="): + _, before_commit = arg.split('=') + else: + args.append(arg) + + if "--" in args: + c = args.index('--') + version = args[c + 1] + argv = args[:c] + bump(*args[1:], version=version, before_commit=before_commit) + +if __name__ == "__main__": + main() diff --git a/extra/release/sphinx-to-rst.py b/extra/release/sphinx-to-rst.py new file mode 100755 index 0000000..e0c1fd0 --- /dev/null +++ b/extra/release/sphinx-to-rst.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python +import os +import re +import sys + +dirname = "" + +RE_CODE_BLOCK = re.compile(r'.. code-block:: (.+?)\s*$') +RE_INCLUDE = re.compile(r'.. include:: (.+?)\s*$') +RE_REFERENCE = re.compile(r':(.+?):`(.+?)`') + + +def include_file(lines, pos, match): + global dirname + orig_filename = match.groups()[0] + filename = os.path.join(dirname, orig_filename) + fh = open(filename) + try: + old_dirname = dirname + dirname = os.path.dirname(orig_filename) + try: + lines[pos] = sphinx_to_rst(fh) + finally: + dirname = old_dirname + finally: + fh.close() + + +def replace_code_block(lines, pos, match): + lines[pos] = "" + curpos = pos - 1 + # Find the first previous line with text to append "::" to it. + while True: + prev_line = lines[curpos] + if not prev_line.isspace(): + prev_line_with_text = curpos + break + curpos -= 1 + + if lines[prev_line_with_text].endswith(":"): + lines[prev_line_with_text] += ":" + else: + lines[prev_line_with_text] += "::" + +TO_RST_MAP = {RE_CODE_BLOCK: replace_code_block, + RE_REFERENCE: r'``\2``', + RE_INCLUDE: include_file} + + +def _process(lines): + lines = list(lines) # non-destructive + for i, line in enumerate(lines): + for regex, alt in TO_RST_MAP.items(): + if callable(alt): + match = regex.match(line) + if match: + alt(lines, i, match) + line = lines[i] + else: + lines[i] = regex.sub(alt, line) + return lines + + +def sphinx_to_rst(fh): + return "".join(_process(fh)) + + +if __name__ == "__main__": + global dirname + dirname = os.path.dirname(sys.argv[1]) + fh = open(sys.argv[1]) + try: + print(sphinx_to_rst(fh)) + finally: + fh.close() diff --git a/extra/update_comments_from_spec.py b/extra/update_comments_from_spec.py new file mode 100644 index 0000000..677dd5c --- /dev/null +++ b/extra/update_comments_from_spec.py @@ -0,0 +1,76 @@ +from __future__ import absolute_import + +import os +import sys +import re + +default_source_file = os.path.join( + os.path.dirname(__file__), + '../amqp/channel.py', +) + +RE_COMMENTS = re.compile( + '(?Pdef\s+(?P[a-zA-Z0-9_]+)\(.*?\)' + ':\n+\s+""")(?P.*?)(?=""")', + re.MULTILINE | re.DOTALL +) + +USAGE = """\ +Usage: %s []\ +""" + + +def update_comments(comments_file, impl_file, result_file): + text_file = open(impl_file, 'r') + source = text_file.read() + + comments = get_comments(comments_file) + for def_name, comment in comments.items(): + source = replace_comment_per_def( + source, result_file, def_name, comment + ) + + new_file = open(result_file, 'w+') + new_file.write(source) + + +def get_comments(filename): + text_file = open(filename, 'r') + whole_source = text_file.read() + comments = {} + + all_matches = RE_COMMENTS.finditer(whole_source) + for match in all_matches: + comments[match.group('mname')] = match.group('comment') + #print('method: %s \ncomment: %s' % ( + # match.group('mname'), match.group('comment'))) + + return comments + + +def replace_comment_per_def(source, result_file, def_name, new_comment): + regex = ('(?Pdef\s+' + + def_name + + '\(.*?\):\n+\s+""".*?\n).*?(?=""")') + #print('method and comment:' + def_name + new_comment) + result = re.sub(regex, '\g' + new_comment, source, 0, + re.MULTILINE | re.DOTALL) + return result + + +def main(argv=None): + if argv is None: + argv = sys.argv + + if len(argv) < 3: + print(USAGE % argv[0]) + return 1 + + impl_file = default_source_file + if len(argv) >= 4: + impl_file = argv[3] + + update_comments(argv[1], impl_file, argv[2]) + +if __name__ == '__main__': + sys.exit(main()) diff --git a/funtests/run_all.py b/funtests/run_all.py new file mode 100755 index 0000000..8d3fb40 --- /dev/null +++ b/funtests/run_all.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python +"""Run all the unittest modules for amqp""" +# Copyright (C) 2007-2008 Barry Pederson +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + +import unittest + +import settings + +TEST_NAMES = [ + 'test_exceptions', + 'test_serialization', + 'test_basic_message', + 'test_connection', + 'test_channel', + 'test_with', +] + + +def main(): + suite = unittest.TestLoader().loadTestsFromNames(TEST_NAMES) + unittest.TextTestRunner(**settings.test_args).run(suite) + +if __name__ == '__main__': + main() diff --git a/funtests/settings.py b/funtests/settings.py new file mode 100644 index 0000000..d935421 --- /dev/null +++ b/funtests/settings.py @@ -0,0 +1,91 @@ +"""Parse commandline args for running unittests. + +Used by the overall run_all.py script, or the various +indivudial test modules that need settings for connecting +to a broker. + +""" +# Copyright (C) 2007-2008 Barry Pederson +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + +import logging +from optparse import OptionParser + +connect_args = {} +test_args = {'verbosity': 1} + + +def parse_args(): + parser = OptionParser(usage='usage: %prog [options]') + parser.add_option( + '--host', dest='host', + help='AMQP server to connect to (default: %default)', + default='localhost', + ) + parser.add_option( + '-u', '--userid', dest='userid', + help='userid to authenticate as (default: %default)', + default='guest', + ) + parser.add_option( + '-p', '--password', dest='password', + help='password to authenticate with (default: %default)', + default='guest', + ) + parser.add_option( + '--ssl', dest='ssl', action='store_true', + help='Enable SSL (default: not enabled)', + default=False, + ) + parser.add_option( + '--debug', dest='debug', action='store_true', + help='Display debugging output', + default=False, + ) + parser.add_option( + '--port', dest='port', + help='port for the broker', + default=5672, + ) + + parser.add_option( + '-v', '--verbosity', dest='verbose', action='store_true', + help='Run unittests with increased verbosity', + default=False, + ) + + options, args = parser.parse_args() + + if options.debug: + console = logging.StreamHandler() + console.setLevel(logging.DEBUG) + formatter = logging.Formatter( + '%(name)-12s: %(levelname)-8s %(message)s', + ) + console.setFormatter(formatter) + amqp_logger = logging.getLogger('amqp') + amqp_logger.addHandler(console) + amqp_logger.setLevel(logging.DEBUG) + + connect_args['host'] = options.host + connect_args['userid'] = options.userid + connect_args['password'] = options.password + connect_args['ssl'] = options.ssl + + if options.verbose: + test_args['verbosity'] = 2 + +parse_args() diff --git a/funtests/test_basic_message.py b/funtests/test_basic_message.py new file mode 100755 index 0000000..268f360 --- /dev/null +++ b/funtests/test_basic_message.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python +""" +Test the amqp.basic_message module. + +""" +# Copyright (C) 2007-2008 Barry Pederson +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + +from datetime import datetime +from decimal import Decimal + +import unittest + +try: + import cPickle as pickle +except ImportError: + import pickle # noqa + + +import settings + +from amqp.basic_message import Message + + +class TestBasicMessage(unittest.TestCase): + + def check_proplist(self, msg): + """Check roundtrip processing of a single object""" + raw_properties = msg._serialize_properties() + + new_msg = Message() + new_msg._load_properties(raw_properties) + new_msg.body = msg.body + + self.assertEqual(msg, new_msg) + + def test_eq(self): + msg = Message('hello', content_type='text/plain') + self.assertNotEqual(msg, None) + + # + # Make sure that something that looks vaguely + # like a Message doesn't raise an Attribute + # error when compared to a Message, and instead + # returns False + # + class FakeMsg(object): + pass + + fake_msg = FakeMsg() + fake_msg.properties = {'content_type': 'text/plain'} + + self.assertNotEqual(msg, fake_msg) + + def test_pickle(self): + msg = Message( + 'some body' * 200000, + content_type='text/plain', + content_encoding='utf-8', + application_headers={ + 'foo': 7, 'bar': 'baz', 'd2': {'foo2': 'xxx', 'foo3': -1}, + }, + delivery_mode=1, + priority=7, + ) + + msg2 = pickle.loads(pickle.dumps(msg)) + self.assertEqual(msg, msg2) + + def test_roundtrip(self): + """Check round-trip processing of content-properties.""" + self.check_proplist(Message()) + + self.check_proplist(Message(content_type='text/plain')) + + self.check_proplist(Message( + content_type='text/plain', + content_encoding='utf-8', + application_headers={ + 'foo': 7, 'bar': 'baz', 'd2': {'foo2': 'xxx', 'foo3': -1}, + }, + delivery_mode=1, + priority=7, + )) + + self.check_proplist(Message( + application_headers={ + 'regular': datetime(2007, 11, 12, 12, 34, 56), + 'dst': datetime(2007, 7, 12, 12, 34, 56), + }, + )) + + n = datetime.now() + # AMQP only does timestamps to 1-second resolution + n = n.replace(microsecond=0) + self.check_proplist(Message( + application_headers={'foo': n}), + ) + + self.check_proplist(Message( + application_headers={'foo': Decimal('10.1')}), + ) + + self.check_proplist(Message( + application_headers={'foo': Decimal('-1987654.193')}), + ) + + self.check_proplist(Message( + timestamp=datetime(1980, 1, 2, 3, 4, 6)), + ) + + +def main(): + suite = unittest.TestLoader().loadTestsFromTestCase(TestBasicMessage) + unittest.TextTestRunner(**settings.test_args).run(suite) + + +if __name__ == '__main__': + main() diff --git a/funtests/test_channel.py b/funtests/test_channel.py new file mode 100755 index 0000000..10b860f --- /dev/null +++ b/funtests/test_channel.py @@ -0,0 +1,317 @@ +#!/usr/bin/env python +""" +Test amqp.channel module + +""" +# Copyright (C) 2007-2008 Barry Pederson +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + +import sys +import unittest + +import settings + +from amqp import ChannelError, Connection, Message, FrameSyntaxError + + +class TestChannel(unittest.TestCase): + + def setUp(self): + self.conn = Connection(**settings.connect_args) + self.ch = self.conn.channel() + + def tearDown(self): + self.ch.close() + self.conn.close() + + def test_defaults(self): + """Test how a queue defaults to being bound to an AMQP default + exchange, and how publishing defaults to the default exchange, and + basic_get defaults to getting from the most recently declared queue, + and queue_delete defaults to deleting the most recently declared + queue.""" + msg = Message( + 'funtest message', + content_type='text/plain', + application_headers={'foo': 7, 'bar': 'baz'}, + ) + + qname, _, _ = self.ch.queue_declare() + self.ch.basic_publish(msg, routing_key=qname) + + msg2 = self.ch.basic_get(no_ack=True) + self.assertEqual(msg, msg2) + + n = self.ch.queue_purge() + self.assertEqual(n, 0) + + n = self.ch.queue_delete() + self.assertEqual(n, 0) + + def test_encoding(self): + my_routing_key = 'funtest.test_queue' + + qname, _, _ = self.ch.queue_declare() + self.ch.queue_bind(qname, 'amq.direct', routing_key=my_routing_key) + + # + # No encoding, body passed through unchanged + # + msg = Message('hello world') + self.ch.basic_publish(msg, 'amq.direct', routing_key=my_routing_key) + msg2 = self.ch.basic_get(qname, no_ack=True) + if sys.version_info[0] < 3: + self.assertFalse(hasattr(msg2, 'content_encoding')) + self.assertTrue(isinstance(msg2.body, str)) + self.assertEqual(msg2.body, 'hello world') + + # + # Default UTF-8 encoding of unicode body, returned as unicode + # + msg = Message(u'hello world') + self.ch.basic_publish(msg, 'amq.direct', routing_key=my_routing_key) + msg2 = self.ch.basic_get(qname, no_ack=True) + self.assertEqual(msg2.content_encoding, 'UTF-8') + self.assertTrue(isinstance(msg2.body, unicode)) + self.assertEqual(msg2.body, u'hello world') + + # + # Explicit latin_1 encoding, still comes back as unicode + # + msg = Message(u'hello world', content_encoding='latin_1') + self.ch.basic_publish(msg, 'amq.direct', routing_key=my_routing_key) + msg2 = self.ch.basic_get(qname, no_ack=True) + self.assertEqual(msg2.content_encoding, 'latin_1') + self.assertTrue(isinstance(msg2.body, unicode)) + self.assertEqual(msg2.body, u'hello world') + + # + # Plain string with specified encoding comes back as unicode + # + msg = Message('hello w\xf6rld', content_encoding='latin_1') + self.ch.basic_publish(msg, 'amq.direct', routing_key=my_routing_key) + msg2 = self.ch.basic_get(qname, no_ack=True) + self.assertEqual(msg2.content_encoding, 'latin_1') + self.assertTrue(isinstance(msg2.body, unicode)) + self.assertEqual(msg2.body, u'hello w\u00f6rld') + + # + # Plain string (bytes in Python 3.x) with bogus encoding + # + + # don't really care about latin_1, just want bytes + test_bytes = u'hello w\xd6rld'.encode('latin_1') + msg = Message(test_bytes, content_encoding='I made this up') + self.ch.basic_publish(msg, 'amq.direct', routing_key=my_routing_key) + msg2 = self.ch.basic_get(qname, no_ack=True) + self.assertEqual(msg2.content_encoding, 'I made this up') + self.assertTrue(isinstance(msg2.body, bytes)) + self.assertEqual(msg2.body, test_bytes) + + # + # Turn off auto_decode for remaining tests + # + self.ch.auto_decode = False + + # + # Unicode body comes back as utf-8 encoded str + # + msg = Message(u'hello w\u00f6rld') + self.ch.basic_publish(msg, 'amq.direct', routing_key=my_routing_key) + msg2 = self.ch.basic_get(qname, no_ack=True) + self.assertEqual(msg2.content_encoding, 'UTF-8') + self.assertTrue(isinstance(msg2.body, bytes)) + self.assertEqual(msg2.body, u'hello w\xc3\xb6rld'.encode('latin_1')) + + # + # Plain string with specified encoding stays plain string + # + msg = Message('hello w\xf6rld', content_encoding='latin_1') + self.ch.basic_publish(msg, 'amq.direct', routing_key=my_routing_key) + msg2 = self.ch.basic_get(qname, no_ack=True) + self.assertEqual(msg2.content_encoding, 'latin_1') + self.assertTrue(isinstance(msg2.body, bytes)) + self.assertEqual(msg2.body, u'hello w\xf6rld'.encode('latin_1')) + + # + # Explicit latin_1 encoding, comes back as str + # + msg = Message(u'hello w\u00f6rld', content_encoding='latin_1') + self.ch.basic_publish(msg, 'amq.direct', routing_key=my_routing_key) + msg2 = self.ch.basic_get(qname, no_ack=True) + self.assertEqual(msg2.content_encoding, 'latin_1') + self.assertTrue(isinstance(msg2.body, bytes)) + self.assertEqual(msg2.body, u'hello w\xf6rld'.encode('latin_1')) + + def test_queue_delete_empty(self): + self.assertFalse( + self.ch.queue_delete('bogus_queue_that_does_not_exist') + ) + + def test_survives_channel_error(self): + with self.assertRaises(ChannelError): + self.ch.queue_declare('krjqheewq_bogus', passive=True) + self.ch.queue_declare('funtest_survive') + self.ch.queue_declare('funtest_survive', passive=True) + self.assertEqual( + 0, self.ch.queue_delete('funtest_survive'), + ) + + def test_invalid_header(self): + """ + Test sending a message with an unserializable object in the header + + http://code.google.com/p/py-amqplib/issues/detail?id=17 + + """ + qname, _, _ = self.ch.queue_declare() + + msg = Message(application_headers={'test': object()}) + + self.assertRaises( + FrameSyntaxError, self.ch.basic_publish, msg, routing_key=qname, + ) + + def test_large(self): + """ + Test sending some extra large messages. + + """ + qname, _, _ = self.ch.queue_declare() + + for multiplier in [100, 1000, 10000]: + msg = Message( + 'funtest message' * multiplier, + content_type='text/plain', + application_headers={'foo': 7, 'bar': 'baz'}, + ) + + self.ch.basic_publish(msg, routing_key=qname) + + msg2 = self.ch.basic_get(no_ack=True) + self.assertEqual(msg, msg2) + + def test_publish(self): + self.ch.exchange_declare('funtest.fanout', 'fanout', auto_delete=True) + + msg = Message( + 'funtest message', + content_type='text/plain', + application_headers={'foo': 7, 'bar': 'baz'}, + ) + + self.ch.basic_publish(msg, 'funtest.fanout') + + def test_queue(self): + my_routing_key = 'funtest.test_queue' + msg = Message( + 'funtest message', + content_type='text/plain', + application_headers={'foo': 7, 'bar': 'baz'}, + ) + + qname, _, _ = self.ch.queue_declare() + self.ch.queue_bind(qname, 'amq.direct', routing_key=my_routing_key) + + self.ch.basic_publish(msg, 'amq.direct', routing_key=my_routing_key) + + msg2 = self.ch.basic_get(qname, no_ack=True) + self.assertEqual(msg, msg2) + + def test_unbind(self): + my_routing_key = 'funtest.test_queue' + + qname, _, _ = self.ch.queue_declare() + self.ch.queue_bind(qname, 'amq.direct', routing_key=my_routing_key) + self.ch.queue_unbind(qname, 'amq.direct', routing_key=my_routing_key) + + def test_basic_return(self): + self.ch.exchange_declare('funtest.fanout', 'fanout', auto_delete=True) + + msg = Message( + 'funtest message', + content_type='text/plain', + application_headers={'foo': 7, 'bar': 'baz'}) + + self.ch.basic_publish(msg, 'funtest.fanout') + self.ch.basic_publish(msg, 'funtest.fanout', mandatory=True) + self.ch.basic_publish(msg, 'funtest.fanout', mandatory=True) + self.ch.basic_publish(msg, 'funtest.fanout', mandatory=True) + self.ch.close() + + # + # 3 of the 4 messages we sent should have been returned + # + self.assertEqual(self.ch.returned_messages.qsize(), 3) + + def test_exchange_bind(self): + """Test exchange binding. + Network configuration is as follows (-> is forwards to : + source_exchange -> dest_exchange -> queue + The test checks that once the message is publish to the + destination exchange(funtest.topic_dest) it is delivered to the queue. + """ + + test_routing_key = 'unit_test__key' + dest_exchange = 'funtest.topic_dest_bind' + source_exchange = 'funtest.topic_source_bind' + + self.ch.exchange_declare(dest_exchange, 'topic', auto_delete=True) + self.ch.exchange_declare(source_exchange, 'topic', auto_delete=True) + + qname, _, _ = self.ch.queue_declare() + self.ch.exchange_bind(destination=dest_exchange, + source=source_exchange, + routing_key=test_routing_key) + + self.ch.queue_bind(qname, dest_exchange, + routing_key=test_routing_key) + + msg = Message('funtest message', + content_type='text/plain', + application_headers={'foo': 7, 'bar': 'baz'}) + + self.ch.basic_publish(msg, source_exchange, + routing_key=test_routing_key) + + msg2 = self.ch.basic_get(qname, no_ack=True) + self.assertEqual(msg, msg2) + + def test_exchange_unbind(self): + dest_exchange = 'funtest.topic_dest_unbind' + source_exchange = 'funtest.topic_source_unbind' + test_routing_key = 'unit_test__key' + + self.ch.exchange_declare(dest_exchange, + 'topic', auto_delete=True) + self.ch.exchange_declare(source_exchange, + 'topic', auto_delete=True) + + self.ch.exchange_bind(destination=dest_exchange, + source=source_exchange, + routing_key=test_routing_key) + + self.ch.exchange_unbind(destination=dest_exchange, + source=source_exchange, + routing_key=test_routing_key) + + +def main(): + suite = unittest.TestLoader().loadTestsFromTestCase(TestChannel) + unittest.TextTestRunner(**settings.test_args).run(suite) + +if __name__ == '__main__': + main() diff --git a/funtests/test_connection.py b/funtests/test_connection.py new file mode 100755 index 0000000..31938be --- /dev/null +++ b/funtests/test_connection.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python +"""Test amqp.connection module""" +# Copyright (C) 2007-2008 Barry Pederson +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + +import gc +import unittest + +import settings + + +from amqp import Connection + + +class TestConnection(unittest.TestCase): + + def setUp(self): + self.conn = Connection(**settings.connect_args) + + def tearDown(self): + if self.conn: + self.conn.close() + + def test_channel(self): + ch = self.conn.channel(1) + self.assertEqual(ch.channel_id, 1) + + ch2 = self.conn.channel() + self.assertNotEqual(ch2.channel_id, 1) + + ch.close() + ch2.close() + + def test_close(self): + """Make sure we've broken various references when closing + channels and connections, to help with GC.""" + # + # Create a channel and make sure it's linked as we'd expect + # + ch = self.conn.channel() + self.assertEqual(1 in self.conn.channels, True) + self.assertEqual(ch.connection, self.conn) + self.assertEqual(ch.is_open, True) + + # + # Close the channel and make sure the references are broken + # that we expect. + # + ch.close() + self.assertEqual(ch.connection, None) + self.assertEqual(1 in self.conn.channels, False) + self.assertEqual(ch.callbacks, {}) + self.assertEqual(ch.is_open, False) + + # + # Close the connection and make sure the references we expect + # are gone. + # + self.conn.close() + self.assertEqual(self.conn.connection, None) + self.assertEqual(self.conn.channels, None) + + def test_gc_closed(self): + """Make sure we've broken various references when closing + channels and connections, to help with GC. + + gc.garbage: http://docs.python.org/library/gc.html#gc.garbage + "A list of objects which the collector found to be + unreachable but could not be freed (uncollectable objects)." + + """ + unreachable_before = len(gc.garbage) + # + # Create a channel and make sure it's linked as we'd expect + # + self.conn.channel() + self.assertEqual(1 in self.conn.channels, True) + + # + # Close the connection and make sure the references we expect + # are gone. + # + self.conn.close() + + gc.collect() + gc.collect() + gc.collect() + self.assertEqual(unreachable_before, len(gc.garbage)) + + def test_gc_forget(self): + """Make sure the connection gets gc'ed when there is no more + references to it.""" + unreachable_before = len(gc.garbage) + + ch = self.conn.channel() + self.assertEqual(1 in self.conn.channels, True) + + # remove all the references + self.conn = None + del(ch) + + gc.collect() + gc.collect() + gc.collect() + self.assertEqual(unreachable_before, len(gc.garbage)) + + +def main(): + suite = unittest.TestLoader().loadTestsFromTestCase(TestConnection) + unittest.TextTestRunner(**settings.test_args).run(suite) + + +if __name__ == '__main__': + main() diff --git a/funtests/test_exceptions.py b/funtests/test_exceptions.py new file mode 100755 index 0000000..f418135 --- /dev/null +++ b/funtests/test_exceptions.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python +""" +Test amqp.exceptions module + +""" +# Copyright (C) 2007-2008 Barry Pederson +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + +import unittest + +import settings + +from amqp.exceptions import AMQPError + + +class TestException(unittest.TestCase): + def test_exception(self): + exc = AMQPError('My Error', (10, 10), reply_code=7) + self.assertEqual(exc.reply_code, 7) + self.assertEqual(exc.reply_text, 'My Error') + self.assertEqual(exc.method_sig, (10, 10)) + self.assertEqual( + exc.args, + (7, 'My Error', (10, 10), 'Connection.start'), + ) + + +def main(): + suite = unittest.TestLoader().loadTestsFromTestCase(TestException) + unittest.TextTestRunner(**settings.test_args).run(suite) + + +if __name__ == '__main__': + main() diff --git a/funtests/test_serialization.py b/funtests/test_serialization.py new file mode 100755 index 0000000..ce70438 --- /dev/null +++ b/funtests/test_serialization.py @@ -0,0 +1,411 @@ +#!/usr/bin/env python +""" +Test amqp.serialization, checking conversions +between byte streams and higher level objects. + +""" +# Copyright (C) 2007-2008 Barry Pederson +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + +from datetime import datetime +from decimal import Decimal +from random import randint +import sys +import unittest + +import settings + +from amqp.serialization import ( + AMQPReader, AMQPWriter, GenericContent, FrameSyntaxError, +) + + +class TestSerialization(unittest.TestCase): + + if sys.version_info[0] >= 3: + + def assertEqualBinary(self, b, s): + """ + Helper for Py3k Compatibility + + """ + self.assertEqual(b, s.encode('latin_1')) + else: + assertEqualBinary = unittest.TestCase.assertEqual + + def test_empty_writer(self): + w = AMQPWriter() + self.assertEqual(w.getvalue(), bytes()) + + # + # Bits + # + def test_single_bit(self): + for val, check in [(True, '\x01'), (False, '\x00')]: + w = AMQPWriter() + w.write_bit(val) + s = w.getvalue() + + self.assertEqualBinary(s, check) + + r = AMQPReader(s) + self.assertEqual(r.read_bit(), val) + + def test_multiple_bits(self): + w = AMQPWriter() + w.write_bit(True) + w.write_bit(True) + w.write_bit(False) + w.write_bit(True) + s = w.getvalue() + + self.assertEqualBinary(s, '\x0b') + + r = AMQPReader(s) + self.assertEqual(r.read_bit(), True) + self.assertEqual(r.read_bit(), True) + self.assertEqual(r.read_bit(), False) + self.assertEqual(r.read_bit(), True) + + def test_multiple_bits2(self): + """ + Check bits mixed with non-bits + """ + w = AMQPWriter() + w.write_bit(True) + w.write_bit(True) + w.write_bit(False) + w.write_octet(10) + w.write_bit(True) + s = w.getvalue() + + self.assertEqualBinary(s, '\x03\x0a\x01') + + r = AMQPReader(s) + self.assertEqual(r.read_bit(), True) + self.assertEqual(r.read_bit(), True) + self.assertEqual(r.read_bit(), False) + self.assertEqual(r.read_octet(), 10) + self.assertEqual(r.read_bit(), True) + + def test_multiple_bits3(self): + """ + Check bit groups that span multiple bytes + """ + w = AMQPWriter() + + # Spit out 20 bits + for i in range(10): + w.write_bit(True) + w.write_bit(False) + + s = w.getvalue() + + self.assertEqualBinary(s, '\x55\x55\x05') + + r = AMQPReader(s) + for i in range(10): + self.assertEqual(r.read_bit(), True) + self.assertEqual(r.read_bit(), False) + + # + # Octets + # + def test_octet(self): + for val in range(256): + w = AMQPWriter() + w.write_octet(val) + s = w.getvalue() + self.assertEqualBinary(s, chr(val)) + + r = AMQPReader(s) + self.assertEqual(r.read_octet(), val) + + def test_octet_invalid(self): + w = AMQPWriter() + self.assertRaises(FrameSyntaxError, w.write_octet, -1) + + def test_octet_invalid2(self): + w = AMQPWriter() + self.assertRaises(FrameSyntaxError, w.write_octet, 256) + + # + # Shorts + # + def test_short(self): + for i in range(256): + val = randint(0, 65535) + w = AMQPWriter() + w.write_short(val) + s = w.getvalue() + + r = AMQPReader(s) + self.assertEqual(r.read_short(), val) + + def test_short_invalid(self): + w = AMQPWriter() + self.assertRaises(FrameSyntaxError, w.write_short, -1) + + def test_short_invalid2(self): + w = AMQPWriter() + self.assertRaises(FrameSyntaxError, w.write_short, 65536) + + # + # Longs + # + def test_long(self): + for i in range(256): + val = randint(0, 4294967295) + w = AMQPWriter() + w.write_long(val) + s = w.getvalue() + + r = AMQPReader(s) + self.assertEqual(r.read_long(), val) + + def test_long_invalid(self): + w = AMQPWriter() + self.assertRaises(FrameSyntaxError, w.write_long, -1) + + def test_long_invalid2(self): + w = AMQPWriter() + self.assertRaises(FrameSyntaxError, w.write_long, 4294967296) + + # + # LongLongs + # + def test_longlong(self): + for i in range(256): + val = randint(0, (2 ** 64) - 1) + w = AMQPWriter() + w.write_longlong(val) + s = w.getvalue() + + r = AMQPReader(s) + self.assertEqual(r.read_longlong(), val) + + def test_longlong_invalid(self): + w = AMQPWriter() + self.assertRaises(FrameSyntaxError, w.write_longlong, -1) + + def test_longlong_invalid2(self): + w = AMQPWriter() + self.assertRaises(FrameSyntaxError, w.write_longlong, 2 ** 64) + + # + # Shortstr + # + def test_empty_shortstr(self): + w = AMQPWriter() + w.write_shortstr('') + s = w.getvalue() + + self.assertEqualBinary(s, '\x00') + + r = AMQPReader(s) + self.assertEqual(r.read_shortstr(), '') + + def test_shortstr(self): + w = AMQPWriter() + w.write_shortstr('hello') + s = w.getvalue() + self.assertEqualBinary(s, '\x05hello') + + r = AMQPReader(s) + self.assertEqual(r.read_shortstr(), 'hello') + + def test_shortstr_unicode(self): + w = AMQPWriter() + w.write_shortstr(u'hello') + s = w.getvalue() + self.assertEqualBinary(s, '\x05hello') + + r = AMQPReader(s) + self.assertEqual(r.read_shortstr(), u'hello') + + def test_long_shortstr(self): + w = AMQPWriter() + self.assertRaises(FrameSyntaxError, w.write_shortstr, 'x' * 256) + + def test_long_shortstr_unicode(self): + w = AMQPWriter() + self.assertRaises(FrameSyntaxError, w.write_shortstr, u'\u0100' * 128) + + # + # Longstr + # + + def test_empty_longstr(self): + w = AMQPWriter() + w.write_longstr('') + s = w.getvalue() + + self.assertEqualBinary(s, '\x00\x00\x00\x00') + + r = AMQPReader(s) + self.assertEqual(r.read_longstr(), '') + + def test_longstr(self): + val = 'a' * 512 + w = AMQPWriter() + w.write_longstr(val) + s = w.getvalue() + + self.assertEqualBinary(s, '\x00\x00\x02\x00' + ('a' * 512)) + + r = AMQPReader(s) + self.assertEqual(r.read_longstr(), str(val)) + + def test_longstr_unicode(self): + val = u'a' * 512 + w = AMQPWriter() + w.write_longstr(val) + s = w.getvalue() + + self.assertEqualBinary(s, '\x00\x00\x02\x00' + ('a' * 512)) + + r = AMQPReader(s) + self.assertEqual(r.read_longstr(), val) + + # + # Table + # + def test_table_empty(self): + val = {} + w = AMQPWriter() + w.write_table(val) + s = w.getvalue() + + self.assertEqualBinary(s, '\x00\x00\x00\x00') + + r = AMQPReader(s) + self.assertEqual(r.read_table(), val) + + def test_table(self): + val = {'foo': 7} + w = AMQPWriter() + w.write_table(val) + s = w.getvalue() + + self.assertEqualBinary(s, '\x00\x00\x00\x09\x03fooI\x00\x00\x00\x07') + + r = AMQPReader(s) + self.assertEqual(r.read_table(), val) + + def test_table_invalid(self): + """ + Check that an un-serializable table entry raises a ValueError + + """ + val = {'test': object()} + w = AMQPWriter() + self.assertRaises(FrameSyntaxError, w.write_table, val) + + def test_table_multi(self): + val = { + 'foo': 7, + 'bar': Decimal('123345.1234'), + 'baz': 'this is some random string I typed', + 'ubaz': u'And something in unicode', + 'dday_aniv': datetime(1994, 6, 6), + 'nothing': None, + 'more': { + 'abc': -123, + 'def': 'hello world', + 'now': datetime(2007, 11, 11, 21, 14, 31), + 'qty': Decimal('-123.45'), + 'blank': {}, + 'extra': { + 'deeper': 'more strings', + 'nums': -12345678, + }, + } + } + + w = AMQPWriter() + w.write_table(val) + s = w.getvalue() + + r = AMQPReader(s) + self.assertEqual(r.read_table(), val) + + # + # Array + # + def test_array_from_list(self): + val = [1, 'foo', None] + w = AMQPWriter() + w.write_array(val) + s = w.getvalue() + + self.assertEqualBinary( + s, '\x00\x00\x00\x0EI\x00\x00\x00\x01S\x00\x00\x00\x03fooV', + ) + + r = AMQPReader(s) + self.assertEqual(r.read_array(), val) + + def test_array_from_tuple(self): + val = (1, 'foo', None) + w = AMQPWriter() + w.write_array(val) + s = w.getvalue() + + self.assertEqualBinary( + s, '\x00\x00\x00\x0EI\x00\x00\x00\x01S\x00\x00\x00\x03fooV', + ) + + r = AMQPReader(s) + self.assertEqual(r.read_array(), list(val)) + + def test_table_with_array(self): + val = { + 'foo': 7, + 'bar': Decimal('123345.1234'), + 'baz': 'this is some random string I typed', + 'blist': [1, 2, 3], + 'nlist': [1, [2, 3, 4]], + 'ndictl': {'nfoo': 8, 'nblist': [5, 6, 7]} + } + + w = AMQPWriter() + w.write_table(val) + s = w.getvalue() + + r = AMQPReader(s) + self.assertEqual(r.read_table(), val) + + # + # GenericContent + # + def test_generic_content_eq(self): + msg_1 = GenericContent(dummy='foo') + msg_2 = GenericContent(dummy='foo') + msg_3 = GenericContent(dummy='bar') + + self.assertEqual(msg_1, msg_1) + self.assertEqual(msg_1, msg_2) + self.assertNotEqual(msg_1, msg_3) + self.assertNotEqual(msg_1, None) + + +def main(): + suite = unittest.TestLoader().loadTestsFromTestCase(TestSerialization) + unittest.TextTestRunner(**settings.test_args).run(suite) + + +if __name__ == '__main__': + main() diff --git a/funtests/test_with.py b/funtests/test_with.py new file mode 100644 index 0000000..b479794 --- /dev/null +++ b/funtests/test_with.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python +""" +Test support for 'with' statements + +""" +# Copyright (C) 2009 Barry Pederson +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + +import unittest + +import settings + +from amqp import Connection, Message + + +class TestChannel(unittest.TestCase): + + def test_with(self): + with Connection(**settings.connect_args) as conn: + self.assertEqual(conn.transport is None, False) + + with conn.channel(1) as ch: + self.assertEqual(1 in conn.channels, True) + + # + # Do something with the channel + # + ch.exchange_declare('unittest.fanout', 'fanout', + auto_delete=True) + + msg = Message( + 'unittest message', + content_type='text/plain', + application_headers={'foo': 7, 'bar': 'baz'}, + ) + + ch.basic_publish(msg, 'unittest.fanout') + + # + # check that the channel was closed + # + self.assertEqual(1 in conn.channels, False) + self.assertEqual(ch.is_open, False) + + # + # Check that the connection was closed + # + self.assertEqual(conn.transport, None) + + +def main(): + suite = unittest.TestLoader().loadTestsFromTestCase(TestChannel) + unittest.TextTestRunner(**settings.test_args).run(suite) + + +if __name__ == '__main__': + main() diff --git a/requirements/docs.txt b/requirements/docs.txt new file mode 100644 index 0000000..0f57c43 --- /dev/null +++ b/requirements/docs.txt @@ -0,0 +1,2 @@ +Sphinx +sphinxcontrib-issuetracker>=0.9 diff --git a/requirements/pkgutils.txt b/requirements/pkgutils.txt new file mode 100644 index 0000000..2e522fc --- /dev/null +++ b/requirements/pkgutils.txt @@ -0,0 +1,5 @@ +paver +flake8 +flakeplus +tox +Sphinx-PyPI-upload diff --git a/requirements/test.txt b/requirements/test.txt new file mode 100644 index 0000000..7554730 --- /dev/null +++ b/requirements/test.txt @@ -0,0 +1,5 @@ +unittest2>=0.4.0 +nose +nose-cover3 +coverage>=3.0 +mock diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..861a9f5 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,5 @@ +[egg_info] +tag_build = +tag_date = 0 +tag_svn_revision = 0 + diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..a1cfed5 --- /dev/null +++ b/setup.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +try: + from setuptools import setup, find_packages + from setuptools.command.test import test +except ImportError: + raise + from ez_setup import use_setuptools + use_setuptools() + from setuptools import setup, find_packages # noqa + from setuptools.command.test import test # noqa + +import os +import sys +import codecs + +if sys.version_info < (2, 6): + raise Exception('amqp requires Python 2.6 or higher.') + +NAME = 'amqp' +entrypoints = {} +extra = {} + +# -*- Classifiers -*- + +classes = """ + Development Status :: 5 - Production/Stable + Programming Language :: Python + Programming Language :: Python :: 2 + Programming Language :: Python :: 2.6 + Programming Language :: Python :: 2.7 + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.0 + Programming Language :: Python :: 3.1 + Programming Language :: Python :: 3.2 + Programming Language :: Python :: 3.3 + License :: OSI Approved :: GNU Library or \ +Lesser General Public License (LGPL) + Intended Audience :: Developers + License :: OSI Approved :: BSD License + Operating System :: OS Independent +""" +classifiers = [s.strip() for s in classes.split('\n') if s] + +# -*- Distribution Meta -*- + +import re +re_meta = re.compile(r'__(\w+?)__\s*=\s*(.*)') +re_vers = re.compile(r'VERSION\s*=\s*\((.*?)\)') +re_doc = re.compile(r'^"""(.+?)"""') +rq = lambda s: s.strip("\"'") + + +def add_default(m): + attr_name, attr_value = m.groups() + return ((attr_name, rq(attr_value)), ) + + +def add_version(m): + v = list(map(rq, m.groups()[0].split(', '))) + return (('VERSION', '.'.join(v[0:3]) + ''.join(v[3:])), ) + + +def add_doc(m): + return (('doc', m.groups()[0]), ) + +pats = {re_meta: add_default, + re_vers: add_version, + re_doc: add_doc} +here = os.path.abspath(os.path.dirname(__file__)) +with open(os.path.join(here, 'amqp/__init__.py')) as meta_fh: + meta = {} + for line in meta_fh: + if line.strip() == '# -eof meta-': + break + for pattern, handler in pats.items(): + m = pattern.match(line.strip()) + if m: + meta.update(handler(m)) + +# -*- Installation Requires -*- + +py_version = sys.version_info +is_jython = sys.platform.startswith('java') +is_pypy = hasattr(sys, 'pypy_version_info') + + +def strip_comments(l): + return l.split('#', 1)[0].strip() + + +def reqs(f): + return filter(None, [strip_comments(l) for l in open( + os.path.join(os.getcwd(), 'requirements', f)).readlines()]) + +install_requires = [] + +# -*- Tests Requires -*- + +tests_require = reqs('test.txt') + +# -*- Long Description -*- + +if os.path.exists('README.rst'): + long_description = codecs.open('README.rst', 'r', 'utf-8').read() +else: + long_description = 'See http://pypi.python.org/pypi/amqp' + +# -*- Entry Points -*- # + +# -*- %%% -*- + +setup( + name=NAME, + version=meta['VERSION'], + description=meta['doc'], + author=meta['author'], + author_email=meta['contact'], + maintainer=meta['maintainer'], + url=meta['homepage'], + platforms=['any'], + license='LGPL', + packages=find_packages(exclude=['ez_setup', 'tests', 'tests.*']), + zip_safe=False, + install_requires=install_requires, + tests_require=tests_require, + test_suite='nose.collector', + classifiers=classifiers, + entry_points=entrypoints, + long_description=long_description, + **extra)