stomper-0.3.0/0000755000076700000240000000000012406527405013456 5ustar oisinstaff00000000000000stomper-0.3.0/lib/0000755000076700000240000000000012406527405014224 5ustar oisinstaff00000000000000stomper-0.3.0/lib/stomper/0000755000076700000240000000000012406527405015715 5ustar oisinstaff00000000000000stomper-0.3.0/lib/stomper/__init__.py0000644000076700000240000000043612404607651020031 0ustar oisinstaff00000000000000from 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.3.0/lib/stomper/examples/0000755000076700000240000000000012406527405017533 5ustar oisinstaff00000000000000stomper-0.3.0/lib/stomper/examples/__init__.py0000644000076700000240000000025612404602235021640 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.3.0/lib/stomper/examples/receiver.py0000644000076700000240000000641512404602235021710 0ustar oisinstaff00000000000000""" A simple twisted STOMP message receiver server. (c) Oisin Mulvihill, 2007-07-26. License: http://www.apache.org/licenses/LICENSE-2.0 """ 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.3.0/lib/stomper/examples/sender.py0000644000076700000240000000773012404602235021365 0ustar oisinstaff00000000000000""" A simple twisted STOMP message sender. (c) Oisin Mulvihill, 2007-07-26. License: http://www.apache.org/licenses/LICENSE-2.0 """ 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 = self.counter.next() 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.3.0/lib/stomper/examples/stompbuffer-rx.py0000644000076700000240000000641012404602235023062 0ustar oisinstaff00000000000000""" A simple twisted STOMP message receiver server. (c) Oisin Mulvihill, 2007-07-26. License: http://www.apache.org/licenses/LICENSE-2.0 """ 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.3.0/lib/stomper/examples/stompbuffer-tx.py0000644000076700000240000000714212404602235023067 0ustar oisinstaff00000000000000""" A simple twisted STOMP message sender. (c) Oisin Mulvihill, 2007-07-26. License: http://www.apache.org/licenses/LICENSE-2.0 """ 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.3.0/lib/stomper/examples/stomper_usage.py0000644000076700000240000000631612404602235022761 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 """ 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.3.0/lib/stomper/stomp_10.py0000644000076700000240000003123312404602771017731 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 """ import re import uuid import types import logging import utils 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', ] 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 types.StringTypes: msg = unpack_frame(msg) elif mtype == types.DictType: pass else: raise FrameError("Unknown message type '%s', I don't know what to do with this!" % mtype) if self.states.has_key(msg['cmd']): # 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 msg['headers'].has_key('transaction-id'): 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 msg['headers'].has_key('message'): 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 msg['headers'].has_key('receipt-id'): 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.3.0/lib/stomper/stomp_11.py0000644000076700000240000003420012404607571017732 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 """ import re import uuid import types import logging import utils 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', ] 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 types.StringTypes: msg = unpack_frame(msg) elif mtype == types.DictType: pass else: raise FrameError("Unknown message type '%s', I don't know what to do with this!" % mtype) if self.states.has_key(msg['cmd']): # 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 msg['headers'].has_key('transaction-id'): 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 msg['headers'].has_key('message'): 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 msg['headers'].has_key('receipt-id'): 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.3.0/lib/stomper/stompbuffer.py0000644000076700000240000002375312404602235020626 0ustar oisinstaff00000000000000""" StompBuffer is an optional utility class accompanying Stomper. Ricky Iacovou, 2008-03-27. License: http://www.apache.org/licenses/LICENSE-2.0 """ 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.3.0/lib/stomper/tests/0000755000076700000240000000000012406527405017057 5ustar oisinstaff00000000000000stomper-0.3.0/lib/stomper/tests/__init__.py0000644000076700000240000000000012404602235021147 0ustar oisinstaff00000000000000stomper-0.3.0/lib/stomper/tests/teststompbuffer.py0000644000076700000240000002410612404602235022661 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 ) != types.DictType: 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.3.0/lib/stomper/tests/teststomper_10.py0000644000076700000240000002333412404607342022324 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.3.0/lib/stomper/tests/teststomper_11.py0000644000076700000240000002560512404607353022332 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.3.0/lib/stomper/utils.py0000644000076700000240000000110112404602235017411 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.3.0/lib/stomper.egg-info/0000755000076700000240000000000012406527405017407 5ustar oisinstaff00000000000000stomper-0.3.0/lib/stomper.egg-info/dependency_links.txt0000644000076700000240000000000112406527375023463 0ustar oisinstaff00000000000000 stomper-0.3.0/lib/stomper.egg-info/PKG-INFO0000644000076700000240000002272612406527375020523 0ustar oisinstaff00000000000000Metadata-Version: 1.1 Name: stomper Version: 0.3.0 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 dot mulvihill at 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 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. 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.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.3.0/lib/stomper.egg-info/SOURCES.txt0000644000076700000240000000121712406527376021303 0ustar oisinstaff00000000000000MANIFEST.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/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.3.0/lib/stomper.egg-info/top_level.txt0000644000076700000240000000001012406527375022136 0ustar oisinstaff00000000000000stomper stomper-0.3.0/MANIFEST.in0000644000076700000240000000002312404602235015200 0ustar oisinstaff00000000000000include README.rst stomper-0.3.0/PKG-INFO0000644000076700000240000002272612406527405014564 0ustar oisinstaff00000000000000Metadata-Version: 1.1 Name: stomper Version: 0.3.0 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 dot mulvihill at 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 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. 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.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.3.0/README.rst0000644000076700000240000001624012404612163015142 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 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. 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.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.3.0/runtests.py0000644000076700000240000000173212404602235015713 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.3.0/setup.cfg0000644000076700000240000000007312406527405015277 0ustar oisinstaff00000000000000[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 stomper-0.3.0/setup.py0000644000076700000240000000300212404607324015160 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.3.0' Author = 'Oisin Mulvihill' AuthorEmail = 'oisin dot mulvihill at 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: fd = file("README.rst") Description = fd.read() fd.close() TestSuite = 'stomper.tests' # stop any logger not found messages if tests are run. #stomper.utils.log_init(logging.CRITICAL) ProjectScripts = [] PackageData = { } needed = [] 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'}, )