diff --git a/neon_data_models/models/base/contexts.py b/neon_data_models/models/base/contexts.py index 131cc71..3836a85 100644 --- a/neon_data_models/models/base/contexts.py +++ b/neon_data_models/models/base/contexts.py @@ -137,6 +137,10 @@ def validate_inputs(cls, values): return values class MQContext(BaseModel): + """ + MQ context information that is included in all MQ message bodies and in + `Message.context['MQ']` for Message objects originating at MQ handlers. + """ routing_key: Optional[str] = None message_id: str = Field(default_factory=lambda: uuid4().hex, description="MQ unique message ID") diff --git a/neon_data_models/models/base/mq.py b/neon_data_models/models/base/mq.py new file mode 100644 index 0000000..e558e4b --- /dev/null +++ b/neon_data_models/models/base/mq.py @@ -0,0 +1,41 @@ +# NEON AI (TM) SOFTWARE, Software Development Kit & Application Development System +# All trademark and other rights reserved by their respective owners +# Copyright 2008-2026 Neongecko.com Inc. +# BSD-3 +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +from typing import Optional +from pydantic import Field +from neon_data_models.models.base.contexts import MQContext + + +class MQResponse(MQContext): + is_final: bool = Field( + default=True, + description="If False, another response message is expected.", + ) + part: Optional[int] = Field( + default=None, + description="Index of this response message in a series " + "(zero-indexed). If `None`, message is not multipart.", + ) diff --git a/tests/models/test_base.py b/tests/models/test_base.py index f6305da..1473472 100644 --- a/tests/models/test_base.py +++ b/tests/models/test_base.py @@ -372,6 +372,46 @@ def test_mq_context(self): minimal_ctx = MQContext(message_id="test_message_id_string") self.assertEqual(minimal_ctx, MQContext(**minimal_ctx.model_dump())) + def test_mq_response(self): + from neon_data_models.models.base import BaseModel + from neon_data_models.models.base.contexts import MQContext + from neon_data_models.models.base.mq import MQResponse + + self.assertTrue(issubclass(MQResponse, MQContext)) + self.assertTrue(issubclass(MQResponse, BaseModel)) + + default = MQResponse() + self.assertTrue(default.is_final) + self.assertIsNone(default.part) + self.assertIsInstance(default.message_id, str) + + serialized = default.model_dump() + self.assertEqual(serialized["is_final"], True) + self.assertIn("part", serialized) + self.assertIsNone(serialized["part"]) + self.assertNotIn("_is_final", serialized) + self.assertNotIn("_part", serialized) + + multipart = MQResponse(message_id="test_message_id", + is_final=False, + part=0) + self.assertFalse(multipart.is_final) + self.assertEqual(multipart.part, 0) + self.assertEqual(multipart.model_dump(), + {"message_id": "test_message_id", + "routing_key": None, + "is_final": False, + "part": 0}) + + final_part = MQResponse(message_id="test_message_id", + is_final=True, + part=2) + self.assertEqual(final_part, MQResponse(**final_part.model_dump())) + + json_str = json.dumps(multipart.model_dump()) + deserialized = MQResponse(**json.loads(json_str)) + self.assertEqual(deserialized, multipart) + class TestMessagebus(TestCase): def test_base_model(self):