acemq_amqp.codecs.yaml
Reads and writes YAML::
pip install "acemq-amqp[yaml]"
from acemq_amqp.codecs.yaml import YamlCodec
Chosen when a message is meant to be read by a person as much as by a program: a configuration change broadcast to a fleet, a deployment instruction, a command replayed by hand from a dead-letter queue. It is worth saying plainly that YAML costs more to parse than JSON and is a poor choice for high volume; it earns its place where somebody will actually look at the message.
Written in block style rather than flow style, which is the whole reason to pick
YAML. Flow style would produce something very close to JSON and would leave
nothing to justify the cost. That is the same choice the Java codec makes with
Jackson's YAMLGenerator and the Go one with yaml.v3.
This codec never volunteers for a message whose sender set no content type.
YAML is a superset of JSON, so its parser accepts JSON bytes quite happily and
would answer for messages meant for ~acemq_amqp.codec.JsonCodec. It
would even give the right value — while recording that a YAML message had
arrived, which is the sort of wrong that is discovered much later. So it claims
only content types that say YAML.
Loading is always safe loading. A message body is untrusted input, and
PyYAML's default loader constructs arbitrary Python objects out of tags like
!!python/object/apply. There is no option to turn that back on, for the same
reason the XML codec's entity handling is not configurable: the configuration
would only ever be wrong.
1# Copyright 2026 AceMQ. 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# https://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15"""Reads and writes YAML:: 16 17 pip install "acemq-amqp[yaml]" 18 19 from acemq_amqp.codecs.yaml import YamlCodec 20 21Chosen when a message is meant to be read by a person as much as by a program: a 22configuration change broadcast to a fleet, a deployment instruction, a command 23replayed by hand from a dead-letter queue. It is worth saying plainly that YAML 24costs more to parse than JSON and is a poor choice for high volume; it earns its 25place where somebody will actually look at the message. 26 27Written in block style rather than flow style, which is the whole reason to pick 28YAML. Flow style would produce something very close to JSON and would leave 29nothing to justify the cost. That is the same choice the Java codec makes with 30Jackson's ``YAMLGenerator`` and the Go one with ``yaml.v3``. 31 32**This codec never volunteers for a message whose sender set no content type.** 33YAML is a superset of JSON, so its parser accepts JSON bytes quite happily and 34would answer for messages meant for :class:`~acemq_amqp.codec.JsonCodec`. It 35would even give the right value — while recording that a YAML message had 36arrived, which is the sort of wrong that is discovered much later. So it claims 37only content types that say YAML. 38 39**Loading is always safe loading.** A message body is untrusted input, and 40PyYAML's default loader constructs arbitrary Python objects out of tags like 41``!!python/object/apply``. There is no option to turn that back on, for the same 42reason the XML codec's entity handling is not configurable: the configuration 43would only ever be wrong. 44""" 45 46from __future__ import annotations 47 48import dataclasses 49from typing import Any 50 51from ..ack import FatalError 52from ..codec import register_codec 53from ..errors import AceMQError 54 55#: What this codec writes, and what Java, Go and .NET write. 56YAML_CONTENT_TYPE = "application/yaml" 57 58#: Every content type this codec answers for. ``application/yaml`` is the 59#: registered one as of RFC 9512; the other three predate it and are what most 60#: senders still write. Java accepts exactly these four and any ``+yaml`` 61#: suffix, and so does Go. 62YAML_ACCEPTS = ( 63 YAML_CONTENT_TYPE, 64 "application/x-yaml", 65 "text/yaml", 66 "text/x-yaml", 67) 68 69 70class YamlCodec: 71 """Reads and writes YAML. 72 73 :param sort_keys: whether to sort mapping keys when writing. Off by default, 74 so a payload goes out in the order it was built — which is what Jackson 75 and ``yaml.v3`` do, and what makes a message a person reads legible 76 :param allow_unicode: whether to write non-ASCII characters as themselves 77 rather than escaping them. On by default, for the same reason 78 :raises AceMQError: when PyYAML is not installed 79 """ 80 81 def __init__(self, *, sort_keys: bool = False, allow_unicode: bool = True) -> None: 82 try: 83 import yaml 84 except ImportError as missing: # pragma: no cover - depends on the install 85 raise AceMQError( 86 "acemq: YamlCodec needs PyYAML, which is an optional extra. " 87 'Install it with pip install "acemq-amqp[yaml]"' 88 ) from missing 89 90 self._yaml: Any = yaml 91 self._sort_keys = sort_keys 92 self._allow_unicode = allow_unicode 93 94 @property 95 def content_type(self) -> str: 96 return YAML_CONTENT_TYPE 97 98 def encode(self, payload: Any) -> bytes: 99 if dataclasses.is_dataclass(payload) and not isinstance(payload, type): 100 payload = dataclasses.asdict(payload) 101 try: 102 written: str = self._yaml.safe_dump( 103 payload, 104 # Block style. Flow style is valid YAML and looks like JSON, so 105 # a message written that way gives up the only thing YAML was 106 # chosen for. 107 default_flow_style=False, 108 sort_keys=self._sort_keys, 109 allow_unicode=self._allow_unicode, 110 # No leading "---". It is valid and it is noise, and a message 111 # body is a single document by definition, so the marker 112 # separates nothing from nothing. Java disables it too. 113 explicit_start=False, 114 ) 115 return written.encode("utf-8") 116 except self._yaml.YAMLError as failure: 117 raise TypeError( 118 f"acemq: a {type(payload).__name__} is not something YAML can carry: {failure}" 119 ) from failure 120 121 def decode(self, body: bytes, content_type: str | None = None) -> Any: 122 try: 123 # safe_load, never load. The full loader builds arbitrary Python 124 # objects from tags in the document, and the document arrived from 125 # a queue. 126 return self._yaml.safe_load(body) 127 except (self._yaml.YAMLError, UnicodeDecodeError) as failure: 128 # Fatal rather than retryable: a body that is not YAML is not YAML 129 # the next time either, and retrying it only holds a queue open 130 # until the message ages out. 131 raise FatalError(f"acemq: this message is not YAML: {failure}") from failure 132 133 def can_decode(self, content_type: str | None) -> bool: 134 """Accepts the four spellings of YAML and any ``+yaml`` type. 135 136 Never a message with no content type, for the reason in the module 137 docstring: YAML reads JSON, so it would claim messages that were not 138 sent as YAML and be right about the value while wrong about the format. 139 """ 140 if not content_type: 141 return False 142 lowered = content_type.lower() 143 return lowered.startswith(YAML_ACCEPTS) or "+yaml" in lowered 144 145 def __repr__(self) -> str: 146 return "YamlCodec()" 147 148 149register_codec("yaml", YamlCodec)
71class YamlCodec: 72 """Reads and writes YAML. 73 74 :param sort_keys: whether to sort mapping keys when writing. Off by default, 75 so a payload goes out in the order it was built — which is what Jackson 76 and ``yaml.v3`` do, and what makes a message a person reads legible 77 :param allow_unicode: whether to write non-ASCII characters as themselves 78 rather than escaping them. On by default, for the same reason 79 :raises AceMQError: when PyYAML is not installed 80 """ 81 82 def __init__(self, *, sort_keys: bool = False, allow_unicode: bool = True) -> None: 83 try: 84 import yaml 85 except ImportError as missing: # pragma: no cover - depends on the install 86 raise AceMQError( 87 "acemq: YamlCodec needs PyYAML, which is an optional extra. " 88 'Install it with pip install "acemq-amqp[yaml]"' 89 ) from missing 90 91 self._yaml: Any = yaml 92 self._sort_keys = sort_keys 93 self._allow_unicode = allow_unicode 94 95 @property 96 def content_type(self) -> str: 97 return YAML_CONTENT_TYPE 98 99 def encode(self, payload: Any) -> bytes: 100 if dataclasses.is_dataclass(payload) and not isinstance(payload, type): 101 payload = dataclasses.asdict(payload) 102 try: 103 written: str = self._yaml.safe_dump( 104 payload, 105 # Block style. Flow style is valid YAML and looks like JSON, so 106 # a message written that way gives up the only thing YAML was 107 # chosen for. 108 default_flow_style=False, 109 sort_keys=self._sort_keys, 110 allow_unicode=self._allow_unicode, 111 # No leading "---". It is valid and it is noise, and a message 112 # body is a single document by definition, so the marker 113 # separates nothing from nothing. Java disables it too. 114 explicit_start=False, 115 ) 116 return written.encode("utf-8") 117 except self._yaml.YAMLError as failure: 118 raise TypeError( 119 f"acemq: a {type(payload).__name__} is not something YAML can carry: {failure}" 120 ) from failure 121 122 def decode(self, body: bytes, content_type: str | None = None) -> Any: 123 try: 124 # safe_load, never load. The full loader builds arbitrary Python 125 # objects from tags in the document, and the document arrived from 126 # a queue. 127 return self._yaml.safe_load(body) 128 except (self._yaml.YAMLError, UnicodeDecodeError) as failure: 129 # Fatal rather than retryable: a body that is not YAML is not YAML 130 # the next time either, and retrying it only holds a queue open 131 # until the message ages out. 132 raise FatalError(f"acemq: this message is not YAML: {failure}") from failure 133 134 def can_decode(self, content_type: str | None) -> bool: 135 """Accepts the four spellings of YAML and any ``+yaml`` type. 136 137 Never a message with no content type, for the reason in the module 138 docstring: YAML reads JSON, so it would claim messages that were not 139 sent as YAML and be right about the value while wrong about the format. 140 """ 141 if not content_type: 142 return False 143 lowered = content_type.lower() 144 return lowered.startswith(YAML_ACCEPTS) or "+yaml" in lowered 145 146 def __repr__(self) -> str: 147 return "YamlCodec()"
Reads and writes YAML.
Parameters
- sort_keys: whether to sort mapping keys when writing. Off by default,
so a payload goes out in the order it was built — which is what Jackson
and
yaml.v3do, and what makes a message a person reads legible - allow_unicode: whether to write non-ASCII characters as themselves rather than escaping them. On by default, for the same reason
Raises
- AceMQError: when PyYAML is not installed
82 def __init__(self, *, sort_keys: bool = False, allow_unicode: bool = True) -> None: 83 try: 84 import yaml 85 except ImportError as missing: # pragma: no cover - depends on the install 86 raise AceMQError( 87 "acemq: YamlCodec needs PyYAML, which is an optional extra. " 88 'Install it with pip install "acemq-amqp[yaml]"' 89 ) from missing 90 91 self._yaml: Any = yaml 92 self._sort_keys = sort_keys 93 self._allow_unicode = allow_unicode
99 def encode(self, payload: Any) -> bytes: 100 if dataclasses.is_dataclass(payload) and not isinstance(payload, type): 101 payload = dataclasses.asdict(payload) 102 try: 103 written: str = self._yaml.safe_dump( 104 payload, 105 # Block style. Flow style is valid YAML and looks like JSON, so 106 # a message written that way gives up the only thing YAML was 107 # chosen for. 108 default_flow_style=False, 109 sort_keys=self._sort_keys, 110 allow_unicode=self._allow_unicode, 111 # No leading "---". It is valid and it is noise, and a message 112 # body is a single document by definition, so the marker 113 # separates nothing from nothing. Java disables it too. 114 explicit_start=False, 115 ) 116 return written.encode("utf-8") 117 except self._yaml.YAMLError as failure: 118 raise TypeError( 119 f"acemq: a {type(payload).__name__} is not something YAML can carry: {failure}" 120 ) from failure
122 def decode(self, body: bytes, content_type: str | None = None) -> Any: 123 try: 124 # safe_load, never load. The full loader builds arbitrary Python 125 # objects from tags in the document, and the document arrived from 126 # a queue. 127 return self._yaml.safe_load(body) 128 except (self._yaml.YAMLError, UnicodeDecodeError) as failure: 129 # Fatal rather than retryable: a body that is not YAML is not YAML 130 # the next time either, and retrying it only holds a queue open 131 # until the message ages out. 132 raise FatalError(f"acemq: this message is not YAML: {failure}") from failure
134 def can_decode(self, content_type: str | None) -> bool: 135 """Accepts the four spellings of YAML and any ``+yaml`` type. 136 137 Never a message with no content type, for the reason in the module 138 docstring: YAML reads JSON, so it would claim messages that were not 139 sent as YAML and be right about the value while wrong about the format. 140 """ 141 if not content_type: 142 return False 143 lowered = content_type.lower() 144 return lowered.startswith(YAML_ACCEPTS) or "+yaml" in lowered
Accepts the four spellings of YAML and any +yaml type.
Never a message with no content type, for the reason in the module docstring: YAML reads JSON, so it would claim messages that were not sent as YAML and be right about the value while wrong about the format.