blob: c6ad93fe1a6ae2f6c0ddd84b369bcaab25bdb89b [file]
#!/usr/bin/env python3
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
#
"""
Python Proton AMQP 1.0 Shim
Native implementation using qpid-proton Python bindings.
Supports send/receive via broker and direct peer-to-peer modes.
"""
import argparse
import json
import math
import struct
import sys
import uuid as uuid_module
from typing import Any
from proton import Array, Data, Described, Message, UNDESCRIBED
from proton.handlers import MessagingHandler
from proton.reactor import Container
AMQP_TYPE_TO_DATA_TYPE = {
"null": Data.NULL, "boolean": Data.BOOL,
"ubyte": Data.UBYTE, "ushort": Data.USHORT, "uint": Data.UINT, "ulong": Data.ULONG,
"byte": Data.BYTE, "short": Data.SHORT, "int": Data.INT, "long": Data.LONG,
"float": Data.FLOAT, "double": Data.DOUBLE,
"char": Data.CHAR, "timestamp": Data.TIMESTAMP, "uuid": Data.UUID,
"binary": Data.BINARY, "string": Data.STRING, "symbol": Data.SYMBOL,
"list": Data.LIST, "map": Data.MAP, "array": Data.ARRAY, "described": Data.DESCRIBED,
}
DATA_TYPE_TO_AMQP_TYPE = {v: k for k, v in AMQP_TYPE_TO_DATA_TYPE.items()}
def encode_typed_element(elem_type, elem_value):
"""Encode a typed element ["type", value] to a proton value. Recurses for complex types."""
if elem_type == "array":
return encode_array(elem_value)
if elem_type == "list":
return encode_list(elem_value)
if elem_type == "map":
return encode_map(elem_value)
if elem_type == "described":
return encode_described(elem_value)
return encode_primitive(elem_type, elem_value)
def encode_array(value):
"""Encode array: {"element_type": str, "elements": [...]}."""
elem_type = value["element_type"]
elements = value.get("elements", [])
data_type = AMQP_TYPE_TO_DATA_TYPE.get(elem_type, Data.NULL)
if not elements:
return Array(UNDESCRIBED, data_type)
encoded = [encode_typed_element(elem_type, e) for e in elements]
return Array(UNDESCRIBED, data_type, *encoded)
def encode_list(value):
"""Encode list: [["type", value], ...]."""
return [encode_typed_element(e[0], e[1]) for e in value]
def encode_map(value):
"""Encode map: [[["ktype", kval], ["vtype", vval]], ...]."""
result = {}
for pair in value:
k = encode_typed_element(pair[0][0], pair[0][1])
v = encode_typed_element(pair[1][0], pair[1][1])
result[k] = v
return result
def encode_described(value):
"""Encode described: {"descriptor": ["type", val], "value": ["type", val]}."""
desc = encode_typed_element(value["descriptor"][0], value["descriptor"][1])
inner = encode_typed_element(value["value"][0], value["value"][1])
return Described(desc, inner)
def encode_primitive(amqp_type, value):
"""Encode a single primitive value to proton type (standalone version)."""
from proton import byte, char, float32, int32, short, symbol, timestamp, ubyte, uint, ulong, ushort
if amqp_type == "null":
return None
if amqp_type == "boolean":
return bool(value)
if amqp_type == "ubyte":
return ubyte(int(value) if isinstance(value, str) else value)
if amqp_type == "ushort":
return ushort(int(value) if isinstance(value, str) else value)
if amqp_type == "uint":
return uint(int(value) if isinstance(value, str) else value)
if amqp_type == "ulong":
return ulong(int(value) if isinstance(value, str) else value)
if amqp_type == "byte":
return byte(int(value) if isinstance(value, str) else value)
if amqp_type == "short":
return short(int(value) if isinstance(value, str) else value)
if amqp_type == "int":
return int32(int(value) if isinstance(value, str) else value)
if amqp_type == "long":
return int(value) if isinstance(value, str) else value
if amqp_type == "float":
if isinstance(value, str) and value.startswith("0x"):
int_val = int(value, 16)
bytes_val = struct.pack(">I", int_val)
return float32(struct.unpack(">f", bytes_val)[0])
return float32(float(value))
if amqp_type == "double":
if isinstance(value, str) and value.startswith("0x"):
int_val = int(value, 16)
bytes_val = struct.pack(">Q", int_val)
return struct.unpack(">d", bytes_val)[0]
return float(value)
if amqp_type == "char":
if isinstance(value, str):
if value == '' or value == '\\x00':
code_point = 0
elif len(value) == 1:
code_point = ord(value)
else:
code_point = int(value)
else:
code_point = value
return char(chr(code_point))
if amqp_type == "timestamp":
return timestamp(int(value) if isinstance(value, str) else value)
if amqp_type == "uuid":
return uuid_module.UUID(value)
if amqp_type == "binary":
if isinstance(value, str):
return bytes.fromhex(value)
return bytes(value)
if amqp_type == "string":
return str(value)
if amqp_type == "symbol":
return symbol(str(value))
raise ValueError(f"Unsupported AMQP type: {amqp_type}")
def decode_value_recursive(value):
"""Decode a proton value to a typed element ["type", decoded_value]. Recurses for complex types."""
if value is None:
return ["null", None]
if isinstance(value, Array):
elem_type_name = DATA_TYPE_TO_AMQP_TYPE.get(value.type, "unknown")
decoded_elements = []
for elem in value.elements:
_, decoded = decode_value_recursive(elem)
decoded_elements.append(decoded)
return ["array", {"element_type": elem_type_name, "elements": decoded_elements}]
if isinstance(value, Described):
desc_elem = decode_value_recursive(value.descriptor)
val_elem = decode_value_recursive(value.value)
return ["described", {"descriptor": desc_elem, "value": val_elem}]
if isinstance(value, dict):
pairs = []
for k, v in value.items():
pairs.append([decode_value_recursive(k), decode_value_recursive(v)])
return ["map", pairs]
if isinstance(value, (list, tuple)):
elements = [decode_value_recursive(elem) for elem in value]
return ["list", elements]
# Primitive value — infer type and decode
return _decode_primitive_to_typed(value)
def _decode_primitive_to_typed(value):
"""Decode a proton primitive value to ["type", json_value]."""
if value is None:
return ["null", None]
if isinstance(value, bool):
return ["boolean", value]
type_name = type(value).__name__
if isinstance(value, uuid_module.UUID):
return ["uuid", str(value)]
if isinstance(value, bytes):
return ["binary", value.hex()]
if isinstance(value, (bytearray, memoryview)):
return ["binary", bytes(value).hex()]
if type_name == "float32":
float_bytes = struct.pack(">f", float(value))
int_val = struct.unpack(">I", float_bytes)[0]
return ["float", f"0x{int_val:08x}"]
if type_name in ("float", "double") or isinstance(value, float):
float_bytes = struct.pack(">d", float(value))
int_val = struct.unpack(">Q", float_bytes)[0]
return ["double", f"0x{int_val:016x}"]
if type_name == "char":
return ["char", ord(str(value))]
if type_name == "timestamp":
return ["timestamp", int(value)]
if type_name == "symbol":
return ["symbol", str(value)]
int_type_map = {
"ubyte": "ubyte", "ushort": "ushort", "uint": "uint", "ulong": "ulong",
"byte": "byte", "short": "short", "int32": "int",
}
if type_name in int_type_map:
return [int_type_map[type_name], int(value)]
if type_name == "int" or isinstance(value, int):
return ["long", int(value)]
if isinstance(value, str):
return ["string", value]
return ["string", str(value)]
class SenderHandler(MessagingHandler):
"""Handler for sending AMQP messages."""
def __init__(
self, url: str, queue: str, messages: list[dict[str, Any]],
jms_mode: bool = False, amqp_type: str = "string",
headers: dict[str, Any] | None = None,
properties: dict[str, Any] | None = None,
message_header: dict[str, Any] | None = None,
) -> None:
super().__init__()
self.url = url
self.queue = queue
self.messages = messages
self.jms_mode = jms_mode
self.amqp_type = amqp_type
self.headers = headers
self.properties = properties
self.message_header = message_header
self.sent_count = 0
self.confirmed_count = 0
def on_start(self, event: Any) -> None:
"""Create sender when container starts."""
connection = event.container.connect(url=self.url, sasl_enabled=False, reconnect=False)
event.container.create_sender(connection, target=self.queue)
def on_sendable(self, event: Any) -> None:
"""Send messages when credit is available."""
while event.sender.credit and self.sent_count < len(self.messages):
msg_data = self.messages[self.sent_count]
msg = Message()
msg.id = msg_data["index"]
# Encode body
if self.jms_mode and self.amqp_type == "map":
sub_type = msg_data["type"]
key = f"{sub_type}_{msg_data['index']:03d}"
encoded_value = self._encode_value(sub_type, msg_data["value"])
msg.body = {key: encoded_value}
elif self.jms_mode and self.amqp_type == "list":
sub_type = msg_data["type"]
encoded_value = self._encode_value(sub_type, msg_data["value"])
msg.body = [encoded_value]
elif self.amqp_type in ("array", "list", "map", "described"):
msg.body = encode_typed_element(self.amqp_type, msg_data["value"])
else:
msg.body = self._encode_value(msg_data["type"], msg_data["value"])
# Add JMS annotations if in JMS mode
if self.jms_mode:
from proton import byte, symbol
# Map type to JMS message type
jms_type = self._get_jms_message_type(self.amqp_type)
if jms_type is not None:
# NOTE: Key MUST be symbol, value MUST be byte (not ubyte)
# This matches Qpid JMS Client wire format
msg.annotations = {symbol("x-opt-jms-msg-type"): byte(jms_type)}
if self.headers:
self._apply_headers(msg)
if self.properties:
self._apply_properties(msg)
if self.message_header:
self._apply_message_header(msg)
event.sender.send(msg)
self.sent_count += 1
def on_accepted(self, event: Any) -> None:
"""Track message confirmations."""
self.confirmed_count += 1
if self.confirmed_count == len(self.messages):
event.connection.close()
def on_rejected(self, event: Any) -> None:
"""Handle rejected messages."""
print(f"Message rejected: {event.delivery.remote}", file=sys.stderr)
event.connection.close()
def _get_jms_message_type(self, amqp_type: str) -> int | None:
"""Map AMQP type to JMS message type byte value."""
# JMS message type constants (from Qpid JMS Client)
JMS_MESSAGE = 0 # Empty message
JMS_MAP_MESSAGE = 2 # Map
JMS_BYTES_MESSAGE = 3 # Binary data
JMS_STREAM_MESSAGE = 4 # List/stream
JMS_TEXT_MESSAGE = 5 # String/text
# Map AMQP types to JMS message types
if amqp_type == "string":
return JMS_TEXT_MESSAGE
elif amqp_type == "binary":
return JMS_BYTES_MESSAGE
elif amqp_type == "null":
return JMS_MESSAGE
elif amqp_type == "map":
return JMS_MAP_MESSAGE
elif amqp_type == "list":
return JMS_STREAM_MESSAGE
return None
def _apply_headers(self, msg: Message) -> None:
"""Set JMS headers as AMQP message properties."""
from proton import byte, symbol
if "JMSCorrelationID" in self.headers:
h = self.headers["JMSCorrelationID"]
if h["type"] == "string":
msg.correlation_id = h["value"]
elif h["type"] == "bytes":
msg.correlation_id = bytes.fromhex(h["value"])
if "JMSReplyTo" in self.headers:
h = self.headers["JMSReplyTo"]
msg.reply_to = h["value"]
reply_type = byte(1) if h["type"] == "topic" else byte(0)
if msg.annotations is None:
msg.annotations = {}
msg.annotations[symbol("x-opt-jms-reply-to")] = reply_type
if "JMSType" in self.headers:
h = self.headers["JMSType"]
msg.subject = h["value"]
def _apply_properties(self, msg: Message) -> None:
"""Set JMS application properties as AMQP application-properties."""
import struct
from proton import byte, float32, int32, short
props = {}
for name, prop in self.properties.items():
ptype = prop["type"]
pval = prop["value"]
if ptype == "boolean":
props[name] = bool(pval) if isinstance(pval, bool) else pval == "True"
elif ptype == "byte":
v = int(pval, 16) if isinstance(pval, str) else int(pval)
props[name] = byte(v if v < 128 else v - 256)
elif ptype == "short":
v = int(pval, 16) if isinstance(pval, str) else int(pval)
props[name] = short(v if v < 32768 else v - 65536)
elif ptype == "int":
v = int(pval, 16) if isinstance(pval, str) else int(pval)
if v >= 0x80000000:
v -= 0x100000000
props[name] = int32(v)
elif ptype == "long":
v = int(pval, 16) if isinstance(pval, str) else int(pval)
if v >= 0x8000000000000000:
v -= 0x10000000000000000
props[name] = v
elif ptype == "float":
bits = int(pval, 16) if isinstance(pval, str) else int(pval)
props[name] = float32(struct.unpack("!f", struct.pack("!I", bits))[0])
elif ptype == "double":
bits = int(pval, 16) if isinstance(pval, str) else int(pval)
props[name] = struct.unpack("!d", struct.pack("!Q", bits))[0]
elif ptype == "string":
props[name] = str(pval)
msg.properties = props
def _apply_message_header(self, msg: Message) -> None:
"""Set AMQP Header section fields on the message."""
if "durable" in self.message_header:
msg.durable = self.message_header["durable"]
if "priority" in self.message_header:
msg.priority = int(self.message_header["priority"])
if "ttl" in self.message_header:
# Proton Python msg.ttl is in seconds; input is AMQP ms
msg.ttl = float(self.message_header["ttl"]) / 1000.0
if "first_acquirer" in self.message_header:
msg.first_acquirer = self.message_header["first_acquirer"]
def _encode_value(self, amqp_type: str, value: Any) -> Any:
"""Encode test value to AMQP type."""
if amqp_type == "null":
return None
if amqp_type == "boolean":
return bool(value)
# Unsigned integers
if amqp_type == "ubyte":
from proton import ubyte
return ubyte(int(value) if isinstance(value, str) else value)
if amqp_type == "ushort":
from proton import ushort
return ushort(int(value) if isinstance(value, str) else value)
if amqp_type == "uint":
from proton import uint
return uint(int(value) if isinstance(value, str) else value)
if amqp_type == "ulong":
from proton import ulong
return ulong(int(value) if isinstance(value, str) else value)
# Signed integers
if amqp_type == "byte":
from proton import byte
return byte(int(value) if isinstance(value, str) else value)
if amqp_type == "short":
from proton import short
return short(int(value) if isinstance(value, str) else value)
if amqp_type == "int":
from proton import int32
return int32(int(value) if isinstance(value, str) else value)
if amqp_type == "long":
return int(value) if isinstance(value, str) else value
# Floating point - from hex representation
if amqp_type == "float":
from proton import float32
if isinstance(value, str) and value.startswith("0x"):
int_val = int(value, 16)
bytes_val = struct.pack(">I", int_val)
float_val = struct.unpack(">f", bytes_val)[0]
return float32(float_val)
return float32(float(value))
if amqp_type == "double":
if isinstance(value, str) and value.startswith("0x"):
int_val = int(value, 16)
bytes_val = struct.pack(">Q", int_val)
return struct.unpack(">d", bytes_val)[0]
return float(value)
# Character (UTF-32)
if amqp_type == "char":
from proton import char
if isinstance(value, str):
# Handle string representations: empty, escape sequence, or numeric
if value == '' or value == '\\x00':
code_point = 0
elif len(value) == 1:
code_point = ord(value)
else:
code_point = int(value)
else:
code_point = value
return char(chr(code_point))
# Timestamp (milliseconds since epoch)
if amqp_type == "timestamp":
from proton import timestamp
return timestamp(int(value) if isinstance(value, str) else value)
# UUID - Proton accepts UUID objects directly
if amqp_type == "uuid":
return uuid_module.UUID(value)
# Binary
if amqp_type == "binary":
if isinstance(value, str):
# Hex string to bytes
return bytes.fromhex(value)
return bytes(value)
# String
if amqp_type == "string":
return str(value)
# Symbol
if amqp_type == "symbol":
from proton import symbol
return symbol(str(value))
# Unknown type
raise ValueError(f"Unsupported AMQP type: {amqp_type}")
class ReceiverHandler(MessagingHandler):
"""Handler for receiving AMQP messages."""
def __init__(self, url: str, queue: str, count: int) -> None:
super().__init__()
self.url = url
self.queue = queue
self.expected_count = count
self.received_messages: list[dict[str, Any]] = []
def on_start(self, event: Any) -> None:
"""Create receiver when container starts."""
connection = event.container.connect(url=self.url, sasl_enabled=False, reconnect=False)
event.container.create_receiver(connection, source=self.queue)
def on_message(self, event: Any) -> None:
"""Process received message."""
msg = event.message
# Check for JMS message type annotation
# NOTE: Qpid JMS Client uses symbol as key
from proton import symbol
jms_msg_type = None
annotation_key = symbol("x-opt-jms-msg-type")
if msg.annotations and annotation_key in msg.annotations:
jms_msg_type = int(msg.annotations[annotation_key])
# Extract message data
if jms_msg_type is not None:
# Decode as JMS message
msg_data = self._decode_jms_message(msg, jms_msg_type)
elif self._is_complex_type(msg.body):
# Decode as complex AMQP type
typed_elem = decode_value_recursive(msg.body)
msg_data = {
"index": msg.id if msg.id is not None else len(self.received_messages),
"type": typed_elem[0],
"value": typed_elem[1],
}
else:
# Decode as regular AMQP primitive
msg_data = {
"index": msg.id if msg.id is not None else len(self.received_messages),
"type": self._infer_type(msg.body),
"value": self._decode_value(msg.body),
}
headers = self._extract_headers(msg)
if headers:
msg_data["headers"] = headers
properties = self._extract_properties(msg)
if properties:
msg_data["properties"] = properties
msg_data["message_header"] = self._extract_message_header(msg)
self.received_messages.append(msg_data)
# Close when all messages received
if len(self.received_messages) >= self.expected_count:
event.receiver.close()
event.connection.close()
def _is_complex_type(self, body: Any) -> bool:
"""Check if body is a complex AMQP type (array, list, map, described)."""
if isinstance(body, Array):
return True
if isinstance(body, Described):
return True
if isinstance(body, dict):
return True
if isinstance(body, (list, tuple)):
return True
return False
def _extract_headers(self, msg: Message) -> dict[str, Any]:
"""Extract JMS headers from AMQP message properties."""
from proton import symbol
headers: dict[str, Any] = {}
if msg.correlation_id is not None:
if isinstance(msg.correlation_id, (bytes, bytearray, memoryview)):
headers["JMSCorrelationID"] = {
"type": "bytes",
"value": bytes(msg.correlation_id).hex(),
}
else:
headers["JMSCorrelationID"] = str(msg.correlation_id)
if msg.reply_to is not None:
reply_to = msg.reply_to
reply_type = "queue"
ann_key = symbol("x-opt-jms-reply-to")
if msg.annotations and ann_key in msg.annotations:
reply_type = "topic" if int(msg.annotations[ann_key]) == 1 else "queue"
elif reply_to.startswith("topic://"):
reply_type = "topic"
reply_to = reply_to[8:]
elif reply_to.startswith("queue://"):
reply_to = reply_to[8:]
headers["JMSReplyTo"] = {"type": reply_type, "value": reply_to}
if msg.subject is not None:
headers["JMSType"] = msg.subject
return headers
def _extract_properties(self, msg: Message) -> dict[str, Any]:
"""Extract JMS application properties from AMQP application-properties."""
import struct
from proton import byte, float32, int32, short
properties: dict[str, Any] = {}
if msg.properties is None:
return properties
for name, value in msg.properties.items():
prop: dict[str, Any] = {}
if isinstance(value, bool):
prop["type"] = "boolean"
prop["value"] = value
elif isinstance(value, byte):
prop["type"] = "byte"
prop["value"] = f"0x{int(value) & 0xFF:02x}"
elif isinstance(value, short):
prop["type"] = "short"
prop["value"] = f"0x{int(value) & 0xFFFF:04x}"
elif isinstance(value, int32):
prop["type"] = "int"
prop["value"] = f"0x{int(value) & 0xFFFFFFFF:08x}"
elif isinstance(value, int):
prop["type"] = "long"
prop["value"] = f"0x{value & 0xFFFFFFFFFFFFFFFF:016x}"
elif isinstance(value, float32):
prop["type"] = "float"
bits = struct.unpack("!I", struct.pack("!f", float(value)))[0]
prop["value"] = f"0x{bits:08x}"
elif isinstance(value, float):
prop["type"] = "double"
bits = struct.unpack("!Q", struct.pack("!d", value))[0]
prop["value"] = f"0x{bits:016x}"
elif isinstance(value, str):
prop["type"] = "string"
prop["value"] = value
else:
prop["type"] = "string"
prop["value"] = str(value)
properties[name] = prop
return properties
def _extract_message_header(self, msg: Message) -> dict[str, Any]:
"""Extract AMQP Header section fields."""
return {
"durable": msg.durable,
"priority": int(msg.priority),
# Proton Python msg.ttl is in seconds; output as AMQP ms
"ttl": int(float(msg.ttl) * 1000),
"first_acquirer": msg.first_acquirer,
"delivery_count": int(msg.delivery_count),
}
def _decode_jms_message(self, msg: Message, jms_msg_type: int) -> dict[str, Any]:
"""Decode JMS message based on message type annotation."""
# JMS message type constants
JMS_MESSAGE = 0
JMS_TEXT_MESSAGE = 5
JMS_BYTES_MESSAGE = 3
JMS_MAP_MESSAGE = 2
JMS_STREAM_MESSAGE = 4
msg_index = msg.id if msg.id is not None else len(self.received_messages)
if jms_msg_type == JMS_TEXT_MESSAGE:
# TextMessage: body is string in AmqpValue section
return {
"index": msg_index,
"type": "text", # Use "text" to match JMS shim output
"value": str(msg.body) if msg.body is not None else None,
}
elif jms_msg_type == JMS_BYTES_MESSAGE:
# BytesMessage: body is binary in Data section
body_val = msg.body
if body_val is None:
return {"index": msg_index, "type": "bytes", "value": ""}
if not isinstance(body_val, bytes):
body_val = bytes(body_val)
return {"index": msg_index, "type": "bytes", "value": body_val.hex()}
elif jms_msg_type == JMS_MESSAGE:
# Empty message
return {"index": msg_index, "type": "null", "value": None}
elif jms_msg_type == JMS_MAP_MESSAGE:
body = msg.body
if body and isinstance(body, dict):
key = next(iter(body))
raw_value = body[key]
return {
"index": msg_index,
"type": self._infer_type(raw_value),
"value": self._decode_value(raw_value),
}
return {"index": msg_index, "type": "none", "value": None}
elif jms_msg_type == JMS_STREAM_MESSAGE:
body = msg.body
if body and isinstance(body, (list, tuple)) and len(body) > 0:
raw_value = body[0]
return {
"index": msg_index,
"type": self._infer_type(raw_value),
"value": self._decode_value(raw_value),
}
return {"index": msg_index, "type": "none", "value": None}
else:
# Unknown JMS type, fall back to regular AMQP decoding
return {
"index": msg_index,
"type": self._infer_type(msg.body),
"value": self._decode_value(msg.body),
}
def _infer_type(self, value: Any) -> str:
"""Infer AMQP type from Python value."""
if value is None:
return "null"
type_name = type(value).__name__
# Check for UUID first (it's from stdlib uuid module)
if isinstance(value, uuid_module.UUID):
return "uuid"
# Proton types
type_map = {
"bool": "boolean",
"ubyte": "ubyte",
"ushort": "ushort",
"uint": "uint",
"ulong": "ulong",
"byte": "byte",
"short": "short",
"int32": "int",
"int": "long",
"float32": "float",
"float": "double",
"char": "char",
"timestamp": "timestamp",
"bytes": "binary",
"memoryview": "binary",
"str": "string",
"symbol": "symbol",
}
return type_map.get(type_name, "unknown")
def _decode_value(self, value: Any) -> Any:
"""Decode AMQP value to JSON-serializable format."""
if value is None:
return None
if isinstance(value, bool):
return value
# Proton numeric types — check BEFORE isinstance(float/int) since
# Proton types like float32 are float subclasses
type_name = type(value).__name__
if type_name in ("ubyte", "ushort", "uint", "ulong", "byte", "short", "int32"):
return int(value)
if type_name == "float32":
float_bytes = struct.pack(">f", float(value))
int_val = struct.unpack(">I", float_bytes)[0]
return f"0x{int_val:08x}"
# Python float (64-bit double) — Proton's "float" type name is double
if isinstance(value, float):
float_bytes = struct.pack(">d", value)
int_val = struct.unpack(">Q", float_bytes)[0]
return f"0x{int_val:016x}"
if isinstance(value, int):
return value
# Character
if type_name == "char":
return ord(str(value))
# Timestamp
if type_name == "timestamp":
return int(value)
# UUID - convert to standard string format
if isinstance(value, uuid_module.UUID):
return str(value)
# Binary
if isinstance(value, (bytes, bytearray)):
return bytes(value).hex()
if isinstance(value, memoryview):
return bytes(value).hex()
# String
if isinstance(value, str):
return value
# Symbol
if type_name == "symbol":
return str(value)
return str(value)
class LargeContentSender(MessagingHandler):
"""Handler for sending a single large content message."""
def __init__(self, url: str, queue: str, message: Message) -> None:
super().__init__()
self.url = url
self.queue = queue
self.message = message
self.sent = False
def on_start(self, event: Any) -> None:
connection = event.container.connect(url=self.url, sasl_enabled=False, reconnect=False)
event.container.create_sender(connection, target=self.queue)
def on_sendable(self, event: Any) -> None:
if not self.sent:
event.sender.send(self.message)
self.sent = True
def on_accepted(self, event: Any) -> None:
event.connection.close()
def on_rejected(self, event: Any) -> None:
print(f"Message rejected: {event.delivery.remote}", file=sys.stderr)
event.connection.close()
class LargeContentReceiver(MessagingHandler):
"""Handler for receiving a single large content message.
Copies body immediately — Proton's internal buffer (memoryview) is only
valid inside on_message.
"""
def __init__(self, url: str, queue: str) -> None:
super().__init__()
self.url = url
self.queue = queue
self.body: bytes | str | None = None
def on_start(self, event: Any) -> None:
connection = event.container.connect(url=self.url, sasl_enabled=False, reconnect=False)
event.container.create_receiver(connection, source=self.queue)
def on_message(self, event: Any) -> None:
raw = event.message.body
if isinstance(raw, (memoryview, bytearray)):
self.body = bytes(raw)
elif isinstance(raw, dict):
self.body = dict(raw)
elif isinstance(raw, (list, tuple)):
self.body = list(raw)
else:
self.body = raw
event.receiver.close()
event.connection.close()
def lcg_generate_bytes(seed: int, size: int) -> bytes:
"""Generate pseudo-random bytes using glibc-style LCG."""
state = seed & 0x7FFFFFFF
result = bytearray(size)
for i in range(size):
state = (state * 1103515245 + 12345) & 0x7FFFFFFF
result[i] = (state >> 16) & 0xFF
return bytes(result)
def lcg_generate_string(seed: int, size: int) -> str:
"""Generate pseudo-random printable ASCII string using LCG."""
raw = lcg_generate_bytes(seed, size)
return "".join(chr(32 + (b % 95)) for b in raw)
def generate_collection_elements(seed: int, count: int, elem_size: int) -> list[str]:
full = lcg_generate_string(seed, count * elem_size)
return [full[i * elem_size : (i + 1) * elem_size] for i in range(count)]
def generate_map_keys(count: int) -> list[str]:
return [f"key_{i:04d}" for i in range(count)]
def send_large_content(args: argparse.Namespace) -> None:
"""Send a single large content message generated from PRNG seed."""
content_type = args.large_content
seed = args.seed
jms_mode = getattr(args, "jms_mode", False)
if content_type == "binary":
body = lcg_generate_bytes(seed, args.size)
jms_msg_type = 3 # JMS_BYTES_MESSAGE
elif content_type == "string":
body = lcg_generate_string(seed, args.size)
jms_msg_type = 5 # JMS_TEXT_MESSAGE
elif content_type in ("list", "array", "map", "described"):
elements = generate_collection_elements(seed, args.elements, args.element_size)
if content_type == "list":
body = elements
jms_msg_type = 4 # JMS_STREAM_MESSAGE
elif content_type == "array":
from proton import Array, Data, UNDESCRIBED
body = Array(UNDESCRIBED, Data.STRING, *elements)
jms_msg_type = -1
elif content_type == "map":
keys = generate_map_keys(args.elements)
body = dict(zip(keys, elements))
jms_msg_type = 2 # JMS_MAP_MESSAGE
elif content_type == "described":
from proton import Described, symbol
body = Described(symbol("test.large.described"), elements)
jms_msg_type = -1
else:
print(f"Unknown large-content type: {content_type}", file=sys.stderr)
sys.exit(1)
msg = Message(body=body)
if jms_mode and jms_msg_type >= 0:
from proton import byte as proton_byte, symbol
msg.annotations = {symbol("x-opt-jms-msg-type"): proton_byte(jms_msg_type)}
handler = LargeContentSender(args.broker, args.queue, msg)
Container(handler).run()
result: dict[str, Any] = {"sent": True}
if content_type in ("list", "array", "map", "described"):
result["elements"] = args.elements
result["element_size"] = args.element_size
else:
result["size"] = args.size
print(json.dumps(result))
def receive_large_content(args: argparse.Namespace) -> None:
"""Receive a single large content message and verify against PRNG seed."""
import signal
content_type = args.large_content
size = args.size
seed = args.seed
handler = LargeContentReceiver(args.broker, args.queue)
def timeout_handler(signum, frame):
raise TimeoutError(f"Receiver timed out after {args.timeout} seconds")
signal.signal(signal.SIGALRM, timeout_handler)
signal.alarm(args.timeout)
try:
Container(handler).run()
except TimeoutError:
pass
finally:
signal.alarm(0)
if handler.body is None:
print(json.dumps({"match": False, "error": "no message received"}))
sys.exit(1)
if content_type == "binary":
expected = lcg_generate_bytes(seed, size)
if isinstance(handler.body, (bytes, bytearray)):
received = handler.body
else:
received = bytes(handler.body)
result: dict[str, Any] = {"size": len(received), "expected_size": size}
if len(received) != len(expected):
result["match"] = False
elif received == expected:
result["match"] = True
else:
result["match"] = False
for i in range(len(expected)):
if received[i] != expected[i]:
result["first_mismatch_offset"] = i
break
elif content_type == "string":
expected = lcg_generate_string(seed, size)
received = str(handler.body) if handler.body is not None else ""
result = {"size": len(received), "expected_size": size}
if len(received) != len(expected):
result["match"] = False
elif received == expected:
result["match"] = True
else:
result["match"] = False
for i in range(len(expected)):
if received[i] != expected[i]:
result["first_mismatch_offset"] = i
break
elif content_type in ("list", "array", "map", "described"):
elements_count = args.elements
element_size = args.element_size
expected_elements = generate_collection_elements(seed, elements_count, element_size)
body = handler.body
if content_type == "list":
if not isinstance(body, (list, tuple)):
print(json.dumps({"match": False, "error": f"expected list, got {type(body).__name__}"}))
sys.exit(1)
received_elements = [str(e) for e in body]
elif content_type == "array":
from proton import Array
if isinstance(body, Array):
received_elements = [str(e) for e in body]
elif isinstance(body, (list, tuple)):
received_elements = [str(e) for e in body]
else:
print(json.dumps({"match": False, "error": f"expected array, got {type(body).__name__}"}))
sys.exit(1)
elif content_type == "map":
if not isinstance(body, dict):
print(json.dumps({"match": False, "error": f"expected map, got {type(body).__name__}"}))
sys.exit(1)
keys = generate_map_keys(elements_count)
received_elements = [str(body.get(k, "")) for k in keys]
elif content_type == "described":
from proton import Described
if isinstance(body, Described):
inner = body.value
else:
inner = body
if isinstance(inner, (list, tuple)):
received_elements = [str(e) for e in inner]
else:
print(json.dumps({"match": False, "error": f"expected described list, got {type(inner).__name__}"}))
sys.exit(1)
result = {"elements": len(received_elements), "element_size": element_size}
if len(received_elements) != elements_count:
result["match"] = False
else:
result["match"] = True
for i, (exp, rcv) in enumerate(zip(expected_elements, received_elements)):
if exp != rcv:
result["match"] = False
result["first_mismatch_element"] = i
for j in range(min(len(exp), len(rcv))):
if exp[j] != rcv[j]:
result["first_mismatch_offset"] = j
break
else:
result["first_mismatch_offset"] = min(len(exp), len(rcv))
break
else:
print(json.dumps({"match": False, "error": f"unknown type: {content_type}"}))
sys.exit(1)
print(json.dumps(result))
if not result["match"]:
sys.exit(1)
def send_messages(args: argparse.Namespace) -> None:
"""Send messages via broker."""
messages = json.loads(args.data)
jms_mode = getattr(args, "jms_mode", False)
headers = json.loads(args.headers) if args.headers else None
properties = json.loads(args.properties) if getattr(args, "properties", None) else None
message_header = json.loads(args.message_header) if getattr(args, "message_header", None) else None
handler = SenderHandler(args.broker, args.queue, messages, jms_mode, args.type, headers, properties, message_header)
Container(handler).run()
# Output result
result = {
"messages": messages,
"stats": {"sent": len(messages)},
}
print(json.dumps(result, indent=2))
def receive_messages(args: argparse.Namespace) -> None:
"""Receive messages via broker."""
import signal
handler = ReceiverHandler(args.broker, args.queue, args.count)
# Set alarm for timeout
def timeout_handler(signum, frame):
raise TimeoutError(f"Receiver timed out after {args.timeout} seconds")
signal.signal(signal.SIGALRM, timeout_handler)
signal.alarm(args.timeout)
try:
Container(handler).run()
except TimeoutError:
pass # Expected if we don't receive all messages
finally:
signal.alarm(0) # Cancel alarm
# Output result
result = {
"messages": handler.received_messages,
"stats": {"received": len(handler.received_messages)},
}
print(json.dumps(result, indent=2))
def main() -> None:
"""Main entry point."""
parser = argparse.ArgumentParser(description="QIT Python Proton Shim")
subparsers = parser.add_subparsers(dest="command", required=True)
# Send command
send_parser = subparsers.add_parser("send", help="Send messages")
send_parser.add_argument("--broker", required=True, help="Broker URL")
send_parser.add_argument("--queue", required=True, help="Queue name")
send_parser.add_argument("--type", required=False, help="AMQP type")
send_parser.add_argument("--count", type=int, required=False, help="Message count")
send_parser.add_argument("--data", required=False, help="JSON message data")
send_parser.add_argument(
"--jms-mode",
action="store_true",
help="Enable JMS message emulation (adds x-opt-jms-msg-type annotation)",
)
send_parser.add_argument("--headers", default=None, help="JSON JMS headers")
send_parser.add_argument("--properties", default=None, help="JSON JMS application properties")
send_parser.add_argument("--message-header", default=None, help="JSON AMQP Header section fields")
send_parser.add_argument("--large-content", default=None, help="Large content type (binary, string, list, array, map, described)")
send_parser.add_argument("--size", type=int, default=None, help="Large content size in bytes (binary/string)")
send_parser.add_argument("--seed", type=int, default=None, help="PRNG seed for large content")
send_parser.add_argument("--elements", type=int, default=None, help="Number of collection elements (list/array/map/described)")
send_parser.add_argument("--element-size", type=int, default=None, help="Size of each element in bytes (list/array/map/described)")
# Receive command
recv_parser = subparsers.add_parser("receive", help="Receive messages")
recv_parser.add_argument("--broker", required=True, help="Broker URL")
recv_parser.add_argument("--queue", required=True, help="Queue name")
recv_parser.add_argument("--count", type=int, required=False, default=1, help="Message count")
recv_parser.add_argument("--timeout", type=int, default=30, help="Timeout in seconds")
recv_parser.add_argument("--large-content", default=None, help="Large content type (binary, string, list, array, map, described)")
recv_parser.add_argument("--size", type=int, default=None, help="Expected large content size in bytes (binary/string)")
recv_parser.add_argument("--seed", type=int, default=None, help="PRNG seed for verification")
recv_parser.add_argument("--elements", type=int, default=None, help="Expected number of collection elements")
recv_parser.add_argument("--element-size", type=int, default=None, help="Expected size of each element in bytes")
args = parser.parse_args()
if args.command == "send":
if args.large_content:
send_large_content(args)
else:
send_messages(args)
elif args.command == "receive":
if args.large_content:
receive_large_content(args)
else:
receive_messages(args)
if __name__ == "__main__":
main()