diff --git a/scripts/pull_and_compile_protos.py b/scripts/pull_and_compile_protos.py index 3a8fff4..aac07cb 100644 --- a/scripts/pull_and_compile_protos.py +++ b/scripts/pull_and_compile_protos.py @@ -22,7 +22,7 @@ REPO_URL = "https://github.com/eclipse-uprotocol/up-spec.git" PROTO_REPO_DIR = os.path.abspath("../target") -TAG_NAME = "v1.6.0-alpha.2" +TAG_NAME = "v1.6.0-alpha.3" PROTO_OUTPUT_DIR = os.path.abspath("../uprotocol/") diff --git a/tests/test_client/test_usubscription/test_v3/test_inmemoryusubcriptionclient.py b/tests/test_client/test_usubscription/test_v3/test_inmemoryusubcriptionclient.py index b0410f8..decdc60 100644 --- a/tests/test_client/test_usubscription/test_v3/test_inmemoryusubcriptionclient.py +++ b/tests/test_client/test_usubscription/test_v3/test_inmemoryusubcriptionclient.py @@ -581,43 +581,26 @@ async def test_unregister_notification_api_for_the_happy_path(self): try: await subscriber.register_for_notifications(self.topic, handler) - await subscriber.unregister_for_notifications(self.topic, handler) + await subscriber.unregister_for_notifications(self.topic) except Exception as e: self.fail(f"Exception occurred: {e}") async def test_unregister_notification_api_topic_missing(self): - handler = MagicMock(spec=SubscriptionChangeHandler) - handler.handle_subscription_change.return_value = NotImplementedError( - "Unimplemented method 'handle_subscription_change'" - ) self.transport.get_source.return_value = self.source subscriber = InMemoryUSubscriptionClient(self.transport, self.rpc_client, self.notifier) self.assertIsNotNone(subscriber) with self.assertRaises(ValueError) as error: - await subscriber.unregister_for_notifications(None, handler) + await subscriber.unregister_for_notifications(None) self.assertEqual("Topic missing", str(error.exception)) - async def test_unregister_notification_api_handler_missing(self): - self.transport.get_source.return_value = self.source - - subscriber = InMemoryUSubscriptionClient(self.transport, self.rpc_client, self.notifier) - self.assertIsNotNone(subscriber) - with self.assertRaises(ValueError) as error: - await subscriber.unregister_for_notifications(self.topic, None) - self.assertEqual("Handler missing", str(error.exception)) - async def test_unregister_notification_api_options_none(self): - handler = MagicMock(spec=SubscriptionChangeHandler) - handler.handle_subscription_change.return_value = NotImplementedError( - "Unimplemented method 'handle_subscription_change'" - ) self.transport.get_source.return_value = self.source subscriber = InMemoryUSubscriptionClient(self.transport, self.rpc_client, self.notifier) self.assertIsNotNone(subscriber) with self.assertRaises(ValueError) as error: - await subscriber.unregister_for_notifications(self.topic, handler, None) + await subscriber.unregister_for_notifications(self.topic, None) self.assertEqual("CallOptions missing", str(error.exception)) async def test_register_notification_api_options_none(self): diff --git a/tests/test_client/test_utwin/__init__.py b/tests/test_client/test_utwin/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_client/test_utwin/test_v2/__init__.py b/tests/test_client/test_utwin/test_v2/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_client/test_utwin/test_v2/test_simpleutwinclient.py b/tests/test_client/test_utwin/test_v2/test_simpleutwinclient.py new file mode 100644 index 0000000..ccea5be --- /dev/null +++ b/tests/test_client/test_utwin/test_v2/test_simpleutwinclient.py @@ -0,0 +1,89 @@ +""" +SPDX-FileCopyrightText: 2024 Contributors to the Eclipse Foundation + +See the NOTICE file(s) distributed with this work for additional +information regarding copyright ownership. + +This program and the accompanying materials are made available under the +terms of the Apache License Version 2.0 which is available at + + http://www.apache.org/licenses/LICENSE-2.0 + +SPDX-License-Identifier: Apache-2.0 +""" + +import unittest +from unittest.mock import AsyncMock + +from uprotocol.client.utwin.v2.simpleutwinclient import SimpleUTwinClient +from uprotocol.communication.rpcclient import RpcClient +from uprotocol.communication.upayload import UPayload +from uprotocol.communication.ustatuserror import UStatusError +from uprotocol.core.utwin.v2.utwin_pb2 import GetLastMessagesResponse +from uprotocol.v1.ucode_pb2 import UCode +from uprotocol.v1.uri_pb2 import UUri, UUriBatch + + +class SimpleUTwinClientTest(unittest.IsolatedAsyncioTestCase): + def setUp(self): + # Mocking RpcClient + self.rpc_client = AsyncMock(spec=RpcClient) + + # Creating a sample UUri for tests + self.topic = UUri(authority_name="test", ue_id=3, ue_version_major=1, resource_id=0x8000) + + async def test_get_last_messages(self): + """ + Test calling get_last_messages() with valid topics. + """ + # Creating a UUriBatch with one topic + topics = UUriBatch(uris=[self.topic]) + + # Mocking RpcClient's invoke_method to return a successful response + self.rpc_client.invoke_method.return_value = UPayload.pack(GetLastMessagesResponse()) + + client = SimpleUTwinClient(self.rpc_client) + response = await client.get_last_messages(topics) + + self.assertIsNotNone(response) + self.assertIsInstance(response, GetLastMessagesResponse) + + async def test_get_last_messages_empty_topics(self): + """ + Test calling get_last_messages() with empty topics. + """ + # Creating an empty UUriBatch + topics = UUriBatch() + + client = SimpleUTwinClient(self.rpc_client) + + with self.assertRaises(UStatusError) as context: + await client.get_last_messages(topics) + + # Asserting the exception type and message + self.assertEqual(context.exception.status.code, UCode.INVALID_ARGUMENT) + self.assertEqual(context.exception.status.message, "topics must not be empty") + + async def test_get_last_messages_exception(self): + """ + Test calling get_last_messages() when the RpcClient completes exceptionally. + """ + # Creating a UUriBatch with one topic + topics = UUriBatch(uris=[self.topic]) + + # Mocking RpcClient's invoke_method to raise an exception + exception = UStatusError.from_code_message(UCode.NOT_FOUND, "Not found") + self.rpc_client.invoke_method.return_value = exception + + client = SimpleUTwinClient(self.rpc_client) + + with self.assertRaises(UStatusError) as context: + await client.get_last_messages(topics) + + # Asserting the exception type and message + self.assertEqual(context.exception.status.code, UCode.NOT_FOUND) + self.assertEqual(context.exception.status.message, "Not found") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_communication/test_simplepublisher.py b/tests/test_communication/test_simplepublisher.py index f1039b8..fc3985f 100644 --- a/tests/test_communication/test_simplepublisher.py +++ b/tests/test_communication/test_simplepublisher.py @@ -26,7 +26,7 @@ class TestSimplePublisher(unittest.IsolatedAsyncioTestCase): def setUp(self): self.transport = MagicMock(spec=UTransport) - self.topic = UUri(authority_name="neelam", ue_id=3, ue_version_major=1, resource_id=2) + self.topic = UUri(authority_name="neelam", ue_id=3, ue_version_major=1, resource_id=0x8000) async def test_send_publish(self): self.transport.send.return_value = UStatus(code=UCode.OK) diff --git a/tests/test_communication/test_upayload.py b/tests/test_communication/test_upayload.py index 6726164..6e1672c 100644 --- a/tests/test_communication/test_upayload.py +++ b/tests/test_communication/test_upayload.py @@ -42,6 +42,7 @@ def test_is_empty_when_passing_null(self): self.assertTrue(UPayload.is_empty(None)) def test_unpacking_a_upayload_calling_unpack_with_null(self): + self.assertFalse(isinstance(UPayload.unpack_from_umessage(None, UUri), message.Message)) self.assertFalse(isinstance(UPayload.unpack(None, UUri), message.Message)) self.assertFalse(isinstance(UPayload.unpack(UPayload.pack(None), UUri), message.Message)) @@ -113,6 +114,14 @@ def test_hash_code(self): payload = UPayload.pack_to_any(uri) self.assertEqual(payload.__hash__(), payload.__hash__()) + def test_unpack_passing_a_valid_umessage(self): + uri = UUri(authority_name="Neelam") + payload = UPayload.pack_to_any(uri) + umsg = UMessage(payload=payload.data) + unpacked = UPayload.unpack_from_umessage(umsg, UUri) + self.assertTrue(isinstance(unpacked, message.Message)) + self.assertEqual(uri, unpacked) + if __name__ == '__main__': unittest.main() diff --git a/tests/test_transport/test_builder/test_umessagebuilder.py b/tests/test_transport/test_builder/test_umessagebuilder.py index 5b8d794..025bf92 100644 --- a/tests/test_transport/test_builder/test_umessagebuilder.py +++ b/tests/test_transport/test_builder/test_umessagebuilder.py @@ -28,6 +28,7 @@ from uprotocol.v1.ucode_pb2 import UCode from uprotocol.v1.umessage_pb2 import UMessage from uprotocol.v1.uri_pb2 import UUri +from uprotocol.v1.uuid_pb2 import UUID def build_source(): @@ -38,6 +39,14 @@ def build_sink(): return UUri(ue_id=2, ue_version_major=1, resource_id=0) +def build_topic(): + return UUri(ue_id=2, ue_version_major=1, resource_id=0x8000) + + +def build_method(): + return UUri(ue_id=2, ue_version_major=1, resource_id=1) + + def get_uuid(): return Factories.UPROTOCOL.create() @@ -47,7 +56,7 @@ def test_publish(self): """ Test Publish """ - publish: UMessage = UMessageBuilder.publish(build_source()).build() + publish: UMessage = UMessageBuilder.publish(build_topic()).build() self.assertIsNotNone(publish) self.assertEqual(UMessageType.UMESSAGE_TYPE_PUBLISH, publish.attributes.type) self.assertEqual(UPriority.UPRIORITY_CS1, publish.attributes.priority) @@ -57,7 +66,7 @@ def test_notification(self): Test Notification """ sink = build_sink() - notification: UMessage = UMessageBuilder.notification(build_source(), sink).build() + notification: UMessage = UMessageBuilder.notification(build_topic(), sink).build() self.assertIsNotNone(notification) self.assertEqual( UMessageType.UMESSAGE_TYPE_NOTIFICATION, @@ -70,7 +79,7 @@ def test_request(self): """ Test Request """ - sink = build_sink() + sink = build_method() ttl = 1000 request: UMessage = UMessageBuilder.request(build_source(), sink, ttl).build() self.assertIsNotNone(request) @@ -83,7 +92,7 @@ def test_request_with_priority(self): """ Test Request """ - sink = build_sink() + sink = build_method() ttl = 1000 request: UMessage = ( UMessageBuilder.request(build_source(), sink, ttl).with_priority(UPriority.UPRIORITY_CS5).build() @@ -100,7 +109,7 @@ def test_response(self): """ sink = build_sink() req_id = get_uuid() - response: UMessage = UMessageBuilder.response(build_source(), sink, req_id).build() + response: UMessage = UMessageBuilder.response(build_method(), sink, req_id).build() self.assertIsNotNone(response) self.assertEqual(UMessageType.UMESSAGE_TYPE_RESPONSE, response.attributes.type) self.assertEqual(UPriority.UPRIORITY_CS4, response.attributes.priority) @@ -111,7 +120,7 @@ def test_response_with_existing_request(self): """ Test Response with existing request """ - request: UMessage = UMessageBuilder.request(build_source(), build_sink(), 1000).build() + request: UMessage = UMessageBuilder.request(build_source(), build_method(), 1000).build() response: UMessage = UMessageBuilder.response_for_request(request.attributes).build() self.assertIsNotNone(response) self.assertEqual(UMessageType.UMESSAGE_TYPE_RESPONSE, response.attributes.type) @@ -126,7 +135,7 @@ def test_build(self): Test Build """ builder: UMessageBuilder = ( - UMessageBuilder.publish(build_source()) + UMessageBuilder.publish(build_topic()) .with_token("test_token") .with_permission_level(2) .with_commstatus(UCode.CANCELLED) @@ -147,7 +156,7 @@ def test_build_with_upayload(self): """ Test building UMessage with UPayload payload """ - message: UMessage = UMessageBuilder.publish(build_source()).build_from_upayload( + message: UMessage = UMessageBuilder.publish(build_topic()).build_from_upayload( UPayload(format=UPayloadFormat.UPAYLOAD_FORMAT_PROTOBUF, data=build_sink().SerializeToString()) ) self.assertIsNotNone(message) @@ -162,7 +171,7 @@ def test_build_with_any_payload(self): """ Test building UMessage with Any payload """ - message: UMessage = UMessageBuilder.publish(build_source()).build_from_upayload( + message: UMessage = UMessageBuilder.publish(build_topic()).build_from_upayload( UPayload(format=UPayloadFormat.UPAYLOAD_FORMAT_PROTOBUF_WRAPPED_IN_ANY, data=Any().SerializeToString()) ) self.assertIsNotNone(message) @@ -179,7 +188,7 @@ def test_build_response_with_wrong_priority(self): """ sink = build_sink() req_id = get_uuid() - response = UMessageBuilder.response(build_source(), sink, req_id).with_priority(UPriority.UPRIORITY_CS3).build() + response = UMessageBuilder.response(build_method(), sink, req_id).with_priority(UPriority.UPRIORITY_CS3).build() self.assertIsNotNone(response) self.assertEqual(UMessageType.UMESSAGE_TYPE_RESPONSE, response.attributes.type) self.assertEqual(UPriority.UPRIORITY_CS4, response.attributes.priority) @@ -190,7 +199,7 @@ def test_build_request_with_wrong_priority(self): """ Test building request with wrong priority """ - sink = build_sink() + sink = build_method() ttl = 1000 request = UMessageBuilder.request(build_source(), sink, ttl).with_priority(UPriority.UPRIORITY_CS0).build() self.assertIsNotNone(request) @@ -204,7 +213,7 @@ def test_build_notification_with_wrong_priority(self): Test building notification with wrong priority """ sink = build_sink() - notification = UMessageBuilder.notification(build_source(), sink).with_priority(UPriority.UPRIORITY_CS0).build() + notification = UMessageBuilder.notification(build_topic(), sink).with_priority(UPriority.UPRIORITY_CS0).build() self.assertIsNotNone(notification) self.assertEqual( UMessageType.UMESSAGE_TYPE_NOTIFICATION, @@ -217,7 +226,7 @@ def test_build_publish_with_wrong_priority(self): """ Test building publish with wrong priority """ - publish = UMessageBuilder.publish(build_source()).with_priority(UPriority.UPRIORITY_CS0).build() + publish = UMessageBuilder.publish(build_topic()).with_priority(UPriority.UPRIORITY_CS0).build() self.assertIsNotNone(publish) self.assertEqual(UMessageType.UMESSAGE_TYPE_PUBLISH, publish.attributes.type) self.assertEqual(UPriority.UPRIORITY_CS1, publish.attributes.priority) @@ -226,7 +235,7 @@ def test_build_publish_with_priority(self): """ Test building publish with priority """ - publish = UMessageBuilder.publish(build_source()).with_priority(UPriority.UPRIORITY_CS4).build() + publish = UMessageBuilder.publish(build_topic()).with_priority(UPriority.UPRIORITY_CS4).build() self.assertIsNotNone(publish) self.assertEqual(UMessageType.UMESSAGE_TYPE_PUBLISH, publish.attributes.type) self.assertEqual(UPriority.UPRIORITY_CS4, publish.attributes.priority) @@ -293,3 +302,63 @@ def test_response_req_id_is_none(self): """ with self.assertRaises(ValueError): UMessageBuilder.response(build_source(), build_sink(), None) + + def test_publish_with_invalid_source(self): + with self.assertRaises(ValueError): + UMessageBuilder.publish(build_source()).build() + + def test_notification_with_invalid_source(self): + with self.assertRaises(ValueError): + UMessageBuilder.notification(build_source(), build_sink()).build() + + def test_notification_with_invalid_sink(self): + with self.assertRaises(ValueError): + UMessageBuilder.notification(build_topic(), build_topic()).build() + + def test_request_with_invalid_source(self): + with self.assertRaises(ValueError): + UMessageBuilder.request(build_method(), build_method(), 1000).build() + + def test_request_with_invalid_sink(self): + with self.assertRaises(ValueError): + UMessageBuilder.request(build_source(), build_source(), 1000).build() + + def test_request_with_invalid_source_and_sink(self): + with self.assertRaises(ValueError): + UMessageBuilder.request(build_method(), build_source(), 1000).build() + + def test_request_with_null_ttl(self): + with self.assertRaises(ValueError): + UMessageBuilder.request(build_source(), build_method(), None).build() + + def test_request_with_negative_ttl(self): + with self.assertRaises(ValueError): + UMessageBuilder.request(build_source(), build_method(), -1).build() + + def test_response_with_invalid_source(self): + with self.assertRaises(ValueError): + UMessageBuilder.response(build_sink(), build_sink(), Factories.UPROTOCOL.create()).build() + + def test_response_with_invalid_sink(self): + with self.assertRaises(ValueError): + UMessageBuilder.response(build_method(), build_method(), Factories.UPROTOCOL.create()).build() + + def test_response_with_invalid_source_and_sink(self): + with self.assertRaises(ValueError): + UMessageBuilder.response(build_source(), build_source(), Factories.UPROTOCOL.create()).build() + + def test_response_with_null_req_id(self): + with self.assertRaises(ValueError): + UMessageBuilder.response(build_method(), build_sink(), None).build() + + def test_response_with_invalid_req_id(self): + with self.assertRaises(ValueError): + UMessageBuilder.response(build_method(), build_sink(), UUID()).build() + + def test_notification_with_invalid_source_and_sink(self): + with self.assertRaises(ValueError): + UMessageBuilder.notification(build_sink(), build_source()).build() + + def test_response_builder_with_invalid_request_type(self): + with self.assertRaises(ValueError): + UMessageBuilder.response_for_request(UAttributes()).build() diff --git a/tests/test_transport/test_validator/test_uattributesvalidator.py b/tests/test_transport/test_validator/test_uattributesvalidator.py index c23348b..dccf520 100644 --- a/tests/test_transport/test_validator/test_uattributesvalidator.py +++ b/tests/test_transport/test_validator/test_uattributesvalidator.py @@ -17,6 +17,7 @@ from uprotocol.transport.builder.umessagebuilder import UMessageBuilder from uprotocol.transport.validator.uattributesvalidator import UAttributesValidator, Validators +from uprotocol.uuid.factory.uuidfactory import Factories from uprotocol.v1.uattributes_pb2 import UAttributes, UMessageType, UPriority from uprotocol.v1.uri_pb2 import UUri from uprotocol.v1.uuid_pb2 import UUID @@ -93,11 +94,17 @@ def test_uattributes_validator_response_with_request_attributes(self): self.assertEqual(str(validator), "UAttributesValidator.Response") def test_uattributes_validator_request_with_publish_validator(self): - message = UMessageBuilder.request(build_default_uuri(), build_topic_uuri(), 1000).build() - + attributes = UAttributes( + source=build_default_uuri(), + sink=build_topic_uuri(), + id=Factories.UPROTOCOL.create(), + type=UMessageType.UMESSAGE_TYPE_REQUEST, + priority=UPriority.UPRIORITY_CS4, + ttl=1000, + ) validator = Validators.PUBLISH.validator() - result = validator.validate(message.attributes) + result = validator.validate(attributes) self.assertFalse(result.is_success()) self.assertEqual(str(validator), "UAttributesValidator.Publish") @@ -144,10 +151,16 @@ def test_uattributes_validator_notification_with_response_validator(self): ) def test_uattribute_validator_request_missing_sink(self): - message = UMessageBuilder.request(build_default_uuri(), build_default_uuri(), 1000).build() - - validator = UAttributesValidator.get_validator(message.attributes) - result = validator.validate(message.attributes) + attributes = UAttributes( + source=build_default_uuri(), + sink=build_default_uuri(), + id=Factories.UPROTOCOL.create(), + type=UMessageType.UMESSAGE_TYPE_REQUEST, + priority=UPriority.UPRIORITY_CS4, + ttl=1000, + ) + validator = UAttributesValidator.get_validator(attributes) + result = validator.validate(attributes) self.assertTrue(result.is_failure()) self.assertEqual(str(validator), "UAttributesValidator.Request") self.assertEqual(result.message, "Invalid Sink Uri") @@ -212,9 +225,15 @@ def test_uattribute_validator_notification_default_sink(self): self.assertEqual(result.message, "Missing Sink") def test_uattribute_validator_notification_default_resource_id(self): - message = UMessageBuilder.notification(build_topic_uuri(), build_topic_uuri()).build() - validator = UAttributesValidator.get_validator(message.attributes) - result = validator.validate(message.attributes) + attributes = UAttributes( + source=build_topic_uuri(), + sink=build_method_uuri(), + id=Factories.UPROTOCOL.create(), + type=UMessageType.UMESSAGE_TYPE_NOTIFICATION, + priority=UPriority.UPRIORITY_CS1, + ) + validator = UAttributesValidator.get_validator(attributes) + result = validator.validate(attributes) self.assertTrue(result.is_failure()) self.assertEqual(str(validator), "UAttributesValidator.Notification") @@ -278,10 +297,17 @@ def test_uattribute_validator_validate_sink_response_default(self): self.assertEqual(result.message, "Missing Sink") def test_uattribute_validator_validate_sink_response_default_resource_id(self): - request = UMessageBuilder.request(build_method_uuri(), build_default_uuri(), 1000).build() - response = UMessageBuilder.response_for_request(request.attributes).build() - validator = UAttributesValidator.get_validator(response.attributes) - result = validator.validate(response.attributes) + attributes = UAttributes( + source=build_method_uuri(), + sink=build_method_uuri(), + id=Factories.UPROTOCOL.create(), + type=UMessageType.UMESSAGE_TYPE_RESPONSE, + priority=UPriority.UPRIORITY_CS4, + reqid=Factories.UPROTOCOL.create(), + ) + + validator = UAttributesValidator.get_validator(attributes) + result = validator.validate(attributes) self.assertTrue(result.is_failure()) self.assertEqual(str(validator), "UAttributesValidator.Response") @@ -325,11 +351,15 @@ def test_uattribute_validator_validate_reqid_invalid(self): self.assertEqual(result.message, "Invalid correlation UUID") def test_validate_priority_is_cs0(self): - message = UMessageBuilder.publish(build_default_uuri()).build() - message.attributes.priority = UPriority.UPRIORITY_CS0 + attributes = UAttributes( + source=build_default_uuri(), + id=Factories.UPROTOCOL.create(), + type=UMessageType.UMESSAGE_TYPE_PUBLISH, + priority=UPriority.UPRIORITY_CS0, + ) - validator = UAttributesValidator.get_validator(message.attributes) - result = validator.validate(message.attributes) + validator = UAttributesValidator.get_validator(attributes) + result = validator.validate(attributes) self.assertTrue(result.is_failure()) self.assertEqual(str(validator), "UAttributesValidator.Publish") diff --git a/uprotocol/client/usubscription/v3/inmemoryusubcriptionclient.py b/uprotocol/client/usubscription/v3/inmemoryusubcriptionclient.py index 0a11746..2fa8ae7 100644 --- a/uprotocol/client/usubscription/v3/inmemoryusubcriptionclient.py +++ b/uprotocol/client/usubscription/v3/inmemoryusubcriptionclient.py @@ -292,14 +292,11 @@ async def register_for_notifications( return notifications_response - async def unregister_for_notifications( - self, topic: UUri, handler: SubscriptionChangeHandler, options: Optional[CallOptions] = CallOptions.DEFAULT - ): + async def unregister_for_notifications(self, topic: UUri, options: Optional[CallOptions] = CallOptions.DEFAULT): """ Unregister for subscription change notifications. :param topic: The topic to unregister for notifications. - :param handler: The `SubscriptionChangeHandler` to handle the subscription changes. :param options: The `CallOptions` to be used for the unregister request. :return: A `NotificationResponse` with the status of the API call to the uSubscription service, or a `UStatus` with the reason for the failure. `UCode.PERMISSION_DENIED` is returned if the @@ -307,8 +304,7 @@ async def unregister_for_notifications( """ if not topic: raise ValueError("Topic missing") - if not handler: - raise ValueError("Handler missing") + if not options: raise ValueError("CallOptions missing") diff --git a/uprotocol/client/usubscription/v3/usubscriptionclient.py b/uprotocol/client/usubscription/v3/usubscriptionclient.py index c7ae64d..9012ced 100644 --- a/uprotocol/client/usubscription/v3/usubscriptionclient.py +++ b/uprotocol/client/usubscription/v3/usubscriptionclient.py @@ -127,13 +127,12 @@ async def register_for_notifications( @abstractmethod async def unregister_for_notifications( - self, topic: UUri, handler: SubscriptionChangeHandler, options: Optional[CallOptions] = CallOptions.DEFAULT + self, topic: UUri, options: Optional[CallOptions] = CallOptions.DEFAULT ) -> NotificationsResponse: """ Unregister for subscription change notifications. :param topic: The topic to unregister for notifications. - :param handler: The SubscriptionChangeHandler to be unregistered. :param options: The CallOptions to be used for the request. Default is CallOptions.DEFAULT. :return: Returns NotificationsResponse completed successfully with the status of the API call to diff --git a/uprotocol/client/utwin/__init__.py b/uprotocol/client/utwin/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/uprotocol/client/utwin/v2/__init__.py b/uprotocol/client/utwin/v2/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/uprotocol/client/utwin/v2/simpleutwinclient.py b/uprotocol/client/utwin/v2/simpleutwinclient.py new file mode 100644 index 0000000..7c92420 --- /dev/null +++ b/uprotocol/client/utwin/v2/simpleutwinclient.py @@ -0,0 +1,72 @@ +""" +SPDX-FileCopyrightText: 2024 Contributors to the Eclipse Foundation + +See the NOTICE file(s) distributed with this work for additional +information regarding copyright ownership. + +This program and the accompanying materials are made available under the +terms of the Apache License Version 2.0 which is available at + + http://www.apache.org/licenses/LICENSE-2.0 + +SPDX-License-Identifier: Apache-2.0 +""" + +from typing import Optional + +from uprotocol.communication.calloptions import CallOptions +from uprotocol.communication.rpcclient import RpcClient +from uprotocol.communication.rpcmapper import RpcMapper +from uprotocol.communication.upayload import UPayload +from uprotocol.communication.ustatuserror import UStatusError +from uprotocol.core.utwin.v2 import utwin_pb2 +from uprotocol.core.utwin.v2.utwin_pb2 import GetLastMessagesRequest, GetLastMessagesResponse +from uprotocol.uri.factory.uri_factory import UriFactory +from uprotocol.v1.ucode_pb2 import UCode +from uprotocol.v1.uri_pb2 import UUriBatch + + +class SimpleUTwinClient: + """ + The uTwin client implementation using the RpcClient uP-L2 communication layer interface. + """ + + descriptor = utwin_pb2.DESCRIPTOR.services_by_name['uTwin'] + # TODO: The following items eventually need to be pulled from generated code + getlastmessage_method = UriFactory.from_proto(descriptor, 1) + + def __init__(self, rpc_client: RpcClient): + """ + Create a new instance of the uTwin client passing in the RPCClient to use for communication. + + :param rpc_client: The RPC client to use for communication. + """ + self.rpc_client = rpc_client + + async def get_last_messages( + self, topics: UUriBatch, options: Optional[CallOptions] = CallOptions.DEFAULT + ) -> GetLastMessagesResponse: + """ + Fetch the last messages for a batch of topics. + + :param topics: UUriBatch - Batch of 1 or more topics to fetch the last messages for. + :param options: CallOptions - The call options. + :return: An asyncio.Future that completes successfully with GetLastMessagesResponse if uTwin was able + to fetch the topics or raises UStatusException with the failure reason such as UCode.NOT_FOUND, + UCode.PERMISSION_DENIED, etc. + """ + if topics is None: + raise ValueError("topics must not be null") + if options is None: + options = CallOptions.DEFAULT + + # Check if topics is empty + if topics == UUriBatch(): + raise UStatusError.from_code_message(UCode.INVALID_ARGUMENT, "topics must not be empty") + + request = GetLastMessagesRequest(topics=topics) + result = await RpcMapper.map_response( + self.rpc_client.invoke_method(self.getlastmessage_method, UPayload.pack(request), options), + GetLastMessagesResponse, + ) + return result diff --git a/uprotocol/client/utwin/v2/utwinclient.py b/uprotocol/client/utwin/v2/utwinclient.py new file mode 100644 index 0000000..93ce0a8 --- /dev/null +++ b/uprotocol/client/utwin/v2/utwinclient.py @@ -0,0 +1,44 @@ +""" +SPDX-FileCopyrightText: 2024 Contributors to the Eclipse Foundation + +See the NOTICE file(s) distributed with this work for additional +information regarding copyright ownership. + +This program and the accompanying materials are made available under the +terms of the Apache License Version 2.0 which is available at + + http://www.apache.org/licenses/LICENSE-2.0 + +SPDX-License-Identifier: Apache-2.0 +""" + +import asyncio +from abc import ABC, abstractmethod +from typing import Optional + +from uprotocol.communication.calloptions import CallOptions +from uprotocol.v1.uri_pb2 import UUriBatch + + +class UTwinClient(ABC): + """ + The uTwin client-side interface. + + UTwin is used to fetch the last published message for a given topic. This is the client-side of the + UTwin Service contract and communicates with a local uTwin service to fetch the last message for a given topic. + """ + + @abstractmethod + def get_last_messages( + self, topics: UUriBatch, options: Optional[CallOptions] = CallOptions.DEFAULT + ) -> asyncio.Future: + """ + Fetch the last messages for a batch of topics. + + :param topics: UUriBatch - Batch of 1 or more topics to fetch the last messages for. + :param options: CallOptions - The Optional call options. + :return: asyncio.Future that completes successfully with GetLastMessagesResponse if uTwin was able + to fetch the topics or completes exceptionally with UStatus with the failure reason. + Such as UCode.NOT_FOUND, UCode.PERMISSION_DENIED, etc. + """ + pass diff --git a/uprotocol/communication/upayload.py b/uprotocol/communication/upayload.py index 45feba0..55dc76a 100644 --- a/uprotocol/communication/upayload.py +++ b/uprotocol/communication/upayload.py @@ -21,6 +21,7 @@ from uprotocol.v1.uattributes_pb2 import ( UPayloadFormat, ) +from uprotocol.v1.umessage_pb2 import UMessage @dataclass(frozen=True) @@ -54,6 +55,20 @@ def pack(message: message.Message) -> 'UPayload': def pack_from_data_and_format(data: bytes, format: UPayloadFormat) -> 'UPayload': return UPayload(data, format) + @staticmethod + def unpack_from_umessage(umsg: UMessage, clazz: Type[message.Message]) -> Optional[Type[message.Message]]: + """ + Unpack a UMessage into a google.protobuf.Message. + + :param umsg: The message to unpack + :param clazz: The class of the message to unpack + :return: The unpacked message or None if the message is None + """ + if umsg is None: + return None + + return UPayload.unpack_data_format(umsg.payload, umsg.attributes.payload_format, clazz) + @staticmethod def unpack(payload: Optional['UPayload'], clazz: Type[message.Message]) -> Optional[message.Message]: if payload is None: @@ -68,7 +83,11 @@ def unpack_data_format( if data is None or len(data) == 0: return None try: - if format == UPayloadFormat.UPAYLOAD_FORMAT_PROTOBUF_WRAPPED_IN_ANY: + # Default is WRAPPED_IN_ANY + if format in [ + UPayloadFormat.UPAYLOAD_FORMAT_UNSPECIFIED, + UPayloadFormat.UPAYLOAD_FORMAT_PROTOBUF_WRAPPED_IN_ANY, + ]: message = clazz() any_message = any_pb2.Any() any_message.ParseFromString(data) diff --git a/uprotocol/transport/builder/umessagebuilder.py b/uprotocol/transport/builder/umessagebuilder.py index 56e2d49..7c620ca 100644 --- a/uprotocol/transport/builder/umessagebuilder.py +++ b/uprotocol/transport/builder/umessagebuilder.py @@ -13,7 +13,10 @@ """ from uprotocol.communication.upayload import UPayload +from uprotocol.transport.validator.uattributesvalidator import UAttributesValidator +from uprotocol.uri.validator.urivalidator import UriValidator from uprotocol.uuid.factory.uuidfactory import Factories +from uprotocol.uuid.validator.uuidvalidator import Validators from uprotocol.v1.uattributes_pb2 import ( UAttributes, UMessageType, @@ -43,6 +46,9 @@ def publish(source: UUri): """ if source is None: raise ValueError(SOURCE_ERROR) + # Validate the source + if not UriValidator.is_topic(source): + raise ValueError("source must be a topic.") return UMessageBuilder( source, Factories.UPROTOCOL.create(), @@ -62,6 +68,9 @@ def notification(source: UUri, sink: UUri): raise ValueError(SOURCE_ERROR) if sink is None: raise ValueError(SINK_ERROR) + # Validate the source and sink + if not (UriValidator.is_topic(source) and UriValidator.is_rpc_response(sink)): + raise ValueError("source must be a topic and sink must be a response.") return UMessageBuilder( source, Factories.UPROTOCOL.create(), @@ -84,6 +93,13 @@ def request(source: UUri, sink: UUri, ttl: int): raise ValueError(SINK_ERROR) if ttl is None: raise ValueError(TTL_ERROR) + # Validate the source and sink + if not (UriValidator.is_rpc_method(sink) and UriValidator.is_rpc_response(source)): + raise ValueError("source must be a response and sink must be an rpc method.") + # Validate the ttl + if ttl <= 0: + raise ValueError("ttl must be greater than 0.") + return ( UMessageBuilder( source, @@ -111,6 +127,11 @@ def response(source: UUri, sink: UUri, reqid: UUID): # noqa: N805 raise ValueError(SINK_ERROR) if reqid is None: raise ValueError(REQID_ERROR) + # Validate the source and sink + if not (UriValidator.is_rpc_response(sink) and UriValidator.is_rpc_method(source)): + raise ValueError("sink must be a response and source must be an rpc method.") + if Validators.UPROTOCOL.validator().validate(reqid).code != UCode.OK: + raise ValueError("reqid is not a valid UUID.") return ( UMessageBuilder( @@ -133,6 +154,8 @@ def response_for_request(request: UAttributes): # noqa: N805 """ if request is None: raise ValueError(REQUEST_ERROR) + if UAttributesValidator.get_validator(request).validate(request).is_failure(): + raise ValueError("request must contain valid request attributes.") return ( UMessageBuilder( request.sink,