stomper-0.4.3/0000755000076500000240000000000013453103756013462 5ustar oisinstaff00000000000000stomper-0.4.3/LICENSE0000644000076500000240000002607513453100403014463 0ustar oisinstaff00000000000000Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. stomper-0.4.3/MANIFEST.in0000644000076500000240000000004313453100403015177 0ustar oisinstaff00000000000000include README.rst include LICENSE stomper-0.4.3/PKG-INFO0000644000076500000240000002651713453103756014572 0ustar oisinstaff00000000000000Metadata-Version: 1.1 Name: stomper Version: 0.4.3 Summary: This is a transport neutral client implementation of the STOMP protocol. Home-page: https://github.com/oisinmulvihill/stomper Author: Oisin Mulvihill Author-email: oisin.mulvihilli@gmail.com License: http://www.apache.org/licenses/LICENSE-2.0 Description: ======= Stomper ======= .. contents:: :Author: Oisin Mulvihill :Contributors: - Micheal Twomey, Ricky Iacovou , - Arfrever Frehtes Taifersar Arahesis , - Niki Pore , - Simon Chopin, - Ian Weller , - Daniele Varrazzo - Ralph Bean - Lumír 'Frenzy' Balhar - Ralph Bean (https://github.com/ralphbean) - https://github.com/pgajdos Introduction ------------ This is a python client implementation of the STOMP protocol. The client is attempting to be transport layer neutral. This module provides functions to create and parse STOMP messages in a programmatic fashion. The messages can be easily generated and parsed, however its up to the user to do the sending and receiving. I've looked at the stomp client by Jason R. Briggs. I've based some of the 'function to message' generation on how his client does it. The client can be found at the follow address however it isn't a dependency. - `stompy `_ In testing this library I run against ActiveMQ project. The server runs in java, however its fairly standalone and easy to set up. The projects page is here: - `ActiveMQ `_ Source Code ----------- The code can be accessed via git on github. Further details can be found here: - `Stomper `_ Examples -------- Basic Usage ~~~~~~~~~~~ To see some basic code usage example see ``example/stomper_usage.py``. The unit test ``tests/teststomper.py`` illustrates how to use all aspects of the code. Receive/Sender ~~~~~~~~~~~~~~ The example ``receiver.py`` and ``sender.py`` show how messages and generated and then transmitted using the twisted framework. Other frameworks could be used instead. The examples also demonstrate the state machine I used to determine a response to received messages. I've also included ``stompbuffer-rx.py`` and ``stompbuffer-tx.py`` as examples of using the new stompbuffer module contributed by Ricky Iacovou. Release Process --------------- Submit a pull request with tests if possible. I'll review and submit. All tests must pass. I tend to run against python3.7 nowadays. I will then increment the version, add attribute and then release to pypi if all is good. Help Oisin remember the relase process:: # clean env for release: mkvirtualenv --clear -p python3.7 stomper # setup and run all tests: python setup.py develop python setup.py test # Build and release to test.pypi.org first: pip install twine python setup.py sdist bdist_wheel twine upload --repository-url https://test.pypi.org/legacy/ dist/* # On success twine upload dist/* # Commit any changes and tag the release git tag X.Y.Z Supported STOMP Versions ------------------------ 1.1 ~~~ This is the default version of the of STOMP used in stomper versions 0.3.x. * https://stomp.github.io/stomp-specification-1.1.html 1.0 ~~~ This is no longer the default protocol version. To use it you can import it as follows:: import stomper.stomp_10 as stomper This is the default version used in stomper version 0.2.x. * https://stomp.github.io/stomp-specification-1.0.html Version History --------------- 0.4.3 ~~~~~ Added missing attribution to contributors section and messed up 0.4.2 release to new pypi. 0.4.2 ~~~~~ Thanks to https://github.com/pgajdos for contributing a fix to include the license in the distribution. OM: Added minor fix to support test.pypi.org uploading before releasing. Document my release process to help me next time around. 0.4.1 ~~~~~ Thanks to Ralph Bean (https://github.com/ralphbean) contributing a fix to setup.py and utf-8 encoding under python3. 0.4.0 ~~~~~ Thanks to Lumír 'Frenzy' Balhar (https://github.com/frenzymadness) contributing python3 support. 0.3.0 ~~~~~ This release makes STOMP v1.1 the default protocol. To stick with STOMP v1.0 you can continue to use stomper v0.2.9 or change the import in your code to:: import stomper.stomp_10 as stomper **Note** Any fixes to STOMP v1.0 will only be applied to version >= 0.3. 0.2.9 ~~~~~ Thanks to Ralph Bean for contributing the new protocol 1.1 support: * https://github.com/oisinmulvihill/stomper/issues/6 * https://github.com/oisinmulvihill/stomper/pull/7 0.2.8 ~~~~~ Thanks to Daniele Varrazzo for contributing the fixes: https://github.com/oisinmulvihill/stomper/pull/4 * Fixed newline prepended to messages without transaction id https://github.com/oisinmulvihill/stomper/pull/5 * Fixed reST syntax. Extension changed to allow github to render it properly. Also changed the source url in the readme. 0.2.7 ~~~~~ I forgot to add a MANIFEST.in which makes sure README.md is present. Without this pip install fails: https://github.com/oisinmulvihill/stomper/issues/3. Thanks to Ian Weller for noticing this. I've also added in the fix suggested by Arfrever https://github.com/oisinmulvihill/stomper/issues/1. 0.2.6 ~~~~~ Add contributed fixes from Simon Chopin. He corrected many spelling mistakes throughout the code base. I've also made the README.md the main 0.2.5 ~~~~~ Add the contributed fix for issue #14 by Niki Pore. The issue was reported by Roger Hoover. This removes the extra line ending which can cause problems. 0.2.4 ~~~~~ OM: A minor release fixing the problem whereby uuid would be installed on python2.5+. It is not needed after python2.4 as it comes with python. Arfrever Frehtes Taifersar Arahesis contributed the fix for this. 0.2.3 ~~~~~ OM: I've fixed issue #9 with the example code. All messages are sent and received correctly. 0.2.2 ~~~~~ - Applied patch from esteve.fernandez to resolve "Issue 4: First Message not received" in the example code (http://code.google.com/p/stomper/issues/detail?id=4&can=1). - I've (Oisin) updated the examples to use twisted's line receiver and got it to "detect" complete stomp messages. The old example would not work if a large amount of data was streamed. In this case dataReceived would be called with all the chunks of a message. This means that it would not be correct for it to attempt to unpack and react until the whole message has been received. Using twisted's line receiver looking for the \x00 works like a charm for this. This release integrates the bug fixes and the optional stompbuffer contributed by Ricky Iacovou: - Removed the trailing '\n\n' inserted by Frame.pack(). I believe that adding this is incorrect, for the following reasons: http://stomp.codehaus.org/Protocol gives the example:: CONNECT login: passcode: ^@ and comments, "the body is empty in this case". This gives the impression that the body is *exactly* defined as "the bytes, if any, between the '\n\n' at the end of the header and the null byte". This works for both binary and ASCII payloads: if I want to send a string without a newline, I should be able to, in which case the body should look like:: this is a string without a newline^@ ... and the receiver should deal with this. This impression is reinforced by the fact that ActiveMQ will complain if you supply a content-length header with any other byte count than that described above. I am also unsure about the newline after the null byte as nothing in the protocol says that there should be a newline after the null byte. Much of the code in StompBuffer actively expects it to be there, but I suspect that *relying* on a frame ending '\x00\n' may well limit compatibility. It's not an issue with Stomper-to-Stomper communication, of course, as the sender puts it, the receiver accepts it, and ActiveMQ happily sends it along. - StompBuffer has had a few fixes; most notably, a fix that prevents a content-length "header" in the *body* from being picked up and used (!). The biggest change is a new method, syncBuffer(), which allows a corrupted buffer to recover from the corruption. Note that I've never actually *seen* the buffer corruption when using Twisted, but the thought occurred to me that a single corrupt buffer could hang the entire message handling process. - Fixed the typo "NO_REPONSE_NEEDED". I've changed it to NO_RESPONSE_NEEDED, but kept the old variable for backwards compatibility; - I've also modified the string format in send() to include the '\n\n' between the header and the body, which I think is missing (it currently has only one '\n'). - Added CONNECTED to VALID_COMMANDS so syncBuffer() does not decide these messages are bogus. - Added new unit test file teststompbuffer which covers the new functionality. Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Programming Language :: Python stomper-0.4.3/README.rst0000644000076500000240000002112713453103644015150 0ustar oisinstaff00000000000000======= Stomper ======= .. contents:: :Author: Oisin Mulvihill :Contributors: - Micheal Twomey, Ricky Iacovou , - Arfrever Frehtes Taifersar Arahesis , - Niki Pore , - Simon Chopin, - Ian Weller , - Daniele Varrazzo - Ralph Bean - Lumír 'Frenzy' Balhar - Ralph Bean (https://github.com/ralphbean) - https://github.com/pgajdos Introduction ------------ This is a python client implementation of the STOMP protocol. The client is attempting to be transport layer neutral. This module provides functions to create and parse STOMP messages in a programmatic fashion. The messages can be easily generated and parsed, however its up to the user to do the sending and receiving. I've looked at the stomp client by Jason R. Briggs. I've based some of the 'function to message' generation on how his client does it. The client can be found at the follow address however it isn't a dependency. - `stompy `_ In testing this library I run against ActiveMQ project. The server runs in java, however its fairly standalone and easy to set up. The projects page is here: - `ActiveMQ `_ Source Code ----------- The code can be accessed via git on github. Further details can be found here: - `Stomper `_ Examples -------- Basic Usage ~~~~~~~~~~~ To see some basic code usage example see ``example/stomper_usage.py``. The unit test ``tests/teststomper.py`` illustrates how to use all aspects of the code. Receive/Sender ~~~~~~~~~~~~~~ The example ``receiver.py`` and ``sender.py`` show how messages and generated and then transmitted using the twisted framework. Other frameworks could be used instead. The examples also demonstrate the state machine I used to determine a response to received messages. I've also included ``stompbuffer-rx.py`` and ``stompbuffer-tx.py`` as examples of using the new stompbuffer module contributed by Ricky Iacovou. Release Process --------------- Submit a pull request with tests if possible. I'll review and submit. All tests must pass. I tend to run against python3.7 nowadays. I will then increment the version, add attribute and then release to pypi if all is good. Help Oisin remember the relase process:: # clean env for release: mkvirtualenv --clear -p python3.7 stomper # setup and run all tests: python setup.py develop python setup.py test # Build and release to test.pypi.org first: pip install twine python setup.py sdist bdist_wheel twine upload --repository-url https://test.pypi.org/legacy/ dist/* # On success twine upload dist/* # Commit any changes and tag the release git tag X.Y.Z Supported STOMP Versions ------------------------ 1.1 ~~~ This is the default version of the of STOMP used in stomper versions 0.3.x. * https://stomp.github.io/stomp-specification-1.1.html 1.0 ~~~ This is no longer the default protocol version. To use it you can import it as follows:: import stomper.stomp_10 as stomper This is the default version used in stomper version 0.2.x. * https://stomp.github.io/stomp-specification-1.0.html Version History --------------- 0.4.3 ~~~~~ Added missing attribution to contributors section and messed up 0.4.2 release to new pypi. 0.4.2 ~~~~~ Thanks to https://github.com/pgajdos for contributing a fix to include the license in the distribution. OM: Added minor fix to support test.pypi.org uploading before releasing. Document my release process to help me next time around. 0.4.1 ~~~~~ Thanks to Ralph Bean (https://github.com/ralphbean) contributing a fix to setup.py and utf-8 encoding under python3. 0.4.0 ~~~~~ Thanks to Lumír 'Frenzy' Balhar (https://github.com/frenzymadness) contributing python3 support. 0.3.0 ~~~~~ This release makes STOMP v1.1 the default protocol. To stick with STOMP v1.0 you can continue to use stomper v0.2.9 or change the import in your code to:: import stomper.stomp_10 as stomper **Note** Any fixes to STOMP v1.0 will only be applied to version >= 0.3. 0.2.9 ~~~~~ Thanks to Ralph Bean for contributing the new protocol 1.1 support: * https://github.com/oisinmulvihill/stomper/issues/6 * https://github.com/oisinmulvihill/stomper/pull/7 0.2.8 ~~~~~ Thanks to Daniele Varrazzo for contributing the fixes: https://github.com/oisinmulvihill/stomper/pull/4 * Fixed newline prepended to messages without transaction id https://github.com/oisinmulvihill/stomper/pull/5 * Fixed reST syntax. Extension changed to allow github to render it properly. Also changed the source url in the readme. 0.2.7 ~~~~~ I forgot to add a MANIFEST.in which makes sure README.md is present. Without this pip install fails: https://github.com/oisinmulvihill/stomper/issues/3. Thanks to Ian Weller for noticing this. I've also added in the fix suggested by Arfrever https://github.com/oisinmulvihill/stomper/issues/1. 0.2.6 ~~~~~ Add contributed fixes from Simon Chopin. He corrected many spelling mistakes throughout the code base. I've also made the README.md the main 0.2.5 ~~~~~ Add the contributed fix for issue #14 by Niki Pore. The issue was reported by Roger Hoover. This removes the extra line ending which can cause problems. 0.2.4 ~~~~~ OM: A minor release fixing the problem whereby uuid would be installed on python2.5+. It is not needed after python2.4 as it comes with python. Arfrever Frehtes Taifersar Arahesis contributed the fix for this. 0.2.3 ~~~~~ OM: I've fixed issue #9 with the example code. All messages are sent and received correctly. 0.2.2 ~~~~~ - Applied patch from esteve.fernandez to resolve "Issue 4: First Message not received" in the example code (http://code.google.com/p/stomper/issues/detail?id=4&can=1). - I've (Oisin) updated the examples to use twisted's line receiver and got it to "detect" complete stomp messages. The old example would not work if a large amount of data was streamed. In this case dataReceived would be called with all the chunks of a message. This means that it would not be correct for it to attempt to unpack and react until the whole message has been received. Using twisted's line receiver looking for the \x00 works like a charm for this. This release integrates the bug fixes and the optional stompbuffer contributed by Ricky Iacovou: - Removed the trailing '\n\n' inserted by Frame.pack(). I believe that adding this is incorrect, for the following reasons: http://stomp.codehaus.org/Protocol gives the example:: CONNECT login: passcode: ^@ and comments, "the body is empty in this case". This gives the impression that the body is *exactly* defined as "the bytes, if any, between the '\n\n' at the end of the header and the null byte". This works for both binary and ASCII payloads: if I want to send a string without a newline, I should be able to, in which case the body should look like:: this is a string without a newline^@ ... and the receiver should deal with this. This impression is reinforced by the fact that ActiveMQ will complain if you supply a content-length header with any other byte count than that described above. I am also unsure about the newline after the null byte as nothing in the protocol says that there should be a newline after the null byte. Much of the code in StompBuffer actively expects it to be there, but I suspect that *relying* on a frame ending '\x00\n' may well limit compatibility. It's not an issue with Stomper-to-Stomper communication, of course, as the sender puts it, the receiver accepts it, and ActiveMQ happily sends it along. - StompBuffer has had a few fixes; most notably, a fix that prevents a content-length "header" in the *body* from being picked up and used (!). The biggest change is a new method, syncBuffer(), which allows a corrupted buffer to recover from the corruption. Note that I've never actually *seen* the buffer corruption when using Twisted, but the thought occurred to me that a single corrupt buffer could hang the entire message handling process. - Fixed the typo "NO_REPONSE_NEEDED". I've changed it to NO_RESPONSE_NEEDED, but kept the old variable for backwards compatibility; - I've also modified the string format in send() to include the '\n\n' between the header and the body, which I think is missing (it currently has only one '\n'). - Added CONNECTED to VALID_COMMANDS so syncBuffer() does not decide these messages are bogus. - Added new unit test file teststompbuffer which covers the new functionality. stomper-0.4.3/lib/0000755000076500000240000000000013453103756014230 5ustar oisinstaff00000000000000stomper-0.4.3/lib/stomper/0000755000076500000240000000000013453103756015721 5ustar oisinstaff00000000000000stomper-0.4.3/lib/stomper/__init__.py0000644000076500000240000000050613453100403020015 0ustar oisinstaff00000000000000from __future__ import absolute_import from .stomp_11 import ( Engine, Frame, FrameError, abort, ack, nack, begin, commit, connect, disconnect, send, subscribe, unpack_frame, unsubscribe, VALID_COMMANDS, NO_RESPONSE_NEEDED, NO_REPONSE_NEEDED, NULL, ) stomper-0.4.3/lib/stomper/examples/0000755000076500000240000000000013453103756017537 5ustar oisinstaff00000000000000stomper-0.4.3/lib/stomper/examples/__init__.py0000644000076500000240000000025613453100403021635 0ustar oisinstaff00000000000000""" The example package contains various examples of how to use the stomper module. (c) Oisin Mulvihill, 2007-07-26. License: http://www.apache.org/licenses/LICENSE-2.0 """stomper-0.4.3/lib/stomper/examples/receiver.py0000644000076500000240000000651613453100403021707 0ustar oisinstaff00000000000000""" A simple twisted STOMP message receiver server. (c) Oisin Mulvihill, 2007-07-26. License: http://www.apache.org/licenses/LICENSE-2.0 """ from __future__ import print_function from builtins import str import uuid import logging import itertools from twisted.internet import reactor from twisted.internet.protocol import Protocol, ReconnectingClientFactory import stomper stomper.utils.log_init(logging.DEBUG) DESTINATION="/topic/inbox" class MyStomp(stomper.Engine): def __init__(self, username='', password=''): super(MyStomp, self).__init__() self.username = username self.password = password self.log = logging.getLogger("receiver") self.receiverId = str(uuid.uuid4()) def connect(self): """Generate the STOMP connect command to get a session. """ return stomper.connect(self.username, self.password) def connected(self, msg): """Once I've connected I want to subscribe to my the message queue. """ super(MyStomp, self).connected(msg) self.log.info("receiverId <%s> connected: session %s" % (self.receiverId, msg['headers']['session'])) f = stomper.Frame() f.unpack(stomper.subscribe(DESTINATION)) return f.pack() def ack(self, msg): """Process the message and determine what to do with it. """ self.log.info("receiverId <%s> Received: <%s> " % (self.receiverId, msg['body'])) #return super(MyStomp, self).ack(msg) return stomper.NO_REPONSE_NEEDED class StompProtocol(Protocol): def __init__(self, username='', password=''): self.sm = MyStomp(username, password) def connectionMade(self): """Register with the stomp server. """ cmd = self.sm.connect() self.transport.write(cmd) def dataReceived(self, data): """Data received, react to it and respond if needed. """ # print "receiver dataReceived: <%s>" % data msg = stomper.unpack_frame(data) returned = self.sm.react(msg) # print "receiver returned <%s>" % returned if returned: self.transport.write(returned) class StompClientFactory(ReconnectingClientFactory): # Will be set up before the factory is created. username, password = '', '' def startedConnecting(self, connector): """Started to connect. """ def buildProtocol(self, addr): """Transport level connected now create the communication protocol. """ return StompProtocol(self.username, self.password) def clientConnectionLost(self, connector, reason): """Lost connection """ print('Lost connection. Reason:', reason) def clientConnectionFailed(self, connector, reason): """Connection failed """ print('Connection failed. Reason:', reason) ReconnectingClientFactory.clientConnectionFailed(self, connector, reason) def start(host='localhost', port=61613, username='', password=''): """Start twisted event loop and the fun should begin... """ StompClientFactory.username = username StompClientFactory.password = password reactor.connectTCP(host, port, StompClientFactory()) reactor.run() if __name__ == '__main__': start() stomper-0.4.3/lib/stomper/examples/sender.py0000644000076500000240000001003013453100403021345 0ustar oisinstaff00000000000000""" A simple twisted STOMP message sender. (c) Oisin Mulvihill, 2007-07-26. License: http://www.apache.org/licenses/LICENSE-2.0 """ from __future__ import print_function from builtins import str import uuid import logging import itertools from twisted.internet import reactor from twisted.internet.task import LoopingCall from twisted.internet.protocol import Protocol, ReconnectingClientFactory import stomper stomper.utils.log_init(logging.DEBUG) DESTINATION="/topic/inbox" class StompProtocol(Protocol, stomper.Engine): def __init__(self, username='', password=''): stomper.Engine.__init__(self) self.username = username self.password = password self.counter = itertools.count(0) self.log = logging.getLogger("sender") self.senderID = str(uuid.uuid4()) def connected(self, msg): """Once I've connected I want to subscribe to my the message queue. """ stomper.Engine.connected(self, msg) self.log.info("senderID:%s Connected: session %s." % ( self.senderID, msg['headers']['session']) ) # I originally called loopingCall(self.send) directly, however it turns # out that we had not fully subscribed. This meant we did not receive # out our first send message. I fixed this by using reactor.callLater # # def setup_looping_call(): lc = LoopingCall(self.send) lc.start(2) reactor.callLater(1, setup_looping_call) f = stomper.Frame() f.unpack(stomper.subscribe(DESTINATION)) # ActiveMQ specific headers: # # prevent the messages we send coming back to us. f.headers['activemq.noLocal'] = 'true' return f.pack() def ack(self, msg): """Processes the received message. I don't need to generate an ack message. """ self.log.info("senderID:%s Received: %s " % (self.senderID, msg['body'])) return stomper.NO_REPONSE_NEEDED def send(self): """Send out a hello message periodically. """ counter = next(self.counter) self.log.info("senderID:%s Saying hello (%d)." % (self.senderID, counter)) f = stomper.Frame() f.unpack(stomper.send(DESTINATION, '(%d) hello there from senderID:<%s>' % ( counter, self.senderID ))) # ActiveMQ specific headers: # #f.headers['persistent'] = 'true' self.transport.write(f.pack()) def connectionMade(self): """Register with stomp server. """ cmd = stomper.connect(self.username, self.password) self.transport.write(cmd) def dataReceived(self, data): """Data received, react to it and respond if needed. """ #print "sender dataReceived: <%s>" % data msg = stomper.unpack_frame(data) returned = self.react(msg) #print "sender returned: <%s>" % returned if returned: self.transport.write(returned) class StompClientFactory(ReconnectingClientFactory): # Will be set up before the factory is created. username, password = '', '' def buildProtocol(self, addr): return StompProtocol(self.username, self.password) def clientConnectionLost(self, connector, reason): """Lost connection """ print('Lost connection. Reason:', reason) def clientConnectionFailed(self, connector, reason): """Connection failed """ print('Connection failed. Reason:', reason) ReconnectingClientFactory.clientConnectionFailed(self, connector, reason) def start(host='localhost', port=61613, username='', password=''): """Start twisted event loop and the fun should begin... """ StompClientFactory.username = username StompClientFactory.password = password reactor.connectTCP(host, port, StompClientFactory()) reactor.run() if __name__ == '__main__': start() stomper-0.4.3/lib/stomper/examples/stompbuffer-rx.py0000644000076500000240000000646013453100403023064 0ustar oisinstaff00000000000000""" A simple twisted STOMP message receiver server. (c) Oisin Mulvihill, 2007-07-26. License: http://www.apache.org/licenses/LICENSE-2.0 """ from __future__ import print_function import logging from twisted.internet import reactor from twisted.internet.protocol import Protocol, ReconnectingClientFactory import stomper from stomper import stompbuffer stomper.utils.log_init(logging.DEBUG) DESTINATION="/topic/inbox" class MyStomp(stomper.Engine): def __init__(self, username='', password=''): super(MyStomp, self).__init__() self.username = username self.password = password self.log = logging.getLogger("receiver") def connect(self): """Generate the STOMP connect command to get a session. """ return stomper.connect(self.username, self.password) def connected(self, msg): """Once I've connected I want to subscribe to my the message queue. """ super(MyStomp, self).connected(msg) self.log.info("connected: session %s" % msg['headers']['session']) f = stomper.Frame() f.unpack(stomper.subscribe(DESTINATION)) return f.pack() def ack(self, msg): """Process the message and determine what to do with it. """ self.log.info("RECEIVER - received: %s " % msg['body']) # return super(MyStomp, self).ack(msg) return stomper.NO_REPONSE_NEEDED class StompProtocol(Protocol): def __init__(self, username='', password=''): self.sm = MyStomp(username, password) self.stompBuffer = stompbuffer.StompBuffer() def connectionMade(self): """Register with the stomp server. """ cmd = self.sm.connect() self.transport.write(cmd) def dataReceived(self, data): """Use stompbuffer to determine when a complete message has been received. """ self.stompBuffer.appendData(data) while True: msg = self.stompBuffer.getOneMessage() if msg is None: break returned = self.sm.react(msg) if returned: self.transport.write(returned) class StompClientFactory(ReconnectingClientFactory): # Will be set up before the factory is created. username, password = '', '' def startedConnecting(self, connector): """Started to connect. """ def buildProtocol(self, addr): """Transport level connected now create the communication protocol. """ return StompProtocol(self.username, self.password) def clientConnectionLost(self, connector, reason): """Lost connection """ print('Lost connection. Reason:', reason) def clientConnectionFailed(self, connector, reason): """Connection failed """ print('Connection failed. Reason:', reason) ReconnectingClientFactory.clientConnectionFailed(self, connector, reason) def start(host='localhost', port=61613, username='', password=''): """Start twisted event loop and the fun should begin... """ StompClientFactory.username = username StompClientFactory.password = password reactor.connectTCP(host, port, StompClientFactory()) reactor.run() if __name__ == '__main__': start() stomper-0.4.3/lib/stomper/examples/stompbuffer-tx.py0000644000076500000240000000721213453100403023062 0ustar oisinstaff00000000000000""" A simple twisted STOMP message sender. (c) Oisin Mulvihill, 2007-07-26. License: http://www.apache.org/licenses/LICENSE-2.0 """ from __future__ import print_function import logging from twisted.internet import reactor from twisted.internet.task import LoopingCall from twisted.internet.protocol import Protocol, ReconnectingClientFactory import stomper from stomper import stompbuffer stomper.utils.log_init(logging.DEBUG) DESTINATION="/topic/inbox" class StompProtocol(Protocol, stomper.Engine): def __init__(self, username='', password=''): stomper.Engine.__init__(self) self.username = username self.password = password self.counter = 1 self.log = logging.getLogger("sender") self.stompBuffer = stompbuffer.StompBuffer() def connected(self, msg): """Once I've connected I want to subscribe to my the message queue. """ stomper.Engine.connected(self, msg) self.log.info("Connected: session %s. Beginning say hello." % msg['headers']['session']) def setup_looping_call(): lc = LoopingCall(self.send) lc.start(2) reactor.callLater(1, setup_looping_call) f = stomper.Frame() f.unpack(stomper.subscribe(DESTINATION)) # ActiveMQ specific headers: # # prevent the messages we send comming back to us. f.headers['activemq.noLocal'] = 'true' return f.pack() def ack(self, msg): """Processes the received message. I don't need to generate an ack message. """ self.log.info("SENDER - received: %s " % msg['body']) return stomper.NO_REPONSE_NEEDED def send(self): """Send out a hello message periodically. """ self.log.info("Saying hello (%d)." % self.counter) f = stomper.Frame() f.unpack(stomper.send(DESTINATION, 'hello there (%d)' % self.counter)) self.counter += 1 # ActiveMQ specific headers: # #f.headers['persistent'] = 'true' self.transport.write(f.pack()) def connectionMade(self): """Register with stomp server. """ cmd = stomper.connect(self.username, self.password) self.transport.write(cmd) def dataReceived(self, data): """Use stompbuffer to determine when a complete message has been received. """ self.stompBuffer.appendData(data) while True: msg = self.stompBuffer.getOneMessage() if msg is None: break returned = self.react(msg) if returned: self.transport.write(returned) class StompClientFactory(ReconnectingClientFactory): # Will be set up before the factory is created. username, password = '', '' def buildProtocol(self, addr): return StompProtocol(self.username, self.password) def clientConnectionLost(self, connector, reason): """Lost connection """ print('Lost connection. Reason:', reason) def clientConnectionFailed(self, connector, reason): """Connection failed """ print('Connection failed. Reason:', reason) ReconnectingClientFactory.clientConnectionFailed(self, connector, reason) def start(host='localhost', port=61613, username='', password=''): """Start twisted event loop and the fun should begin... """ StompClientFactory.username = username StompClientFactory.password = password reactor.connectTCP(host, port, StompClientFactory()) reactor.run() if __name__ == '__main__': start() stomper-0.4.3/lib/stomper/examples/stomper_usage.py0000644000076500000240000000637213453100403022760 0ustar oisinstaff00000000000000""" This is an example of generating messages, handling response messages and anything else I can think of demonstrating. (c) Oisin Mulvihill, 2007-07-28. License: http://www.apache.org/licenses/LICENSE-2.0 """ from __future__ import print_function import pprint import stomper responder = stomper.Engine() # Generate the connect command to tell the server about us: msg = stomper.connect('bob','1234') print("msg:\n%s\n" % pprint.pformat(msg)) #>>> 'CONNECT\nlogin:bob\npasscode:1234\n\n\x00\n' # Send the message to the server and you'll get a response like: # server_response = """CONNECTED session:ID:snorky.local-49191-1185461799654-3:18 \x00 """ # We can react to this using the state machine to generate a response. # # The state machine can handle the raw message: response = responder.react(server_response) # or we could unpack the message into a dict and use it: # pprint.pprint(stomper.unpack_frame(response)) resp = responder.react(stomper.unpack_frame(response)) # The engine will store the session id from the CONNECTED # response. It doesn't generate a message response. It # just returns an empty string. # After a successful connect you might want to subscribe # for messages from a destination and tell the server you'll # acknowledge all messages. # DESTINATION='/queue/inbox' sub = stomper.subscribe(DESTINATION, ack='client') # Send the message to the server... # # At some point in the future you'll get messages # from the server. An example message might be: # server_msg = """MESSAGE destination: /queue/a message-id: some-message-id hello queue a \x00 """ # We need to acknowledge this so we can pass this message # into the engine, and by default it will generate and # ACK message: response = responder.react(server_msg) print("response:\n%s\n" % pprint.pformat(response)) #>>> 'ACK\nmessage-id: some-message-id\n\n\x00\n' # We could over ride the default engine and do more customer # reaction to receiving messages. For example: class Pong(stomper.Engine): def ack(self, msg): """Override this and do some customer message handler. """ print("Got a message:\n%s\n" % msg['body']) # do something with the message... # Generate the ack or not if you subscribed with ack='auto' return super(Pong, self).ack(msg) responder2 = Pong() response = responder2.react(server_msg) print("response:\n%s\n" % pprint.pformat(response)) #>>> 'ACK\nmessage-id: some-message-id\n\n\x00\n' # We might want to send a message at some point. We could do this # in two ways # 1. using the the function for send() send_message = stomper.send(DESTINATION, 'hello there') print("1. send_message:\n%s\n" % pprint.pformat(send_message)) #>>> 'SEND\ndestination: /queue/inbox\n\nhello there\x00\n' # 2. using the frame class to add extra custom headers: msg = stomper.Frame() msg.cmd = 'SEND' msg.headers = {'destination':'/queue/a','custom-header':'1234'} msg.body = "hello queue a" print("2. send_message:\n%s\n" % pprint.pformat(msg.pack())) #>>> 'SEND\ncustom-header:1234\ndestination:/queue/a\n\nhello queue a\n\n\x00\n' # And thats pretty much it. There are other functions to send various # messages such as UNSUBSCRIBE, BEGIN, etc. Check out the stomper code # for further details. # stomper-0.4.3/lib/stomper/stomp_10.py0000644000076500000240000003142613453100403017725 0ustar oisinstaff00000000000000""" This is a python client implementation of the STOMP protocol. It aims to be transport layer neutral. This module provides functions to create and parse STOMP messages in a programmatic fashion. The examples package contains two examples using twisted as the transport framework. Other frameworks can be used and I may add other examples as time goes on. The STOMP protocol specification maybe found here: * http://stomp.codehaus.org/Protocol I've looked at the stomp client by Jason R. Briggs and have based the message generation on how his client does it. The client can be found at the follow address however it isn't a dependancy. * http://www.briggs.net.nz/log/projects/stomppy In testing this library I run against ActiveMQ project. The server runs in java, however its fairly standalone and easy to set up. The projects page is here: * http://activemq.apache.org/ (c) Oisin Mulvihill, 2007-07-26. License: http://www.apache.org/licenses/LICENSE-2.0 """ from __future__ import absolute_import from builtins import object import re import uuid import types import logging from . import utils from . import stompbuffer # This is used as a return from message responses functions. # It is used more for readability more then anything or reason. NO_RESPONSE_NEEDED = '' # For backwards compatibility NO_REPONSE_NEEDED = '' # The version of the protocol we implement. STOMP_VERSION = '1.0' # Message terminator: NULL = '\x00' # STOMP Spec v1.0 valid commands: VALID_COMMANDS = [ 'ABORT', 'ACK', 'BEGIN', 'COMMIT', 'CONNECT', 'CONNECTED', 'DISCONNECT', 'MESSAGE', 'SEND', 'SUBSCRIBE', 'UNSUBSCRIBE', 'RECEIPT', 'ERROR', ] try: stringTypes = (str, unicode) except NameError: stringTypes = (str,) def get_log(): return logging.getLogger("stomper") class FrameError(Exception): """Raise for problem with frame generation or parsing. """ class Frame(object): """This class is used to create or read STOMP message frames. The method pack() is used to create a STOMP message ready for transmission. The method unpack() is used to read a STOMP message into a frame instance. It uses the unpack_frame(...) function to do the initial parsing. The frame has three important member variables: * cmd * headers * body The 'cmd' is a property that represents the STOMP message command. When you assign this a check is done to make sure its one of the VALID_COMMANDS. If not then FrameError will be raised. The 'headers' is a dictionary which the user can added to if needed. There are no restrictions or checks imposed on what values are inserted. The 'body' is just a member variable that the body text is assigned to. """ def __init__(self): """Setup the internal state.""" self._cmd = '' self.body = '' self.headers = {} def getCmd(self): """Don't use _cmd directly!""" return self._cmd def setCmd(self, cmd): """Check the cmd is valid, FrameError will be raised if its not.""" cmd = cmd.upper() if cmd not in VALID_COMMANDS: raise FrameError("The cmd '%s' is not valid! It must be one of '%s' (STOMP v%s)." % ( cmd, VALID_COMMANDS, STOMP_VERSION) ) else: self._cmd = cmd cmd = property(getCmd, setCmd) def pack(self): """Called to create a STOMP message from the internal values. """ headers = ''.join( ['%s:%s\n' % (f, v) for f, v in sorted(self.headers.items())] ) stomp_message = "%s\n%s\n%s%s\n" % (self._cmd, headers, self.body, NULL) # import pprint # print "stomp_message: ", pprint.pprint(stomp_message) return stomp_message def unpack(self, message): """Called to extract a STOMP message into this instance. message: This is a text string representing a valid STOMP (v1.0) message. This method uses unpack_frame(...) to extract the information, before it is assigned internally. retuned: The result of the unpack_frame(...) call. """ if not message: raise FrameError("Unpack error! The given message isn't valid '%s'!" % message) msg = unpack_frame(message) self.cmd = msg['cmd'] self.headers = msg['headers'] # Assign directly as the message will have the null # character in the message already. self.body = msg['body'] return msg def unpack_frame(message): """Called to unpack a STOMP message into a dictionary. returned = { # STOMP Command: 'cmd' : '...', # Headers e.g. 'headers' : { 'destination' : 'xyz', 'message-id' : 'some event', : etc, } # Body: 'body' : '...1234...\x00', } """ body = [] returned = dict(cmd='', headers={}, body='') breakdown = message.split('\n') # Get the message command: returned['cmd'] = breakdown[0] breakdown = breakdown[1:] def headD(field): # find the first ':' everything to the left of this is a # header, everything to the right is data: index = field.find(':') if index: header = field[:index].strip() data = field[index+1:].strip() # print "header '%s' data '%s'" % (header, data) returned['headers'][header.strip()] = data.strip() def bodyD(field): field = field.strip() if field: body.append(field) # Recover the header fields and body data handler = headD for field in breakdown: # print "field:", field if field.strip() == '': # End of headers, it body data next. handler = bodyD continue handler(field) # Stich the body data together: # print "1. body: ", body body = "".join(body) returned['body'] = body.replace('\x00', '') # print "2. body: <%s>" % returned['body'] return returned def abort(transactionid): """STOMP abort transaction command. Rollback whatever actions in this transaction. transactionid: This is the id that all actions in this transaction. """ return "ABORT\ntransaction: %s\n\n\x00\n" % transactionid def ack(messageid, transactionid=None): """STOMP acknowledge command. Acknowledge receipt of a specific message from the server. messageid: This is the id of the message we are acknowledging, what else could it be? ;) transactionid: This is the id that all actions in this transaction will have. If this is not given then a random UUID will be generated for this. """ header = 'message-id: %s' % messageid if transactionid: header = 'message-id: %s\ntransaction: %s' % (messageid, transactionid) return "ACK\n%s\n\n\x00\n" % header def begin(transactionid=None): """STOMP begin command. Start a transaction... transactionid: This is the id that all actions in this transaction will have. If this is not given then a random UUID will be generated for this. """ if not transactionid: # Generate a random UUID: transactionid = uuid.uuid4() return "BEGIN\ntransaction: %s\n\n\x00\n" % transactionid def commit(transactionid): """STOMP commit command. Do whatever is required to make the series of actions permanent for this transactionid. transactionid: This is the id that all actions in this transaction. """ return "COMMIT\ntransaction: %s\n\n\x00\n" % transactionid def connect(username, password): """STOMP connect command. username, password: These are the needed auth details to connect to the message server. After sending this we will receive a CONNECTED message which will contain our session id. """ return "CONNECT\nlogin:%s\npasscode:%s\n\n\x00\n" % (username, password) def disconnect(): """STOMP disconnect command. Tell the server we finished and we'll be closing the socket soon. """ return "DISCONNECT\n\n\x00\n" def send(dest, msg, transactionid=None): """STOMP send command. dest: This is the channel we wish to subscribe to msg: This is the message body to be sent. transactionid: This is an optional field and is not needed by default. """ transheader = '' if transactionid: transheader = 'transaction: %s\n' % transactionid return "SEND\ndestination: %s\n%s\n%s\x00\n" % (dest, transheader, msg) def subscribe(dest, ack='auto'): """STOMP subscribe command. dest: This is the channel we wish to subscribe to ack: 'auto' | 'client' If the ack is set to client, then messages received will have to have an acknowledge as a reply. Otherwise the server will assume delivery failure. """ return "SUBSCRIBE\ndestination: %s\nack: %s\n\n\x00\n" % (dest, ack) def unsubscribe(dest): """STOMP unsubscribe command. dest: This is the channel we wish to subscribe to Tell the server we no longer wish to receive any further messages for the given subscription. """ return "UNSUBSCRIBE\ndestination:%s\n\n\x00\n" % dest class Engine(object): """This is a simple state machine to return a response to received message if needed. """ def __init__(self, testing=False): self.testing = testing self.log = logging.getLogger("stomper.Engine") self.sessionId = '' # Entry Format: # # COMMAND : Handler_Function # self.states = { 'CONNECTED' : self.connected, 'MESSAGE' : self.ack, 'ERROR' : self.error, 'RECEIPT' : self.receipt, } def react(self, msg): """Called to provide a response to a message if needed. msg: This is a dictionary as returned by unpack_frame(...) or it can be a straight STOMP message. This function will attempt to determine which an deal with it. returned: A message to return or an empty string. """ returned = "" # If its not a string assume its a dict. mtype = type(msg) if mtype in stringTypes: msg = unpack_frame(msg) elif mtype == dict: pass else: raise FrameError("Unknown message type '%s', I don't know what to do with this!" % mtype) if msg['cmd'] in self.states: # print("reacting to message - %s" % msg['cmd']) returned = self.states[msg['cmd']](msg) return returned def connected(self, msg): """No response is needed to a connected frame. This method stores the session id as the member sessionId for later use. returned: NO_RESPONSE_NEEDED """ self.sessionId = msg['headers']['session'] #print "connected: session id '%s'." % self.sessionId return NO_RESPONSE_NEEDED def ack(self, msg): """Called when a MESSAGE has been received. Override this method to handle received messages. This function will generate an acknowledge message for the given message and transaction (if present). """ message_id = msg['headers']['message-id'] transaction_id = None if 'transaction-id' in msg['headers']: transaction_id = msg['headers']['transaction-id'] # print "acknowledging message id <%s>." % message_id return ack(message_id, transaction_id) def error(self, msg): """Called to handle an error message received from the server. This method just logs the error message returned: NO_RESPONSE_NEEDED """ body = msg['body'].replace(NULL, '') brief_msg = "" if 'message' in msg['headers']: brief_msg = msg['headers']['message'] self.log.error("Received server error - message%s\n\n%s" % (brief_msg, body)) returned = NO_RESPONSE_NEEDED if self.testing: returned = 'error' return returned def receipt(self, msg): """Called to handle a receipt message received from the server. This method just logs the receipt message returned: NO_RESPONSE_NEEDED """ body = msg['body'].replace(NULL, '') brief_msg = "" if 'receipt-id' in msg['headers']: brief_msg = msg['headers']['receipt-id'] self.log.info("Received server receipt message - receipt-id:%s\n\n%s" % (brief_msg, body)) returned = NO_RESPONSE_NEEDED if self.testing: returned = 'receipt' return returned stomper-0.4.3/lib/stomper/stomp_11.py0000644000076500000240000003437313453100403017732 0ustar oisinstaff00000000000000""" This is a python client implementation of the STOMP protocol. It aims to be transport layer neutral. This module provides functions to create and parse STOMP messages in a programmatic fashion. The examples package contains two examples using twisted as the transport framework. Other frameworks can be used and I may add other examples as time goes on. The STOMP protocol specification maybe found here: * http://stomp.codehaus.org/Protocol I've looked at the stomp client by Jason R. Briggs and have based the message generation on how his client does it. The client can be found at the follow address however it isn't a dependancy. * http://www.briggs.net.nz/log/projects/stomppy In testing this library I run against ActiveMQ project. The server runs in java, however its fairly standalone and easy to set up. The projects page is here: * http://activemq.apache.org/ (c) Oisin Mulvihill, 2007-07-26. Ralph Bean, 2014-09-09. License: http://www.apache.org/licenses/LICENSE-2.0 """ from __future__ import absolute_import from builtins import object import re import uuid import types import logging from . import utils from . import stompbuffer # This is used as a return from message responses functions. # It is used more for readability more then anything or reason. NO_RESPONSE_NEEDED = '' # For backwards compatibility NO_REPONSE_NEEDED = '' # The version of the protocol we implement. STOMP_VERSION = '1.1' # Message terminator: NULL = '\x00' # STOMP Spec v1.1 valid commands: VALID_COMMANDS = [ 'ABORT', 'ACK', 'BEGIN', 'COMMIT', 'CONNECT', 'CONNECTED', 'DISCONNECT', 'MESSAGE', 'NACK', 'SEND', 'SUBSCRIBE', 'UNSUBSCRIBE', 'RECEIPT', 'ERROR', ] try: stringTypes = (str, unicode) except NameError: stringTypes = (str,) def get_log(): return logging.getLogger("stomper") class FrameError(Exception): """Raise for problem with frame generation or parsing. """ class Frame(object): """This class is used to create or read STOMP message frames. The method pack() is used to create a STOMP message ready for transmission. The method unpack() is used to read a STOMP message into a frame instance. It uses the unpack_frame(...) function to do the initial parsing. The frame has three important member variables: * cmd * headers * body The 'cmd' is a property that represents the STOMP message command. When you assign this a check is done to make sure its one of the VALID_COMMANDS. If not then FrameError will be raised. The 'headers' is a dictionary which the user can added to if needed. There are no restrictions or checks imposed on what values are inserted. The 'body' is just a member variable that the body text is assigned to. """ def __init__(self): """Setup the internal state.""" self._cmd = '' self.body = '' self.headers = {} def getCmd(self): """Don't use _cmd directly!""" return self._cmd def setCmd(self, cmd): """Check the cmd is valid, FrameError will be raised if its not.""" cmd = cmd.upper() if cmd not in VALID_COMMANDS: raise FrameError("The cmd '%s' is not valid! It must be one of '%s' (STOMP v%s)." % ( cmd, VALID_COMMANDS, STOMP_VERSION) ) else: self._cmd = cmd cmd = property(getCmd, setCmd) def pack(self): """Called to create a STOMP message from the internal values. """ headers = ''.join( ['%s:%s\n' % (f, v) for f, v in sorted(self.headers.items())] ) stomp_message = "%s\n%s\n%s%s\n" % (self._cmd, headers, self.body, NULL) return stomp_message def unpack(self, message): """Called to extract a STOMP message into this instance. message: This is a text string representing a valid STOMP (v1.1) message. This method uses unpack_frame(...) to extract the information, before it is assigned internally. retuned: The result of the unpack_frame(...) call. """ if not message: raise FrameError("Unpack error! The given message isn't valid '%s'!" % message) msg = unpack_frame(message) self.cmd = msg['cmd'] self.headers = msg['headers'] # Assign directly as the message will have the null # character in the message already. self.body = msg['body'] return msg def unpack_frame(message): """Called to unpack a STOMP message into a dictionary. returned = { # STOMP Command: 'cmd' : '...', # Headers e.g. 'headers' : { 'destination' : 'xyz', 'message-id' : 'some event', : etc, } # Body: 'body' : '...1234...\x00', } """ body = [] returned = dict(cmd='', headers={}, body='') breakdown = message.split('\n') # Get the message command: returned['cmd'] = breakdown[0] breakdown = breakdown[1:] def headD(field): # find the first ':' everything to the left of this is a # header, everything to the right is data: index = field.find(':') if index: header = field[:index].strip() data = field[index+1:].strip() # print "header '%s' data '%s'" % (header, data) returned['headers'][header.strip()] = data.strip() def bodyD(field): field = field.strip() if field: body.append(field) # Recover the header fields and body data handler = headD for field in breakdown: # print "field:", field if field.strip() == '': # End of headers, it body data next. handler = bodyD continue handler(field) # Stich the body data together: # print "1. body: ", body body = "".join(body) returned['body'] = body.replace('\x00', '') # print "2. body: <%s>" % returned['body'] return returned def abort(transactionid): """STOMP abort transaction command. Rollback whatever actions in this transaction. transactionid: This is the id that all actions in this transaction. """ return "ABORT\ntransaction:%s\n\n\x00\n" % transactionid def ack(messageid, subscriptionid, transactionid=None): """STOMP acknowledge command. Acknowledge receipt of a specific message from the server. messageid: This is the id of the message we are acknowledging, what else could it be? ;) subscriptionid: This is the id of the subscription that applies to the message. transactionid: This is the id that all actions in this transaction will have. If this is not given then a random UUID will be generated for this. """ header = 'subscription:%s\nmessage-id:%s' % (subscriptionid, messageid) if transactionid: header += '\ntransaction:%s' % transactionid return "ACK\n%s\n\n\x00\n" % header def nack(messageid, subscriptionid, transactionid=None): """STOMP negative acknowledge command. NACK is the opposite of ACK. It is used to tell the server that the client did not consume the message. The server can then either send the message to a different client, discard it, or put it in a dead letter queue. The exact behavior is server specific. messageid: This is the id of the message we are acknowledging, what else could it be? ;) subscriptionid: This is the id of the subscription that applies to the message. transactionid: This is the id that all actions in this transaction will have. If this is not given then a random UUID will be generated for this. """ header = 'subscription:%s\nmessage-id:%s' % (subscriptionid, messageid) if transactionid: header += '\ntransaction:%s' % transactionid return "NACK\n%s\n\n\x00\n" % header def begin(transactionid=None): """STOMP begin command. Start a transaction... transactionid: This is the id that all actions in this transaction will have. If this is not given then a random UUID will be generated for this. """ if not transactionid: # Generate a random UUID: transactionid = uuid.uuid4() return "BEGIN\ntransaction:%s\n\n\x00\n" % transactionid def commit(transactionid): """STOMP commit command. Do whatever is required to make the series of actions permanent for this transactionid. transactionid: This is the id that all actions in this transaction. """ return "COMMIT\ntransaction:%s\n\n\x00\n" % transactionid def connect(username, password, host, heartbeats=(0,0)): """STOMP connect command. username, password: These are the needed auth details to connect to the message server. After sending this we will receive a CONNECTED message which will contain our session id. """ if len(heartbeats) != 2: raise ValueError('Invalid heartbeat %r' % heartbeats) cx, cy = heartbeats return "CONNECT\naccept-version:1.1\nhost:%s\nheart-beat:%i,%i\nlogin:%s\npasscode:%s\n\n\x00\n" % (host, cx, cy, username, password) def disconnect(receipt=None): """STOMP disconnect command. Tell the server we finished and we'll be closing the socket soon. """ if not receipt: receipt = uuid.uuid4() return "DISCONNECT\nreceipt:%s\n\x00\n" % receipt def send(dest, msg, transactionid=None, content_type='text/plain'): """STOMP send command. dest: This is the channel we wish to subscribe to msg: This is the message body to be sent. transactionid: This is an optional field and is not needed by default. """ transheader = '' if transactionid: transheader = 'transaction:%s\n' % transactionid return "SEND\ndestination:%s\ncontent-type:%s\n%s\n%s\x00\n" % ( dest, content_type, transheader, msg) def subscribe(dest, idx, ack='auto'): """STOMP subscribe command. dest: This is the channel we wish to subscribe to idx: The ID that should uniquely identify the subscription ack: 'auto' | 'client' If the ack is set to client, then messages received will have to have an acknowledge as a reply. Otherwise the server will assume delivery failure. """ return "SUBSCRIBE\nid:%s\ndestination:%s\nack:%s\n\n\x00\n" % ( idx, dest, ack) def unsubscribe(idx): """STOMP unsubscribe command. idx: This is the id of the subscription Tell the server we no longer wish to receive any further messages for the given subscription. """ return "UNSUBSCRIBE\nid:%s\n\n\x00\n" % idx class Engine(object): """This is a simple state machine to return a response to received message if needed. """ def __init__(self, testing=False): self.testing = testing self.log = logging.getLogger("stomper.Engine") self.sessionId = '' # Entry Format: # # COMMAND : Handler_Function # self.states = { 'CONNECTED' : self.connected, 'MESSAGE' : self.ack, 'ERROR' : self.error, 'RECEIPT' : self.receipt, } def react(self, msg): """Called to provide a response to a message if needed. msg: This is a dictionary as returned by unpack_frame(...) or it can be a straight STOMP message. This function will attempt to determine which an deal with it. returned: A message to return or an empty string. """ returned = "" # If its not a string assume its a dict. mtype = type(msg) if mtype in stringTypes: msg = unpack_frame(msg) elif mtype == dict: pass else: raise FrameError("Unknown message type '%s', I don't know what to do with this!" % mtype) if msg['cmd'] in self.states: # print("reacting to message - %s" % msg['cmd']) returned = self.states[msg['cmd']](msg) return returned def connected(self, msg): """No response is needed to a connected frame. This method stores the session id as the member sessionId for later use. returned: NO_RESPONSE_NEEDED """ self.sessionId = msg['headers']['session'] #print "connected: session id '%s'." % self.sessionId return NO_RESPONSE_NEEDED def ack(self, msg): """Called when a MESSAGE has been received. Override this method to handle received messages. This function will generate an acknowledge message for the given message and transaction (if present). """ message_id = msg['headers']['message-id'] subscription = msg['headers']['subscription'] transaction_id = None if 'transaction-id' in msg['headers']: transaction_id = msg['headers']['transaction-id'] # print "acknowledging message id <%s>." % message_id return ack(message_id, subscription, transaction_id) def error(self, msg): """Called to handle an error message received from the server. This method just logs the error message returned: NO_RESPONSE_NEEDED """ body = msg['body'].replace(NULL, '') brief_msg = "" if 'message' in msg['headers']: brief_msg = msg['headers']['message'] self.log.error("Received server error - message%s\n\n%s" % (brief_msg, body)) returned = NO_RESPONSE_NEEDED if self.testing: returned = 'error' return returned def receipt(self, msg): """Called to handle a receipt message received from the server. This method just logs the receipt message returned: NO_RESPONSE_NEEDED """ body = msg['body'].replace(NULL, '') brief_msg = "" if 'receipt-id' in msg['headers']: brief_msg = msg['headers']['receipt-id'] self.log.info("Received server receipt message - receipt-id:%s\n\n%s" % (brief_msg, body)) returned = NO_RESPONSE_NEEDED if self.testing: returned = 'receipt' return returned stomper-0.4.3/lib/stomper/stompbuffer.py0000644000076500000240000002400713453100403020614 0ustar oisinstaff00000000000000""" StompBuffer is an optional utility class accompanying Stomper. Ricky Iacovou, 2008-03-27. License: http://www.apache.org/licenses/LICENSE-2.0 """ from builtins import object import re import stomper # regexp to check that the buffer starts with a command. command_re = re.compile ( '^(.*?)\n' ) # regexp to remove everything up to and including the first # instance of '\x00\n' (used in resynching the buffer). sync_re = re.compile ( '^.*?\x00\n' ) # regexp to determine the content length. The buffer should always start # with a command followed by the headers, so the content-length header will # always be preceded by a newline. content_length_re = re.compile ( '\ncontent-length\s*:\s*(\d+)\s*\n' ) # Separator between the header and the body. len_sep = len ( '\n\n' ) # Footer after the message body. len_footer = len ( '\x00\n' ) class StompBuffer ( object ): """ I can be used to deal with partial frames if your transport does not guarantee complete frames. I maintain an internal buffer of received bytes and offer a way of pulling off the first complete message off the buffer. """ def __init__ ( self ): self.buffer = '' def bufferLen ( self ): """ I return the length of the buffer, in bytes. """ return len ( self.buffer ) def bufferIsEmpty ( self ): """ I return True if the buffer contains zero bytes, False otherwise. """ return self.bufferLen() == 0 def appendData ( self, data ): """ I should be called by a transport that receives a raw sequence of bytes that may or may not contain a complete message. I return """ # log.msg ( "Received [%s] bytes in dataReceived()" % ( len ( data ), ) ) # import pprint # pprint.pprint ( data ) self.buffer += data def getOneMessage ( self ): """ I pull one complete message off the buffer and return it decoded as a dict. If there is no complete message in the buffer, I return None. Note that the buffer can contain more than once message. You should therefore call me in a loop until I return None. """ ( mbytes, hbytes ) = self._findMessageBytes ( self.buffer ) if not mbytes: return None msgdata = self.buffer[:mbytes] self.buffer = self.buffer[mbytes:] hdata = msgdata[:hbytes] elems = hdata.split ( '\n' ) cmd = elems.pop ( 0 ) headers = {} # We can't use a simple split because the value can legally contain # colon characters (for example, the session returned by ActiveMQ). for e in elems: try: i = e.find ( ':' ) except ValueError: continue k = e[:i].strip() v = e[i+1:].strip() headers [ k ] = v # hbytes points to the start of the '\n\n' at the end of the header, # so 2 bytes beyond this is the start of the body. The body EXCLUDES # the final two bytes, which are '\x00\n'. Note that these 2 bytes # are UNRELATED to the 2-byte '\n\n' that Frame.pack() used to insert # into the data stream. body = msgdata[hbytes+2:-2] msg = { 'cmd' : cmd, 'headers' : headers, 'body' : body, } return msg def _findMessageBytes ( self, data ): """ I examine the data passed to me and return a 2-tuple of the form: ( message_length, header_length ) where message_length is the length in bytes of the first complete message, if it contains at least one message, or 0 if it contains no message. If message_length is non-zero, header_length contains the length in bytes of the header. If message_length is zero, header_length should be ignored. You should probably not call me directly. Call getOneMessage instead. """ # Sanity check. See the docstring for the method to see what it # does an why we need it. self.syncBuffer() # If the string '\n\n' does not exist, we don't even have the complete # header yet and we MUST exit. try: i = data.index ( '\n\n' ) except ValueError: return ( 0, 0 ) # If the string '\n\n' exists, then we have the entire header and can # check for the content-length header. If it exists, we can check # the length of the buffer for the number of bytes, else we check for # the existence of a null byte. # Pull out the header before we perform the regexp search. This # prevents us from matching (possibly malicious) strings in the # body. _hdr = self.buffer[:i] match = content_length_re.search ( _hdr ) if match: # There was a content-length header, so read out the value. content_length = int ( match.groups()[0] ) # THIS IS NO LONGER THE CASE IF WE REMOVE THE '\n\n' in # Frame.pack() # This is the content length of the body up until the null # byte, not the entire message. Note that this INCLUDES the 2 # '\n\n' bytes inserted by the STOMP encoder after the body # (see the calculation of content_length in # StompEngine.callRemote()), so we only need to add 2 final bytes # for the footer. # #The message looks like: # #
\n\n\n\n\x00\n # ^ ^^^^ # (i) included in content_length! # # We have the location of the end of the header (i), so we # need to ensure that the message contains at least: # # i + len ( '\n\n' ) + content_length + len ( '\x00\n' ) # # Note that i is also the count of bytes in the header, because # of the fact that str.index() returns a 0-indexed value. req_len = i + len_sep + content_length + len_footer # log.msg ( "We have [%s] bytes and need [%s] bytes" % # ( len ( data ), req_len, ) ) if len ( data ) < req_len: # We don't have enough bytes in the buffer. return ( 0, 0 ) else: # We have enough bytes in the buffer return ( req_len, i ) else: # There was no content-length header, so just look for the # message terminator ('\x00\n' ). try: j = data.index ( '\x00\n' ) except ValueError: return ( 0, 0 ) # j points to the 0-indexed location of the null byte. However, # we need to add 1 (to turn it into a byte count) and 1 to take # account of the final '\n' character after the null byte. return ( j + 2, i ) def syncBuffer( self ): """ I detect and correct corruption in the buffer. Corruption in the buffer is defined as the following conditions both being true: 1. The buffer contains at least one newline; 2. The text until the first newline is not a STOMP command. In this case, we heuristically try to flush bits of the buffer until one of the following conditions becomes true: 1. the buffer starts with a STOMP command; 2. the buffer does not contain a newline. 3. the buffer is empty; If the buffer is deemed corrupt, the first step is to flush the buffer up to and including the first occurrence of the string '\x00\n', which is likely to be a frame boundary. Note that this is not guaranteed to be a frame boundary, as a binary payload could contain the string '\x00\n'. That condition would get handled on the next loop iteration. If the string '\x00\n' does not occur, the entire buffer is cleared. An earlier version progressively removed strings until the next newline, but this gets complicated because the body could contain strings that look like STOMP commands. Note that we do not check "partial" strings to see if they *could* match a command; that would be too resource-intensive. In other words, a buffer containing the string 'BUNK' with no newline is clearly corrupt, but we sit and wait until the buffer contains a newline before attempting to see if it's a STOMP command. """ while True: if not self.buffer: # Buffer is empty; no need to do anything. break m = command_re.match ( self.buffer ) if m is None: # Buffer doesn't even contain a single newline, so we can't # determine whether it's corrupt or not. Assume it's OK. break cmd = m.groups()[0] if cmd in stomper.VALID_COMMANDS: # Good: the buffer starts with a command. break else: # Bad: the buffer starts with bunk, so strip it out. We first # try to strip to the first occurrence of '\x00\n', which # is likely to be a frame boundary, but if this fails, we # strip until the first newline. ( self.buffer, nsubs ) = sync_re.subn ( '', self.buffer ) if nsubs: # Good: we managed to strip something out, so restart the # loop to see if things look better. continue else: # Bad: we failed to strip anything out, so kill the # entire buffer. Since this resets the buffer to a # known good state, we can break out of the loop. self.buffer = '' break stomper-0.4.3/lib/stomper/tests/0000755000076500000240000000000013453103756017063 5ustar oisinstaff00000000000000stomper-0.4.3/lib/stomper/tests/__init__.py0000644000076500000240000000000013453100403021144 0ustar oisinstaff00000000000000stomper-0.4.3/lib/stomper/tests/teststompbuffer.py0000644000076500000240000002407413453100403022662 0ustar oisinstaff00000000000000###################################################################### # Unit tests for StompBuffer ###################################################################### import unittest import types import stomper from stomper.stompbuffer import StompBuffer CMD = 'SEND' DEST = '/queue/a' BODY = 'This is the body text' BINBODY = '\x00\x01\x03\x04\x05\x06' def makeTextMessage ( body = BODY, cmd = CMD ): msg = stomper.Frame() msg.cmd = cmd msg.headers = {'destination':DEST} msg.body = body return msg.pack() def makeBinaryMessage ( body = BINBODY, cmd = CMD ): msg = stomper.Frame() msg.cmd = cmd msg.headers = {'destination':DEST, 'content-length':len(body)} msg.body = body return msg.pack() def messageIsGood ( msg, body = BODY, cmd = CMD ): if msg is None: return False if type ( msg ) != dict: return False if msg [ 'cmd' ] != cmd: return False try: dest = msg [ 'headers' ][ 'destination' ] except KeyError: return False if dest != DEST: return False if msg [ 'body' ] != body: return False return True class StompBufferTestCase ( unittest.TestCase ): def setUp ( self ): self.sb = StompBuffer() # We do this so often we put it into a separate method. def putAndGetText ( self ): msg = makeTextMessage() self.sb.appendData ( msg ) return self.sb.getOneMessage() def putAndGetBinary ( self ): msg = makeBinaryMessage() self.sb.appendData ( msg ) return self.sb.getOneMessage() def test001_testBufferAccretionText ( self ): """ Test to see that the buffer accumulates text messages with no additional padding. """ msg1 = makeTextMessage ( 'blah1' ) msg2 = makeTextMessage ( 'blah2' ) msg3 = makeTextMessage ( 'blah3' ) self.sb.appendData ( msg1 ) self.sb.appendData ( msg2 ) self.sb.appendData ( msg3 ) expect = len ( msg1 ) + len ( msg2 ) + len ( msg3 ) self.failUnless ( self.sb.bufferLen() == expect ) def test002_testBufferAccretionBinary ( self ): """ Test to see that the buffer accumulates binary messages with no additional padding. """ msg1 = makeBinaryMessage ( 'blah1' ) msg2 = makeBinaryMessage ( 'blah2' ) msg3 = makeBinaryMessage ( 'blah3' ) self.sb.appendData ( msg1 ) self.sb.appendData ( msg2 ) self.sb.appendData ( msg3 ) expect = len ( msg1 ) + len ( msg2 ) + len ( msg3 ) self.failUnless ( self.sb.bufferLen() == expect ) def test003_oneCompleteTextMessage ( self ): """ Put a complete text message into the buffer, read it out again, and verify the decoded message. There are actually lots of little tests, but they're all part of the process of verifying the decoded message. Note that this test will FAIL if Frame.pack() inserts the additional '\n\n' bytes. """ msg = self.putAndGetText() self.failUnless ( messageIsGood ) def test004_oneCompleteBinaryMessage ( self ): """ Put a complete binary message into the buffer, read it out again, and verify the decoded message. Note that this test will FAIL if Frame.pack() inserts the additional '\n\n' bytes. """ msg = self.putAndGetBinary() self.failUnless ( messageIsGood ( msg, BINBODY ) ) def test005_emptyBufferText ( self ): """ Put a complete text message into the buffer, read it out again, and verify that there is nothing left in the buffer. """ # Verify that there are no more messages in the buffer. msg1 = self.putAndGetText() msg2 = self.sb.getOneMessage() self.failUnless ( msg2 is None ) # Verify that in fact the buffer is empty. self.failUnless ( self.sb.bufferIsEmpty() ) def test006_emptyBufferBinary ( self ): """ Put a complete binary message into the buffer, read it out again, and verify that there is nothing left in the buffer. """ # Verify that there are no more messages in the buffer. msg1 = self.putAndGetBinary() msg2 = self.sb.getOneMessage() self.failUnless ( msg2 is None ) # Verify that in fact the buffer is empty. self.failUnless ( self.sb.bufferIsEmpty() ) def test007_messageFragmentsText ( self ): """ Create a text message and check that the we can't read it out until we've fed all parts into the buffer """ msg = makeTextMessage() fragment1 = msg [:20] fragment2 = msg [20:] self.sb.appendData ( fragment1 ) m = self.sb.getOneMessage() self.failUnless ( m is None ) self.sb.appendData ( fragment2 ) m = self.sb.getOneMessage() self.failIf ( m is None ) self.failUnless ( self.sb.bufferIsEmpty() ) def test008_messageFragmentsBinary ( self ): """ Create a binary message and check that the we can't read it out until we've fed all parts into the buffer """ msg = makeBinaryMessage() fragment1 = msg [:20] fragment2 = msg [20:] self.sb.appendData ( fragment1 ) m = self.sb.getOneMessage() self.failUnless ( m is None ) self.sb.appendData ( fragment2 ) m = self.sb.getOneMessage() self.failIf ( m is None ) self.failUnless ( self.sb.bufferIsEmpty() ) def test009_confusingMessage ( self ): """ Create a confusing message and ensure that the decoder doesn't get tripped up. """ # Throw in fake commands, headers, nulls, newlines, everything. body = 'SUBSCRIBE\ncontent-length:27\n\x00\ndestination:/queue/confusion\n\n\x00\n' msg = makeBinaryMessage ( body ) self.sb.appendData ( msg ) m = self.sb.getOneMessage() # Ensure the headers weren't mangled self.failUnless ( m [ 'cmd' ] == CMD ) self.failUnless ( m [ 'headers' ] [ 'destination' ] == DEST ) # Ensure the body wasn't mangled. self.failUnless ( m [ 'body' ] == body ) # But ensure that there isn't object identity going on behind the # scenes. self.failIf ( m [ 'body' ] is body ) # Ensure the message was consumed in its entirety. self.failUnless ( self.sb.bufferIsEmpty() ) def test010_syncBufferNoClobber ( self ): """ Test that syncBuffer doesn't clobber the buffer if it doesn't contain a newline. """ self.sb.buffer = 'BLAHBLAH' self.sb.syncBuffer() self.failUnless ( self.sb.buffer == "BLAHBLAH" ) def test011_syncBufferClobberEverything ( self ): """ Put bunk into the buffer and ensure that it gets detected and removed. In this case, the entire buffer should be killed. """ self.sb.buffer = 'rubbish\nmorerubbish' self.sb.syncBuffer() self.failUnless ( self.sb.bufferIsEmpty() ) def test012_syncBufferClobberRubbish ( self ): """ Put bunk into the buffer and ensure that it gets detected and removed. In this case, only the start of the buffer should be killed, and the remainder should be left alone as it could be a partial command. """ self.sb.buffer = 'rubbish\x00\nREMAINDER' self.sb.syncBuffer() self.failUnless ( self.sb.buffer == "REMAINDER" ) def test013_syncBufferClobberEverythingTwice ( self ): """ Put bunk into the buffer and ensure that it gets detected and removed. In this case, the entire buffer should be clobbered as once we have removed the corrupt start of the data, it should become obvious that the rest of the buffer is corrupt too. """ self.sb.buffer = 'rubbish\x00\nNOTACOMMAND\n' self.sb.syncBuffer() self.failUnless ( self.sb.bufferIsEmpty() ) def test014_syncBufferGetGoodMessage ( self ): """ Put bunk into the buffer, followed by a real message and ensure that the bunk gets detected and removed and the message gets retrieved. """ msg = makeTextMessage() self.sb.buffer = 'rubbish\x00\n%s' % ( msg, ) self.sb.syncBuffer() m = self.sb.getOneMessage() self.failUnless ( messageIsGood ( m ) ) def test015_syncBufferClobberGoodMessage ( self ): """ Put bunk into the buffer, followed by a real message NOT separated by '\x00\n' and ensure that the whole buffer gets deleted. """ msg = makeTextMessage() self.sb.buffer = 'rubbish\n%s' % ( msg, ) self.sb.syncBuffer() self.failUnless ( self.sb.bufferIsEmpty() ) def test016_syncBufferHandleEmbeddedNulls ( self ): """ Put bunk into the buffer including an embedded '\x00\n' string, followed by a real message, and ensure that all the junk gets deleted but the real message is kept. """ msg = makeTextMessage() # The first null byte is embedded rubbish. The next null byte # should be treated as an end-of-frame delineator preceding the # real message. self.sb.buffer = 'rubbish\x00\nmorerubbish\x00\n%s' % ( msg, ) self.sb.syncBuffer() m = self.sb.getOneMessage() self.failUnless ( messageIsGood ( m ) ) def test017_testAllCommands ( self ): # Intentionally NOT using stomper.VALID_COMMANDS for cmd in [ 'SEND', 'SUBSCRIBE', 'UNSUBSCRIBE', 'BEGIN', 'COMMIT', 'ABORT', 'ACK', 'DISCONNECT', 'CONNECTED', 'MESSAGE', 'RECEIPT', 'ERROR' ]: msg = makeTextMessage ( body = BODY, cmd = cmd ) self.sb.appendData ( msg ) m = self.sb.getOneMessage() self.failUnless ( messageIsGood ( m, BODY, cmd ) ) self.failUnless ( self.sb.bufferIsEmpty() ) if __name__ == "__main__": unittest.main() # run all tests stomper-0.4.3/lib/stomper/tests/teststomper_10.py0000644000076500000240000002333413453100403022315 0ustar oisinstaff00000000000000""" This is the unittest to verify my stomper module. The STOMP protocol specification maybe found here: * http://stomp.codehaus.org/Protocol I've looked and the stomp client by Jason R. Briggs and have based the message generation on how his client did it. The client can be found at the follow address however it isn't a dependancy. * http://www.briggs.net.nz/log/projects/stomppy (c) Oisin Mulvihill, 2007-07-26. License: http://www.apache.org/licenses/LICENSE-2.0 """ import pprint import unittest import stomper.stomp_10 as stomper class TestEngine(stomper.Engine): """Test that these methods are called by the default engine. """ def __init__(self): super(TestEngine, self).__init__() self.ackCalled = False self.errorCalled = False self.receiptCalled = False def ack(self, msg): super(TestEngine, self).ack(msg) self.ackCalled = True return 'ack' def error(self, msg): super(TestEngine, self).error(msg) self.errorCalled = True return 'error' def receipt(self, msg): super(TestEngine, self).receipt(msg) self.receiptCalled = True return 'receipt' class Stomper10Test(unittest.TestCase): def testEngineToServerMessages(self): """Test the state machines reaction """ e = TestEngine() # React to a message which should be an ack: msg = stomper.Frame() msg.cmd = 'MESSAGE' msg.headers = { 'destination:': '/queue/a', 'message-id:': 'some-message-id' } msg.body = "hello queue a" rc = e.react(msg.pack()) self.assertEquals(rc, 'ack') self.assertEquals(e.ackCalled, True) # React to an error: error = stomper.Frame() error.cmd = 'ERROR' error.headers = {'message:': 'malformed packet received!'} error.body = """The message: ----- MESSAGE destined:/queue/a Hello queue a! ----- Did not contain a destination header, which is required for message propagation. \x00 """ rc = e.react(error.pack()) self.assertEquals(rc, 'error') self.assertEquals(e.errorCalled, True) # React to an receipt: receipt = stomper.Frame() receipt.cmd = 'RECEIPT' receipt.headers = {'receipt-id:': 'message-12345'} rc = e.react(receipt.pack()) self.assertEquals(rc, 'receipt') self.assertEquals(e.receiptCalled, True) def testEngine(self): """Test the basic state machine. """ e = stomper.Engine(testing=True) # test session connected message: msg = """CONNECTED session:ID:snorky.local-49191-1185461799654-3:18 \x00 """ result = stomper.unpack_frame(msg) correct = '' returned = e.react(result) self.assertEquals(returned, correct) # test message: msg = """MESSAGE destination: /queue/a message-id: some-message-id hello queue a \x00 """ returned = e.react(msg) correct = 'ACK\nmessage-id: some-message-id\n\n\x00\n' self.assertEquals(returned, correct) # test error: msg = """ERROR message:some error There was a problem with your last message \x00 """ returned = e.react(msg) correct = 'error' self.assertEquals(returned, correct) # test receipt: msg = """RECEIPT message-id: some-message-id \x00 """ returned = e.react(msg) correct = 'receipt' self.assertEquals(returned, correct) def testFramepack1(self): """Testing pack, unpacking and the Frame class. """ # Check bad frame generation: frame = stomper.Frame() def bad(): frame.cmd = 'SOME UNNOWN CMD' self.assertRaises(stomper.FrameError, bad) # Generate a MESSAGE frame: frame = stomper.Frame() frame.cmd = 'MESSAGE' frame.headers['destination'] = '/queue/a' frame.headers['message-id'] = 'card_data' frame.body = "hello queue a" result = frame.pack() # print "\n-- result " + "----" * 10 # pprint.pprint(result) # print # Try bad message unpack catching: bad_frame = stomper.Frame() self.assertRaises(stomper.FrameError, bad_frame.unpack, None) self.assertRaises(stomper.FrameError, bad_frame.unpack, '') # Try to read the generated frame back in # and then check the variables are set up # correctly: frame2 = stomper.Frame() frame2.unpack(result) self.assertEquals(frame2.cmd, 'MESSAGE') self.assertEquals(frame2.headers['destination'], '/queue/a') self.assertEquals(frame2.headers['message-id'], 'card_data') self.assertEquals(frame2.body, 'hello queue a') result = frame2.pack() correct = "MESSAGE\ndestination:/queue/a\nmessage-id:card_data\n\nhello queue a\x00\n" # print "result: " # pprint.pprint(result) # print # print "correct: " # pprint.pprint(correct) # print # self.assertEquals(result, correct) result = stomper.unpack_frame(result) self.assertEquals(result['cmd'], 'MESSAGE') self.assertEquals(result['headers']['destination'], '/queue/a') self.assertEquals(result['headers']['message-id'], 'card_data') self.assertEquals(result['body'], 'hello queue a') def testFramepack2(self): """Testing pack, unpacking and the Frame class. """ # Check bad frame generation: frame = stomper.Frame() frame.cmd = 'DISCONNECT' result = frame.pack() correct = 'DISCONNECT\n\n\x00\n' self.assertEquals(result, correct) def testFrameUnpack2(self): """Testing unpack frame function against MESSAGE """ msg = """MESSAGE destination:/queue/a message-id: card_data hello queue a""" result = stomper.unpack_frame(msg) self.assertEquals(result['cmd'], 'MESSAGE') self.assertEquals(result['headers']['destination'], '/queue/a') self.assertEquals(result['headers']['message-id'], 'card_data') self.assertEquals(result['body'], 'hello queue a') def testFrameUnpack3(self): """Testing unpack frame function against CONNECTED """ msg = """CONNECTED session:ID:snorky.local-49191-1185461799654-3:18 """ result = stomper.unpack_frame(msg) self.assertEquals(result['cmd'], 'CONNECTED') self.assertEquals(result['headers']['session'], 'ID:snorky.local-49191-1185461799654-3:18') self.assertEquals(result['body'], '') def testBugInFrameUnpack1(self): msg = """MESSAGE destination:/queue/a message-id: card_data hello queue a \x00 """ result = stomper.unpack_frame(msg) self.assertEquals(result['cmd'], 'MESSAGE') self.assertEquals(result['headers']['destination'], '/queue/a') self.assertEquals(result['headers']['message-id'], 'card_data') self.assertEquals(result['body'], 'hello queue a') def testCommit(self): transactionid = '1234' correct = "COMMIT\ntransaction: %s\n\n\x00\n" % transactionid self.assertEquals(stomper.commit(transactionid), correct) def testAbort(self): transactionid = '1234' correct = "ABORT\ntransaction: %s\n\n\x00\n" % transactionid self.assertEquals(stomper.abort(transactionid), correct) def testBegin(self): transactionid = '1234' correct = "BEGIN\ntransaction: %s\n\n\x00\n" % transactionid self.assertEquals(stomper.begin(transactionid), correct) def testAck(self): messageid = '1234' transactionid = '9876' header = 'message-id: %s\ntransaction: %s' % (messageid, transactionid) correct = "ACK\n%s\n\n\x00\n" % header self.assertEquals(stomper.ack(messageid, transactionid), correct) messageid = '1234' correct = "ACK\nmessage-id: %s\n\n\x00\n" % messageid self.assertEquals(stomper.ack(messageid), correct) def testUnsubscribe(self): dest = '/queue/all' correct = "UNSUBSCRIBE\ndestination:%s\n\n\x00\n" % dest self.assertEquals(stomper.unsubscribe(dest), correct) def testSubscribe(self): dest, ack = '/queue/all', 'client' correct = "SUBSCRIBE\ndestination: %s\nack: %s\n\n\x00\n" % (dest, ack) self.assertEquals(stomper.subscribe(dest, ack), correct) dest, ack = '/queue/all', 'auto' correct = "SUBSCRIBE\ndestination: %s\nack: %s\n\n\x00\n" % (dest, ack) self.assertEquals(stomper.subscribe(dest, ack), correct) correct = "SUBSCRIBE\ndestination: %s\nack: %s\n\n\x00\n" % (dest, ack) self.assertEquals(stomper.subscribe(dest), correct) def testConnect(self): username, password = 'bob', '123' correct = "CONNECT\nlogin:%s\npasscode:%s\n\n\x00\n" % (username, password) self.assertEquals(stomper.connect(username, password), correct) def testDisconnect(self): correct = "DISCONNECT\n\n\x00\n" self.assertEquals(stomper.disconnect(), correct) def testSend(self): dest, transactionid, msg = '/queue/myplace', '', '123 456 789' correct = "SEND\ndestination: %s\n\n%s\x00\n" % (dest, msg) result = stomper.send(dest, msg, transactionid) # print "result: " # pprint.pprint(result) # print # print "correct: " # pprint.pprint(correct) # print self.assertEquals(result, correct) dest, transactionid, msg = '/queue/myplace', '987', '123 456 789' correct = "SEND\ndestination: %s\ntransaction: %s\n\n%s\x00\n" % (dest, transactionid, msg) self.assertEquals(stomper.send(dest, msg, transactionid), correct) if __name__ == "__main__": unittest.main() stomper-0.4.3/lib/stomper/tests/teststomper_11.py0000644000076500000240000002560513453100403022321 0ustar oisinstaff00000000000000""" This is the unittest to verify my stomper module. The STOMP protocol specification maybe found here: * http://stomp.codehaus.org/Protocol I've looked and the stomp client by Jason R. Briggs and have based the message generation on how his client did it. The client can be found at the follow address however it isn't a dependancy. * http://www.briggs.net.nz/log/projects/stomppy (c) Oisin Mulvihill, 2007-07-26. License: http://www.apache.org/licenses/LICENSE-2.0 """ import pprint import unittest import stomper #.stomp_11 as stomper class TestEngine(stomper.Engine): """Test that these methods are called by the default engine. """ def __init__(self): super(TestEngine, self).__init__() self.ackCalled = False self.errorCalled = False self.receiptCalled = False def ack(self, msg): super(TestEngine, self).ack(msg) self.ackCalled = True return 'ack' def error(self, msg): super(TestEngine, self).error(msg) self.errorCalled = True return 'error' def receipt(self, msg): super(TestEngine, self).receipt(msg) self.receiptCalled = True return 'receipt' class Stomper11Test(unittest.TestCase): def testEngineToServerMessages(self): """Test the state machines reaction """ e = TestEngine() # React to a message which should be an ack: msg = stomper.Frame() msg.cmd = 'MESSAGE' msg.headers = { 'subscription': 1, 'destination:': '/queue/a', 'message-id:': 'some-message-id', 'content-type': 'text/plain', } msg.body = "hello queue a" rc = e.react(msg.pack()) self.assertEquals(rc, 'ack') self.assertEquals(e.ackCalled, True) # React to an error: error = stomper.Frame() error.cmd = 'ERROR' error.headers = {'message:': 'malformed packet received!'} error.body = """The message: ----- MESSAGE destined:/queue/a Hello queue a! ----- Did not contain a destination header, which is required for message propagation. \x00 """ rc = e.react(error.pack()) self.assertEquals(rc, 'error') self.assertEquals(e.errorCalled, True) # React to an receipt: receipt = stomper.Frame() receipt.cmd = 'RECEIPT' receipt.headers = {'receipt-id:': 'message-12345'} rc = e.react(receipt.pack()) self.assertEquals(rc, 'receipt') self.assertEquals(e.receiptCalled, True) def testEngine(self): """Test the basic state machine. """ e = stomper.Engine(testing=True) # test session connected message: msg = """CONNECTED version:1.1 session:ID:snorky.local-49191-1185461799654-3:18 \x00 """ result = stomper.unpack_frame(msg) correct = '' returned = e.react(result) self.assertEquals(returned, correct) # test message: msg = """MESSAGE subscription:1 destination:/queue/a message-id:some-message-id content-type:text/plain hello queue a \x00 """ returned = e.react(msg) correct = 'ACK\nsubscription:1\nmessage-id:some-message-id\n\n\x00\n' self.assertEquals(returned, correct) # test error: msg = """ERROR message:some error There was a problem with your last message \x00 """ returned = e.react(msg) correct = 'error' self.assertEquals(returned, correct) # test receipt: msg = """RECEIPT message-id:some-message-id \x00 """ returned = e.react(msg) correct = 'receipt' self.assertEquals(returned, correct) def testFramepack1(self): """Testing pack, unpacking and the Frame class. """ # Check bad frame generation: frame = stomper.Frame() def bad(): frame.cmd = 'SOME UNNOWN CMD' self.assertRaises(stomper.FrameError, bad) # Generate a MESSAGE frame: frame = stomper.Frame() frame.cmd = 'MESSAGE' frame.headers['destination'] = '/queue/a' frame.headers['message-id'] = 'card_data' frame.body = "hello queue a" result = frame.pack() # print "\n-- result " + "----" * 10 # pprint.pprint(result) # print # Try bad message unpack catching: bad_frame = stomper.Frame() self.assertRaises(stomper.FrameError, bad_frame.unpack, None) self.assertRaises(stomper.FrameError, bad_frame.unpack, '') # Try to read the generated frame back in # and then check the variables are set up # correctly: frame2 = stomper.Frame() frame2.unpack(result) self.assertEquals(frame2.cmd, 'MESSAGE') self.assertEquals(frame2.headers['destination'], '/queue/a') self.assertEquals(frame2.headers['message-id'], 'card_data') self.assertEquals(frame2.body, 'hello queue a') result = frame2.pack() correct = "MESSAGE\ndestination:/queue/a\nmessage-id:card_data\n\nhello queue a\x00\n" self.assertEquals(result, correct) result = stomper.unpack_frame(result) self.assertEquals(result['cmd'], 'MESSAGE') self.assertEquals(result['headers']['destination'], '/queue/a') self.assertEquals(result['headers']['message-id'], 'card_data') self.assertEquals(result['body'], 'hello queue a') def testFramepack2(self): """Testing pack, unpacking and the Frame class. """ # Check bad frame generation: frame = stomper.Frame() frame.cmd = 'DISCONNECT' result = frame.pack() correct = 'DISCONNECT\n\n\x00\n' self.assertEquals(result, correct) def testFrameUnpack2(self): """Testing unpack frame function against MESSAGE """ msg = """MESSAGE destination:/queue/a message-id:card_data hello queue a""" result = stomper.unpack_frame(msg) self.assertEquals(result['cmd'], 'MESSAGE') self.assertEquals(result['headers']['destination'], '/queue/a') self.assertEquals(result['headers']['message-id'], 'card_data') self.assertEquals(result['body'], 'hello queue a') def testFrameUnpack3(self): """Testing unpack frame function against CONNECTED """ msg = """CONNECTED version:1.1 session:ID:snorky.local-49191-1185461799654-3:18 """ result = stomper.unpack_frame(msg) self.assertEquals(result['cmd'], 'CONNECTED') self.assertEquals(result['headers']['session'], 'ID:snorky.local-49191-1185461799654-3:18') self.assertEquals(result['body'], '') def testBugInFrameUnpack1(self): msg = """MESSAGE destination:/queue/a message-id:card_data hello queue a \x00 """ result = stomper.unpack_frame(msg) self.assertEquals(result['cmd'], 'MESSAGE') self.assertEquals(result['headers']['destination'], '/queue/a') self.assertEquals(result['headers']['message-id'], 'card_data') self.assertEquals(result['body'], 'hello queue a') def testCommit(self): transactionid = '1234' correct = "COMMIT\ntransaction:%s\n\n\x00\n" % transactionid self.assertEquals(stomper.commit(transactionid), correct) def testAbort(self): transactionid = '1234' correct = "ABORT\ntransaction:%s\n\n\x00\n" % transactionid self.assertEquals(stomper.abort(transactionid), correct) def testBegin(self): transactionid = '1234' correct = "BEGIN\ntransaction:%s\n\n\x00\n" % transactionid self.assertEquals(stomper.begin(transactionid), correct) def testAck(self): subscription = '1' messageid = '1234' transactionid = '9876' header = 'subscription:%s\nmessage-id:%s\ntransaction:%s' % ( subscription, messageid, transactionid) correct = "ACK\n%s\n\n\x00\n" % header actual = stomper.ack(messageid, subscription, transactionid) self.assertEquals(actual, correct) subscription = '1' messageid = '1234' correct = "ACK\nsubscription:%s\nmessage-id:%s\n\n\x00\n" % ( subscription, messageid) self.assertEquals(stomper.ack(messageid, subscription), correct) def testNack(self): subscription = '1' messageid = '1234' transactionid = '9876' header = 'subscription:%s\nmessage-id:%s\ntransaction:%s' % ( subscription, messageid, transactionid) correct = "NACK\n%s\n\n\x00\n" % header actual = stomper.nack(messageid, subscription, transactionid) self.assertEquals(actual, correct) subscription = '1' messageid = '1234' correct = "NACK\nsubscription:%s\nmessage-id:%s\n\n\x00\n" % ( subscription, messageid) self.assertEquals(stomper.nack(messageid, subscription), correct) def testUnsubscribe(self): subscription = '1' correct = "UNSUBSCRIBE\nid:%s\n\n\x00\n" % subscription self.assertEquals(stomper.unsubscribe(subscription), correct) def testSubscribe(self): dest, ack = '/queue/all', 'client' correct = "SUBSCRIBE\nid:0\ndestination:%s\nack:%s\n\n\x00\n" % (dest, ack) self.assertEquals(stomper.subscribe(dest, 0, ack), correct) dest, ack = '/queue/all', 'auto' correct = "SUBSCRIBE\nid:0\ndestination:%s\nack:%s\n\n\x00\n" % (dest, ack) self.assertEquals(stomper.subscribe(dest, 0, ack), correct) correct = "SUBSCRIBE\nid:0\ndestination:%s\nack:%s\n\n\x00\n" % (dest, ack) self.assertEquals(stomper.subscribe(dest, 0), correct) def testConnect(self): username, password = 'bob', '123' correct = "CONNECT\naccept-version:1.1\nhost:localhost\nheart-beat:0,0\nlogin:%s\npasscode:%s\n\n\x00\n" % (username, password) self.assertEquals(stomper.connect(username, password, 'localhost'), correct) def testConnectWithHeartbeats(self): username, password = 'bob', '123' heartbeats = (1000, 1000) correct = "CONNECT\naccept-version:1.1\nhost:localhost\nheart-beat:1000,1000\nlogin:%s\npasscode:%s\n\n\x00\n" % (username, password) self.assertEquals(stomper.connect(username, password, 'localhost', heartbeats=heartbeats), correct) def testDisconnect(self): correct = "DISCONNECT\nreceipt:77\n\x00\n" self.assertEquals(stomper.disconnect(77), correct) def testSend(self): dest, transactionid, msg = '/queue/myplace', '', '123 456 789' correct = "SEND\ndestination:%s\ncontent-type:text/plain\n\n%s\x00\n" % (dest, msg) result = stomper.send(dest, msg, transactionid) self.assertEquals(result, correct) dest, transactionid, msg = '/queue/myplace', '987', '123 456 789' correct = "SEND\ndestination:%s\ncontent-type:text/plain\ntransaction:%s\n\n%s\x00\n" % (dest, transactionid, msg) self.assertEquals(stomper.send(dest, msg, transactionid), correct) if __name__ == "__main__": unittest.main() stomper-0.4.3/lib/stomper/utils.py0000644000076500000240000000110113453100403017406 0ustar oisinstaff00000000000000""" This module contains various utility functions used in various locations. (c) Oisin Mulvihill, 2007-07-28. License: http://www.apache.org/licenses/LICENSE-2.0 """ import logging def log_init(level): """Set up a logger that catches all channels and logs it to stdout. This is used to set up logging when testing. """ log = logging.getLogger() hdlr = logging.StreamHandler() formatter = logging.Formatter('%(asctime)s %(name)s %(levelname)s %(message)s') hdlr.setFormatter(formatter) log.addHandler(hdlr) log.setLevel(level) stomper-0.4.3/lib/stomper.egg-info/0000755000076500000240000000000013453103756017413 5ustar oisinstaff00000000000000stomper-0.4.3/lib/stomper.egg-info/PKG-INFO0000644000076500000240000002651713453103756020523 0ustar oisinstaff00000000000000Metadata-Version: 1.1 Name: stomper Version: 0.4.3 Summary: This is a transport neutral client implementation of the STOMP protocol. Home-page: https://github.com/oisinmulvihill/stomper Author: Oisin Mulvihill Author-email: oisin.mulvihilli@gmail.com License: http://www.apache.org/licenses/LICENSE-2.0 Description: ======= Stomper ======= .. contents:: :Author: Oisin Mulvihill :Contributors: - Micheal Twomey, Ricky Iacovou , - Arfrever Frehtes Taifersar Arahesis , - Niki Pore , - Simon Chopin, - Ian Weller , - Daniele Varrazzo - Ralph Bean - Lumír 'Frenzy' Balhar - Ralph Bean (https://github.com/ralphbean) - https://github.com/pgajdos Introduction ------------ This is a python client implementation of the STOMP protocol. The client is attempting to be transport layer neutral. This module provides functions to create and parse STOMP messages in a programmatic fashion. The messages can be easily generated and parsed, however its up to the user to do the sending and receiving. I've looked at the stomp client by Jason R. Briggs. I've based some of the 'function to message' generation on how his client does it. The client can be found at the follow address however it isn't a dependency. - `stompy `_ In testing this library I run against ActiveMQ project. The server runs in java, however its fairly standalone and easy to set up. The projects page is here: - `ActiveMQ `_ Source Code ----------- The code can be accessed via git on github. Further details can be found here: - `Stomper `_ Examples -------- Basic Usage ~~~~~~~~~~~ To see some basic code usage example see ``example/stomper_usage.py``. The unit test ``tests/teststomper.py`` illustrates how to use all aspects of the code. Receive/Sender ~~~~~~~~~~~~~~ The example ``receiver.py`` and ``sender.py`` show how messages and generated and then transmitted using the twisted framework. Other frameworks could be used instead. The examples also demonstrate the state machine I used to determine a response to received messages. I've also included ``stompbuffer-rx.py`` and ``stompbuffer-tx.py`` as examples of using the new stompbuffer module contributed by Ricky Iacovou. Release Process --------------- Submit a pull request with tests if possible. I'll review and submit. All tests must pass. I tend to run against python3.7 nowadays. I will then increment the version, add attribute and then release to pypi if all is good. Help Oisin remember the relase process:: # clean env for release: mkvirtualenv --clear -p python3.7 stomper # setup and run all tests: python setup.py develop python setup.py test # Build and release to test.pypi.org first: pip install twine python setup.py sdist bdist_wheel twine upload --repository-url https://test.pypi.org/legacy/ dist/* # On success twine upload dist/* # Commit any changes and tag the release git tag X.Y.Z Supported STOMP Versions ------------------------ 1.1 ~~~ This is the default version of the of STOMP used in stomper versions 0.3.x. * https://stomp.github.io/stomp-specification-1.1.html 1.0 ~~~ This is no longer the default protocol version. To use it you can import it as follows:: import stomper.stomp_10 as stomper This is the default version used in stomper version 0.2.x. * https://stomp.github.io/stomp-specification-1.0.html Version History --------------- 0.4.3 ~~~~~ Added missing attribution to contributors section and messed up 0.4.2 release to new pypi. 0.4.2 ~~~~~ Thanks to https://github.com/pgajdos for contributing a fix to include the license in the distribution. OM: Added minor fix to support test.pypi.org uploading before releasing. Document my release process to help me next time around. 0.4.1 ~~~~~ Thanks to Ralph Bean (https://github.com/ralphbean) contributing a fix to setup.py and utf-8 encoding under python3. 0.4.0 ~~~~~ Thanks to Lumír 'Frenzy' Balhar (https://github.com/frenzymadness) contributing python3 support. 0.3.0 ~~~~~ This release makes STOMP v1.1 the default protocol. To stick with STOMP v1.0 you can continue to use stomper v0.2.9 or change the import in your code to:: import stomper.stomp_10 as stomper **Note** Any fixes to STOMP v1.0 will only be applied to version >= 0.3. 0.2.9 ~~~~~ Thanks to Ralph Bean for contributing the new protocol 1.1 support: * https://github.com/oisinmulvihill/stomper/issues/6 * https://github.com/oisinmulvihill/stomper/pull/7 0.2.8 ~~~~~ Thanks to Daniele Varrazzo for contributing the fixes: https://github.com/oisinmulvihill/stomper/pull/4 * Fixed newline prepended to messages without transaction id https://github.com/oisinmulvihill/stomper/pull/5 * Fixed reST syntax. Extension changed to allow github to render it properly. Also changed the source url in the readme. 0.2.7 ~~~~~ I forgot to add a MANIFEST.in which makes sure README.md is present. Without this pip install fails: https://github.com/oisinmulvihill/stomper/issues/3. Thanks to Ian Weller for noticing this. I've also added in the fix suggested by Arfrever https://github.com/oisinmulvihill/stomper/issues/1. 0.2.6 ~~~~~ Add contributed fixes from Simon Chopin. He corrected many spelling mistakes throughout the code base. I've also made the README.md the main 0.2.5 ~~~~~ Add the contributed fix for issue #14 by Niki Pore. The issue was reported by Roger Hoover. This removes the extra line ending which can cause problems. 0.2.4 ~~~~~ OM: A minor release fixing the problem whereby uuid would be installed on python2.5+. It is not needed after python2.4 as it comes with python. Arfrever Frehtes Taifersar Arahesis contributed the fix for this. 0.2.3 ~~~~~ OM: I've fixed issue #9 with the example code. All messages are sent and received correctly. 0.2.2 ~~~~~ - Applied patch from esteve.fernandez to resolve "Issue 4: First Message not received" in the example code (http://code.google.com/p/stomper/issues/detail?id=4&can=1). - I've (Oisin) updated the examples to use twisted's line receiver and got it to "detect" complete stomp messages. The old example would not work if a large amount of data was streamed. In this case dataReceived would be called with all the chunks of a message. This means that it would not be correct for it to attempt to unpack and react until the whole message has been received. Using twisted's line receiver looking for the \x00 works like a charm for this. This release integrates the bug fixes and the optional stompbuffer contributed by Ricky Iacovou: - Removed the trailing '\n\n' inserted by Frame.pack(). I believe that adding this is incorrect, for the following reasons: http://stomp.codehaus.org/Protocol gives the example:: CONNECT login: passcode: ^@ and comments, "the body is empty in this case". This gives the impression that the body is *exactly* defined as "the bytes, if any, between the '\n\n' at the end of the header and the null byte". This works for both binary and ASCII payloads: if I want to send a string without a newline, I should be able to, in which case the body should look like:: this is a string without a newline^@ ... and the receiver should deal with this. This impression is reinforced by the fact that ActiveMQ will complain if you supply a content-length header with any other byte count than that described above. I am also unsure about the newline after the null byte as nothing in the protocol says that there should be a newline after the null byte. Much of the code in StompBuffer actively expects it to be there, but I suspect that *relying* on a frame ending '\x00\n' may well limit compatibility. It's not an issue with Stomper-to-Stomper communication, of course, as the sender puts it, the receiver accepts it, and ActiveMQ happily sends it along. - StompBuffer has had a few fixes; most notably, a fix that prevents a content-length "header" in the *body* from being picked up and used (!). The biggest change is a new method, syncBuffer(), which allows a corrupted buffer to recover from the corruption. Note that I've never actually *seen* the buffer corruption when using Twisted, but the thought occurred to me that a single corrupt buffer could hang the entire message handling process. - Fixed the typo "NO_REPONSE_NEEDED". I've changed it to NO_RESPONSE_NEEDED, but kept the old variable for backwards compatibility; - I've also modified the string format in send() to include the '\n\n' between the header and the body, which I think is missing (it currently has only one '\n'). - Added CONNECTED to VALID_COMMANDS so syncBuffer() does not decide these messages are bogus. - Added new unit test file teststompbuffer which covers the new functionality. Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Programming Language :: Python stomper-0.4.3/lib/stomper.egg-info/SOURCES.txt0000644000076500000240000000127113453103756021300 0ustar oisinstaff00000000000000LICENSE MANIFEST.in README.rst runtests.py setup.py lib/stomper/__init__.py lib/stomper/stomp_10.py lib/stomper/stomp_11.py lib/stomper/stompbuffer.py lib/stomper/utils.py lib/stomper.egg-info/PKG-INFO lib/stomper.egg-info/SOURCES.txt lib/stomper.egg-info/dependency_links.txt lib/stomper.egg-info/requires.txt lib/stomper.egg-info/top_level.txt lib/stomper/examples/__init__.py lib/stomper/examples/receiver.py lib/stomper/examples/sender.py lib/stomper/examples/stompbuffer-rx.py lib/stomper/examples/stompbuffer-tx.py lib/stomper/examples/stomper_usage.py lib/stomper/tests/__init__.py lib/stomper/tests/teststompbuffer.py lib/stomper/tests/teststomper_10.py lib/stomper/tests/teststomper_11.pystomper-0.4.3/lib/stomper.egg-info/dependency_links.txt0000644000076500000240000000000113453103756023461 0ustar oisinstaff00000000000000 stomper-0.4.3/lib/stomper.egg-info/requires.txt0000644000076500000240000000000713453103756022010 0ustar oisinstaff00000000000000future stomper-0.4.3/lib/stomper.egg-info/top_level.txt0000644000076500000240000000001013453103756022134 0ustar oisinstaff00000000000000stomper stomper-0.4.3/runtests.py0000644000076500000240000000173213453100403015710 0ustar oisinstaff00000000000000#!/usr/bin/env python # # Use nosetests to run the acceptance tests for this project. # # This script sets up the paths to find packages (see package_paths) # and limits the test discovery to only the listed set of locations # (see test_paths). # # Oisin Mulvihill # 2007-07-28 # import os import sys import nose import logging # These are where to find the various app and webapp # packages, along with any other thirdparty stuff. package_paths = [ "./lib", ] sys.path.extend(package_paths) # Only bother looking for tests in these locations: # (Note: these need to be absolute paths) current = os.path.abspath(os.path.curdir) test_paths = [ current + "/lib/stomper/tests", ] env = os.environ env['NOSE_WHERE'] = ' '.join(test_paths) # Set up logging so we don't get any logger not found messages: import stomper #stomper.utils.log_init(logging.DEBUG) stomper.utils.log_init(logging.CRITICAL) result = nose.core.TestProgram(env=env).success nose.result.end_capture() stomper-0.4.3/setup.cfg0000644000076500000240000000004613453103756015303 0ustar oisinstaff00000000000000[egg_info] tag_build = tag_date = 0 stomper-0.4.3/setup.py0000644000076500000240000000300713453103524015165 0ustar oisinstaff00000000000000""" Stomper distutils file. (c) Oisin Mulvihill 2007-07-26 """ import sys from setuptools import setup, find_packages Name = 'stomper' ProjectUrl = "https://github.com/oisinmulvihill/stomper" Version = '0.4.3' Author = 'Oisin Mulvihill' AuthorEmail = 'oisin.mulvihilli@gmail.com' Maintainer = 'Oisin Mulvihill' Summary = ( 'This is a transport neutral client implementation ' 'of the STOMP protocol.' ) License = 'http://www.apache.org/licenses/LICENSE-2.0' ShortDescription = ( "This is a transport neutral client implementation of the " "STOMP protocol." ) Classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", ] # Recover the ReStructuredText docs: Description = open("README.rst", "rb").read().decode("utf-8") TestSuite = 'stomper.tests' # stop any logger not found messages if tests are run. #stomper.utils.log_init(logging.CRITICAL) ProjectScripts = [] PackageData = { } needed = ['future'] if sys.version_info < (2, 5): needed += [ 'uuid>=1.2', ] setup( name=Name, version=Version, author=Author, author_email=AuthorEmail, description=ShortDescription, long_description=Description, url=ProjectUrl, license=License, classifiers=Classifiers, install_requires=needed, test_suite=TestSuite, scripts=ProjectScripts, packages=find_packages('lib'), package_data=PackageData, package_dir={'': 'lib'}, )