I put together this code as part of a practice problem. The instructions for the program are to accept JSON messages, transform the messages and then dispatch them to the right queue according to a series of rules.
The message service must accept messages when the following method is called: enqueue(msg)
. It must provide a single method that returns the next message for the queue with the number queue_number
.
Transformation Rules
You must implement the following transformations on input messages. These rules must be applied in order, using the transformed output in later steps. Multiple rules may apply to a single tuple.
- You must string-reverse any string value in the message that contains the exact string Mootium. For instance,
{"company": "Mootium, Inc.", "agent": "007"}
changes to{"company": ".cnI ,muitooM", "agent": "007"}
.- You must replace any integer values with the value produced by computing the bitwise negation of that integer's value. For instance,
{"value": 512}
changes to{"value": -513}
- You must add a field hash to any message that has a field
_hash
. The value of_hash
may be the name of another field. The value of your new field hash must contain the base64-encoded SHA-256 digest of the UTF-8-encoded value of that field. You may assume that the value you're given to hash is a string. If a hash field already exists in the message and the value is different to the computed hash value, then an exception should be thrown.Transformation rules, except the hash rule, must ignore the values of "private" fields whose names begin with an underscore (
_
).Dispatch Rules
There are five output queues, numbered 0 through 4.
You must implement the following "dispatch" rules to decide which queue gets a message. These rules must be applied in order; the first rule that matches is the one you should use.
- If a message contains the key
_special
, send it to queue 0.- If a message contains a hash field, send it to queue 1.
- If a message has a value that includes
muidaQ
(Qadium
in reverse), send it to queue 2.- If a message has an integer value, send it to queue 3.
- Otherwise, send the message to queue 4.
Dispatch rules must ignore the values of "private" fields whose names begin with an underscore (
_
). (Of course, rules that test the presence of keys that begin with_
still apply.)Sequences
Certain messages may be parts of a sequence. Such messages include some special fields:
_sequence
: an opaque string identifier for the sequence this message is part of_part
: an integer indicating which message this is in the sequence, starting at 0Sequences must be outputted in order. Dispatch rules are to be applied based on the first message in a sequence (message 0) only, while transformation rules must be applied to all messages.
The output queue must enqueue messages from a sequence as soon as it can; don't try to wait to output all messages of a sequence at a time. The output queue must return messages within a sequence in the correct order by part number (message 0 before message 1, before message 2 ...).
import queue
import json
import re
import base64
import hashlib
class AutoVivification(dict):
"""Implementation of perl's autovivification feature."""
def __getitem__(self, item):
try:
return dict.__getitem__(self, item)
except KeyError:
value = self[item] = type(self)()
return value
class MootiumError(Exception):
"""Raised in the case of requesting a message from an empty queue """
pass
class Queue:
"""Simple delivery message service. Transforms incoming messages
and sends each message to the appropriate queue, keeping sequences in order.
"""
def __init__(self):
#list of output queues
self.queue_dict = {
'0': queue.Queue(),
'1': queue.Queue(),
'2': queue.Queue(),
'3': queue.Queue(),
'4': queue.Queue()
}
#store sequences in a dictionary-like structure
self.sequenceDict = AutoVivification()
def transform(self, msg):
"""Transforms incoming messages to match the transformation rules.
Args:
msg: A string with the message.
Raises:
ValueError: If the message has a key for "hash" and the value
differs from the calculated hash.
Returns:
The transformed data as a dictionary.
"""
messageDict = json.loads(msg)
keys = [i for i in messageDict.keys()]
for i in keys:
# skip private fields
if re.match("_", i):
continue
# reverse strings that include Mootium
if re.search("Mootium", str(messageDict[i])):
messageDict[i] = messageDict[i][::-1]
# replace integer values with its bitwise negation
if isinstance(messageDict[i], int):
messageDict[i] = ~messageDict[i]
if "_hash" in keys:
# if _hash references another field, encode the value from that field.
# otherwise, encode the value associated with _hash
if messageDict["_hash"] in keys:
toEncode = messageDict[messageDict["_hash"]].encode()
else:
toEncode = messageDict["_hash"].encode()
# base64-encoded SHA-256 digest of the UTF-8 encoded value
encodedSHA256 = base64.b64encode(
(hashlib.sha256(toEncode)).digest())
if "hash" in keys:
# make sure the values are the same, if a hash field already exists
if encodedSHA256 != messageDict["hash"]:
raise ValueError(
'The computed hash has a different value from the existing hash field'
)
messageDict["hash"] = encodedSHA256
return messageDict
def dispatch(self, msg, output=None):
"""Delivers a message to the right queue to match the dispatch rules.
Args:
msg: A dictionary with the message.
output: The queue for the message, if it belongs to a sequence
and is not the first part.
Returns:
The number for the queue where the message was delivered.
"""
# set keys and separate public and private message contents
keys = [i for i in msg.keys()]
publicKeys = [i for i in keys if not i.startswith('_')]
publicContents = [msg[i] for i in publicKeys]
#turn the output into string
msgDump = json.dumps(msg)
#for sequenced messages with part > 0
if output:
self.queue_dict[str(output)].put(msgDump)
else:
if "_special" in keys:
output = 0
elif "hash" in publicKeys:
output = 1
elif re.search("muidaQ", str(publicContents)):
output = 2
elif sum([isinstance(msg[i], int) for i in publicKeys]) > 0:
output = 3
else:
output = 4
self.queue_dict[str(output)].put(msgDump)
return output
def enqueue(self, msg):
"""Adds a standard JSON message to the right queue, after applying
the rules for transformation. It will apply the transformations to
messages that belong to a sequence, determine the queue from
the first message in the sequence and add them in proper order.
Args:
msg: A standard JSON message with either strings or numerics.
"""
cleanmsg = self.transform(msg)
if "_sequence" in [i for i in cleanmsg.keys()]:
sequence = cleanmsg["_sequence"]
part = cleanmsg["_part"]
self.sequenceDict[sequence][part] = cleanmsg
if part == "0":
#dispatch the first message and get the queue number
self.sequenceDict[sequence]["output"] = self.dispatch(cleanmsg)
self.sequenceDict[sequence]["current"] = 0
# send the next message, if it is available and the output is set
if self.sequenceDict[sequence]["output"]:
output = self.sequenceDict[sequence]["output"]
while self.sequenceDict[sequence][str(
self.sequenceDict[sequence]["current"] + 1)]:
self.sequenceDict[sequence][
"current"] = self.sequenceDict[sequence]["current"] + 1
self.dispatch(
self.sequenceDict[sequence][str(
self.sequenceDict[sequence]["current"])], output)
else:
self.dispatch(cleanmsg)
def next(self, queue_number):
"""Pulls the next value from the specified queue.
Args:
msg: A standard JSON message with either strings or numerics.
Raises:
ValueError: If the queue number is outside the range of options.
QadiumError: If the requested queue is empty.
Returns:
The next value in the selected queue.
"""
valueError = 'Check your queue_number. Valid options include: 0, 1, 2, 3, 4.'
if queue_number not in [0, 1, 2, 3, 4]:
raise ValueError(valueError)
try:
return self.queue_dict[str(queue_number)].get(block=False)
except queue.Empty:
raise MootiumError("Nothing is available on the queue")
def get_message_service():
"""Returns a new, "clean" Q service."""
return Queue()
I'm curious for feedback. In particular, what would you have used instead of the auto vivification and are there ways you would make this more efficient or pieces of code you would exclude?