From 3bb7e6081dc1534c218de56e0bd50a28c33e3e54 Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Sat, 21 Sep 2024 20:07:53 +0100 Subject: [PATCH 01/26] works Signed-off-by: Elena Kolevska --- dapr/clients/grpc/client.py | 18 +- dapr/clients/grpc/subscription.py | 102 +++++ dapr/proto/common/v1/common_pb2.py | 12 +- dapr/proto/common/v1/common_pb2_grpc.py | 9 +- dapr/proto/runtime/v1/appcallback_pb2.py | 12 +- dapr/proto/runtime/v1/appcallback_pb2_grpc.py | 12 +- dapr/proto/runtime/v1/dapr_pb2.py | 400 +++++++++--------- dapr/proto/runtime/v1/dapr_pb2.pyi | 115 ++++- dapr/proto/runtime/v1/dapr_pb2_grpc.py | 17 +- examples/pubsub_streaming/publisher.py | 68 +++ examples/pubsub_streaming/subscriber.py | 45 ++ 11 files changed, 568 insertions(+), 242 deletions(-) create mode 100644 dapr/clients/grpc/subscription.py create mode 100644 examples/pubsub_streaming/publisher.py create mode 100644 examples/pubsub_streaming/subscriber.py diff --git a/dapr/clients/grpc/client.py b/dapr/clients/grpc/client.py index 64a26408..71009b83 100644 --- a/dapr/clients/grpc/client.py +++ b/dapr/clients/grpc/client.py @@ -41,11 +41,12 @@ from dapr.clients.grpc._state import StateOptions, StateItem from dapr.clients.grpc._helpers import getWorkflowRuntimeStatus from dapr.clients.grpc._crypto import EncryptOptions, DecryptOptions +from dapr.clients.grpc.subscription import Subscription from dapr.clients.grpc.interceptors import DaprClientInterceptor, DaprClientTimeoutInterceptor from dapr.clients.health import DaprHealth from dapr.clients.retry import RetryPolicy from dapr.conf import settings -from dapr.proto import api_v1, api_service_v1, common_v1 +from dapr.proto import api_v1, api_service_v1, common_v1, appcallback_v1 from dapr.proto.runtime.v1.dapr_pb2 import UnsubscribeConfigurationResponse from dapr.version import __version__ @@ -481,6 +482,21 @@ def publish_event( return DaprResponse(call.initial_metadata()) + # def subscribe(self, pubsub_name, topic, metadata=None, dead_letter_topic=None): + # stream = self._stub.SubscribeTopicEventsAlpha1() + # + # # Send InitialRequest + # initial_request = api_v1.SubscribeTopicEventsInitialRequestAlpha1(pubsub_name=pubsub_name, topic=topic, metadata=metadata, dead_letter_topic=dead_letter_topic) + # request = api_v1.SubscribeTopicEventsRequestAlpha1(initial_request=initial_request) + # stream.write(request) + # + # return stream + + def subscribe(self, pubsub_name, topic, metadata=None, dead_letter_topic=None): + subscription = Subscription(self._stub, pubsub_name, topic, metadata, dead_letter_topic) + subscription.start() + return subscription + def get_state( self, store_name: str, diff --git a/dapr/clients/grpc/subscription.py b/dapr/clients/grpc/subscription.py new file mode 100644 index 00000000..c4082411 --- /dev/null +++ b/dapr/clients/grpc/subscription.py @@ -0,0 +1,102 @@ +import grpc +from dapr.proto import api_v1, appcallback_v1 +import queue +import threading + + +def success(): + return appcallback_v1.TopicEventResponse.SUCCESS + + +def retry(): + return appcallback_v1.TopicEventResponse.RETRY + + +def drop(): + return appcallback_v1.TopicEventResponse.DROP + + +class Subscription: + def __init__(self, stub, pubsub_name, topic, metadata=None, dead_letter_topic=None): + self._stub = stub + self.pubsub_name = pubsub_name + self.topic = topic + self.metadata = metadata or {} + self.dead_letter_topic = dead_letter_topic or '' + self._stream = None + self._send_queue = queue.Queue() + self._receive_queue = queue.Queue() + self._stream_active = False + + def start(self): + def request_iterator(): + try: + # Send InitialRequest needed to establish the stream + initial_request = api_v1.SubscribeTopicEventsRequestAlpha1( + initial_request=api_v1.SubscribeTopicEventsRequestInitialAlpha1( + pubsub_name=self.pubsub_name, topic=self.topic, metadata=self.metadata or {}, + dead_letter_topic=self.dead_letter_topic or '')) + yield initial_request + + while self._stream_active: + try: + request = self._send_queue.get() + if request is None: + break + + yield request + except queue.Empty: + continue + except Exception as e: + print(f"Exception in request_iterator: {e}") + raise e + + # Create the bidirectional stream + self._stream = self._stub.SubscribeTopicEventsAlpha1(request_iterator()) + self._stream_active = True + + # Start a thread to handle incoming messages + threading.Thread(target=self._handle_responses, daemon=True).start() + + def _handle_responses(self): + try: + # The first message dapr sends on the stream is for signalling only, so discard it + next(self._stream) + + for msg in self._stream: + print(f"Received message from dapr on stream: {msg.event_message.id}") # SubscribeTopicEventsResponseAlpha1 + self._receive_queue.put(msg.event_message) + except grpc.RpcError as e: + print(f"gRPC error in stream: {e}") + except Exception as e: + print(f"Unexpected error in stream: {e}") + finally: + self._stream_active = False + + def next_message(self, timeout=None): + print("in next_message") + try: + return self._receive_queue.get(timeout=timeout) + except queue.Empty as e : + print("queue empty", e) + return None + except Exception as e: + print(f"Exception in next_message: {e}") + return None + + def respond(self, message, status): + try: + status = appcallback_v1.TopicEventResponse(status=status.value) + response = api_v1.SubscribeTopicEventsRequestProcessedAlpha1(id=message.id, + status=status) + msg = api_v1.SubscribeTopicEventsRequestAlpha1(event_processed=response) + + self._send_queue.put(msg) + except Exception as e: + print(f"Exception in send_message: {e}") + + def close(self): + self._stream_active = False + self._send_queue.put(None) + if self._stream: + self._stream.cancel() \ No newline at end of file diff --git a/dapr/proto/common/v1/common_pb2.py b/dapr/proto/common/v1/common_pb2.py index b4f795de..3f7d8f25 100644 --- a/dapr/proto/common/v1/common_pb2.py +++ b/dapr/proto/common/v1/common_pb2.py @@ -1,12 +1,22 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE # source: dapr/proto/common/v1/common.proto -# Protobuf Python Version: 5.26.1 +# Protobuf Python Version: 5.27.2 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version from google.protobuf import symbol_database as _symbol_database from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 27, + 2, + '', + 'dapr/proto/common/v1/common.proto' +) # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() diff --git a/dapr/proto/common/v1/common_pb2_grpc.py b/dapr/proto/common/v1/common_pb2_grpc.py index cd86bc6a..310e7b40 100644 --- a/dapr/proto/common/v1/common_pb2_grpc.py +++ b/dapr/proto/common/v1/common_pb2_grpc.py @@ -4,10 +4,8 @@ import warnings -GRPC_GENERATED_VERSION = '1.63.0' +GRPC_GENERATED_VERSION = '1.66.1' GRPC_VERSION = grpc.__version__ -EXPECTED_ERROR_RELEASE = '1.65.0' -SCHEDULED_RELEASE_DATE = 'June 25, 2024' _version_not_supported = False try: @@ -17,13 +15,10 @@ _version_not_supported = True if _version_not_supported: - warnings.warn( + raise RuntimeError( f'The grpc package installed is at version {GRPC_VERSION},' + f' but the generated code in dapr/proto/common/v1/common_pb2_grpc.py depends on' + f' grpcio>={GRPC_GENERATED_VERSION}.' + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' - + f' This warning will become an error in {EXPECTED_ERROR_RELEASE},' - + f' scheduled for release on {SCHEDULED_RELEASE_DATE}.', - RuntimeWarning ) diff --git a/dapr/proto/runtime/v1/appcallback_pb2.py b/dapr/proto/runtime/v1/appcallback_pb2.py index b6f27030..118d1959 100644 --- a/dapr/proto/runtime/v1/appcallback_pb2.py +++ b/dapr/proto/runtime/v1/appcallback_pb2.py @@ -1,12 +1,22 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE # source: dapr/proto/runtime/v1/appcallback.proto -# Protobuf Python Version: 5.26.1 +# Protobuf Python Version: 5.27.2 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version from google.protobuf import symbol_database as _symbol_database from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 27, + 2, + '', + 'dapr/proto/runtime/v1/appcallback.proto' +) # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() diff --git a/dapr/proto/runtime/v1/appcallback_pb2_grpc.py b/dapr/proto/runtime/v1/appcallback_pb2_grpc.py index 92a05f46..cd3e63c8 100644 --- a/dapr/proto/runtime/v1/appcallback_pb2_grpc.py +++ b/dapr/proto/runtime/v1/appcallback_pb2_grpc.py @@ -7,10 +7,8 @@ from dapr.proto.runtime.v1 import appcallback_pb2 as dapr_dot_proto_dot_runtime_dot_v1_dot_appcallback__pb2 from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2 -GRPC_GENERATED_VERSION = '1.63.0' +GRPC_GENERATED_VERSION = '1.66.1' GRPC_VERSION = grpc.__version__ -EXPECTED_ERROR_RELEASE = '1.65.0' -SCHEDULED_RELEASE_DATE = 'June 25, 2024' _version_not_supported = False try: @@ -20,15 +18,12 @@ _version_not_supported = True if _version_not_supported: - warnings.warn( + raise RuntimeError( f'The grpc package installed is at version {GRPC_VERSION},' + f' but the generated code in dapr/proto/runtime/v1/appcallback_pb2_grpc.py depends on' + f' grpcio>={GRPC_GENERATED_VERSION}.' + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' - + f' This warning will become an error in {EXPECTED_ERROR_RELEASE},' - + f' scheduled for release on {SCHEDULED_RELEASE_DATE}.', - RuntimeWarning ) @@ -147,6 +142,7 @@ def add_AppCallbackServicer_to_server(servicer, server): generic_handler = grpc.method_handlers_generic_handler( 'dapr.proto.runtime.v1.AppCallback', rpc_method_handlers) server.add_generic_rpc_handlers((generic_handler,)) + server.add_registered_method_handlers('dapr.proto.runtime.v1.AppCallback', rpc_method_handlers) # This class is part of an EXPERIMENTAL API. @@ -334,6 +330,7 @@ def add_AppCallbackHealthCheckServicer_to_server(servicer, server): generic_handler = grpc.method_handlers_generic_handler( 'dapr.proto.runtime.v1.AppCallbackHealthCheck', rpc_method_handlers) server.add_generic_rpc_handlers((generic_handler,)) + server.add_registered_method_handlers('dapr.proto.runtime.v1.AppCallbackHealthCheck', rpc_method_handlers) # This class is part of an EXPERIMENTAL API. @@ -429,6 +426,7 @@ def add_AppCallbackAlphaServicer_to_server(servicer, server): generic_handler = grpc.method_handlers_generic_handler( 'dapr.proto.runtime.v1.AppCallbackAlpha', rpc_method_handlers) server.add_generic_rpc_handlers((generic_handler,)) + server.add_registered_method_handlers('dapr.proto.runtime.v1.AppCallbackAlpha', rpc_method_handlers) # This class is part of an EXPERIMENTAL API. diff --git a/dapr/proto/runtime/v1/dapr_pb2.py b/dapr/proto/runtime/v1/dapr_pb2.py index e46a132a..21e766bf 100644 --- a/dapr/proto/runtime/v1/dapr_pb2.py +++ b/dapr/proto/runtime/v1/dapr_pb2.py @@ -1,12 +1,22 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE # source: dapr/proto/runtime/v1/dapr.proto -# Protobuf Python Version: 5.26.1 +# Protobuf Python Version: 5.27.2 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version from google.protobuf import symbol_database as _symbol_database from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 27, + 2, + '', + 'dapr/proto/runtime/v1/dapr.proto' +) # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() @@ -19,7 +29,7 @@ from dapr.proto.runtime.v1 import appcallback_pb2 as dapr_dot_proto_dot_runtime_dot_v1_dot_appcallback__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n dapr/proto/runtime/v1/dapr.proto\x12\x15\x64\x61pr.proto.runtime.v1\x1a\x19google/protobuf/any.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a!dapr/proto/common/v1/common.proto\x1a\'dapr/proto/runtime/v1/appcallback.proto\"X\n\x14InvokeServiceRequest\x12\n\n\x02id\x18\x01 \x01(\t\x12\x34\n\x07message\x18\x03 \x01(\x0b\x32#.dapr.proto.common.v1.InvokeRequest\"\xf5\x01\n\x0fGetStateRequest\x12\x12\n\nstore_name\x18\x01 \x01(\t\x12\x0b\n\x03key\x18\x02 \x01(\t\x12H\n\x0b\x63onsistency\x18\x03 \x01(\x0e\x32\x33.dapr.proto.common.v1.StateOptions.StateConsistency\x12\x46\n\x08metadata\x18\x04 \x03(\x0b\x32\x34.dapr.proto.runtime.v1.GetStateRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xc9\x01\n\x13GetBulkStateRequest\x12\x12\n\nstore_name\x18\x01 \x01(\t\x12\x0c\n\x04keys\x18\x02 \x03(\t\x12\x13\n\x0bparallelism\x18\x03 \x01(\x05\x12J\n\x08metadata\x18\x04 \x03(\x0b\x32\x38.dapr.proto.runtime.v1.GetBulkStateRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"K\n\x14GetBulkStateResponse\x12\x33\n\x05items\x18\x01 \x03(\x0b\x32$.dapr.proto.runtime.v1.BulkStateItem\"\xbe\x01\n\rBulkStateItem\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\x12\x0c\n\x04\x65tag\x18\x03 \x01(\t\x12\r\n\x05\x65rror\x18\x04 \x01(\t\x12\x44\n\x08metadata\x18\x05 \x03(\x0b\x32\x32.dapr.proto.runtime.v1.BulkStateItem.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xa8\x01\n\x10GetStateResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\x12\x0c\n\x04\x65tag\x18\x02 \x01(\t\x12G\n\x08metadata\x18\x03 \x03(\x0b\x32\x35.dapr.proto.runtime.v1.GetStateResponse.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x90\x02\n\x12\x44\x65leteStateRequest\x12\x12\n\nstore_name\x18\x01 \x01(\t\x12\x0b\n\x03key\x18\x02 \x01(\t\x12(\n\x04\x65tag\x18\x03 \x01(\x0b\x32\x1a.dapr.proto.common.v1.Etag\x12\x33\n\x07options\x18\x04 \x01(\x0b\x32\".dapr.proto.common.v1.StateOptions\x12I\n\x08metadata\x18\x05 \x03(\x0b\x32\x37.dapr.proto.runtime.v1.DeleteStateRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"]\n\x16\x44\x65leteBulkStateRequest\x12\x12\n\nstore_name\x18\x01 \x01(\t\x12/\n\x06states\x18\x02 \x03(\x0b\x32\x1f.dapr.proto.common.v1.StateItem\"W\n\x10SaveStateRequest\x12\x12\n\nstore_name\x18\x01 \x01(\t\x12/\n\x06states\x18\x02 \x03(\x0b\x32\x1f.dapr.proto.common.v1.StateItem\"\xbc\x01\n\x11QueryStateRequest\x12\x1d\n\nstore_name\x18\x01 \x01(\tR\tstoreName\x12\r\n\x05query\x18\x02 \x01(\t\x12H\n\x08metadata\x18\x03 \x03(\x0b\x32\x36.dapr.proto.runtime.v1.QueryStateRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"H\n\x0eQueryStateItem\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\x12\x0c\n\x04\x65tag\x18\x03 \x01(\t\x12\r\n\x05\x65rror\x18\x04 \x01(\t\"\xd7\x01\n\x12QueryStateResponse\x12\x36\n\x07results\x18\x01 \x03(\x0b\x32%.dapr.proto.runtime.v1.QueryStateItem\x12\r\n\x05token\x18\x02 \x01(\t\x12I\n\x08metadata\x18\x03 \x03(\x0b\x32\x37.dapr.proto.runtime.v1.QueryStateResponse.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xdf\x01\n\x13PublishEventRequest\x12\x13\n\x0bpubsub_name\x18\x01 \x01(\t\x12\r\n\x05topic\x18\x02 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x03 \x01(\x0c\x12\x19\n\x11\x64\x61ta_content_type\x18\x04 \x01(\t\x12J\n\x08metadata\x18\x05 \x03(\x0b\x32\x38.dapr.proto.runtime.v1.PublishEventRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xf5\x01\n\x12\x42ulkPublishRequest\x12\x13\n\x0bpubsub_name\x18\x01 \x01(\t\x12\r\n\x05topic\x18\x02 \x01(\t\x12?\n\x07\x65ntries\x18\x03 \x03(\x0b\x32..dapr.proto.runtime.v1.BulkPublishRequestEntry\x12I\n\x08metadata\x18\x04 \x03(\x0b\x32\x37.dapr.proto.runtime.v1.BulkPublishRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xd1\x01\n\x17\x42ulkPublishRequestEntry\x12\x10\n\x08\x65ntry_id\x18\x01 \x01(\t\x12\r\n\x05\x65vent\x18\x02 \x01(\x0c\x12\x14\n\x0c\x63ontent_type\x18\x03 \x01(\t\x12N\n\x08metadata\x18\x04 \x03(\x0b\x32<.dapr.proto.runtime.v1.BulkPublishRequestEntry.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"c\n\x13\x42ulkPublishResponse\x12L\n\rfailedEntries\x18\x01 \x03(\x0b\x32\x35.dapr.proto.runtime.v1.BulkPublishResponseFailedEntry\"A\n\x1e\x42ulkPublishResponseFailedEntry\x12\x10\n\x08\x65ntry_id\x18\x01 \x01(\t\x12\r\n\x05\x65rror\x18\x02 \x01(\t\"\xfb\x01\n!SubscribeTopicEventsRequestAlpha1\x12Z\n\x0finitial_request\x18\x01 \x01(\x0b\x32?.dapr.proto.runtime.v1.SubscribeTopicEventsInitialRequestAlpha1H\x00\x12S\n\x0e\x65vent_response\x18\x02 \x01(\x0b\x32\x39.dapr.proto.runtime.v1.SubscribeTopicEventsResponseAlpha1H\x00\x42%\n#subscribe_topic_events_request_type\"\x96\x02\n(SubscribeTopicEventsInitialRequestAlpha1\x12\x13\n\x0bpubsub_name\x18\x01 \x01(\t\x12\r\n\x05topic\x18\x02 \x01(\t\x12_\n\x08metadata\x18\x03 \x03(\x0b\x32M.dapr.proto.runtime.v1.SubscribeTopicEventsInitialRequestAlpha1.MetadataEntry\x12\x1e\n\x11\x64\x65\x61\x64_letter_topic\x18\x04 \x01(\tH\x00\x88\x01\x01\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42\x14\n\x12_dead_letter_topic\"k\n\"SubscribeTopicEventsResponseAlpha1\x12\n\n\x02id\x18\x01 \x01(\t\x12\x39\n\x06status\x18\x02 \x01(\x0b\x32).dapr.proto.runtime.v1.TopicEventResponse\"\xc3\x01\n\x14InvokeBindingRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\x12K\n\x08metadata\x18\x03 \x03(\x0b\x32\x39.dapr.proto.runtime.v1.InvokeBindingRequest.MetadataEntry\x12\x11\n\toperation\x18\x04 \x01(\t\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xa4\x01\n\x15InvokeBindingResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\x12L\n\x08metadata\x18\x02 \x03(\x0b\x32:.dapr.proto.runtime.v1.InvokeBindingResponse.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xb8\x01\n\x10GetSecretRequest\x12\x1d\n\nstore_name\x18\x01 \x01(\tR\tstoreName\x12\x0b\n\x03key\x18\x02 \x01(\t\x12G\n\x08metadata\x18\x03 \x03(\x0b\x32\x35.dapr.proto.runtime.v1.GetSecretRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x82\x01\n\x11GetSecretResponse\x12@\n\x04\x64\x61ta\x18\x01 \x03(\x0b\x32\x32.dapr.proto.runtime.v1.GetSecretResponse.DataEntry\x1a+\n\tDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xb3\x01\n\x14GetBulkSecretRequest\x12\x1d\n\nstore_name\x18\x01 \x01(\tR\tstoreName\x12K\n\x08metadata\x18\x02 \x03(\x0b\x32\x39.dapr.proto.runtime.v1.GetBulkSecretRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x85\x01\n\x0eSecretResponse\x12\x43\n\x07secrets\x18\x01 \x03(\x0b\x32\x32.dapr.proto.runtime.v1.SecretResponse.SecretsEntry\x1a.\n\x0cSecretsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xb1\x01\n\x15GetBulkSecretResponse\x12\x44\n\x04\x64\x61ta\x18\x01 \x03(\x0b\x32\x36.dapr.proto.runtime.v1.GetBulkSecretResponse.DataEntry\x1aR\n\tDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x34\n\x05value\x18\x02 \x01(\x0b\x32%.dapr.proto.runtime.v1.SecretResponse:\x02\x38\x01\"f\n\x1bTransactionalStateOperation\x12\x15\n\roperationType\x18\x01 \x01(\t\x12\x30\n\x07request\x18\x02 \x01(\x0b\x32\x1f.dapr.proto.common.v1.StateItem\"\x83\x02\n\x1e\x45xecuteStateTransactionRequest\x12\x11\n\tstoreName\x18\x01 \x01(\t\x12\x46\n\noperations\x18\x02 \x03(\x0b\x32\x32.dapr.proto.runtime.v1.TransactionalStateOperation\x12U\n\x08metadata\x18\x03 \x03(\x0b\x32\x43.dapr.proto.runtime.v1.ExecuteStateTransactionRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xbb\x01\n\x19RegisterActorTimerRequest\x12\x1d\n\nactor_type\x18\x01 \x01(\tR\tactorType\x12\x19\n\x08\x61\x63tor_id\x18\x02 \x01(\tR\x07\x61\x63torId\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x19\n\x08\x64ue_time\x18\x04 \x01(\tR\x07\x64ueTime\x12\x0e\n\x06period\x18\x05 \x01(\t\x12\x10\n\x08\x63\x61llback\x18\x06 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x07 \x01(\x0c\x12\x0b\n\x03ttl\x18\x08 \x01(\t\"e\n\x1bUnregisterActorTimerRequest\x12\x1d\n\nactor_type\x18\x01 \x01(\tR\tactorType\x12\x19\n\x08\x61\x63tor_id\x18\x02 \x01(\tR\x07\x61\x63torId\x12\x0c\n\x04name\x18\x03 \x01(\t\"\xac\x01\n\x1cRegisterActorReminderRequest\x12\x1d\n\nactor_type\x18\x01 \x01(\tR\tactorType\x12\x19\n\x08\x61\x63tor_id\x18\x02 \x01(\tR\x07\x61\x63torId\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x19\n\x08\x64ue_time\x18\x04 \x01(\tR\x07\x64ueTime\x12\x0e\n\x06period\x18\x05 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x06 \x01(\x0c\x12\x0b\n\x03ttl\x18\x07 \x01(\t\"h\n\x1eUnregisterActorReminderRequest\x12\x1d\n\nactor_type\x18\x01 \x01(\tR\tactorType\x12\x19\n\x08\x61\x63tor_id\x18\x02 \x01(\tR\x07\x61\x63torId\x12\x0c\n\x04name\x18\x03 \x01(\t\"]\n\x14GetActorStateRequest\x12\x1d\n\nactor_type\x18\x01 \x01(\tR\tactorType\x12\x19\n\x08\x61\x63tor_id\x18\x02 \x01(\tR\x07\x61\x63torId\x12\x0b\n\x03key\x18\x03 \x01(\t\"\xa4\x01\n\x15GetActorStateResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\x12L\n\x08metadata\x18\x02 \x03(\x0b\x32:.dapr.proto.runtime.v1.GetActorStateResponse.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xac\x01\n#ExecuteActorStateTransactionRequest\x12\x1d\n\nactor_type\x18\x01 \x01(\tR\tactorType\x12\x19\n\x08\x61\x63tor_id\x18\x02 \x01(\tR\x07\x61\x63torId\x12K\n\noperations\x18\x03 \x03(\x0b\x32\x37.dapr.proto.runtime.v1.TransactionalActorStateOperation\"\xf5\x01\n TransactionalActorStateOperation\x12\x15\n\roperationType\x18\x01 \x01(\t\x12\x0b\n\x03key\x18\x02 \x01(\t\x12#\n\x05value\x18\x03 \x01(\x0b\x32\x14.google.protobuf.Any\x12W\n\x08metadata\x18\x04 \x03(\x0b\x32\x45.dapr.proto.runtime.v1.TransactionalActorStateOperation.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xe8\x01\n\x12InvokeActorRequest\x12\x1d\n\nactor_type\x18\x01 \x01(\tR\tactorType\x12\x19\n\x08\x61\x63tor_id\x18\x02 \x01(\tR\x07\x61\x63torId\x12\x0e\n\x06method\x18\x03 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x04 \x01(\x0c\x12I\n\x08metadata\x18\x05 \x03(\x0b\x32\x37.dapr.proto.runtime.v1.InvokeActorRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"#\n\x13InvokeActorResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"\x14\n\x12GetMetadataRequest\"\x9b\x06\n\x13GetMetadataResponse\x12\n\n\x02id\x18\x01 \x01(\t\x12Q\n\x13\x61\x63tive_actors_count\x18\x02 \x03(\x0b\x32(.dapr.proto.runtime.v1.ActiveActorsCountB\x02\x18\x01R\x06\x61\x63tors\x12V\n\x15registered_components\x18\x03 \x03(\x0b\x32+.dapr.proto.runtime.v1.RegisteredComponentsR\ncomponents\x12\x65\n\x11\x65xtended_metadata\x18\x04 \x03(\x0b\x32@.dapr.proto.runtime.v1.GetMetadataResponse.ExtendedMetadataEntryR\x08\x65xtended\x12O\n\rsubscriptions\x18\x05 \x03(\x0b\x32).dapr.proto.runtime.v1.PubsubSubscriptionR\rsubscriptions\x12R\n\x0ehttp_endpoints\x18\x06 \x03(\x0b\x32+.dapr.proto.runtime.v1.MetadataHTTPEndpointR\rhttpEndpoints\x12j\n\x19\x61pp_connection_properties\x18\x07 \x01(\x0b\x32..dapr.proto.runtime.v1.AppConnectionPropertiesR\x17\x61ppConnectionProperties\x12\'\n\x0fruntime_version\x18\x08 \x01(\tR\x0eruntimeVersion\x12)\n\x10\x65nabled_features\x18\t \x03(\tR\x0f\x65nabledFeatures\x12H\n\ractor_runtime\x18\n \x01(\x0b\x32#.dapr.proto.runtime.v1.ActorRuntimeR\x0c\x61\x63torRuntime\x1a\x37\n\x15\x45xtendedMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xbc\x02\n\x0c\x41\x63torRuntime\x12]\n\x0eruntime_status\x18\x01 \x01(\x0e\x32\x36.dapr.proto.runtime.v1.ActorRuntime.ActorRuntimeStatusR\rruntimeStatus\x12M\n\ractive_actors\x18\x02 \x03(\x0b\x32(.dapr.proto.runtime.v1.ActiveActorsCountR\x0c\x61\x63tiveActors\x12\x1d\n\nhost_ready\x18\x03 \x01(\x08R\thostReady\x12\x1c\n\tplacement\x18\x04 \x01(\tR\tplacement\"A\n\x12\x41\x63torRuntimeStatus\x12\x10\n\x0cINITIALIZING\x10\x00\x12\x0c\n\x08\x44ISABLED\x10\x01\x12\x0b\n\x07RUNNING\x10\x02\"0\n\x11\x41\x63tiveActorsCount\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\r\n\x05\x63ount\x18\x02 \x01(\x05\"Y\n\x14RegisteredComponents\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04type\x18\x02 \x01(\t\x12\x0f\n\x07version\x18\x03 \x01(\t\x12\x14\n\x0c\x63\x61pabilities\x18\x04 \x03(\t\"*\n\x14MetadataHTTPEndpoint\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\"\xd1\x01\n\x17\x41ppConnectionProperties\x12\x0c\n\x04port\x18\x01 \x01(\x05\x12\x10\n\x08protocol\x18\x02 \x01(\t\x12\'\n\x0f\x63hannel_address\x18\x03 \x01(\tR\x0e\x63hannelAddress\x12\'\n\x0fmax_concurrency\x18\x04 \x01(\x05R\x0emaxConcurrency\x12\x44\n\x06health\x18\x05 \x01(\x0b\x32\x34.dapr.proto.runtime.v1.AppConnectionHealthProperties\"\xdc\x01\n\x1d\x41ppConnectionHealthProperties\x12*\n\x11health_check_path\x18\x01 \x01(\tR\x0fhealthCheckPath\x12\x32\n\x15health_probe_interval\x18\x02 \x01(\tR\x13healthProbeInterval\x12\x30\n\x14health_probe_timeout\x18\x03 \x01(\tR\x12healthProbeTimeout\x12)\n\x10health_threshold\x18\x04 \x01(\x05R\x0fhealthThreshold\"\x86\x03\n\x12PubsubSubscription\x12\x1f\n\x0bpubsub_name\x18\x01 \x01(\tR\npubsubname\x12\x14\n\x05topic\x18\x02 \x01(\tR\x05topic\x12S\n\x08metadata\x18\x03 \x03(\x0b\x32\x37.dapr.proto.runtime.v1.PubsubSubscription.MetadataEntryR\x08metadata\x12\x44\n\x05rules\x18\x04 \x01(\x0b\x32..dapr.proto.runtime.v1.PubsubSubscriptionRulesR\x05rules\x12*\n\x11\x64\x65\x61\x64_letter_topic\x18\x05 \x01(\tR\x0f\x64\x65\x61\x64LetterTopic\x12\x41\n\x04type\x18\x06 \x01(\x0e\x32-.dapr.proto.runtime.v1.PubsubSubscriptionTypeR\x04type\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"W\n\x17PubsubSubscriptionRules\x12<\n\x05rules\x18\x01 \x03(\x0b\x32-.dapr.proto.runtime.v1.PubsubSubscriptionRule\"5\n\x16PubsubSubscriptionRule\x12\r\n\x05match\x18\x01 \x01(\t\x12\x0c\n\x04path\x18\x02 \x01(\t\"0\n\x12SetMetadataRequest\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\"\xbc\x01\n\x17GetConfigurationRequest\x12\x12\n\nstore_name\x18\x01 \x01(\t\x12\x0c\n\x04keys\x18\x02 \x03(\t\x12N\n\x08metadata\x18\x03 \x03(\x0b\x32<.dapr.proto.runtime.v1.GetConfigurationRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xbc\x01\n\x18GetConfigurationResponse\x12I\n\x05items\x18\x01 \x03(\x0b\x32:.dapr.proto.runtime.v1.GetConfigurationResponse.ItemsEntry\x1aU\n\nItemsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x36\n\x05value\x18\x02 \x01(\x0b\x32\'.dapr.proto.common.v1.ConfigurationItem:\x02\x38\x01\"\xc8\x01\n\x1dSubscribeConfigurationRequest\x12\x12\n\nstore_name\x18\x01 \x01(\t\x12\x0c\n\x04keys\x18\x02 \x03(\t\x12T\n\x08metadata\x18\x03 \x03(\x0b\x32\x42.dapr.proto.runtime.v1.SubscribeConfigurationRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"A\n\x1fUnsubscribeConfigurationRequest\x12\x12\n\nstore_name\x18\x01 \x01(\t\x12\n\n\x02id\x18\x02 \x01(\t\"\xd4\x01\n\x1eSubscribeConfigurationResponse\x12\n\n\x02id\x18\x01 \x01(\t\x12O\n\x05items\x18\x02 \x03(\x0b\x32@.dapr.proto.runtime.v1.SubscribeConfigurationResponse.ItemsEntry\x1aU\n\nItemsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x36\n\x05value\x18\x02 \x01(\x0b\x32\'.dapr.proto.common.v1.ConfigurationItem:\x02\x38\x01\"?\n UnsubscribeConfigurationResponse\x12\n\n\x02ok\x18\x01 \x01(\x08\x12\x0f\n\x07message\x18\x02 \x01(\t\"\x9b\x01\n\x0eTryLockRequest\x12\x1d\n\nstore_name\x18\x01 \x01(\tR\tstoreName\x12\x1f\n\x0bresource_id\x18\x02 \x01(\tR\nresourceId\x12\x1d\n\nlock_owner\x18\x03 \x01(\tR\tlockOwner\x12*\n\x11\x65xpiry_in_seconds\x18\x04 \x01(\x05R\x0f\x65xpiryInSeconds\"\"\n\x0fTryLockResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\"n\n\rUnlockRequest\x12\x1d\n\nstore_name\x18\x01 \x01(\tR\tstoreName\x12\x1f\n\x0bresource_id\x18\x02 \x01(\tR\nresourceId\x12\x1d\n\nlock_owner\x18\x03 \x01(\tR\tlockOwner\"\xae\x01\n\x0eUnlockResponse\x12<\n\x06status\x18\x01 \x01(\x0e\x32,.dapr.proto.runtime.v1.UnlockResponse.Status\"^\n\x06Status\x12\x0b\n\x07SUCCESS\x10\x00\x12\x17\n\x13LOCK_DOES_NOT_EXIST\x10\x01\x12\x1a\n\x16LOCK_BELONGS_TO_OTHERS\x10\x02\x12\x12\n\x0eINTERNAL_ERROR\x10\x03\"\xb0\x01\n\x13SubtleGetKeyRequest\x12%\n\x0e\x63omponent_name\x18\x01 \x01(\tR\rcomponentName\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x44\n\x06\x66ormat\x18\x03 \x01(\x0e\x32\x34.dapr.proto.runtime.v1.SubtleGetKeyRequest.KeyFormat\"\x1e\n\tKeyFormat\x12\x07\n\x03PEM\x10\x00\x12\x08\n\x04JSON\x10\x01\"C\n\x14SubtleGetKeyResponse\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x1d\n\npublic_key\x18\x02 \x01(\tR\tpublicKey\"\xb6\x01\n\x14SubtleEncryptRequest\x12%\n\x0e\x63omponent_name\x18\x01 \x01(\tR\rcomponentName\x12\x11\n\tplaintext\x18\x02 \x01(\x0c\x12\x11\n\talgorithm\x18\x03 \x01(\t\x12\x19\n\x08key_name\x18\x04 \x01(\tR\x07keyName\x12\r\n\x05nonce\x18\x05 \x01(\x0c\x12\'\n\x0f\x61ssociated_data\x18\x06 \x01(\x0cR\x0e\x61ssociatedData\"8\n\x15SubtleEncryptResponse\x12\x12\n\nciphertext\x18\x01 \x01(\x0c\x12\x0b\n\x03tag\x18\x02 \x01(\x0c\"\xc4\x01\n\x14SubtleDecryptRequest\x12%\n\x0e\x63omponent_name\x18\x01 \x01(\tR\rcomponentName\x12\x12\n\nciphertext\x18\x02 \x01(\x0c\x12\x11\n\talgorithm\x18\x03 \x01(\t\x12\x19\n\x08key_name\x18\x04 \x01(\tR\x07keyName\x12\r\n\x05nonce\x18\x05 \x01(\x0c\x12\x0b\n\x03tag\x18\x06 \x01(\x0c\x12\'\n\x0f\x61ssociated_data\x18\x07 \x01(\x0cR\x0e\x61ssociatedData\"*\n\x15SubtleDecryptResponse\x12\x11\n\tplaintext\x18\x01 \x01(\x0c\"\xc8\x01\n\x14SubtleWrapKeyRequest\x12%\n\x0e\x63omponent_name\x18\x01 \x01(\tR\rcomponentName\x12#\n\rplaintext_key\x18\x02 \x01(\x0cR\x0cplaintextKey\x12\x11\n\talgorithm\x18\x03 \x01(\t\x12\x19\n\x08key_name\x18\x04 \x01(\tR\x07keyName\x12\r\n\x05nonce\x18\x05 \x01(\x0c\x12\'\n\x0f\x61ssociated_data\x18\x06 \x01(\x0cR\x0e\x61ssociatedData\"E\n\x15SubtleWrapKeyResponse\x12\x1f\n\x0bwrapped_key\x18\x01 \x01(\x0cR\nwrappedKey\x12\x0b\n\x03tag\x18\x02 \x01(\x0c\"\xd3\x01\n\x16SubtleUnwrapKeyRequest\x12%\n\x0e\x63omponent_name\x18\x01 \x01(\tR\rcomponentName\x12\x1f\n\x0bwrapped_key\x18\x02 \x01(\x0cR\nwrappedKey\x12\x11\n\talgorithm\x18\x03 \x01(\t\x12\x19\n\x08key_name\x18\x04 \x01(\tR\x07keyName\x12\r\n\x05nonce\x18\x05 \x01(\x0c\x12\x0b\n\x03tag\x18\x06 \x01(\x0c\x12\'\n\x0f\x61ssociated_data\x18\x07 \x01(\x0cR\x0e\x61ssociatedData\">\n\x17SubtleUnwrapKeyResponse\x12#\n\rplaintext_key\x18\x01 \x01(\x0cR\x0cplaintextKey\"x\n\x11SubtleSignRequest\x12%\n\x0e\x63omponent_name\x18\x01 \x01(\tR\rcomponentName\x12\x0e\n\x06\x64igest\x18\x02 \x01(\x0c\x12\x11\n\talgorithm\x18\x03 \x01(\t\x12\x19\n\x08key_name\x18\x04 \x01(\tR\x07keyName\"\'\n\x12SubtleSignResponse\x12\x11\n\tsignature\x18\x01 \x01(\x0c\"\x8d\x01\n\x13SubtleVerifyRequest\x12%\n\x0e\x63omponent_name\x18\x01 \x01(\tR\rcomponentName\x12\x0e\n\x06\x64igest\x18\x02 \x01(\x0c\x12\x11\n\talgorithm\x18\x03 \x01(\t\x12\x19\n\x08key_name\x18\x04 \x01(\tR\x07keyName\x12\x11\n\tsignature\x18\x05 \x01(\x0c\"%\n\x14SubtleVerifyResponse\x12\r\n\x05valid\x18\x01 \x01(\x08\"\x85\x01\n\x0e\x45ncryptRequest\x12=\n\x07options\x18\x01 \x01(\x0b\x32,.dapr.proto.runtime.v1.EncryptRequestOptions\x12\x34\n\x07payload\x18\x02 \x01(\x0b\x32#.dapr.proto.common.v1.StreamPayload\"\xfe\x01\n\x15\x45ncryptRequestOptions\x12%\n\x0e\x63omponent_name\x18\x01 \x01(\tR\rcomponentName\x12\x19\n\x08key_name\x18\x02 \x01(\tR\x07keyName\x12\x1a\n\x12key_wrap_algorithm\x18\x03 \x01(\t\x12\x1e\n\x16\x64\x61ta_encryption_cipher\x18\n \x01(\t\x12\x37\n\x18omit_decryption_key_name\x18\x0b \x01(\x08R\x15omitDecryptionKeyName\x12.\n\x13\x64\x65\x63ryption_key_name\x18\x0c \x01(\tR\x11\x64\x65\x63ryptionKeyName\"G\n\x0f\x45ncryptResponse\x12\x34\n\x07payload\x18\x01 \x01(\x0b\x32#.dapr.proto.common.v1.StreamPayload\"\x85\x01\n\x0e\x44\x65\x63ryptRequest\x12=\n\x07options\x18\x01 \x01(\x0b\x32,.dapr.proto.runtime.v1.DecryptRequestOptions\x12\x34\n\x07payload\x18\x02 \x01(\x0b\x32#.dapr.proto.common.v1.StreamPayload\"Y\n\x15\x44\x65\x63ryptRequestOptions\x12%\n\x0e\x63omponent_name\x18\x01 \x01(\tR\rcomponentName\x12\x19\n\x08key_name\x18\x0c \x01(\tR\x07keyName\"G\n\x0f\x44\x65\x63ryptResponse\x12\x34\n\x07payload\x18\x01 \x01(\x0b\x32#.dapr.proto.common.v1.StreamPayload\"d\n\x12GetWorkflowRequest\x12\x1f\n\x0binstance_id\x18\x01 \x01(\tR\ninstanceID\x12-\n\x12workflow_component\x18\x02 \x01(\tR\x11workflowComponent\"\x84\x03\n\x13GetWorkflowResponse\x12\x1f\n\x0binstance_id\x18\x01 \x01(\tR\ninstanceID\x12#\n\rworkflow_name\x18\x02 \x01(\tR\x0cworkflowName\x12\x39\n\ncreated_at\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\tcreatedAt\x12\x42\n\x0flast_updated_at\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\rlastUpdatedAt\x12%\n\x0eruntime_status\x18\x05 \x01(\tR\rruntimeStatus\x12N\n\nproperties\x18\x06 \x03(\x0b\x32:.dapr.proto.runtime.v1.GetWorkflowResponse.PropertiesEntry\x1a\x31\n\x0fPropertiesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x95\x02\n\x14StartWorkflowRequest\x12\x1f\n\x0binstance_id\x18\x01 \x01(\tR\ninstanceID\x12-\n\x12workflow_component\x18\x02 \x01(\tR\x11workflowComponent\x12#\n\rworkflow_name\x18\x03 \x01(\tR\x0cworkflowName\x12I\n\x07options\x18\x04 \x03(\x0b\x32\x38.dapr.proto.runtime.v1.StartWorkflowRequest.OptionsEntry\x12\r\n\x05input\x18\x05 \x01(\x0c\x1a.\n\x0cOptionsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"8\n\x15StartWorkflowResponse\x12\x1f\n\x0binstance_id\x18\x01 \x01(\tR\ninstanceID\"j\n\x18TerminateWorkflowRequest\x12\x1f\n\x0binstance_id\x18\x01 \x01(\tR\ninstanceID\x12-\n\x12workflow_component\x18\x02 \x01(\tR\x11workflowComponent\"f\n\x14PauseWorkflowRequest\x12\x1f\n\x0binstance_id\x18\x01 \x01(\tR\ninstanceID\x12-\n\x12workflow_component\x18\x02 \x01(\tR\x11workflowComponent\"g\n\x15ResumeWorkflowRequest\x12\x1f\n\x0binstance_id\x18\x01 \x01(\tR\ninstanceID\x12-\n\x12workflow_component\x18\x02 \x01(\tR\x11workflowComponent\"\x9e\x01\n\x19RaiseEventWorkflowRequest\x12\x1f\n\x0binstance_id\x18\x01 \x01(\tR\ninstanceID\x12-\n\x12workflow_component\x18\x02 \x01(\tR\x11workflowComponent\x12\x1d\n\nevent_name\x18\x03 \x01(\tR\teventName\x12\x12\n\nevent_data\x18\x04 \x01(\x0c\"f\n\x14PurgeWorkflowRequest\x12\x1f\n\x0binstance_id\x18\x01 \x01(\tR\ninstanceID\x12-\n\x12workflow_component\x18\x02 \x01(\tR\x11workflowComponent\"\x11\n\x0fShutdownRequest\"\xbb\x01\n\x03Job\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x15\n\x08schedule\x18\x02 \x01(\tH\x00\x88\x01\x01\x12\x14\n\x07repeats\x18\x03 \x01(\rH\x01\x88\x01\x01\x12\x15\n\x08\x64ue_time\x18\x04 \x01(\tH\x02\x88\x01\x01\x12\x10\n\x03ttl\x18\x05 \x01(\tH\x03\x88\x01\x01\x12\"\n\x04\x64\x61ta\x18\x06 \x01(\x0b\x32\x14.google.protobuf.AnyB\x0b\n\t_scheduleB\n\n\x08_repeatsB\x0b\n\t_due_timeB\x06\n\x04_ttl\"=\n\x12ScheduleJobRequest\x12\'\n\x03job\x18\x01 \x01(\x0b\x32\x1a.dapr.proto.runtime.v1.Job\"\x15\n\x13ScheduleJobResponse\"\x1d\n\rGetJobRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\"9\n\x0eGetJobResponse\x12\'\n\x03job\x18\x01 \x01(\x0b\x32\x1a.dapr.proto.runtime.v1.Job\" \n\x10\x44\x65leteJobRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\"\x13\n\x11\x44\x65leteJobResponse*W\n\x16PubsubSubscriptionType\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x0f\n\x0b\x44\x45\x43LARATIVE\x10\x01\x12\x10\n\x0cPROGRAMMATIC\x10\x02\x12\r\n\tSTREAMING\x10\x03\x32\xab\x30\n\x04\x44\x61pr\x12\x64\n\rInvokeService\x12+.dapr.proto.runtime.v1.InvokeServiceRequest\x1a$.dapr.proto.common.v1.InvokeResponse\"\x00\x12]\n\x08GetState\x12&.dapr.proto.runtime.v1.GetStateRequest\x1a\'.dapr.proto.runtime.v1.GetStateResponse\"\x00\x12i\n\x0cGetBulkState\x12*.dapr.proto.runtime.v1.GetBulkStateRequest\x1a+.dapr.proto.runtime.v1.GetBulkStateResponse\"\x00\x12N\n\tSaveState\x12\'.dapr.proto.runtime.v1.SaveStateRequest\x1a\x16.google.protobuf.Empty\"\x00\x12i\n\x10QueryStateAlpha1\x12(.dapr.proto.runtime.v1.QueryStateRequest\x1a).dapr.proto.runtime.v1.QueryStateResponse\"\x00\x12R\n\x0b\x44\x65leteState\x12).dapr.proto.runtime.v1.DeleteStateRequest\x1a\x16.google.protobuf.Empty\"\x00\x12Z\n\x0f\x44\x65leteBulkState\x12-.dapr.proto.runtime.v1.DeleteBulkStateRequest\x1a\x16.google.protobuf.Empty\"\x00\x12j\n\x17\x45xecuteStateTransaction\x12\x35.dapr.proto.runtime.v1.ExecuteStateTransactionRequest\x1a\x16.google.protobuf.Empty\"\x00\x12T\n\x0cPublishEvent\x12*.dapr.proto.runtime.v1.PublishEventRequest\x1a\x16.google.protobuf.Empty\"\x00\x12q\n\x16\x42ulkPublishEventAlpha1\x12).dapr.proto.runtime.v1.BulkPublishRequest\x1a*.dapr.proto.runtime.v1.BulkPublishResponse\"\x00\x12\x86\x01\n\x1aSubscribeTopicEventsAlpha1\x12\x38.dapr.proto.runtime.v1.SubscribeTopicEventsRequestAlpha1\x1a(.dapr.proto.runtime.v1.TopicEventRequest\"\x00(\x01\x30\x01\x12l\n\rInvokeBinding\x12+.dapr.proto.runtime.v1.InvokeBindingRequest\x1a,.dapr.proto.runtime.v1.InvokeBindingResponse\"\x00\x12`\n\tGetSecret\x12\'.dapr.proto.runtime.v1.GetSecretRequest\x1a(.dapr.proto.runtime.v1.GetSecretResponse\"\x00\x12l\n\rGetBulkSecret\x12+.dapr.proto.runtime.v1.GetBulkSecretRequest\x1a,.dapr.proto.runtime.v1.GetBulkSecretResponse\"\x00\x12`\n\x12RegisterActorTimer\x12\x30.dapr.proto.runtime.v1.RegisterActorTimerRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x64\n\x14UnregisterActorTimer\x12\x32.dapr.proto.runtime.v1.UnregisterActorTimerRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x66\n\x15RegisterActorReminder\x12\x33.dapr.proto.runtime.v1.RegisterActorReminderRequest\x1a\x16.google.protobuf.Empty\"\x00\x12j\n\x17UnregisterActorReminder\x12\x35.dapr.proto.runtime.v1.UnregisterActorReminderRequest\x1a\x16.google.protobuf.Empty\"\x00\x12l\n\rGetActorState\x12+.dapr.proto.runtime.v1.GetActorStateRequest\x1a,.dapr.proto.runtime.v1.GetActorStateResponse\"\x00\x12t\n\x1c\x45xecuteActorStateTransaction\x12:.dapr.proto.runtime.v1.ExecuteActorStateTransactionRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x66\n\x0bInvokeActor\x12).dapr.proto.runtime.v1.InvokeActorRequest\x1a*.dapr.proto.runtime.v1.InvokeActorResponse\"\x00\x12{\n\x16GetConfigurationAlpha1\x12..dapr.proto.runtime.v1.GetConfigurationRequest\x1a/.dapr.proto.runtime.v1.GetConfigurationResponse\"\x00\x12u\n\x10GetConfiguration\x12..dapr.proto.runtime.v1.GetConfigurationRequest\x1a/.dapr.proto.runtime.v1.GetConfigurationResponse\"\x00\x12\x8f\x01\n\x1cSubscribeConfigurationAlpha1\x12\x34.dapr.proto.runtime.v1.SubscribeConfigurationRequest\x1a\x35.dapr.proto.runtime.v1.SubscribeConfigurationResponse\"\x00\x30\x01\x12\x89\x01\n\x16SubscribeConfiguration\x12\x34.dapr.proto.runtime.v1.SubscribeConfigurationRequest\x1a\x35.dapr.proto.runtime.v1.SubscribeConfigurationResponse\"\x00\x30\x01\x12\x93\x01\n\x1eUnsubscribeConfigurationAlpha1\x12\x36.dapr.proto.runtime.v1.UnsubscribeConfigurationRequest\x1a\x37.dapr.proto.runtime.v1.UnsubscribeConfigurationResponse\"\x00\x12\x8d\x01\n\x18UnsubscribeConfiguration\x12\x36.dapr.proto.runtime.v1.UnsubscribeConfigurationRequest\x1a\x37.dapr.proto.runtime.v1.UnsubscribeConfigurationResponse\"\x00\x12`\n\rTryLockAlpha1\x12%.dapr.proto.runtime.v1.TryLockRequest\x1a&.dapr.proto.runtime.v1.TryLockResponse\"\x00\x12]\n\x0cUnlockAlpha1\x12$.dapr.proto.runtime.v1.UnlockRequest\x1a%.dapr.proto.runtime.v1.UnlockResponse\"\x00\x12\x62\n\rEncryptAlpha1\x12%.dapr.proto.runtime.v1.EncryptRequest\x1a&.dapr.proto.runtime.v1.EncryptResponse(\x01\x30\x01\x12\x62\n\rDecryptAlpha1\x12%.dapr.proto.runtime.v1.DecryptRequest\x1a&.dapr.proto.runtime.v1.DecryptResponse(\x01\x30\x01\x12\x66\n\x0bGetMetadata\x12).dapr.proto.runtime.v1.GetMetadataRequest\x1a*.dapr.proto.runtime.v1.GetMetadataResponse\"\x00\x12R\n\x0bSetMetadata\x12).dapr.proto.runtime.v1.SetMetadataRequest\x1a\x16.google.protobuf.Empty\"\x00\x12m\n\x12SubtleGetKeyAlpha1\x12*.dapr.proto.runtime.v1.SubtleGetKeyRequest\x1a+.dapr.proto.runtime.v1.SubtleGetKeyResponse\x12p\n\x13SubtleEncryptAlpha1\x12+.dapr.proto.runtime.v1.SubtleEncryptRequest\x1a,.dapr.proto.runtime.v1.SubtleEncryptResponse\x12p\n\x13SubtleDecryptAlpha1\x12+.dapr.proto.runtime.v1.SubtleDecryptRequest\x1a,.dapr.proto.runtime.v1.SubtleDecryptResponse\x12p\n\x13SubtleWrapKeyAlpha1\x12+.dapr.proto.runtime.v1.SubtleWrapKeyRequest\x1a,.dapr.proto.runtime.v1.SubtleWrapKeyResponse\x12v\n\x15SubtleUnwrapKeyAlpha1\x12-.dapr.proto.runtime.v1.SubtleUnwrapKeyRequest\x1a..dapr.proto.runtime.v1.SubtleUnwrapKeyResponse\x12g\n\x10SubtleSignAlpha1\x12(.dapr.proto.runtime.v1.SubtleSignRequest\x1a).dapr.proto.runtime.v1.SubtleSignResponse\x12m\n\x12SubtleVerifyAlpha1\x12*.dapr.proto.runtime.v1.SubtleVerifyRequest\x1a+.dapr.proto.runtime.v1.SubtleVerifyResponse\x12r\n\x13StartWorkflowAlpha1\x12+.dapr.proto.runtime.v1.StartWorkflowRequest\x1a,.dapr.proto.runtime.v1.StartWorkflowResponse\"\x00\x12l\n\x11GetWorkflowAlpha1\x12).dapr.proto.runtime.v1.GetWorkflowRequest\x1a*.dapr.proto.runtime.v1.GetWorkflowResponse\"\x00\x12\\\n\x13PurgeWorkflowAlpha1\x12+.dapr.proto.runtime.v1.PurgeWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x64\n\x17TerminateWorkflowAlpha1\x12/.dapr.proto.runtime.v1.TerminateWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\\\n\x13PauseWorkflowAlpha1\x12+.dapr.proto.runtime.v1.PauseWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x00\x12^\n\x14ResumeWorkflowAlpha1\x12,.dapr.proto.runtime.v1.ResumeWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x66\n\x18RaiseEventWorkflowAlpha1\x12\x30.dapr.proto.runtime.v1.RaiseEventWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x00\x12q\n\x12StartWorkflowBeta1\x12+.dapr.proto.runtime.v1.StartWorkflowRequest\x1a,.dapr.proto.runtime.v1.StartWorkflowResponse\"\x00\x12k\n\x10GetWorkflowBeta1\x12).dapr.proto.runtime.v1.GetWorkflowRequest\x1a*.dapr.proto.runtime.v1.GetWorkflowResponse\"\x00\x12[\n\x12PurgeWorkflowBeta1\x12+.dapr.proto.runtime.v1.PurgeWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x63\n\x16TerminateWorkflowBeta1\x12/.dapr.proto.runtime.v1.TerminateWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x00\x12[\n\x12PauseWorkflowBeta1\x12+.dapr.proto.runtime.v1.PauseWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x00\x12]\n\x13ResumeWorkflowBeta1\x12,.dapr.proto.runtime.v1.ResumeWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x65\n\x17RaiseEventWorkflowBeta1\x12\x30.dapr.proto.runtime.v1.RaiseEventWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x00\x12L\n\x08Shutdown\x12&.dapr.proto.runtime.v1.ShutdownRequest\x1a\x16.google.protobuf.Empty\"\x00\x12l\n\x11ScheduleJobAlpha1\x12).dapr.proto.runtime.v1.ScheduleJobRequest\x1a*.dapr.proto.runtime.v1.ScheduleJobResponse\"\x00\x12]\n\x0cGetJobAlpha1\x12$.dapr.proto.runtime.v1.GetJobRequest\x1a%.dapr.proto.runtime.v1.GetJobResponse\"\x00\x12\x66\n\x0f\x44\x65leteJobAlpha1\x12\'.dapr.proto.runtime.v1.DeleteJobRequest\x1a(.dapr.proto.runtime.v1.DeleteJobResponse\"\x00\x42i\n\nio.dapr.v1B\nDaprProtosZ1github.com/dapr/dapr/pkg/proto/runtime/v1;runtime\xaa\x02\x1b\x44\x61pr.Client.Autogen.Grpc.v1b\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n dapr/proto/runtime/v1/dapr.proto\x12\x15\x64\x61pr.proto.runtime.v1\x1a\x19google/protobuf/any.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a!dapr/proto/common/v1/common.proto\x1a\'dapr/proto/runtime/v1/appcallback.proto\"X\n\x14InvokeServiceRequest\x12\n\n\x02id\x18\x01 \x01(\t\x12\x34\n\x07message\x18\x03 \x01(\x0b\x32#.dapr.proto.common.v1.InvokeRequest\"\xf5\x01\n\x0fGetStateRequest\x12\x12\n\nstore_name\x18\x01 \x01(\t\x12\x0b\n\x03key\x18\x02 \x01(\t\x12H\n\x0b\x63onsistency\x18\x03 \x01(\x0e\x32\x33.dapr.proto.common.v1.StateOptions.StateConsistency\x12\x46\n\x08metadata\x18\x04 \x03(\x0b\x32\x34.dapr.proto.runtime.v1.GetStateRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xc9\x01\n\x13GetBulkStateRequest\x12\x12\n\nstore_name\x18\x01 \x01(\t\x12\x0c\n\x04keys\x18\x02 \x03(\t\x12\x13\n\x0bparallelism\x18\x03 \x01(\x05\x12J\n\x08metadata\x18\x04 \x03(\x0b\x32\x38.dapr.proto.runtime.v1.GetBulkStateRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"K\n\x14GetBulkStateResponse\x12\x33\n\x05items\x18\x01 \x03(\x0b\x32$.dapr.proto.runtime.v1.BulkStateItem\"\xbe\x01\n\rBulkStateItem\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\x12\x0c\n\x04\x65tag\x18\x03 \x01(\t\x12\r\n\x05\x65rror\x18\x04 \x01(\t\x12\x44\n\x08metadata\x18\x05 \x03(\x0b\x32\x32.dapr.proto.runtime.v1.BulkStateItem.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xa8\x01\n\x10GetStateResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\x12\x0c\n\x04\x65tag\x18\x02 \x01(\t\x12G\n\x08metadata\x18\x03 \x03(\x0b\x32\x35.dapr.proto.runtime.v1.GetStateResponse.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x90\x02\n\x12\x44\x65leteStateRequest\x12\x12\n\nstore_name\x18\x01 \x01(\t\x12\x0b\n\x03key\x18\x02 \x01(\t\x12(\n\x04\x65tag\x18\x03 \x01(\x0b\x32\x1a.dapr.proto.common.v1.Etag\x12\x33\n\x07options\x18\x04 \x01(\x0b\x32\".dapr.proto.common.v1.StateOptions\x12I\n\x08metadata\x18\x05 \x03(\x0b\x32\x37.dapr.proto.runtime.v1.DeleteStateRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"]\n\x16\x44\x65leteBulkStateRequest\x12\x12\n\nstore_name\x18\x01 \x01(\t\x12/\n\x06states\x18\x02 \x03(\x0b\x32\x1f.dapr.proto.common.v1.StateItem\"W\n\x10SaveStateRequest\x12\x12\n\nstore_name\x18\x01 \x01(\t\x12/\n\x06states\x18\x02 \x03(\x0b\x32\x1f.dapr.proto.common.v1.StateItem\"\xbc\x01\n\x11QueryStateRequest\x12\x1d\n\nstore_name\x18\x01 \x01(\tR\tstoreName\x12\r\n\x05query\x18\x02 \x01(\t\x12H\n\x08metadata\x18\x03 \x03(\x0b\x32\x36.dapr.proto.runtime.v1.QueryStateRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"H\n\x0eQueryStateItem\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\x12\x0c\n\x04\x65tag\x18\x03 \x01(\t\x12\r\n\x05\x65rror\x18\x04 \x01(\t\"\xd7\x01\n\x12QueryStateResponse\x12\x36\n\x07results\x18\x01 \x03(\x0b\x32%.dapr.proto.runtime.v1.QueryStateItem\x12\r\n\x05token\x18\x02 \x01(\t\x12I\n\x08metadata\x18\x03 \x03(\x0b\x32\x37.dapr.proto.runtime.v1.QueryStateResponse.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xdf\x01\n\x13PublishEventRequest\x12\x13\n\x0bpubsub_name\x18\x01 \x01(\t\x12\r\n\x05topic\x18\x02 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x03 \x01(\x0c\x12\x19\n\x11\x64\x61ta_content_type\x18\x04 \x01(\t\x12J\n\x08metadata\x18\x05 \x03(\x0b\x32\x38.dapr.proto.runtime.v1.PublishEventRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xf5\x01\n\x12\x42ulkPublishRequest\x12\x13\n\x0bpubsub_name\x18\x01 \x01(\t\x12\r\n\x05topic\x18\x02 \x01(\t\x12?\n\x07\x65ntries\x18\x03 \x03(\x0b\x32..dapr.proto.runtime.v1.BulkPublishRequestEntry\x12I\n\x08metadata\x18\x04 \x03(\x0b\x32\x37.dapr.proto.runtime.v1.BulkPublishRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xd1\x01\n\x17\x42ulkPublishRequestEntry\x12\x10\n\x08\x65ntry_id\x18\x01 \x01(\t\x12\r\n\x05\x65vent\x18\x02 \x01(\x0c\x12\x14\n\x0c\x63ontent_type\x18\x03 \x01(\t\x12N\n\x08metadata\x18\x04 \x03(\x0b\x32<.dapr.proto.runtime.v1.BulkPublishRequestEntry.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"c\n\x13\x42ulkPublishResponse\x12L\n\rfailedEntries\x18\x01 \x03(\x0b\x32\x35.dapr.proto.runtime.v1.BulkPublishResponseFailedEntry\"A\n\x1e\x42ulkPublishResponseFailedEntry\x12\x10\n\x08\x65ntry_id\x18\x01 \x01(\t\x12\r\n\x05\x65rror\x18\x02 \x01(\t\"\x84\x02\n!SubscribeTopicEventsRequestAlpha1\x12Z\n\x0finitial_request\x18\x01 \x01(\x0b\x32?.dapr.proto.runtime.v1.SubscribeTopicEventsRequestInitialAlpha1H\x00\x12\\\n\x0f\x65vent_processed\x18\x02 \x01(\x0b\x32\x41.dapr.proto.runtime.v1.SubscribeTopicEventsRequestProcessedAlpha1H\x00\x42%\n#subscribe_topic_events_request_type\"\x96\x02\n(SubscribeTopicEventsRequestInitialAlpha1\x12\x13\n\x0bpubsub_name\x18\x01 \x01(\t\x12\r\n\x05topic\x18\x02 \x01(\t\x12_\n\x08metadata\x18\x03 \x03(\x0b\x32M.dapr.proto.runtime.v1.SubscribeTopicEventsRequestInitialAlpha1.MetadataEntry\x12\x1e\n\x11\x64\x65\x61\x64_letter_topic\x18\x04 \x01(\tH\x00\x88\x01\x01\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42\x14\n\x12_dead_letter_topic\"s\n*SubscribeTopicEventsRequestProcessedAlpha1\x12\n\n\x02id\x18\x01 \x01(\t\x12\x39\n\x06status\x18\x02 \x01(\x0b\x32).dapr.proto.runtime.v1.TopicEventResponse\"\xed\x01\n\"SubscribeTopicEventsResponseAlpha1\x12\\\n\x10initial_response\x18\x01 \x01(\x0b\x32@.dapr.proto.runtime.v1.SubscribeTopicEventsResponseInitialAlpha1H\x00\x12\x41\n\revent_message\x18\x02 \x01(\x0b\x32(.dapr.proto.runtime.v1.TopicEventRequestH\x00\x42&\n$subscribe_topic_events_response_type\"+\n)SubscribeTopicEventsResponseInitialAlpha1\"\xc3\x01\n\x14InvokeBindingRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\x12K\n\x08metadata\x18\x03 \x03(\x0b\x32\x39.dapr.proto.runtime.v1.InvokeBindingRequest.MetadataEntry\x12\x11\n\toperation\x18\x04 \x01(\t\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xa4\x01\n\x15InvokeBindingResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\x12L\n\x08metadata\x18\x02 \x03(\x0b\x32:.dapr.proto.runtime.v1.InvokeBindingResponse.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xb8\x01\n\x10GetSecretRequest\x12\x1d\n\nstore_name\x18\x01 \x01(\tR\tstoreName\x12\x0b\n\x03key\x18\x02 \x01(\t\x12G\n\x08metadata\x18\x03 \x03(\x0b\x32\x35.dapr.proto.runtime.v1.GetSecretRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x82\x01\n\x11GetSecretResponse\x12@\n\x04\x64\x61ta\x18\x01 \x03(\x0b\x32\x32.dapr.proto.runtime.v1.GetSecretResponse.DataEntry\x1a+\n\tDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xb3\x01\n\x14GetBulkSecretRequest\x12\x1d\n\nstore_name\x18\x01 \x01(\tR\tstoreName\x12K\n\x08metadata\x18\x02 \x03(\x0b\x32\x39.dapr.proto.runtime.v1.GetBulkSecretRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x85\x01\n\x0eSecretResponse\x12\x43\n\x07secrets\x18\x01 \x03(\x0b\x32\x32.dapr.proto.runtime.v1.SecretResponse.SecretsEntry\x1a.\n\x0cSecretsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xb1\x01\n\x15GetBulkSecretResponse\x12\x44\n\x04\x64\x61ta\x18\x01 \x03(\x0b\x32\x36.dapr.proto.runtime.v1.GetBulkSecretResponse.DataEntry\x1aR\n\tDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x34\n\x05value\x18\x02 \x01(\x0b\x32%.dapr.proto.runtime.v1.SecretResponse:\x02\x38\x01\"f\n\x1bTransactionalStateOperation\x12\x15\n\roperationType\x18\x01 \x01(\t\x12\x30\n\x07request\x18\x02 \x01(\x0b\x32\x1f.dapr.proto.common.v1.StateItem\"\x83\x02\n\x1e\x45xecuteStateTransactionRequest\x12\x11\n\tstoreName\x18\x01 \x01(\t\x12\x46\n\noperations\x18\x02 \x03(\x0b\x32\x32.dapr.proto.runtime.v1.TransactionalStateOperation\x12U\n\x08metadata\x18\x03 \x03(\x0b\x32\x43.dapr.proto.runtime.v1.ExecuteStateTransactionRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xbb\x01\n\x19RegisterActorTimerRequest\x12\x1d\n\nactor_type\x18\x01 \x01(\tR\tactorType\x12\x19\n\x08\x61\x63tor_id\x18\x02 \x01(\tR\x07\x61\x63torId\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x19\n\x08\x64ue_time\x18\x04 \x01(\tR\x07\x64ueTime\x12\x0e\n\x06period\x18\x05 \x01(\t\x12\x10\n\x08\x63\x61llback\x18\x06 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x07 \x01(\x0c\x12\x0b\n\x03ttl\x18\x08 \x01(\t\"e\n\x1bUnregisterActorTimerRequest\x12\x1d\n\nactor_type\x18\x01 \x01(\tR\tactorType\x12\x19\n\x08\x61\x63tor_id\x18\x02 \x01(\tR\x07\x61\x63torId\x12\x0c\n\x04name\x18\x03 \x01(\t\"\xac\x01\n\x1cRegisterActorReminderRequest\x12\x1d\n\nactor_type\x18\x01 \x01(\tR\tactorType\x12\x19\n\x08\x61\x63tor_id\x18\x02 \x01(\tR\x07\x61\x63torId\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x19\n\x08\x64ue_time\x18\x04 \x01(\tR\x07\x64ueTime\x12\x0e\n\x06period\x18\x05 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x06 \x01(\x0c\x12\x0b\n\x03ttl\x18\x07 \x01(\t\"h\n\x1eUnregisterActorReminderRequest\x12\x1d\n\nactor_type\x18\x01 \x01(\tR\tactorType\x12\x19\n\x08\x61\x63tor_id\x18\x02 \x01(\tR\x07\x61\x63torId\x12\x0c\n\x04name\x18\x03 \x01(\t\"]\n\x14GetActorStateRequest\x12\x1d\n\nactor_type\x18\x01 \x01(\tR\tactorType\x12\x19\n\x08\x61\x63tor_id\x18\x02 \x01(\tR\x07\x61\x63torId\x12\x0b\n\x03key\x18\x03 \x01(\t\"\xa4\x01\n\x15GetActorStateResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\x12L\n\x08metadata\x18\x02 \x03(\x0b\x32:.dapr.proto.runtime.v1.GetActorStateResponse.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xac\x01\n#ExecuteActorStateTransactionRequest\x12\x1d\n\nactor_type\x18\x01 \x01(\tR\tactorType\x12\x19\n\x08\x61\x63tor_id\x18\x02 \x01(\tR\x07\x61\x63torId\x12K\n\noperations\x18\x03 \x03(\x0b\x32\x37.dapr.proto.runtime.v1.TransactionalActorStateOperation\"\xf5\x01\n TransactionalActorStateOperation\x12\x15\n\roperationType\x18\x01 \x01(\t\x12\x0b\n\x03key\x18\x02 \x01(\t\x12#\n\x05value\x18\x03 \x01(\x0b\x32\x14.google.protobuf.Any\x12W\n\x08metadata\x18\x04 \x03(\x0b\x32\x45.dapr.proto.runtime.v1.TransactionalActorStateOperation.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xe8\x01\n\x12InvokeActorRequest\x12\x1d\n\nactor_type\x18\x01 \x01(\tR\tactorType\x12\x19\n\x08\x61\x63tor_id\x18\x02 \x01(\tR\x07\x61\x63torId\x12\x0e\n\x06method\x18\x03 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x04 \x01(\x0c\x12I\n\x08metadata\x18\x05 \x03(\x0b\x32\x37.dapr.proto.runtime.v1.InvokeActorRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"#\n\x13InvokeActorResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"\x14\n\x12GetMetadataRequest\"\x9b\x06\n\x13GetMetadataResponse\x12\n\n\x02id\x18\x01 \x01(\t\x12Q\n\x13\x61\x63tive_actors_count\x18\x02 \x03(\x0b\x32(.dapr.proto.runtime.v1.ActiveActorsCountB\x02\x18\x01R\x06\x61\x63tors\x12V\n\x15registered_components\x18\x03 \x03(\x0b\x32+.dapr.proto.runtime.v1.RegisteredComponentsR\ncomponents\x12\x65\n\x11\x65xtended_metadata\x18\x04 \x03(\x0b\x32@.dapr.proto.runtime.v1.GetMetadataResponse.ExtendedMetadataEntryR\x08\x65xtended\x12O\n\rsubscriptions\x18\x05 \x03(\x0b\x32).dapr.proto.runtime.v1.PubsubSubscriptionR\rsubscriptions\x12R\n\x0ehttp_endpoints\x18\x06 \x03(\x0b\x32+.dapr.proto.runtime.v1.MetadataHTTPEndpointR\rhttpEndpoints\x12j\n\x19\x61pp_connection_properties\x18\x07 \x01(\x0b\x32..dapr.proto.runtime.v1.AppConnectionPropertiesR\x17\x61ppConnectionProperties\x12\'\n\x0fruntime_version\x18\x08 \x01(\tR\x0eruntimeVersion\x12)\n\x10\x65nabled_features\x18\t \x03(\tR\x0f\x65nabledFeatures\x12H\n\ractor_runtime\x18\n \x01(\x0b\x32#.dapr.proto.runtime.v1.ActorRuntimeR\x0c\x61\x63torRuntime\x1a\x37\n\x15\x45xtendedMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xbc\x02\n\x0c\x41\x63torRuntime\x12]\n\x0eruntime_status\x18\x01 \x01(\x0e\x32\x36.dapr.proto.runtime.v1.ActorRuntime.ActorRuntimeStatusR\rruntimeStatus\x12M\n\ractive_actors\x18\x02 \x03(\x0b\x32(.dapr.proto.runtime.v1.ActiveActorsCountR\x0c\x61\x63tiveActors\x12\x1d\n\nhost_ready\x18\x03 \x01(\x08R\thostReady\x12\x1c\n\tplacement\x18\x04 \x01(\tR\tplacement\"A\n\x12\x41\x63torRuntimeStatus\x12\x10\n\x0cINITIALIZING\x10\x00\x12\x0c\n\x08\x44ISABLED\x10\x01\x12\x0b\n\x07RUNNING\x10\x02\"0\n\x11\x41\x63tiveActorsCount\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\r\n\x05\x63ount\x18\x02 \x01(\x05\"Y\n\x14RegisteredComponents\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04type\x18\x02 \x01(\t\x12\x0f\n\x07version\x18\x03 \x01(\t\x12\x14\n\x0c\x63\x61pabilities\x18\x04 \x03(\t\"*\n\x14MetadataHTTPEndpoint\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\"\xd1\x01\n\x17\x41ppConnectionProperties\x12\x0c\n\x04port\x18\x01 \x01(\x05\x12\x10\n\x08protocol\x18\x02 \x01(\t\x12\'\n\x0f\x63hannel_address\x18\x03 \x01(\tR\x0e\x63hannelAddress\x12\'\n\x0fmax_concurrency\x18\x04 \x01(\x05R\x0emaxConcurrency\x12\x44\n\x06health\x18\x05 \x01(\x0b\x32\x34.dapr.proto.runtime.v1.AppConnectionHealthProperties\"\xdc\x01\n\x1d\x41ppConnectionHealthProperties\x12*\n\x11health_check_path\x18\x01 \x01(\tR\x0fhealthCheckPath\x12\x32\n\x15health_probe_interval\x18\x02 \x01(\tR\x13healthProbeInterval\x12\x30\n\x14health_probe_timeout\x18\x03 \x01(\tR\x12healthProbeTimeout\x12)\n\x10health_threshold\x18\x04 \x01(\x05R\x0fhealthThreshold\"\x86\x03\n\x12PubsubSubscription\x12\x1f\n\x0bpubsub_name\x18\x01 \x01(\tR\npubsubname\x12\x14\n\x05topic\x18\x02 \x01(\tR\x05topic\x12S\n\x08metadata\x18\x03 \x03(\x0b\x32\x37.dapr.proto.runtime.v1.PubsubSubscription.MetadataEntryR\x08metadata\x12\x44\n\x05rules\x18\x04 \x01(\x0b\x32..dapr.proto.runtime.v1.PubsubSubscriptionRulesR\x05rules\x12*\n\x11\x64\x65\x61\x64_letter_topic\x18\x05 \x01(\tR\x0f\x64\x65\x61\x64LetterTopic\x12\x41\n\x04type\x18\x06 \x01(\x0e\x32-.dapr.proto.runtime.v1.PubsubSubscriptionTypeR\x04type\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"W\n\x17PubsubSubscriptionRules\x12<\n\x05rules\x18\x01 \x03(\x0b\x32-.dapr.proto.runtime.v1.PubsubSubscriptionRule\"5\n\x16PubsubSubscriptionRule\x12\r\n\x05match\x18\x01 \x01(\t\x12\x0c\n\x04path\x18\x02 \x01(\t\"0\n\x12SetMetadataRequest\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\"\xbc\x01\n\x17GetConfigurationRequest\x12\x12\n\nstore_name\x18\x01 \x01(\t\x12\x0c\n\x04keys\x18\x02 \x03(\t\x12N\n\x08metadata\x18\x03 \x03(\x0b\x32<.dapr.proto.runtime.v1.GetConfigurationRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xbc\x01\n\x18GetConfigurationResponse\x12I\n\x05items\x18\x01 \x03(\x0b\x32:.dapr.proto.runtime.v1.GetConfigurationResponse.ItemsEntry\x1aU\n\nItemsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x36\n\x05value\x18\x02 \x01(\x0b\x32\'.dapr.proto.common.v1.ConfigurationItem:\x02\x38\x01\"\xc8\x01\n\x1dSubscribeConfigurationRequest\x12\x12\n\nstore_name\x18\x01 \x01(\t\x12\x0c\n\x04keys\x18\x02 \x03(\t\x12T\n\x08metadata\x18\x03 \x03(\x0b\x32\x42.dapr.proto.runtime.v1.SubscribeConfigurationRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"A\n\x1fUnsubscribeConfigurationRequest\x12\x12\n\nstore_name\x18\x01 \x01(\t\x12\n\n\x02id\x18\x02 \x01(\t\"\xd4\x01\n\x1eSubscribeConfigurationResponse\x12\n\n\x02id\x18\x01 \x01(\t\x12O\n\x05items\x18\x02 \x03(\x0b\x32@.dapr.proto.runtime.v1.SubscribeConfigurationResponse.ItemsEntry\x1aU\n\nItemsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x36\n\x05value\x18\x02 \x01(\x0b\x32\'.dapr.proto.common.v1.ConfigurationItem:\x02\x38\x01\"?\n UnsubscribeConfigurationResponse\x12\n\n\x02ok\x18\x01 \x01(\x08\x12\x0f\n\x07message\x18\x02 \x01(\t\"\x9b\x01\n\x0eTryLockRequest\x12\x1d\n\nstore_name\x18\x01 \x01(\tR\tstoreName\x12\x1f\n\x0bresource_id\x18\x02 \x01(\tR\nresourceId\x12\x1d\n\nlock_owner\x18\x03 \x01(\tR\tlockOwner\x12*\n\x11\x65xpiry_in_seconds\x18\x04 \x01(\x05R\x0f\x65xpiryInSeconds\"\"\n\x0fTryLockResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\"n\n\rUnlockRequest\x12\x1d\n\nstore_name\x18\x01 \x01(\tR\tstoreName\x12\x1f\n\x0bresource_id\x18\x02 \x01(\tR\nresourceId\x12\x1d\n\nlock_owner\x18\x03 \x01(\tR\tlockOwner\"\xae\x01\n\x0eUnlockResponse\x12<\n\x06status\x18\x01 \x01(\x0e\x32,.dapr.proto.runtime.v1.UnlockResponse.Status\"^\n\x06Status\x12\x0b\n\x07SUCCESS\x10\x00\x12\x17\n\x13LOCK_DOES_NOT_EXIST\x10\x01\x12\x1a\n\x16LOCK_BELONGS_TO_OTHERS\x10\x02\x12\x12\n\x0eINTERNAL_ERROR\x10\x03\"\xb0\x01\n\x13SubtleGetKeyRequest\x12%\n\x0e\x63omponent_name\x18\x01 \x01(\tR\rcomponentName\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x44\n\x06\x66ormat\x18\x03 \x01(\x0e\x32\x34.dapr.proto.runtime.v1.SubtleGetKeyRequest.KeyFormat\"\x1e\n\tKeyFormat\x12\x07\n\x03PEM\x10\x00\x12\x08\n\x04JSON\x10\x01\"C\n\x14SubtleGetKeyResponse\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x1d\n\npublic_key\x18\x02 \x01(\tR\tpublicKey\"\xb6\x01\n\x14SubtleEncryptRequest\x12%\n\x0e\x63omponent_name\x18\x01 \x01(\tR\rcomponentName\x12\x11\n\tplaintext\x18\x02 \x01(\x0c\x12\x11\n\talgorithm\x18\x03 \x01(\t\x12\x19\n\x08key_name\x18\x04 \x01(\tR\x07keyName\x12\r\n\x05nonce\x18\x05 \x01(\x0c\x12\'\n\x0f\x61ssociated_data\x18\x06 \x01(\x0cR\x0e\x61ssociatedData\"8\n\x15SubtleEncryptResponse\x12\x12\n\nciphertext\x18\x01 \x01(\x0c\x12\x0b\n\x03tag\x18\x02 \x01(\x0c\"\xc4\x01\n\x14SubtleDecryptRequest\x12%\n\x0e\x63omponent_name\x18\x01 \x01(\tR\rcomponentName\x12\x12\n\nciphertext\x18\x02 \x01(\x0c\x12\x11\n\talgorithm\x18\x03 \x01(\t\x12\x19\n\x08key_name\x18\x04 \x01(\tR\x07keyName\x12\r\n\x05nonce\x18\x05 \x01(\x0c\x12\x0b\n\x03tag\x18\x06 \x01(\x0c\x12\'\n\x0f\x61ssociated_data\x18\x07 \x01(\x0cR\x0e\x61ssociatedData\"*\n\x15SubtleDecryptResponse\x12\x11\n\tplaintext\x18\x01 \x01(\x0c\"\xc8\x01\n\x14SubtleWrapKeyRequest\x12%\n\x0e\x63omponent_name\x18\x01 \x01(\tR\rcomponentName\x12#\n\rplaintext_key\x18\x02 \x01(\x0cR\x0cplaintextKey\x12\x11\n\talgorithm\x18\x03 \x01(\t\x12\x19\n\x08key_name\x18\x04 \x01(\tR\x07keyName\x12\r\n\x05nonce\x18\x05 \x01(\x0c\x12\'\n\x0f\x61ssociated_data\x18\x06 \x01(\x0cR\x0e\x61ssociatedData\"E\n\x15SubtleWrapKeyResponse\x12\x1f\n\x0bwrapped_key\x18\x01 \x01(\x0cR\nwrappedKey\x12\x0b\n\x03tag\x18\x02 \x01(\x0c\"\xd3\x01\n\x16SubtleUnwrapKeyRequest\x12%\n\x0e\x63omponent_name\x18\x01 \x01(\tR\rcomponentName\x12\x1f\n\x0bwrapped_key\x18\x02 \x01(\x0cR\nwrappedKey\x12\x11\n\talgorithm\x18\x03 \x01(\t\x12\x19\n\x08key_name\x18\x04 \x01(\tR\x07keyName\x12\r\n\x05nonce\x18\x05 \x01(\x0c\x12\x0b\n\x03tag\x18\x06 \x01(\x0c\x12\'\n\x0f\x61ssociated_data\x18\x07 \x01(\x0cR\x0e\x61ssociatedData\">\n\x17SubtleUnwrapKeyResponse\x12#\n\rplaintext_key\x18\x01 \x01(\x0cR\x0cplaintextKey\"x\n\x11SubtleSignRequest\x12%\n\x0e\x63omponent_name\x18\x01 \x01(\tR\rcomponentName\x12\x0e\n\x06\x64igest\x18\x02 \x01(\x0c\x12\x11\n\talgorithm\x18\x03 \x01(\t\x12\x19\n\x08key_name\x18\x04 \x01(\tR\x07keyName\"\'\n\x12SubtleSignResponse\x12\x11\n\tsignature\x18\x01 \x01(\x0c\"\x8d\x01\n\x13SubtleVerifyRequest\x12%\n\x0e\x63omponent_name\x18\x01 \x01(\tR\rcomponentName\x12\x0e\n\x06\x64igest\x18\x02 \x01(\x0c\x12\x11\n\talgorithm\x18\x03 \x01(\t\x12\x19\n\x08key_name\x18\x04 \x01(\tR\x07keyName\x12\x11\n\tsignature\x18\x05 \x01(\x0c\"%\n\x14SubtleVerifyResponse\x12\r\n\x05valid\x18\x01 \x01(\x08\"\x85\x01\n\x0e\x45ncryptRequest\x12=\n\x07options\x18\x01 \x01(\x0b\x32,.dapr.proto.runtime.v1.EncryptRequestOptions\x12\x34\n\x07payload\x18\x02 \x01(\x0b\x32#.dapr.proto.common.v1.StreamPayload\"\xfe\x01\n\x15\x45ncryptRequestOptions\x12%\n\x0e\x63omponent_name\x18\x01 \x01(\tR\rcomponentName\x12\x19\n\x08key_name\x18\x02 \x01(\tR\x07keyName\x12\x1a\n\x12key_wrap_algorithm\x18\x03 \x01(\t\x12\x1e\n\x16\x64\x61ta_encryption_cipher\x18\n \x01(\t\x12\x37\n\x18omit_decryption_key_name\x18\x0b \x01(\x08R\x15omitDecryptionKeyName\x12.\n\x13\x64\x65\x63ryption_key_name\x18\x0c \x01(\tR\x11\x64\x65\x63ryptionKeyName\"G\n\x0f\x45ncryptResponse\x12\x34\n\x07payload\x18\x01 \x01(\x0b\x32#.dapr.proto.common.v1.StreamPayload\"\x85\x01\n\x0e\x44\x65\x63ryptRequest\x12=\n\x07options\x18\x01 \x01(\x0b\x32,.dapr.proto.runtime.v1.DecryptRequestOptions\x12\x34\n\x07payload\x18\x02 \x01(\x0b\x32#.dapr.proto.common.v1.StreamPayload\"Y\n\x15\x44\x65\x63ryptRequestOptions\x12%\n\x0e\x63omponent_name\x18\x01 \x01(\tR\rcomponentName\x12\x19\n\x08key_name\x18\x0c \x01(\tR\x07keyName\"G\n\x0f\x44\x65\x63ryptResponse\x12\x34\n\x07payload\x18\x01 \x01(\x0b\x32#.dapr.proto.common.v1.StreamPayload\"d\n\x12GetWorkflowRequest\x12\x1f\n\x0binstance_id\x18\x01 \x01(\tR\ninstanceID\x12-\n\x12workflow_component\x18\x02 \x01(\tR\x11workflowComponent\"\x84\x03\n\x13GetWorkflowResponse\x12\x1f\n\x0binstance_id\x18\x01 \x01(\tR\ninstanceID\x12#\n\rworkflow_name\x18\x02 \x01(\tR\x0cworkflowName\x12\x39\n\ncreated_at\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\tcreatedAt\x12\x42\n\x0flast_updated_at\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\rlastUpdatedAt\x12%\n\x0eruntime_status\x18\x05 \x01(\tR\rruntimeStatus\x12N\n\nproperties\x18\x06 \x03(\x0b\x32:.dapr.proto.runtime.v1.GetWorkflowResponse.PropertiesEntry\x1a\x31\n\x0fPropertiesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x95\x02\n\x14StartWorkflowRequest\x12\x1f\n\x0binstance_id\x18\x01 \x01(\tR\ninstanceID\x12-\n\x12workflow_component\x18\x02 \x01(\tR\x11workflowComponent\x12#\n\rworkflow_name\x18\x03 \x01(\tR\x0cworkflowName\x12I\n\x07options\x18\x04 \x03(\x0b\x32\x38.dapr.proto.runtime.v1.StartWorkflowRequest.OptionsEntry\x12\r\n\x05input\x18\x05 \x01(\x0c\x1a.\n\x0cOptionsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"8\n\x15StartWorkflowResponse\x12\x1f\n\x0binstance_id\x18\x01 \x01(\tR\ninstanceID\"j\n\x18TerminateWorkflowRequest\x12\x1f\n\x0binstance_id\x18\x01 \x01(\tR\ninstanceID\x12-\n\x12workflow_component\x18\x02 \x01(\tR\x11workflowComponent\"f\n\x14PauseWorkflowRequest\x12\x1f\n\x0binstance_id\x18\x01 \x01(\tR\ninstanceID\x12-\n\x12workflow_component\x18\x02 \x01(\tR\x11workflowComponent\"g\n\x15ResumeWorkflowRequest\x12\x1f\n\x0binstance_id\x18\x01 \x01(\tR\ninstanceID\x12-\n\x12workflow_component\x18\x02 \x01(\tR\x11workflowComponent\"\x9e\x01\n\x19RaiseEventWorkflowRequest\x12\x1f\n\x0binstance_id\x18\x01 \x01(\tR\ninstanceID\x12-\n\x12workflow_component\x18\x02 \x01(\tR\x11workflowComponent\x12\x1d\n\nevent_name\x18\x03 \x01(\tR\teventName\x12\x12\n\nevent_data\x18\x04 \x01(\x0c\"f\n\x14PurgeWorkflowRequest\x12\x1f\n\x0binstance_id\x18\x01 \x01(\tR\ninstanceID\x12-\n\x12workflow_component\x18\x02 \x01(\tR\x11workflowComponent\"\x11\n\x0fShutdownRequest\"\xe8\x01\n\x03Job\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12\x1f\n\x08schedule\x18\x02 \x01(\tH\x00R\x08schedule\x88\x01\x01\x12\x1d\n\x07repeats\x18\x03 \x01(\rH\x01R\x07repeats\x88\x01\x01\x12\x1e\n\x08\x64ue_time\x18\x04 \x01(\tH\x02R\x07\x64ueTime\x88\x01\x01\x12\x15\n\x03ttl\x18\x05 \x01(\tH\x03R\x03ttl\x88\x01\x01\x12(\n\x04\x64\x61ta\x18\x06 \x01(\x0b\x32\x14.google.protobuf.AnyR\x04\x64\x61taB\x0b\n\t_scheduleB\n\n\x08_repeatsB\x0b\n\t_due_timeB\x06\n\x04_ttl\"=\n\x12ScheduleJobRequest\x12\'\n\x03job\x18\x01 \x01(\x0b\x32\x1a.dapr.proto.runtime.v1.Job\"\x15\n\x13ScheduleJobResponse\"\x1d\n\rGetJobRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\"9\n\x0eGetJobResponse\x12\'\n\x03job\x18\x01 \x01(\x0b\x32\x1a.dapr.proto.runtime.v1.Job\" \n\x10\x44\x65leteJobRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\"\x13\n\x11\x44\x65leteJobResponse*W\n\x16PubsubSubscriptionType\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x0f\n\x0b\x44\x45\x43LARATIVE\x10\x01\x12\x10\n\x0cPROGRAMMATIC\x10\x02\x12\r\n\tSTREAMING\x10\x03\x32\xbc\x30\n\x04\x44\x61pr\x12\x64\n\rInvokeService\x12+.dapr.proto.runtime.v1.InvokeServiceRequest\x1a$.dapr.proto.common.v1.InvokeResponse\"\x00\x12]\n\x08GetState\x12&.dapr.proto.runtime.v1.GetStateRequest\x1a\'.dapr.proto.runtime.v1.GetStateResponse\"\x00\x12i\n\x0cGetBulkState\x12*.dapr.proto.runtime.v1.GetBulkStateRequest\x1a+.dapr.proto.runtime.v1.GetBulkStateResponse\"\x00\x12N\n\tSaveState\x12\'.dapr.proto.runtime.v1.SaveStateRequest\x1a\x16.google.protobuf.Empty\"\x00\x12i\n\x10QueryStateAlpha1\x12(.dapr.proto.runtime.v1.QueryStateRequest\x1a).dapr.proto.runtime.v1.QueryStateResponse\"\x00\x12R\n\x0b\x44\x65leteState\x12).dapr.proto.runtime.v1.DeleteStateRequest\x1a\x16.google.protobuf.Empty\"\x00\x12Z\n\x0f\x44\x65leteBulkState\x12-.dapr.proto.runtime.v1.DeleteBulkStateRequest\x1a\x16.google.protobuf.Empty\"\x00\x12j\n\x17\x45xecuteStateTransaction\x12\x35.dapr.proto.runtime.v1.ExecuteStateTransactionRequest\x1a\x16.google.protobuf.Empty\"\x00\x12T\n\x0cPublishEvent\x12*.dapr.proto.runtime.v1.PublishEventRequest\x1a\x16.google.protobuf.Empty\"\x00\x12q\n\x16\x42ulkPublishEventAlpha1\x12).dapr.proto.runtime.v1.BulkPublishRequest\x1a*.dapr.proto.runtime.v1.BulkPublishResponse\"\x00\x12\x97\x01\n\x1aSubscribeTopicEventsAlpha1\x12\x38.dapr.proto.runtime.v1.SubscribeTopicEventsRequestAlpha1\x1a\x39.dapr.proto.runtime.v1.SubscribeTopicEventsResponseAlpha1\"\x00(\x01\x30\x01\x12l\n\rInvokeBinding\x12+.dapr.proto.runtime.v1.InvokeBindingRequest\x1a,.dapr.proto.runtime.v1.InvokeBindingResponse\"\x00\x12`\n\tGetSecret\x12\'.dapr.proto.runtime.v1.GetSecretRequest\x1a(.dapr.proto.runtime.v1.GetSecretResponse\"\x00\x12l\n\rGetBulkSecret\x12+.dapr.proto.runtime.v1.GetBulkSecretRequest\x1a,.dapr.proto.runtime.v1.GetBulkSecretResponse\"\x00\x12`\n\x12RegisterActorTimer\x12\x30.dapr.proto.runtime.v1.RegisterActorTimerRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x64\n\x14UnregisterActorTimer\x12\x32.dapr.proto.runtime.v1.UnregisterActorTimerRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x66\n\x15RegisterActorReminder\x12\x33.dapr.proto.runtime.v1.RegisterActorReminderRequest\x1a\x16.google.protobuf.Empty\"\x00\x12j\n\x17UnregisterActorReminder\x12\x35.dapr.proto.runtime.v1.UnregisterActorReminderRequest\x1a\x16.google.protobuf.Empty\"\x00\x12l\n\rGetActorState\x12+.dapr.proto.runtime.v1.GetActorStateRequest\x1a,.dapr.proto.runtime.v1.GetActorStateResponse\"\x00\x12t\n\x1c\x45xecuteActorStateTransaction\x12:.dapr.proto.runtime.v1.ExecuteActorStateTransactionRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x66\n\x0bInvokeActor\x12).dapr.proto.runtime.v1.InvokeActorRequest\x1a*.dapr.proto.runtime.v1.InvokeActorResponse\"\x00\x12{\n\x16GetConfigurationAlpha1\x12..dapr.proto.runtime.v1.GetConfigurationRequest\x1a/.dapr.proto.runtime.v1.GetConfigurationResponse\"\x00\x12u\n\x10GetConfiguration\x12..dapr.proto.runtime.v1.GetConfigurationRequest\x1a/.dapr.proto.runtime.v1.GetConfigurationResponse\"\x00\x12\x8f\x01\n\x1cSubscribeConfigurationAlpha1\x12\x34.dapr.proto.runtime.v1.SubscribeConfigurationRequest\x1a\x35.dapr.proto.runtime.v1.SubscribeConfigurationResponse\"\x00\x30\x01\x12\x89\x01\n\x16SubscribeConfiguration\x12\x34.dapr.proto.runtime.v1.SubscribeConfigurationRequest\x1a\x35.dapr.proto.runtime.v1.SubscribeConfigurationResponse\"\x00\x30\x01\x12\x93\x01\n\x1eUnsubscribeConfigurationAlpha1\x12\x36.dapr.proto.runtime.v1.UnsubscribeConfigurationRequest\x1a\x37.dapr.proto.runtime.v1.UnsubscribeConfigurationResponse\"\x00\x12\x8d\x01\n\x18UnsubscribeConfiguration\x12\x36.dapr.proto.runtime.v1.UnsubscribeConfigurationRequest\x1a\x37.dapr.proto.runtime.v1.UnsubscribeConfigurationResponse\"\x00\x12`\n\rTryLockAlpha1\x12%.dapr.proto.runtime.v1.TryLockRequest\x1a&.dapr.proto.runtime.v1.TryLockResponse\"\x00\x12]\n\x0cUnlockAlpha1\x12$.dapr.proto.runtime.v1.UnlockRequest\x1a%.dapr.proto.runtime.v1.UnlockResponse\"\x00\x12\x62\n\rEncryptAlpha1\x12%.dapr.proto.runtime.v1.EncryptRequest\x1a&.dapr.proto.runtime.v1.EncryptResponse(\x01\x30\x01\x12\x62\n\rDecryptAlpha1\x12%.dapr.proto.runtime.v1.DecryptRequest\x1a&.dapr.proto.runtime.v1.DecryptResponse(\x01\x30\x01\x12\x66\n\x0bGetMetadata\x12).dapr.proto.runtime.v1.GetMetadataRequest\x1a*.dapr.proto.runtime.v1.GetMetadataResponse\"\x00\x12R\n\x0bSetMetadata\x12).dapr.proto.runtime.v1.SetMetadataRequest\x1a\x16.google.protobuf.Empty\"\x00\x12m\n\x12SubtleGetKeyAlpha1\x12*.dapr.proto.runtime.v1.SubtleGetKeyRequest\x1a+.dapr.proto.runtime.v1.SubtleGetKeyResponse\x12p\n\x13SubtleEncryptAlpha1\x12+.dapr.proto.runtime.v1.SubtleEncryptRequest\x1a,.dapr.proto.runtime.v1.SubtleEncryptResponse\x12p\n\x13SubtleDecryptAlpha1\x12+.dapr.proto.runtime.v1.SubtleDecryptRequest\x1a,.dapr.proto.runtime.v1.SubtleDecryptResponse\x12p\n\x13SubtleWrapKeyAlpha1\x12+.dapr.proto.runtime.v1.SubtleWrapKeyRequest\x1a,.dapr.proto.runtime.v1.SubtleWrapKeyResponse\x12v\n\x15SubtleUnwrapKeyAlpha1\x12-.dapr.proto.runtime.v1.SubtleUnwrapKeyRequest\x1a..dapr.proto.runtime.v1.SubtleUnwrapKeyResponse\x12g\n\x10SubtleSignAlpha1\x12(.dapr.proto.runtime.v1.SubtleSignRequest\x1a).dapr.proto.runtime.v1.SubtleSignResponse\x12m\n\x12SubtleVerifyAlpha1\x12*.dapr.proto.runtime.v1.SubtleVerifyRequest\x1a+.dapr.proto.runtime.v1.SubtleVerifyResponse\x12r\n\x13StartWorkflowAlpha1\x12+.dapr.proto.runtime.v1.StartWorkflowRequest\x1a,.dapr.proto.runtime.v1.StartWorkflowResponse\"\x00\x12l\n\x11GetWorkflowAlpha1\x12).dapr.proto.runtime.v1.GetWorkflowRequest\x1a*.dapr.proto.runtime.v1.GetWorkflowResponse\"\x00\x12\\\n\x13PurgeWorkflowAlpha1\x12+.dapr.proto.runtime.v1.PurgeWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x64\n\x17TerminateWorkflowAlpha1\x12/.dapr.proto.runtime.v1.TerminateWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\\\n\x13PauseWorkflowAlpha1\x12+.dapr.proto.runtime.v1.PauseWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x00\x12^\n\x14ResumeWorkflowAlpha1\x12,.dapr.proto.runtime.v1.ResumeWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x66\n\x18RaiseEventWorkflowAlpha1\x12\x30.dapr.proto.runtime.v1.RaiseEventWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x00\x12q\n\x12StartWorkflowBeta1\x12+.dapr.proto.runtime.v1.StartWorkflowRequest\x1a,.dapr.proto.runtime.v1.StartWorkflowResponse\"\x00\x12k\n\x10GetWorkflowBeta1\x12).dapr.proto.runtime.v1.GetWorkflowRequest\x1a*.dapr.proto.runtime.v1.GetWorkflowResponse\"\x00\x12[\n\x12PurgeWorkflowBeta1\x12+.dapr.proto.runtime.v1.PurgeWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x63\n\x16TerminateWorkflowBeta1\x12/.dapr.proto.runtime.v1.TerminateWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x00\x12[\n\x12PauseWorkflowBeta1\x12+.dapr.proto.runtime.v1.PauseWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x00\x12]\n\x13ResumeWorkflowBeta1\x12,.dapr.proto.runtime.v1.ResumeWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x65\n\x17RaiseEventWorkflowBeta1\x12\x30.dapr.proto.runtime.v1.RaiseEventWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x00\x12L\n\x08Shutdown\x12&.dapr.proto.runtime.v1.ShutdownRequest\x1a\x16.google.protobuf.Empty\"\x00\x12l\n\x11ScheduleJobAlpha1\x12).dapr.proto.runtime.v1.ScheduleJobRequest\x1a*.dapr.proto.runtime.v1.ScheduleJobResponse\"\x00\x12]\n\x0cGetJobAlpha1\x12$.dapr.proto.runtime.v1.GetJobRequest\x1a%.dapr.proto.runtime.v1.GetJobResponse\"\x00\x12\x66\n\x0f\x44\x65leteJobAlpha1\x12\'.dapr.proto.runtime.v1.DeleteJobRequest\x1a(.dapr.proto.runtime.v1.DeleteJobResponse\"\x00\x42i\n\nio.dapr.v1B\nDaprProtosZ1github.com/dapr/dapr/pkg/proto/runtime/v1;runtime\xaa\x02\x1b\x44\x61pr.Client.Autogen.Grpc.v1b\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -47,8 +57,8 @@ _globals['_BULKPUBLISHREQUEST_METADATAENTRY']._serialized_options = b'8\001' _globals['_BULKPUBLISHREQUESTENTRY_METADATAENTRY']._loaded_options = None _globals['_BULKPUBLISHREQUESTENTRY_METADATAENTRY']._serialized_options = b'8\001' - _globals['_SUBSCRIBETOPICEVENTSINITIALREQUESTALPHA1_METADATAENTRY']._loaded_options = None - _globals['_SUBSCRIBETOPICEVENTSINITIALREQUESTALPHA1_METADATAENTRY']._serialized_options = b'8\001' + _globals['_SUBSCRIBETOPICEVENTSREQUESTINITIALALPHA1_METADATAENTRY']._loaded_options = None + _globals['_SUBSCRIBETOPICEVENTSREQUESTINITIALALPHA1_METADATAENTRY']._serialized_options = b'8\001' _globals['_INVOKEBINDINGREQUEST_METADATAENTRY']._loaded_options = None _globals['_INVOKEBINDINGREQUEST_METADATAENTRY']._serialized_options = b'8\001' _globals['_INVOKEBINDINGRESPONSE_METADATAENTRY']._loaded_options = None @@ -89,8 +99,8 @@ _globals['_GETWORKFLOWRESPONSE_PROPERTIESENTRY']._serialized_options = b'8\001' _globals['_STARTWORKFLOWREQUEST_OPTIONSENTRY']._loaded_options = None _globals['_STARTWORKFLOWREQUEST_OPTIONSENTRY']._serialized_options = b'8\001' - _globals['_PUBSUBSUBSCRIPTIONTYPE']._serialized_start=14739 - _globals['_PUBSUBSUBSCRIPTIONTYPE']._serialized_end=14826 + _globals['_PUBSUBSUBSCRIPTIONTYPE']._serialized_start=15086 + _globals['_PUBSUBSUBSCRIPTIONTYPE']._serialized_end=15173 _globals['_INVOKESERVICEREQUEST']._serialized_start=224 _globals['_INVOKESERVICEREQUEST']._serialized_end=312 _globals['_GETSTATEREQUEST']._serialized_start=315 @@ -146,213 +156,217 @@ _globals['_BULKPUBLISHRESPONSEFAILEDENTRY']._serialized_start=2936 _globals['_BULKPUBLISHRESPONSEFAILEDENTRY']._serialized_end=3001 _globals['_SUBSCRIBETOPICEVENTSREQUESTALPHA1']._serialized_start=3004 - _globals['_SUBSCRIBETOPICEVENTSREQUESTALPHA1']._serialized_end=3255 - _globals['_SUBSCRIBETOPICEVENTSINITIALREQUESTALPHA1']._serialized_start=3258 - _globals['_SUBSCRIBETOPICEVENTSINITIALREQUESTALPHA1']._serialized_end=3536 - _globals['_SUBSCRIBETOPICEVENTSINITIALREQUESTALPHA1_METADATAENTRY']._serialized_start=513 - _globals['_SUBSCRIBETOPICEVENTSINITIALREQUESTALPHA1_METADATAENTRY']._serialized_end=560 - _globals['_SUBSCRIBETOPICEVENTSRESPONSEALPHA1']._serialized_start=3538 - _globals['_SUBSCRIBETOPICEVENTSRESPONSEALPHA1']._serialized_end=3645 - _globals['_INVOKEBINDINGREQUEST']._serialized_start=3648 - _globals['_INVOKEBINDINGREQUEST']._serialized_end=3843 + _globals['_SUBSCRIBETOPICEVENTSREQUESTALPHA1']._serialized_end=3264 + _globals['_SUBSCRIBETOPICEVENTSREQUESTINITIALALPHA1']._serialized_start=3267 + _globals['_SUBSCRIBETOPICEVENTSREQUESTINITIALALPHA1']._serialized_end=3545 + _globals['_SUBSCRIBETOPICEVENTSREQUESTINITIALALPHA1_METADATAENTRY']._serialized_start=513 + _globals['_SUBSCRIBETOPICEVENTSREQUESTINITIALALPHA1_METADATAENTRY']._serialized_end=560 + _globals['_SUBSCRIBETOPICEVENTSREQUESTPROCESSEDALPHA1']._serialized_start=3547 + _globals['_SUBSCRIBETOPICEVENTSREQUESTPROCESSEDALPHA1']._serialized_end=3662 + _globals['_SUBSCRIBETOPICEVENTSRESPONSEALPHA1']._serialized_start=3665 + _globals['_SUBSCRIBETOPICEVENTSRESPONSEALPHA1']._serialized_end=3902 + _globals['_SUBSCRIBETOPICEVENTSRESPONSEINITIALALPHA1']._serialized_start=3904 + _globals['_SUBSCRIBETOPICEVENTSRESPONSEINITIALALPHA1']._serialized_end=3947 + _globals['_INVOKEBINDINGREQUEST']._serialized_start=3950 + _globals['_INVOKEBINDINGREQUEST']._serialized_end=4145 _globals['_INVOKEBINDINGREQUEST_METADATAENTRY']._serialized_start=513 _globals['_INVOKEBINDINGREQUEST_METADATAENTRY']._serialized_end=560 - _globals['_INVOKEBINDINGRESPONSE']._serialized_start=3846 - _globals['_INVOKEBINDINGRESPONSE']._serialized_end=4010 + _globals['_INVOKEBINDINGRESPONSE']._serialized_start=4148 + _globals['_INVOKEBINDINGRESPONSE']._serialized_end=4312 _globals['_INVOKEBINDINGRESPONSE_METADATAENTRY']._serialized_start=513 _globals['_INVOKEBINDINGRESPONSE_METADATAENTRY']._serialized_end=560 - _globals['_GETSECRETREQUEST']._serialized_start=4013 - _globals['_GETSECRETREQUEST']._serialized_end=4197 + _globals['_GETSECRETREQUEST']._serialized_start=4315 + _globals['_GETSECRETREQUEST']._serialized_end=4499 _globals['_GETSECRETREQUEST_METADATAENTRY']._serialized_start=513 _globals['_GETSECRETREQUEST_METADATAENTRY']._serialized_end=560 - _globals['_GETSECRETRESPONSE']._serialized_start=4200 - _globals['_GETSECRETRESPONSE']._serialized_end=4330 - _globals['_GETSECRETRESPONSE_DATAENTRY']._serialized_start=4287 - _globals['_GETSECRETRESPONSE_DATAENTRY']._serialized_end=4330 - _globals['_GETBULKSECRETREQUEST']._serialized_start=4333 - _globals['_GETBULKSECRETREQUEST']._serialized_end=4512 + _globals['_GETSECRETRESPONSE']._serialized_start=4502 + _globals['_GETSECRETRESPONSE']._serialized_end=4632 + _globals['_GETSECRETRESPONSE_DATAENTRY']._serialized_start=4589 + _globals['_GETSECRETRESPONSE_DATAENTRY']._serialized_end=4632 + _globals['_GETBULKSECRETREQUEST']._serialized_start=4635 + _globals['_GETBULKSECRETREQUEST']._serialized_end=4814 _globals['_GETBULKSECRETREQUEST_METADATAENTRY']._serialized_start=513 _globals['_GETBULKSECRETREQUEST_METADATAENTRY']._serialized_end=560 - _globals['_SECRETRESPONSE']._serialized_start=4515 - _globals['_SECRETRESPONSE']._serialized_end=4648 - _globals['_SECRETRESPONSE_SECRETSENTRY']._serialized_start=4602 - _globals['_SECRETRESPONSE_SECRETSENTRY']._serialized_end=4648 - _globals['_GETBULKSECRETRESPONSE']._serialized_start=4651 - _globals['_GETBULKSECRETRESPONSE']._serialized_end=4828 - _globals['_GETBULKSECRETRESPONSE_DATAENTRY']._serialized_start=4746 - _globals['_GETBULKSECRETRESPONSE_DATAENTRY']._serialized_end=4828 - _globals['_TRANSACTIONALSTATEOPERATION']._serialized_start=4830 - _globals['_TRANSACTIONALSTATEOPERATION']._serialized_end=4932 - _globals['_EXECUTESTATETRANSACTIONREQUEST']._serialized_start=4935 - _globals['_EXECUTESTATETRANSACTIONREQUEST']._serialized_end=5194 + _globals['_SECRETRESPONSE']._serialized_start=4817 + _globals['_SECRETRESPONSE']._serialized_end=4950 + _globals['_SECRETRESPONSE_SECRETSENTRY']._serialized_start=4904 + _globals['_SECRETRESPONSE_SECRETSENTRY']._serialized_end=4950 + _globals['_GETBULKSECRETRESPONSE']._serialized_start=4953 + _globals['_GETBULKSECRETRESPONSE']._serialized_end=5130 + _globals['_GETBULKSECRETRESPONSE_DATAENTRY']._serialized_start=5048 + _globals['_GETBULKSECRETRESPONSE_DATAENTRY']._serialized_end=5130 + _globals['_TRANSACTIONALSTATEOPERATION']._serialized_start=5132 + _globals['_TRANSACTIONALSTATEOPERATION']._serialized_end=5234 + _globals['_EXECUTESTATETRANSACTIONREQUEST']._serialized_start=5237 + _globals['_EXECUTESTATETRANSACTIONREQUEST']._serialized_end=5496 _globals['_EXECUTESTATETRANSACTIONREQUEST_METADATAENTRY']._serialized_start=513 _globals['_EXECUTESTATETRANSACTIONREQUEST_METADATAENTRY']._serialized_end=560 - _globals['_REGISTERACTORTIMERREQUEST']._serialized_start=5197 - _globals['_REGISTERACTORTIMERREQUEST']._serialized_end=5384 - _globals['_UNREGISTERACTORTIMERREQUEST']._serialized_start=5386 - _globals['_UNREGISTERACTORTIMERREQUEST']._serialized_end=5487 - _globals['_REGISTERACTORREMINDERREQUEST']._serialized_start=5490 - _globals['_REGISTERACTORREMINDERREQUEST']._serialized_end=5662 - _globals['_UNREGISTERACTORREMINDERREQUEST']._serialized_start=5664 - _globals['_UNREGISTERACTORREMINDERREQUEST']._serialized_end=5768 - _globals['_GETACTORSTATEREQUEST']._serialized_start=5770 - _globals['_GETACTORSTATEREQUEST']._serialized_end=5863 - _globals['_GETACTORSTATERESPONSE']._serialized_start=5866 - _globals['_GETACTORSTATERESPONSE']._serialized_end=6030 + _globals['_REGISTERACTORTIMERREQUEST']._serialized_start=5499 + _globals['_REGISTERACTORTIMERREQUEST']._serialized_end=5686 + _globals['_UNREGISTERACTORTIMERREQUEST']._serialized_start=5688 + _globals['_UNREGISTERACTORTIMERREQUEST']._serialized_end=5789 + _globals['_REGISTERACTORREMINDERREQUEST']._serialized_start=5792 + _globals['_REGISTERACTORREMINDERREQUEST']._serialized_end=5964 + _globals['_UNREGISTERACTORREMINDERREQUEST']._serialized_start=5966 + _globals['_UNREGISTERACTORREMINDERREQUEST']._serialized_end=6070 + _globals['_GETACTORSTATEREQUEST']._serialized_start=6072 + _globals['_GETACTORSTATEREQUEST']._serialized_end=6165 + _globals['_GETACTORSTATERESPONSE']._serialized_start=6168 + _globals['_GETACTORSTATERESPONSE']._serialized_end=6332 _globals['_GETACTORSTATERESPONSE_METADATAENTRY']._serialized_start=513 _globals['_GETACTORSTATERESPONSE_METADATAENTRY']._serialized_end=560 - _globals['_EXECUTEACTORSTATETRANSACTIONREQUEST']._serialized_start=6033 - _globals['_EXECUTEACTORSTATETRANSACTIONREQUEST']._serialized_end=6205 - _globals['_TRANSACTIONALACTORSTATEOPERATION']._serialized_start=6208 - _globals['_TRANSACTIONALACTORSTATEOPERATION']._serialized_end=6453 + _globals['_EXECUTEACTORSTATETRANSACTIONREQUEST']._serialized_start=6335 + _globals['_EXECUTEACTORSTATETRANSACTIONREQUEST']._serialized_end=6507 + _globals['_TRANSACTIONALACTORSTATEOPERATION']._serialized_start=6510 + _globals['_TRANSACTIONALACTORSTATEOPERATION']._serialized_end=6755 _globals['_TRANSACTIONALACTORSTATEOPERATION_METADATAENTRY']._serialized_start=513 _globals['_TRANSACTIONALACTORSTATEOPERATION_METADATAENTRY']._serialized_end=560 - _globals['_INVOKEACTORREQUEST']._serialized_start=6456 - _globals['_INVOKEACTORREQUEST']._serialized_end=6688 + _globals['_INVOKEACTORREQUEST']._serialized_start=6758 + _globals['_INVOKEACTORREQUEST']._serialized_end=6990 _globals['_INVOKEACTORREQUEST_METADATAENTRY']._serialized_start=513 _globals['_INVOKEACTORREQUEST_METADATAENTRY']._serialized_end=560 - _globals['_INVOKEACTORRESPONSE']._serialized_start=6690 - _globals['_INVOKEACTORRESPONSE']._serialized_end=6725 - _globals['_GETMETADATAREQUEST']._serialized_start=6727 - _globals['_GETMETADATAREQUEST']._serialized_end=6747 - _globals['_GETMETADATARESPONSE']._serialized_start=6750 - _globals['_GETMETADATARESPONSE']._serialized_end=7545 - _globals['_GETMETADATARESPONSE_EXTENDEDMETADATAENTRY']._serialized_start=7490 - _globals['_GETMETADATARESPONSE_EXTENDEDMETADATAENTRY']._serialized_end=7545 - _globals['_ACTORRUNTIME']._serialized_start=7548 - _globals['_ACTORRUNTIME']._serialized_end=7864 - _globals['_ACTORRUNTIME_ACTORRUNTIMESTATUS']._serialized_start=7799 - _globals['_ACTORRUNTIME_ACTORRUNTIMESTATUS']._serialized_end=7864 - _globals['_ACTIVEACTORSCOUNT']._serialized_start=7866 - _globals['_ACTIVEACTORSCOUNT']._serialized_end=7914 - _globals['_REGISTEREDCOMPONENTS']._serialized_start=7916 - _globals['_REGISTEREDCOMPONENTS']._serialized_end=8005 - _globals['_METADATAHTTPENDPOINT']._serialized_start=8007 - _globals['_METADATAHTTPENDPOINT']._serialized_end=8049 - _globals['_APPCONNECTIONPROPERTIES']._serialized_start=8052 - _globals['_APPCONNECTIONPROPERTIES']._serialized_end=8261 - _globals['_APPCONNECTIONHEALTHPROPERTIES']._serialized_start=8264 - _globals['_APPCONNECTIONHEALTHPROPERTIES']._serialized_end=8484 - _globals['_PUBSUBSUBSCRIPTION']._serialized_start=8487 - _globals['_PUBSUBSUBSCRIPTION']._serialized_end=8877 + _globals['_INVOKEACTORRESPONSE']._serialized_start=6992 + _globals['_INVOKEACTORRESPONSE']._serialized_end=7027 + _globals['_GETMETADATAREQUEST']._serialized_start=7029 + _globals['_GETMETADATAREQUEST']._serialized_end=7049 + _globals['_GETMETADATARESPONSE']._serialized_start=7052 + _globals['_GETMETADATARESPONSE']._serialized_end=7847 + _globals['_GETMETADATARESPONSE_EXTENDEDMETADATAENTRY']._serialized_start=7792 + _globals['_GETMETADATARESPONSE_EXTENDEDMETADATAENTRY']._serialized_end=7847 + _globals['_ACTORRUNTIME']._serialized_start=7850 + _globals['_ACTORRUNTIME']._serialized_end=8166 + _globals['_ACTORRUNTIME_ACTORRUNTIMESTATUS']._serialized_start=8101 + _globals['_ACTORRUNTIME_ACTORRUNTIMESTATUS']._serialized_end=8166 + _globals['_ACTIVEACTORSCOUNT']._serialized_start=8168 + _globals['_ACTIVEACTORSCOUNT']._serialized_end=8216 + _globals['_REGISTEREDCOMPONENTS']._serialized_start=8218 + _globals['_REGISTEREDCOMPONENTS']._serialized_end=8307 + _globals['_METADATAHTTPENDPOINT']._serialized_start=8309 + _globals['_METADATAHTTPENDPOINT']._serialized_end=8351 + _globals['_APPCONNECTIONPROPERTIES']._serialized_start=8354 + _globals['_APPCONNECTIONPROPERTIES']._serialized_end=8563 + _globals['_APPCONNECTIONHEALTHPROPERTIES']._serialized_start=8566 + _globals['_APPCONNECTIONHEALTHPROPERTIES']._serialized_end=8786 + _globals['_PUBSUBSUBSCRIPTION']._serialized_start=8789 + _globals['_PUBSUBSUBSCRIPTION']._serialized_end=9179 _globals['_PUBSUBSUBSCRIPTION_METADATAENTRY']._serialized_start=513 _globals['_PUBSUBSUBSCRIPTION_METADATAENTRY']._serialized_end=560 - _globals['_PUBSUBSUBSCRIPTIONRULES']._serialized_start=8879 - _globals['_PUBSUBSUBSCRIPTIONRULES']._serialized_end=8966 - _globals['_PUBSUBSUBSCRIPTIONRULE']._serialized_start=8968 - _globals['_PUBSUBSUBSCRIPTIONRULE']._serialized_end=9021 - _globals['_SETMETADATAREQUEST']._serialized_start=9023 - _globals['_SETMETADATAREQUEST']._serialized_end=9071 - _globals['_GETCONFIGURATIONREQUEST']._serialized_start=9074 - _globals['_GETCONFIGURATIONREQUEST']._serialized_end=9262 + _globals['_PUBSUBSUBSCRIPTIONRULES']._serialized_start=9181 + _globals['_PUBSUBSUBSCRIPTIONRULES']._serialized_end=9268 + _globals['_PUBSUBSUBSCRIPTIONRULE']._serialized_start=9270 + _globals['_PUBSUBSUBSCRIPTIONRULE']._serialized_end=9323 + _globals['_SETMETADATAREQUEST']._serialized_start=9325 + _globals['_SETMETADATAREQUEST']._serialized_end=9373 + _globals['_GETCONFIGURATIONREQUEST']._serialized_start=9376 + _globals['_GETCONFIGURATIONREQUEST']._serialized_end=9564 _globals['_GETCONFIGURATIONREQUEST_METADATAENTRY']._serialized_start=513 _globals['_GETCONFIGURATIONREQUEST_METADATAENTRY']._serialized_end=560 - _globals['_GETCONFIGURATIONRESPONSE']._serialized_start=9265 - _globals['_GETCONFIGURATIONRESPONSE']._serialized_end=9453 - _globals['_GETCONFIGURATIONRESPONSE_ITEMSENTRY']._serialized_start=9368 - _globals['_GETCONFIGURATIONRESPONSE_ITEMSENTRY']._serialized_end=9453 - _globals['_SUBSCRIBECONFIGURATIONREQUEST']._serialized_start=9456 - _globals['_SUBSCRIBECONFIGURATIONREQUEST']._serialized_end=9656 + _globals['_GETCONFIGURATIONRESPONSE']._serialized_start=9567 + _globals['_GETCONFIGURATIONRESPONSE']._serialized_end=9755 + _globals['_GETCONFIGURATIONRESPONSE_ITEMSENTRY']._serialized_start=9670 + _globals['_GETCONFIGURATIONRESPONSE_ITEMSENTRY']._serialized_end=9755 + _globals['_SUBSCRIBECONFIGURATIONREQUEST']._serialized_start=9758 + _globals['_SUBSCRIBECONFIGURATIONREQUEST']._serialized_end=9958 _globals['_SUBSCRIBECONFIGURATIONREQUEST_METADATAENTRY']._serialized_start=513 _globals['_SUBSCRIBECONFIGURATIONREQUEST_METADATAENTRY']._serialized_end=560 - _globals['_UNSUBSCRIBECONFIGURATIONREQUEST']._serialized_start=9658 - _globals['_UNSUBSCRIBECONFIGURATIONREQUEST']._serialized_end=9723 - _globals['_SUBSCRIBECONFIGURATIONRESPONSE']._serialized_start=9726 - _globals['_SUBSCRIBECONFIGURATIONRESPONSE']._serialized_end=9938 - _globals['_SUBSCRIBECONFIGURATIONRESPONSE_ITEMSENTRY']._serialized_start=9368 - _globals['_SUBSCRIBECONFIGURATIONRESPONSE_ITEMSENTRY']._serialized_end=9453 - _globals['_UNSUBSCRIBECONFIGURATIONRESPONSE']._serialized_start=9940 - _globals['_UNSUBSCRIBECONFIGURATIONRESPONSE']._serialized_end=10003 - _globals['_TRYLOCKREQUEST']._serialized_start=10006 - _globals['_TRYLOCKREQUEST']._serialized_end=10161 - _globals['_TRYLOCKRESPONSE']._serialized_start=10163 - _globals['_TRYLOCKRESPONSE']._serialized_end=10197 - _globals['_UNLOCKREQUEST']._serialized_start=10199 - _globals['_UNLOCKREQUEST']._serialized_end=10309 - _globals['_UNLOCKRESPONSE']._serialized_start=10312 - _globals['_UNLOCKRESPONSE']._serialized_end=10486 - _globals['_UNLOCKRESPONSE_STATUS']._serialized_start=10392 - _globals['_UNLOCKRESPONSE_STATUS']._serialized_end=10486 - _globals['_SUBTLEGETKEYREQUEST']._serialized_start=10489 - _globals['_SUBTLEGETKEYREQUEST']._serialized_end=10665 - _globals['_SUBTLEGETKEYREQUEST_KEYFORMAT']._serialized_start=10635 - _globals['_SUBTLEGETKEYREQUEST_KEYFORMAT']._serialized_end=10665 - _globals['_SUBTLEGETKEYRESPONSE']._serialized_start=10667 - _globals['_SUBTLEGETKEYRESPONSE']._serialized_end=10734 - _globals['_SUBTLEENCRYPTREQUEST']._serialized_start=10737 - _globals['_SUBTLEENCRYPTREQUEST']._serialized_end=10919 - _globals['_SUBTLEENCRYPTRESPONSE']._serialized_start=10921 - _globals['_SUBTLEENCRYPTRESPONSE']._serialized_end=10977 - _globals['_SUBTLEDECRYPTREQUEST']._serialized_start=10980 - _globals['_SUBTLEDECRYPTREQUEST']._serialized_end=11176 - _globals['_SUBTLEDECRYPTRESPONSE']._serialized_start=11178 - _globals['_SUBTLEDECRYPTRESPONSE']._serialized_end=11220 - _globals['_SUBTLEWRAPKEYREQUEST']._serialized_start=11223 - _globals['_SUBTLEWRAPKEYREQUEST']._serialized_end=11423 - _globals['_SUBTLEWRAPKEYRESPONSE']._serialized_start=11425 - _globals['_SUBTLEWRAPKEYRESPONSE']._serialized_end=11494 - _globals['_SUBTLEUNWRAPKEYREQUEST']._serialized_start=11497 - _globals['_SUBTLEUNWRAPKEYREQUEST']._serialized_end=11708 - _globals['_SUBTLEUNWRAPKEYRESPONSE']._serialized_start=11710 - _globals['_SUBTLEUNWRAPKEYRESPONSE']._serialized_end=11772 - _globals['_SUBTLESIGNREQUEST']._serialized_start=11774 - _globals['_SUBTLESIGNREQUEST']._serialized_end=11894 - _globals['_SUBTLESIGNRESPONSE']._serialized_start=11896 - _globals['_SUBTLESIGNRESPONSE']._serialized_end=11935 - _globals['_SUBTLEVERIFYREQUEST']._serialized_start=11938 - _globals['_SUBTLEVERIFYREQUEST']._serialized_end=12079 - _globals['_SUBTLEVERIFYRESPONSE']._serialized_start=12081 - _globals['_SUBTLEVERIFYRESPONSE']._serialized_end=12118 - _globals['_ENCRYPTREQUEST']._serialized_start=12121 - _globals['_ENCRYPTREQUEST']._serialized_end=12254 - _globals['_ENCRYPTREQUESTOPTIONS']._serialized_start=12257 - _globals['_ENCRYPTREQUESTOPTIONS']._serialized_end=12511 - _globals['_ENCRYPTRESPONSE']._serialized_start=12513 - _globals['_ENCRYPTRESPONSE']._serialized_end=12584 - _globals['_DECRYPTREQUEST']._serialized_start=12587 - _globals['_DECRYPTREQUEST']._serialized_end=12720 - _globals['_DECRYPTREQUESTOPTIONS']._serialized_start=12722 - _globals['_DECRYPTREQUESTOPTIONS']._serialized_end=12811 - _globals['_DECRYPTRESPONSE']._serialized_start=12813 - _globals['_DECRYPTRESPONSE']._serialized_end=12884 - _globals['_GETWORKFLOWREQUEST']._serialized_start=12886 - _globals['_GETWORKFLOWREQUEST']._serialized_end=12986 - _globals['_GETWORKFLOWRESPONSE']._serialized_start=12989 - _globals['_GETWORKFLOWRESPONSE']._serialized_end=13377 - _globals['_GETWORKFLOWRESPONSE_PROPERTIESENTRY']._serialized_start=13328 - _globals['_GETWORKFLOWRESPONSE_PROPERTIESENTRY']._serialized_end=13377 - _globals['_STARTWORKFLOWREQUEST']._serialized_start=13380 - _globals['_STARTWORKFLOWREQUEST']._serialized_end=13657 - _globals['_STARTWORKFLOWREQUEST_OPTIONSENTRY']._serialized_start=13611 - _globals['_STARTWORKFLOWREQUEST_OPTIONSENTRY']._serialized_end=13657 - _globals['_STARTWORKFLOWRESPONSE']._serialized_start=13659 - _globals['_STARTWORKFLOWRESPONSE']._serialized_end=13715 - _globals['_TERMINATEWORKFLOWREQUEST']._serialized_start=13717 - _globals['_TERMINATEWORKFLOWREQUEST']._serialized_end=13823 - _globals['_PAUSEWORKFLOWREQUEST']._serialized_start=13825 - _globals['_PAUSEWORKFLOWREQUEST']._serialized_end=13927 - _globals['_RESUMEWORKFLOWREQUEST']._serialized_start=13929 - _globals['_RESUMEWORKFLOWREQUEST']._serialized_end=14032 - _globals['_RAISEEVENTWORKFLOWREQUEST']._serialized_start=14035 - _globals['_RAISEEVENTWORKFLOWREQUEST']._serialized_end=14193 - _globals['_PURGEWORKFLOWREQUEST']._serialized_start=14195 - _globals['_PURGEWORKFLOWREQUEST']._serialized_end=14297 - _globals['_SHUTDOWNREQUEST']._serialized_start=14299 - _globals['_SHUTDOWNREQUEST']._serialized_end=14316 - _globals['_JOB']._serialized_start=14319 - _globals['_JOB']._serialized_end=14506 - _globals['_SCHEDULEJOBREQUEST']._serialized_start=14508 - _globals['_SCHEDULEJOBREQUEST']._serialized_end=14569 - _globals['_SCHEDULEJOBRESPONSE']._serialized_start=14571 - _globals['_SCHEDULEJOBRESPONSE']._serialized_end=14592 - _globals['_GETJOBREQUEST']._serialized_start=14594 - _globals['_GETJOBREQUEST']._serialized_end=14623 - _globals['_GETJOBRESPONSE']._serialized_start=14625 - _globals['_GETJOBRESPONSE']._serialized_end=14682 - _globals['_DELETEJOBREQUEST']._serialized_start=14684 - _globals['_DELETEJOBREQUEST']._serialized_end=14716 - _globals['_DELETEJOBRESPONSE']._serialized_start=14718 - _globals['_DELETEJOBRESPONSE']._serialized_end=14737 - _globals['_DAPR']._serialized_start=14829 - _globals['_DAPR']._serialized_end=21016 + _globals['_UNSUBSCRIBECONFIGURATIONREQUEST']._serialized_start=9960 + _globals['_UNSUBSCRIBECONFIGURATIONREQUEST']._serialized_end=10025 + _globals['_SUBSCRIBECONFIGURATIONRESPONSE']._serialized_start=10028 + _globals['_SUBSCRIBECONFIGURATIONRESPONSE']._serialized_end=10240 + _globals['_SUBSCRIBECONFIGURATIONRESPONSE_ITEMSENTRY']._serialized_start=9670 + _globals['_SUBSCRIBECONFIGURATIONRESPONSE_ITEMSENTRY']._serialized_end=9755 + _globals['_UNSUBSCRIBECONFIGURATIONRESPONSE']._serialized_start=10242 + _globals['_UNSUBSCRIBECONFIGURATIONRESPONSE']._serialized_end=10305 + _globals['_TRYLOCKREQUEST']._serialized_start=10308 + _globals['_TRYLOCKREQUEST']._serialized_end=10463 + _globals['_TRYLOCKRESPONSE']._serialized_start=10465 + _globals['_TRYLOCKRESPONSE']._serialized_end=10499 + _globals['_UNLOCKREQUEST']._serialized_start=10501 + _globals['_UNLOCKREQUEST']._serialized_end=10611 + _globals['_UNLOCKRESPONSE']._serialized_start=10614 + _globals['_UNLOCKRESPONSE']._serialized_end=10788 + _globals['_UNLOCKRESPONSE_STATUS']._serialized_start=10694 + _globals['_UNLOCKRESPONSE_STATUS']._serialized_end=10788 + _globals['_SUBTLEGETKEYREQUEST']._serialized_start=10791 + _globals['_SUBTLEGETKEYREQUEST']._serialized_end=10967 + _globals['_SUBTLEGETKEYREQUEST_KEYFORMAT']._serialized_start=10937 + _globals['_SUBTLEGETKEYREQUEST_KEYFORMAT']._serialized_end=10967 + _globals['_SUBTLEGETKEYRESPONSE']._serialized_start=10969 + _globals['_SUBTLEGETKEYRESPONSE']._serialized_end=11036 + _globals['_SUBTLEENCRYPTREQUEST']._serialized_start=11039 + _globals['_SUBTLEENCRYPTREQUEST']._serialized_end=11221 + _globals['_SUBTLEENCRYPTRESPONSE']._serialized_start=11223 + _globals['_SUBTLEENCRYPTRESPONSE']._serialized_end=11279 + _globals['_SUBTLEDECRYPTREQUEST']._serialized_start=11282 + _globals['_SUBTLEDECRYPTREQUEST']._serialized_end=11478 + _globals['_SUBTLEDECRYPTRESPONSE']._serialized_start=11480 + _globals['_SUBTLEDECRYPTRESPONSE']._serialized_end=11522 + _globals['_SUBTLEWRAPKEYREQUEST']._serialized_start=11525 + _globals['_SUBTLEWRAPKEYREQUEST']._serialized_end=11725 + _globals['_SUBTLEWRAPKEYRESPONSE']._serialized_start=11727 + _globals['_SUBTLEWRAPKEYRESPONSE']._serialized_end=11796 + _globals['_SUBTLEUNWRAPKEYREQUEST']._serialized_start=11799 + _globals['_SUBTLEUNWRAPKEYREQUEST']._serialized_end=12010 + _globals['_SUBTLEUNWRAPKEYRESPONSE']._serialized_start=12012 + _globals['_SUBTLEUNWRAPKEYRESPONSE']._serialized_end=12074 + _globals['_SUBTLESIGNREQUEST']._serialized_start=12076 + _globals['_SUBTLESIGNREQUEST']._serialized_end=12196 + _globals['_SUBTLESIGNRESPONSE']._serialized_start=12198 + _globals['_SUBTLESIGNRESPONSE']._serialized_end=12237 + _globals['_SUBTLEVERIFYREQUEST']._serialized_start=12240 + _globals['_SUBTLEVERIFYREQUEST']._serialized_end=12381 + _globals['_SUBTLEVERIFYRESPONSE']._serialized_start=12383 + _globals['_SUBTLEVERIFYRESPONSE']._serialized_end=12420 + _globals['_ENCRYPTREQUEST']._serialized_start=12423 + _globals['_ENCRYPTREQUEST']._serialized_end=12556 + _globals['_ENCRYPTREQUESTOPTIONS']._serialized_start=12559 + _globals['_ENCRYPTREQUESTOPTIONS']._serialized_end=12813 + _globals['_ENCRYPTRESPONSE']._serialized_start=12815 + _globals['_ENCRYPTRESPONSE']._serialized_end=12886 + _globals['_DECRYPTREQUEST']._serialized_start=12889 + _globals['_DECRYPTREQUEST']._serialized_end=13022 + _globals['_DECRYPTREQUESTOPTIONS']._serialized_start=13024 + _globals['_DECRYPTREQUESTOPTIONS']._serialized_end=13113 + _globals['_DECRYPTRESPONSE']._serialized_start=13115 + _globals['_DECRYPTRESPONSE']._serialized_end=13186 + _globals['_GETWORKFLOWREQUEST']._serialized_start=13188 + _globals['_GETWORKFLOWREQUEST']._serialized_end=13288 + _globals['_GETWORKFLOWRESPONSE']._serialized_start=13291 + _globals['_GETWORKFLOWRESPONSE']._serialized_end=13679 + _globals['_GETWORKFLOWRESPONSE_PROPERTIESENTRY']._serialized_start=13630 + _globals['_GETWORKFLOWRESPONSE_PROPERTIESENTRY']._serialized_end=13679 + _globals['_STARTWORKFLOWREQUEST']._serialized_start=13682 + _globals['_STARTWORKFLOWREQUEST']._serialized_end=13959 + _globals['_STARTWORKFLOWREQUEST_OPTIONSENTRY']._serialized_start=13913 + _globals['_STARTWORKFLOWREQUEST_OPTIONSENTRY']._serialized_end=13959 + _globals['_STARTWORKFLOWRESPONSE']._serialized_start=13961 + _globals['_STARTWORKFLOWRESPONSE']._serialized_end=14017 + _globals['_TERMINATEWORKFLOWREQUEST']._serialized_start=14019 + _globals['_TERMINATEWORKFLOWREQUEST']._serialized_end=14125 + _globals['_PAUSEWORKFLOWREQUEST']._serialized_start=14127 + _globals['_PAUSEWORKFLOWREQUEST']._serialized_end=14229 + _globals['_RESUMEWORKFLOWREQUEST']._serialized_start=14231 + _globals['_RESUMEWORKFLOWREQUEST']._serialized_end=14334 + _globals['_RAISEEVENTWORKFLOWREQUEST']._serialized_start=14337 + _globals['_RAISEEVENTWORKFLOWREQUEST']._serialized_end=14495 + _globals['_PURGEWORKFLOWREQUEST']._serialized_start=14497 + _globals['_PURGEWORKFLOWREQUEST']._serialized_end=14599 + _globals['_SHUTDOWNREQUEST']._serialized_start=14601 + _globals['_SHUTDOWNREQUEST']._serialized_end=14618 + _globals['_JOB']._serialized_start=14621 + _globals['_JOB']._serialized_end=14853 + _globals['_SCHEDULEJOBREQUEST']._serialized_start=14855 + _globals['_SCHEDULEJOBREQUEST']._serialized_end=14916 + _globals['_SCHEDULEJOBRESPONSE']._serialized_start=14918 + _globals['_SCHEDULEJOBRESPONSE']._serialized_end=14939 + _globals['_GETJOBREQUEST']._serialized_start=14941 + _globals['_GETJOBREQUEST']._serialized_end=14970 + _globals['_GETJOBRESPONSE']._serialized_start=14972 + _globals['_GETJOBRESPONSE']._serialized_end=15029 + _globals['_DELETEJOBREQUEST']._serialized_start=15031 + _globals['_DELETEJOBREQUEST']._serialized_end=15063 + _globals['_DELETEJOBRESPONSE']._serialized_start=15065 + _globals['_DELETEJOBRESPONSE']._serialized_end=15084 + _globals['_DAPR']._serialized_start=15176 + _globals['_DAPR']._serialized_end=21380 # @@protoc_insertion_point(module_scope) diff --git a/dapr/proto/runtime/v1/dapr_pb2.pyi b/dapr/proto/runtime/v1/dapr_pb2.pyi index dd4a98f2..c9a99f8b 100644 --- a/dapr/proto/runtime/v1/dapr_pb2.pyi +++ b/dapr/proto/runtime/v1/dapr_pb2.pyi @@ -737,33 +737,33 @@ class SubscribeTopicEventsRequestAlpha1(google.protobuf.message.Message): """SubscribeTopicEventsRequestAlpha1 is a message containing the details for subscribing to a topic via streaming. The first message must always be the initial request. All subsequent - messages must be event responses. + messages must be event processed responses. """ DESCRIPTOR: google.protobuf.descriptor.Descriptor INITIAL_REQUEST_FIELD_NUMBER: builtins.int - EVENT_RESPONSE_FIELD_NUMBER: builtins.int + EVENT_PROCESSED_FIELD_NUMBER: builtins.int @property - def initial_request(self) -> global___SubscribeTopicEventsInitialRequestAlpha1: ... + def initial_request(self) -> global___SubscribeTopicEventsRequestInitialAlpha1: ... @property - def event_response(self) -> global___SubscribeTopicEventsResponseAlpha1: ... + def event_processed(self) -> global___SubscribeTopicEventsRequestProcessedAlpha1: ... def __init__( self, *, - initial_request: global___SubscribeTopicEventsInitialRequestAlpha1 | None = ..., - event_response: global___SubscribeTopicEventsResponseAlpha1 | None = ..., + initial_request: global___SubscribeTopicEventsRequestInitialAlpha1 | None = ..., + event_processed: global___SubscribeTopicEventsRequestProcessedAlpha1 | None = ..., ) -> None: ... - def HasField(self, field_name: typing.Literal["event_response", b"event_response", "initial_request", b"initial_request", "subscribe_topic_events_request_type", b"subscribe_topic_events_request_type"]) -> builtins.bool: ... - def ClearField(self, field_name: typing.Literal["event_response", b"event_response", "initial_request", b"initial_request", "subscribe_topic_events_request_type", b"subscribe_topic_events_request_type"]) -> None: ... - def WhichOneof(self, oneof_group: typing.Literal["subscribe_topic_events_request_type", b"subscribe_topic_events_request_type"]) -> typing.Literal["initial_request", "event_response"] | None: ... + def HasField(self, field_name: typing.Literal["event_processed", b"event_processed", "initial_request", b"initial_request", "subscribe_topic_events_request_type", b"subscribe_topic_events_request_type"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["event_processed", b"event_processed", "initial_request", b"initial_request", "subscribe_topic_events_request_type", b"subscribe_topic_events_request_type"]) -> None: ... + def WhichOneof(self, oneof_group: typing.Literal["subscribe_topic_events_request_type", b"subscribe_topic_events_request_type"]) -> typing.Literal["initial_request", "event_processed"] | None: ... global___SubscribeTopicEventsRequestAlpha1 = SubscribeTopicEventsRequestAlpha1 @typing.final -class SubscribeTopicEventsInitialRequestAlpha1(google.protobuf.message.Message): - """SubscribeTopicEventsInitialRequestAlpha1 is the initial message containing the - details for subscribing to a topic via streaming. +class SubscribeTopicEventsRequestInitialAlpha1(google.protobuf.message.Message): + """SubscribeTopicEventsRequestInitialAlpha1 is the initial message containing + the details for subscribing to a topic via streaming. """ DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -816,11 +816,11 @@ class SubscribeTopicEventsInitialRequestAlpha1(google.protobuf.message.Message): def ClearField(self, field_name: typing.Literal["_dead_letter_topic", b"_dead_letter_topic", "dead_letter_topic", b"dead_letter_topic", "metadata", b"metadata", "pubsub_name", b"pubsub_name", "topic", b"topic"]) -> None: ... def WhichOneof(self, oneof_group: typing.Literal["_dead_letter_topic", b"_dead_letter_topic"]) -> typing.Literal["dead_letter_topic"] | None: ... -global___SubscribeTopicEventsInitialRequestAlpha1 = SubscribeTopicEventsInitialRequestAlpha1 +global___SubscribeTopicEventsRequestInitialAlpha1 = SubscribeTopicEventsRequestInitialAlpha1 @typing.final -class SubscribeTopicEventsResponseAlpha1(google.protobuf.message.Message): - """SubscribeTopicEventsResponseAlpha1 is a message containing the result of a +class SubscribeTopicEventsRequestProcessedAlpha1(google.protobuf.message.Message): + """SubscribeTopicEventsRequestProcessedAlpha1 is the message containing the subscription to a topic. """ @@ -843,8 +843,48 @@ class SubscribeTopicEventsResponseAlpha1(google.protobuf.message.Message): def HasField(self, field_name: typing.Literal["status", b"status"]) -> builtins.bool: ... def ClearField(self, field_name: typing.Literal["id", b"id", "status", b"status"]) -> None: ... +global___SubscribeTopicEventsRequestProcessedAlpha1 = SubscribeTopicEventsRequestProcessedAlpha1 + +@typing.final +class SubscribeTopicEventsResponseAlpha1(google.protobuf.message.Message): + """SubscribeTopicEventsResponseAlpha1 is a message returned from daprd + when subscribing to a topic via streaming. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + INITIAL_RESPONSE_FIELD_NUMBER: builtins.int + EVENT_MESSAGE_FIELD_NUMBER: builtins.int + @property + def initial_response(self) -> global___SubscribeTopicEventsResponseInitialAlpha1: ... + @property + def event_message(self) -> dapr.proto.runtime.v1.appcallback_pb2.TopicEventRequest: ... + def __init__( + self, + *, + initial_response: global___SubscribeTopicEventsResponseInitialAlpha1 | None = ..., + event_message: dapr.proto.runtime.v1.appcallback_pb2.TopicEventRequest | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["event_message", b"event_message", "initial_response", b"initial_response", "subscribe_topic_events_response_type", b"subscribe_topic_events_response_type"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["event_message", b"event_message", "initial_response", b"initial_response", "subscribe_topic_events_response_type", b"subscribe_topic_events_response_type"]) -> None: ... + def WhichOneof(self, oneof_group: typing.Literal["subscribe_topic_events_response_type", b"subscribe_topic_events_response_type"]) -> typing.Literal["initial_response", "event_message"] | None: ... + global___SubscribeTopicEventsResponseAlpha1 = SubscribeTopicEventsResponseAlpha1 +@typing.final +class SubscribeTopicEventsResponseInitialAlpha1(google.protobuf.message.Message): + """SubscribeTopicEventsResponseInitialAlpha1 is the initial response from daprd + when subscribing to a topic. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + def __init__( + self, + ) -> None: ... + +global___SubscribeTopicEventsResponseInitialAlpha1 = SubscribeTopicEventsResponseInitialAlpha1 + @typing.final class InvokeBindingRequest(google.protobuf.message.Message): """InvokeBindingRequest is the message to send data to output bindings""" @@ -3088,7 +3128,9 @@ global___ShutdownRequest = ShutdownRequest @typing.final class Job(google.protobuf.message.Message): - """Job is the definition of a job.""" + """Job is the definition of a job. At least one of schedule or due_time must be + provided but can also be provided together. + """ DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -3101,16 +3143,47 @@ class Job(google.protobuf.message.Message): name: builtins.str """The unique name for the job.""" schedule: builtins.str - """The schedule for the job.""" + """schedule is an optional schedule at which the job is to be run. + Accepts both systemd timer style cron expressions, as well as human + readable '@' prefixed period strings as defined below. + + Systemd timer style cron accepts 6 fields: + seconds | minutes | hours | day of month | month | day of week + 0-59 | 0-59 | 0-23 | 1-31 | 1-12/jan-dec | 0-7/sun-sat + + "0 30 * * * *" - every hour on the half hour + "0 15 3 * * *" - every day at 03:15 + + Period string expressions: + Entry | Description | Equivalent To + ----- | ----------- | ------------- + @every | Run every (e.g. '@every 1h30m') | N/A + @yearly (or @annually) | Run once a year, midnight, Jan. 1st | 0 0 0 1 1 * + @monthly | Run once a month, midnight, first of month | 0 0 0 1 * * + @weekly | Run once a week, midnight on Sunday | 0 0 0 * * 0 + @daily (or @midnight) | Run once a day, midnight | 0 0 0 * * * + @hourly | Run once an hour, beginning of hour | 0 0 * * * * + """ repeats: builtins.int - """Optional: jobs with fixed repeat counts (accounting for Actor Reminders).""" + """repeats is the optional number of times in which the job should be + triggered. If not set, the job will run indefinitely or until expiration. + """ due_time: builtins.str - """Optional: sets time at which or time interval before the callback is invoked for the first time.""" + """due_time is the optional time at which the job should be active, or the + "one shot" time if other scheduling type fields are not provided. Accepts + a "point in time" string in the format of RFC3339, Go duration string + (calculated from job creation time), or non-repeating ISO8601. + """ ttl: builtins.str - """Optional: Time To Live to allow for auto deletes (accounting for Actor Reminders).""" + """ttl is the optional time to live or expiration of the job. Accepts a + "point in time" string in the format of RFC3339, Go duration string + (calculated from job creation time), or non-repeating ISO8601. + """ @property def data(self) -> google.protobuf.any_pb2.Any: - """Job data.""" + """payload is the serialized job payload that will be sent to the recipient + when the job is triggered. + """ def __init__( self, diff --git a/dapr/proto/runtime/v1/dapr_pb2_grpc.py b/dapr/proto/runtime/v1/dapr_pb2_grpc.py index da7ae756..b97d7f02 100644 --- a/dapr/proto/runtime/v1/dapr_pb2_grpc.py +++ b/dapr/proto/runtime/v1/dapr_pb2_grpc.py @@ -4,14 +4,11 @@ import warnings from dapr.proto.common.v1 import common_pb2 as dapr_dot_proto_dot_common_dot_v1_dot_common__pb2 -from dapr.proto.runtime.v1 import appcallback_pb2 as dapr_dot_proto_dot_runtime_dot_v1_dot_appcallback__pb2 from dapr.proto.runtime.v1 import dapr_pb2 as dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2 from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2 -GRPC_GENERATED_VERSION = '1.63.0' +GRPC_GENERATED_VERSION = '1.66.1' GRPC_VERSION = grpc.__version__ -EXPECTED_ERROR_RELEASE = '1.65.0' -SCHEDULED_RELEASE_DATE = 'June 25, 2024' _version_not_supported = False try: @@ -21,15 +18,12 @@ _version_not_supported = True if _version_not_supported: - warnings.warn( + raise RuntimeError( f'The grpc package installed is at version {GRPC_VERSION},' + f' but the generated code in dapr/proto/runtime/v1/dapr_pb2_grpc.py depends on' + f' grpcio>={GRPC_GENERATED_VERSION}.' + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' - + f' This warning will become an error in {EXPECTED_ERROR_RELEASE},' - + f' scheduled for release on {SCHEDULED_RELEASE_DATE}.', - RuntimeWarning ) @@ -96,7 +90,7 @@ def __init__(self, channel): self.SubscribeTopicEventsAlpha1 = channel.stream_stream( '/dapr.proto.runtime.v1.Dapr/SubscribeTopicEventsAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubscribeTopicEventsRequestAlpha1.SerializeToString, - response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_appcallback__pb2.TopicEventRequest.FromString, + response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubscribeTopicEventsResponseAlpha1.FromString, _registered_method=True) self.InvokeBinding = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/InvokeBinding', @@ -803,7 +797,7 @@ def add_DaprServicer_to_server(servicer, server): 'SubscribeTopicEventsAlpha1': grpc.stream_stream_rpc_method_handler( servicer.SubscribeTopicEventsAlpha1, request_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubscribeTopicEventsRequestAlpha1.FromString, - response_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_appcallback__pb2.TopicEventRequest.SerializeToString, + response_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubscribeTopicEventsResponseAlpha1.SerializeToString, ), 'InvokeBinding': grpc.unary_unary_rpc_method_handler( servicer.InvokeBinding, @@ -1044,6 +1038,7 @@ def add_DaprServicer_to_server(servicer, server): generic_handler = grpc.method_handlers_generic_handler( 'dapr.proto.runtime.v1.Dapr', rpc_method_handlers) server.add_generic_rpc_handlers((generic_handler,)) + server.add_registered_method_handlers('dapr.proto.runtime.v1.Dapr', rpc_method_handlers) # This class is part of an EXPERIMENTAL API. @@ -1337,7 +1332,7 @@ def SubscribeTopicEventsAlpha1(request_iterator, target, '/dapr.proto.runtime.v1.Dapr/SubscribeTopicEventsAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubscribeTopicEventsRequestAlpha1.SerializeToString, - dapr_dot_proto_dot_runtime_dot_v1_dot_appcallback__pb2.TopicEventRequest.FromString, + dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubscribeTopicEventsResponseAlpha1.FromString, options, channel_credentials, insecure, diff --git a/examples/pubsub_streaming/publisher.py b/examples/pubsub_streaming/publisher.py new file mode 100644 index 00000000..32e6db51 --- /dev/null +++ b/examples/pubsub_streaming/publisher.py @@ -0,0 +1,68 @@ +# ------------------------------------------------------------ +# Copyright 2022 The Dapr Authors +# Licensed 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. +# ------------------------------------------------------------ + +import json +import time + +from dapr.clients import DaprClient + +with DaprClient() as d: + id = 0 + while id < 3: + id += 1 + req_data = {'id': time.time(), 'message': 'hello world'} + + # Create a typed message with content type and body + resp = d.publish_event( + pubsub_name='pubsub', + topic_name='TOPIC_A', + data=json.dumps(req_data), + data_content_type='application/json', + ) + + # Print the request + print(req_data, flush=True) + + time.sleep(1) + + # we can publish events to different topics but handle them with the same method + # by disabling topic validation in the subscriber + # + # id = 3 + # while id < 6: + # id += 1 + # req_data = {'id': id, 'message': 'hello world'} + # resp = d.publish_event( + # pubsub_name='pubsub', + # topic_name=f'topic/{id}', + # data=json.dumps(req_data), + # data_content_type='application/json', + # ) + # + # # Print the request + # print(req_data, flush=True) + # + # time.sleep(0.5) + # + # # This topic will fail - initiate a retry which gets routed to the dead letter topic + # req_data['id'] = 7 + # resp = d.publish_event( + # pubsub_name='pubsub', + # topic_name='TOPIC_D', + # data=json.dumps(req_data), + # data_content_type='application/json', + # publish_metadata={'custommeta': 'somevalue'}, + # ) + # + # # Print the request + # print(req_data, flush=True) diff --git a/examples/pubsub_streaming/subscriber.py b/examples/pubsub_streaming/subscriber.py new file mode 100644 index 00000000..59d95cf8 --- /dev/null +++ b/examples/pubsub_streaming/subscriber.py @@ -0,0 +1,45 @@ +from dapr.clients import DaprClient +from dapr.clients.grpc._response import TopicEventResponse +from dapr.clients.grpc.subscription import success, retry, drop + + +def process_message(message): + # Process the message here + print(f"Processing message: {message.data}") + return TopicEventResponse('success').status + + +def main(): + with DaprClient() as client: + + subscription = client.subscribe(pubsub_name="pubsub", topic="TOPIC_A", dead_letter_topic="TOPIC_A_DEAD") + + try: + while True: + try: + try: + message = subscription.next_message(timeout=5) + except Exception as e: + print(f"An error occurred: {e}") + + if message is None: + print("No message received within timeout period.") + continue + + print(f"Received message with ID: {message.id}") + + # Process the message + try: + subscription.respond(message, process_message(message)) + except Exception as e: + print(f"An error occurred while sending the message: {e}") + except KeyboardInterrupt: + print("Received interrupt, shutting down...") + break + + finally: + subscription.close() + + +if __name__ == "__main__": + main() From 2add93f97a683e41578d311eb0b20876898173b3 Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Sun, 22 Sep 2024 20:54:48 +0100 Subject: [PATCH 02/26] works Signed-off-by: Elena Kolevska --- dapr/clients/exceptions.py | 27 +++--- dapr/clients/grpc/subscription.py | 114 +++++++++++++++++------- examples/pubsub_streaming/subscriber.py | 31 ++++--- tests/clients/fake_dapr_server.py | 10 ++- tests/clients/test_dapr_grpc_client.py | 41 +++++++-- 5 files changed, 150 insertions(+), 73 deletions(-) diff --git a/dapr/clients/exceptions.py b/dapr/clients/exceptions.py index 91bc04a8..6650ec6a 100644 --- a/dapr/clients/exceptions.py +++ b/dapr/clients/exceptions.py @@ -27,22 +27,15 @@ class DaprInternalError(Exception): """DaprInternalError encapsulates all Dapr exceptions""" - def __init__( - self, - message: Optional[str], - error_code: Optional[str] = ERROR_CODE_UNKNOWN, - raw_response_bytes: Optional[bytes] = None, - ): + def __init__(self, message: Optional[str], error_code: Optional[str] = ERROR_CODE_UNKNOWN, + raw_response_bytes: Optional[bytes] = None, ): self._message = message self._error_code = error_code self._raw_response_bytes = raw_response_bytes def as_dict(self): - return { - 'message': self._message, - 'errorCode': self._error_code, - 'raw_response_bytes': self._raw_response_bytes, - } + return {'message': self._message, 'errorCode': self._error_code, + 'raw_response_bytes': self._raw_response_bytes, } class StatusDetails: @@ -119,12 +112,8 @@ def get_grpc_status(self): return self._grpc_status def json(self): - error_details = { - 'status_code': self.code().name, - 'message': self.details(), - 'error_code': self.error_code(), - 'details': self._details.as_dict(), - } + error_details = {'status_code': self.code().name, 'message': self.details(), + 'error_code': self.error_code(), 'details': self._details.as_dict(), } return json.dumps(error_details) @@ -132,3 +121,7 @@ def serialize_status_detail(status_detail): if not status_detail: return None return MessageToDict(status_detail, preserving_proto_field_name=True) + + +class StreamInactiveError(Exception): + pass diff --git a/dapr/clients/grpc/subscription.py b/dapr/clients/grpc/subscription.py index c4082411..3439fdca 100644 --- a/dapr/clients/grpc/subscription.py +++ b/dapr/clients/grpc/subscription.py @@ -1,4 +1,7 @@ import grpc + +from dapr.clients.exceptions import StreamInactiveError +from dapr.clients.grpc._response import TopicEventResponse from dapr.proto import api_v1, appcallback_v1 import queue import threading @@ -24,9 +27,11 @@ def __init__(self, stub, pubsub_name, topic, metadata=None, dead_letter_topic=No self.metadata = metadata or {} self.dead_letter_topic = dead_letter_topic or '' self._stream = None + self._response_thread = None self._send_queue = queue.Queue() self._receive_queue = queue.Queue() self._stream_active = False + self._stream_lock = threading.Lock() # Protects _stream_active def start(self): def request_iterator(): @@ -38,65 +43,112 @@ def request_iterator(): dead_letter_topic=self.dead_letter_topic or '')) yield initial_request - while self._stream_active: + while self._is_stream_active(): try: - request = self._send_queue.get() - if request is None: - break - - yield request + yield self._send_queue.get() # TODO Should I add a timeout? except queue.Empty: continue except Exception as e: - print(f"Exception in request_iterator: {e}") - raise e + raise Exception(f"Error in request iterator: {e}") # Create the bidirectional stream self._stream = self._stub.SubscribeTopicEventsAlpha1(request_iterator()) - self._stream_active = True + self._set_stream_active() # Start a thread to handle incoming messages - threading.Thread(target=self._handle_responses, daemon=True).start() + self._response_thread = threading.Thread(target=self._handle_responses, daemon=True) + self._response_thread.start() def _handle_responses(self): try: # The first message dapr sends on the stream is for signalling only, so discard it next(self._stream) - for msg in self._stream: - print(f"Received message from dapr on stream: {msg.event_message.id}") # SubscribeTopicEventsResponseAlpha1 - self._receive_queue.put(msg.event_message) + # Read messages from the stream and put them in the receive queue + for message in self._stream: + if self._is_stream_active(): + self._receive_queue.put(message.event_message) + else: + break except grpc.RpcError as e: - print(f"gRPC error in stream: {e}") + if e.code() != grpc.StatusCode.CANCELLED: + print(f"gRPC error in stream: {e.details()}, Status Code: {e.code()}") except Exception as e: - print(f"Unexpected error in stream: {e}") + raise Exception(f"Error while handling responses: {e}") finally: - self._stream_active = False + self._set_stream_inactive() - def next_message(self, timeout=None): - print("in next_message") - try: - return self._receive_queue.get(timeout=timeout) - except queue.Empty as e : - print("queue empty", e) - return None - except Exception as e: - print(f"Exception in next_message: {e}") - return None + def next_message(self, timeout=1): + """ + Gets the next message from the receive queue + @param timeout: Timeout in seconds + @return: The next message + """ + return self.read_message_from_queue(self._receive_queue, timeout=timeout) - def respond(self, message, status): + def _respond(self, message, status): try: status = appcallback_v1.TopicEventResponse(status=status.value) response = api_v1.SubscribeTopicEventsRequestProcessedAlpha1(id=message.id, status=status) msg = api_v1.SubscribeTopicEventsRequestAlpha1(event_processed=response) - self._send_queue.put(msg) + self.send_message_to_queue(self._send_queue, msg) except Exception as e: print(f"Exception in send_message: {e}") + def respond_success(self, message): + self._respond(message, TopicEventResponse('success').status) + + def respond_retry(self, message): + self._respond(message, TopicEventResponse('retry').status) + + def respond_drop(self, message): + self._respond(message, TopicEventResponse('drop').status) + + def send_message_to_queue(self, q, message): + if not self._is_stream_active(): + raise StreamInactiveError("Stream is not active") + q.put(message) + + def read_message_from_queue(self, q, timeout): + if not self._is_stream_active(): + raise StreamInactiveError("Stream is not active") + try: + return q.get(timeout=timeout) + except queue.Empty: + return None + + def _set_stream_active(self): + with self._stream_lock: + self._stream_active = True + + def _set_stream_inactive(self): + with self._stream_lock: + self._stream_active = False + + def _is_stream_active(self): + with self._stream_lock: + return self._stream_active + def close(self): - self._stream_active = False - self._send_queue.put(None) + if not self._is_stream_active(): + return + + self._set_stream_inactive() + + # Cancel the stream if self._stream: - self._stream.cancel() \ No newline at end of file + try: + self._stream.cancel() + except grpc.RpcError as e: + if e.code() != grpc.StatusCode.CANCELLED: + raise Exception(f"Error while closing stream: {e}") + except Exception as e: + raise Exception(f"Error while closing stream: {e}") + + # Join the response-handling thread to ensure it has finished + if self._response_thread: + self._response_thread.join() + self._response_thread = None + diff --git a/examples/pubsub_streaming/subscriber.py b/examples/pubsub_streaming/subscriber.py index 59d95cf8..d2029c4e 100644 --- a/examples/pubsub_streaming/subscriber.py +++ b/examples/pubsub_streaming/subscriber.py @@ -5,8 +5,8 @@ def process_message(message): # Process the message here - print(f"Processing message: {message.data}") - return TopicEventResponse('success').status + print(f"Processing message: {message}") + return "success" def main(): @@ -15,26 +15,25 @@ def main(): subscription = client.subscribe(pubsub_name="pubsub", topic="TOPIC_A", dead_letter_topic="TOPIC_A_DEAD") try: - while True: + for i in range(5): try: - try: - message = subscription.next_message(timeout=5) - except Exception as e: - print(f"An error occurred: {e}") - + message = subscription.next_message(timeout=0.1) if message is None: print("No message received within timeout period.") continue - print(f"Received message with ID: {message.id}") - # Process the message - try: - subscription.respond(message, process_message(message)) - except Exception as e: - print(f"An error occurred while sending the message: {e}") - except KeyboardInterrupt: - print("Received interrupt, shutting down...") + response_status = process_message(message) + + if response_status == "success": + subscription.respond_success(message) + elif response_status == "retry": + subscription.respond_retry(message) + elif response_status == "drop": + subscription.respond_drop(message) + + except Exception as e: + print(f"Error getting message: {e}") break finally: diff --git a/tests/clients/fake_dapr_server.py b/tests/clients/fake_dapr_server.py index d2f57a82..392be45c 100644 --- a/tests/clients/fake_dapr_server.py +++ b/tests/clients/fake_dapr_server.py @@ -7,7 +7,7 @@ from grpc_status import rpc_status from dapr.clients.grpc._helpers import to_bytes -from dapr.proto import api_service_v1, common_v1, api_v1 +from dapr.proto import api_service_v1, common_v1, api_v1, appcallback_v1 from dapr.proto.common.v1.common_pb2 import ConfigurationItem from dapr.clients.grpc._response import WorkflowRuntimeStatus from dapr.proto.runtime.v1.dapr_pb2 import ( @@ -177,6 +177,14 @@ def PublishEvent(self, request, context): context.set_trailing_metadata(trailers) return empty_pb2.Empty() + def SubscribeTopicEventsAlpha1(self, request_iterator, context): + yield api_v1.SubscribeTopicEventsResponseAlpha1( + initial_response=api_v1.SubscribeTopicEventsResponseInitialAlpha1()) + yield api_v1.SubscribeTopicEventsResponseAlpha1( + event_message=appcallback_v1.TopicEventRequest(id='123', topic="TOPIC_A", data=b'hello1')) + yield api_v1.SubscribeTopicEventsResponseAlpha1( + event_message=appcallback_v1.TopicEventRequest(id='456', topic="TOPIC_A", data=b'hello2')) + def SaveState(self, request, context): self.check_for_exception(context) diff --git a/tests/clients/test_dapr_grpc_client.py b/tests/clients/test_dapr_grpc_client.py index 1ddb8bc2..3588867e 100644 --- a/tests/clients/test_dapr_grpc_client.py +++ b/tests/clients/test_dapr_grpc_client.py @@ -24,7 +24,7 @@ from google.rpc import status_pb2, code_pb2 -from dapr.clients.exceptions import DaprGrpcError +from dapr.clients.exceptions import DaprGrpcError, StreamInactiveError from dapr.clients.grpc.client import DaprGrpcClient from dapr.clients import DaprClient from dapr.proto import common_v1 @@ -34,13 +34,9 @@ from dapr.clients.grpc._request import TransactionalStateOperation from dapr.clients.grpc._state import StateOptions, Consistency, Concurrency, StateItem from dapr.clients.grpc._crypto import EncryptOptions, DecryptOptions -from dapr.clients.grpc._response import ( - ConfigurationItem, - ConfigurationResponse, - ConfigurationWatcher, - UnlockResponseStatus, - WorkflowRuntimeStatus, -) +from dapr.clients.grpc._response import (ConfigurationItem, ConfigurationResponse, + ConfigurationWatcher, UnlockResponseStatus, + WorkflowRuntimeStatus, TopicEventResponse, ) class DaprGrpcClientTests(unittest.TestCase): @@ -262,6 +258,35 @@ def test_publish_error(self): data=111, ) + def test_subscribe_topic(self): + dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') + subscription = dapr.subscribe(pubsub_name='pubsub', topic='example') + + # First message + message1 = subscription.next_message(timeout=5) + subscription.respond_success(message1) + + self.assertEqual('123', message1.id) + self.assertEqual(b'hello1', message1.data) + self.assertEqual('TOPIC_A', message1.topic) + + # Second message + message2 = subscription.next_message(timeout=5) + subscription.respond_success(message2) + + self.assertEqual('456', message2.id) + self.assertEqual(b'hello2', message2.data) + self.assertEqual('TOPIC_A', message2.topic) + + def test_subscribe_topic_early_close(self): + dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') + subscription = dapr.subscribe(pubsub_name='pubsub', topic='example') + subscription.close() + + with self.assertRaises(StreamInactiveError): + subscription.next_message(timeout=5) + + @patch.object(settings, 'DAPR_API_TOKEN', 'test-token') def test_dapr_api_token_insertion(self): dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') From c325a2aa8b417475ec0fb015d902c9f4ae84ab15 Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Mon, 23 Sep 2024 01:29:18 +0100 Subject: [PATCH 03/26] Sync bidi streaming and tests Signed-off-by: Elena Kolevska --- dapr/clients/exceptions.py | 23 ++- dapr/clients/grpc/client.py | 12 +- dapr/clients/grpc/subscription.py | 138 ++++++++++++++---- examples/pubsub-streaming/README.md | 76 ++++++++++ .../publisher.py | 36 +---- .../subscriber.py | 23 +-- tests/clients/fake_dapr_server.py | 33 ++++- tests/clients/test_dapr_grpc_client.py | 42 ++++-- tox.ini | 1 + 9 files changed, 279 insertions(+), 105 deletions(-) create mode 100644 examples/pubsub-streaming/README.md rename examples/{pubsub_streaming => pubsub-streaming}/publisher.py (51%) rename examples/{pubsub_streaming => pubsub-streaming}/subscriber.py (57%) diff --git a/dapr/clients/exceptions.py b/dapr/clients/exceptions.py index 6650ec6a..c872b65a 100644 --- a/dapr/clients/exceptions.py +++ b/dapr/clients/exceptions.py @@ -27,15 +27,22 @@ class DaprInternalError(Exception): """DaprInternalError encapsulates all Dapr exceptions""" - def __init__(self, message: Optional[str], error_code: Optional[str] = ERROR_CODE_UNKNOWN, - raw_response_bytes: Optional[bytes] = None, ): + def __init__( + self, + message: Optional[str], + error_code: Optional[str] = ERROR_CODE_UNKNOWN, + raw_response_bytes: Optional[bytes] = None, + ): self._message = message self._error_code = error_code self._raw_response_bytes = raw_response_bytes def as_dict(self): - return {'message': self._message, 'errorCode': self._error_code, - 'raw_response_bytes': self._raw_response_bytes, } + return { + 'message': self._message, + 'errorCode': self._error_code, + 'raw_response_bytes': self._raw_response_bytes, + } class StatusDetails: @@ -112,8 +119,12 @@ def get_grpc_status(self): return self._grpc_status def json(self): - error_details = {'status_code': self.code().name, 'message': self.details(), - 'error_code': self.error_code(), 'details': self._details.as_dict(), } + error_details = { + 'status_code': self.code().name, + 'message': self.details(), + 'error_code': self.error_code(), + 'details': self._details.as_dict(), + } return json.dumps(error_details) diff --git a/dapr/clients/grpc/client.py b/dapr/clients/grpc/client.py index 71009b83..81e65c7e 100644 --- a/dapr/clients/grpc/client.py +++ b/dapr/clients/grpc/client.py @@ -46,7 +46,7 @@ from dapr.clients.health import DaprHealth from dapr.clients.retry import RetryPolicy from dapr.conf import settings -from dapr.proto import api_v1, api_service_v1, common_v1, appcallback_v1 +from dapr.proto import api_v1, api_service_v1, common_v1 from dapr.proto.runtime.v1.dapr_pb2 import UnsubscribeConfigurationResponse from dapr.version import __version__ @@ -482,16 +482,6 @@ def publish_event( return DaprResponse(call.initial_metadata()) - # def subscribe(self, pubsub_name, topic, metadata=None, dead_letter_topic=None): - # stream = self._stub.SubscribeTopicEventsAlpha1() - # - # # Send InitialRequest - # initial_request = api_v1.SubscribeTopicEventsInitialRequestAlpha1(pubsub_name=pubsub_name, topic=topic, metadata=metadata, dead_letter_topic=dead_letter_topic) - # request = api_v1.SubscribeTopicEventsRequestAlpha1(initial_request=initial_request) - # stream.write(request) - # - # return stream - def subscribe(self, pubsub_name, topic, metadata=None, dead_letter_topic=None): subscription = Subscription(self._stub, pubsub_name, topic, metadata, dead_letter_topic) subscription.start() diff --git a/dapr/clients/grpc/subscription.py b/dapr/clients/grpc/subscription.py index 3439fdca..1cdf1ef5 100644 --- a/dapr/clients/grpc/subscription.py +++ b/dapr/clients/grpc/subscription.py @@ -1,3 +1,5 @@ +import json + import grpc from dapr.clients.exceptions import StreamInactiveError @@ -34,32 +36,45 @@ def __init__(self, stub, pubsub_name, topic, metadata=None, dead_letter_topic=No self._stream_lock = threading.Lock() # Protects _stream_active def start(self): - def request_iterator(): + def outgoing_request_iterator(): + """ + Generator function to create the request iterator for the stream + """ try: # Send InitialRequest needed to establish the stream initial_request = api_v1.SubscribeTopicEventsRequestAlpha1( initial_request=api_v1.SubscribeTopicEventsRequestInitialAlpha1( - pubsub_name=self.pubsub_name, topic=self.topic, metadata=self.metadata or {}, - dead_letter_topic=self.dead_letter_topic or '')) + pubsub_name=self.pubsub_name, + topic=self.topic, + metadata=self.metadata or {}, + dead_letter_topic=self.dead_letter_topic or '', + ) + ) yield initial_request + # Start sending back acknowledgement messages from the send queue while self._is_stream_active(): try: - yield self._send_queue.get() # TODO Should I add a timeout? + response = self._send_queue.get() + # The above blocks until a message is available or the stream is closed + # so that's why we need to check again if the stream is still active + if not self._is_stream_active(): + break + yield response except queue.Empty: continue except Exception as e: - raise Exception(f"Error in request iterator: {e}") + raise Exception(f'Error in request iterator: {e}') # Create the bidirectional stream - self._stream = self._stub.SubscribeTopicEventsAlpha1(request_iterator()) + self._stream = self._stub.SubscribeTopicEventsAlpha1(outgoing_request_iterator()) self._set_stream_active() # Start a thread to handle incoming messages - self._response_thread = threading.Thread(target=self._handle_responses, daemon=True) + self._response_thread = threading.Thread(target=self._handle_incoming_messages, daemon=True) self._response_thread.start() - def _handle_responses(self): + def _handle_incoming_messages(self): try: # The first message dapr sends on the stream is for signalling only, so discard it next(self._stream) @@ -72,30 +87,31 @@ def _handle_responses(self): break except grpc.RpcError as e: if e.code() != grpc.StatusCode.CANCELLED: - print(f"gRPC error in stream: {e.details()}, Status Code: {e.code()}") + print(f'gRPC error in stream: {e.details()}, Status Code: {e.code()}') except Exception as e: - raise Exception(f"Error while handling responses: {e}") + raise Exception(f'Error while handling responses: {e}') finally: self._set_stream_inactive() - def next_message(self, timeout=1): - """ - Gets the next message from the receive queue - @param timeout: Timeout in seconds - @return: The next message - """ - return self.read_message_from_queue(self._receive_queue, timeout=timeout) + def next_message(self, timeout=None): + msg = self.read_message_from_queue(self._receive_queue, timeout=timeout) + + if msg is None: + return None + + return SubscriptionMessage(msg) def _respond(self, message, status): try: status = appcallback_v1.TopicEventResponse(status=status.value) - response = api_v1.SubscribeTopicEventsRequestProcessedAlpha1(id=message.id, - status=status) + response = api_v1.SubscribeTopicEventsRequestProcessedAlpha1( + id=message.id(), status=status + ) msg = api_v1.SubscribeTopicEventsRequestAlpha1(event_processed=response) self.send_message_to_queue(self._send_queue, msg) except Exception as e: - print(f"Exception in send_message: {e}") + print(f'Exception in send_message: {e}') def respond_success(self, message): self._respond(message, TopicEventResponse('success').status) @@ -108,12 +124,12 @@ def respond_drop(self, message): def send_message_to_queue(self, q, message): if not self._is_stream_active(): - raise StreamInactiveError("Stream is not active") + raise StreamInactiveError('Stream is not active') q.put(message) - def read_message_from_queue(self, q, timeout): + def read_message_from_queue(self, q, timeout=None): if not self._is_stream_active(): - raise StreamInactiveError("Stream is not active") + raise StreamInactiveError('Stream is not active') try: return q.get(timeout=timeout) except queue.Empty: @@ -143,12 +159,84 @@ def close(self): self._stream.cancel() except grpc.RpcError as e: if e.code() != grpc.StatusCode.CANCELLED: - raise Exception(f"Error while closing stream: {e}") + raise Exception(f'Error while closing stream: {e}') except Exception as e: - raise Exception(f"Error while closing stream: {e}") + raise Exception(f'Error while closing stream: {e}') # Join the response-handling thread to ensure it has finished if self._response_thread: self._response_thread.join() self._response_thread = None + +class SubscriptionMessage: + def __init__(self, msg): + self._id = msg.id + self._source = msg.source + self._type = msg.type + self._spec_version = msg.spec_version + self._data_content_type = msg.data_content_type + self._topic = msg.topic + self._pubsub_name = msg.pubsub_name + self._raw_data = msg.data + self._extensions = msg.extensions + self._data = None + + # Parse the content based on its media type + if self._raw_data and len(self._raw_data) > 0: + self._parse_data_content() + + def id(self): + return self._id + + def source(self): + return self._source + + def type(self): + return self._type + + def spec_version(self): + return self._spec_version + + def data_content_type(self): + return self._data_content_type + + def topic(self): + return self._topic + + def pubsub_name(self): + return self._pubsub_name + + def raw_data(self): + return self._raw_data + + def extensions(self): + return self._extensions + + def data(self): + return self._data + + def _parse_data_content(self): + try: + if self._data_content_type == 'application/json': + try: + self._data = json.loads(self._raw_data) + except json.JSONDecodeError: + pass # If JSON parsing fails, keep `data` as None + elif self._data_content_type == 'text/plain': + # Assume UTF-8 encoding + try: + self._data = self._raw_data.decode('utf-8') + except UnicodeDecodeError: + pass + elif self._data_content_type.startswith( + 'application/' + ) and self._data_content_type.endswith('+json'): + # Handle custom JSON-based media types (e.g., application/vnd.api+json) + try: + self._data = json.loads(self._raw_data) + except json.JSONDecodeError: + pass # If JSON parsing fails, keep `data` as None + except Exception as e: + # Log or handle any unexpected exceptions + print(f'Error parsing media type: {e}') diff --git a/examples/pubsub-streaming/README.md b/examples/pubsub-streaming/README.md new file mode 100644 index 00000000..5d80cf0b --- /dev/null +++ b/examples/pubsub-streaming/README.md @@ -0,0 +1,76 @@ +# Example - Publish and subscribe to messages + +This example utilizes a publisher and a subscriber to show the bidirectional pubsub pattern. +It creates a publisher and calls the `publish_event` method in the `DaprClient`. +In the s`subscriber.py` file it creates a subscriber object that can call the `next_message` method to get new messages from the stream. After processing the new message, it returns a status to the stream. + + +> **Note:** Make sure to use the latest proto bindings + +## Pre-requisites + +- [Dapr CLI and initialized environment](https://docs.dapr.io/getting-started) +- [Install Python 3.8+](https://www.python.org/downloads/) + +## Install Dapr python-SDK + + + +```bash +pip3 install dapr +``` + +## Run the example + +Run the following command in a terminal/command prompt: + + + +```bash +# 1. Start Subscriber +dapr run --app-id python-subscriber --app-protocol grpc python3 subscriber.py +``` + + + +In another terminal/command prompt run: + + + +```bash +# 2. Start Publisher +dapr run --app-id python-publisher --app-protocol grpc --dapr-grpc-port=3500 --enable-app-health-check python3 publisher.py +``` + + + +## Cleanup + + diff --git a/examples/pubsub_streaming/publisher.py b/examples/pubsub-streaming/publisher.py similarity index 51% rename from examples/pubsub_streaming/publisher.py rename to examples/pubsub-streaming/publisher.py index 32e6db51..f7af0f10 100644 --- a/examples/pubsub_streaming/publisher.py +++ b/examples/pubsub-streaming/publisher.py @@ -18,9 +18,9 @@ with DaprClient() as d: id = 0 - while id < 3: + while id < 5: id += 1 - req_data = {'id': time.time(), 'message': 'hello world'} + req_data = {'id': id, 'message': 'hello world'} # Create a typed message with content type and body resp = d.publish_event( @@ -34,35 +34,3 @@ print(req_data, flush=True) time.sleep(1) - - # we can publish events to different topics but handle them with the same method - # by disabling topic validation in the subscriber - # - # id = 3 - # while id < 6: - # id += 1 - # req_data = {'id': id, 'message': 'hello world'} - # resp = d.publish_event( - # pubsub_name='pubsub', - # topic_name=f'topic/{id}', - # data=json.dumps(req_data), - # data_content_type='application/json', - # ) - # - # # Print the request - # print(req_data, flush=True) - # - # time.sleep(0.5) - # - # # This topic will fail - initiate a retry which gets routed to the dead letter topic - # req_data['id'] = 7 - # resp = d.publish_event( - # pubsub_name='pubsub', - # topic_name='TOPIC_D', - # data=json.dumps(req_data), - # data_content_type='application/json', - # publish_metadata={'custommeta': 'somevalue'}, - # ) - # - # # Print the request - # print(req_data, flush=True) diff --git a/examples/pubsub_streaming/subscriber.py b/examples/pubsub-streaming/subscriber.py similarity index 57% rename from examples/pubsub_streaming/subscriber.py rename to examples/pubsub-streaming/subscriber.py index d2029c4e..f6f9078a 100644 --- a/examples/pubsub_streaming/subscriber.py +++ b/examples/pubsub-streaming/subscriber.py @@ -5,40 +5,41 @@ def process_message(message): # Process the message here - print(f"Processing message: {message}") - return "success" + print(f'Processing message: {message.data()} from {message.topic()}') + return 'success' def main(): with DaprClient() as client: - - subscription = client.subscribe(pubsub_name="pubsub", topic="TOPIC_A", dead_letter_topic="TOPIC_A_DEAD") + subscription = client.subscribe( + pubsub_name='pubsub', topic='TOPIC_A', dead_letter_topic='TOPIC_A_DEAD' + ) try: for i in range(5): try: - message = subscription.next_message(timeout=0.1) + message = subscription.next_message() if message is None: - print("No message received within timeout period.") + print('No message received within timeout period.') continue # Process the message response_status = process_message(message) - if response_status == "success": + if response_status == 'success': subscription.respond_success(message) - elif response_status == "retry": + elif response_status == 'retry': subscription.respond_retry(message) - elif response_status == "drop": + elif response_status == 'drop': subscription.respond_drop(message) except Exception as e: - print(f"Error getting message: {e}") + print(f'Error getting message: {e}') break finally: subscription.close() -if __name__ == "__main__": +if __name__ == '__main__': main() diff --git a/tests/clients/fake_dapr_server.py b/tests/clients/fake_dapr_server.py index 392be45c..1080ab30 100644 --- a/tests/clients/fake_dapr_server.py +++ b/tests/clients/fake_dapr_server.py @@ -179,11 +179,34 @@ def PublishEvent(self, request, context): def SubscribeTopicEventsAlpha1(self, request_iterator, context): yield api_v1.SubscribeTopicEventsResponseAlpha1( - initial_response=api_v1.SubscribeTopicEventsResponseInitialAlpha1()) - yield api_v1.SubscribeTopicEventsResponseAlpha1( - event_message=appcallback_v1.TopicEventRequest(id='123', topic="TOPIC_A", data=b'hello1')) - yield api_v1.SubscribeTopicEventsResponseAlpha1( - event_message=appcallback_v1.TopicEventRequest(id='456', topic="TOPIC_A", data=b'hello2')) + initial_response=api_v1.SubscribeTopicEventsResponseInitialAlpha1() + ) + + msg2 = appcallback_v1.TopicEventRequest( + id='123', + topic='TOPIC_A', + data=b'hello2', + source='app1', + data_content_type='text/plain', + type='com.example.type2', + pubsub_name='pubsub', + spec_version='1.0', + ) + yield api_v1.SubscribeTopicEventsResponseAlpha1(event_message=msg2) + + msg3 = appcallback_v1.TopicEventRequest( + id='456', + topic='TOPIC_A', + data=b'{"a": 1}', + source='app1', + data_content_type='application/json', + type='com.example.type2', + pubsub_name='pubsub', + spec_version='1.0', + ) + yield api_v1.SubscribeTopicEventsResponseAlpha1(event_message=msg3) + # Simulate the stream being closed with an error + context.abort(grpc.StatusCode.CANCELLED, 'Stream closed by server') def SaveState(self, request, context): self.check_for_exception(context) diff --git a/tests/clients/test_dapr_grpc_client.py b/tests/clients/test_dapr_grpc_client.py index 3588867e..4ef9e02a 100644 --- a/tests/clients/test_dapr_grpc_client.py +++ b/tests/clients/test_dapr_grpc_client.py @@ -34,9 +34,13 @@ from dapr.clients.grpc._request import TransactionalStateOperation from dapr.clients.grpc._state import StateOptions, Consistency, Concurrency, StateItem from dapr.clients.grpc._crypto import EncryptOptions, DecryptOptions -from dapr.clients.grpc._response import (ConfigurationItem, ConfigurationResponse, - ConfigurationWatcher, UnlockResponseStatus, - WorkflowRuntimeStatus, TopicEventResponse, ) +from dapr.clients.grpc._response import ( + ConfigurationItem, + ConfigurationResponse, + ConfigurationWatcher, + UnlockResponseStatus, + WorkflowRuntimeStatus, +) class DaprGrpcClientTests(unittest.TestCase): @@ -263,20 +267,33 @@ def test_subscribe_topic(self): subscription = dapr.subscribe(pubsub_name='pubsub', topic='example') # First message - message1 = subscription.next_message(timeout=5) + message1 = subscription.next_message() subscription.respond_success(message1) - self.assertEqual('123', message1.id) - self.assertEqual(b'hello1', message1.data) - self.assertEqual('TOPIC_A', message1.topic) + self.assertEqual('123', message1.id()) + self.assertEqual('app1', message1.source()) + self.assertEqual('com.example.type2', message1.type()) + self.assertEqual('1.0', message1.spec_version()) + self.assertEqual('text/plain', message1.data_content_type()) + self.assertEqual('TOPIC_A', message1.topic()) + self.assertEqual('pubsub', message1.pubsub_name()) + self.assertEqual(b'hello2', message1.raw_data()) + self.assertEqual('text/plain', message1.data_content_type()) + self.assertEqual('hello2', message1.data()) # Second message - message2 = subscription.next_message(timeout=5) + message2 = subscription.next_message() subscription.respond_success(message2) - self.assertEqual('456', message2.id) - self.assertEqual(b'hello2', message2.data) - self.assertEqual('TOPIC_A', message2.topic) + self.assertEqual('456', message2.id()) + self.assertEqual('app1', message2.source()) + self.assertEqual('com.example.type2', message2.type()) + self.assertEqual('1.0', message2.spec_version()) + self.assertEqual('TOPIC_A', message2.topic()) + self.assertEqual('pubsub', message2.pubsub_name()) + self.assertEqual(b'{"a": 1}', message2.raw_data()) + self.assertEqual('application/json', message2.data_content_type()) + self.assertEqual({'a': 1}, message2.data()) def test_subscribe_topic_early_close(self): dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') @@ -284,8 +301,7 @@ def test_subscribe_topic_early_close(self): subscription.close() with self.assertRaises(StreamInactiveError): - subscription.next_message(timeout=5) - + subscription.next_message() @patch.object(settings, 'DAPR_API_TOKEN', 'test-token') def test_dapr_api_token_insertion(self): diff --git a/tox.ini b/tox.ini index e7f9a672..6400e329 100644 --- a/tox.ini +++ b/tox.ini @@ -50,6 +50,7 @@ commands = ./validate.sh metadata ./validate.sh error_handling ./validate.sh pubsub-simple + ./validate.sh pubsub-streaming ./validate.sh state_store ./validate.sh state_store_query ./validate.sh secret_store From fbd12a7c5b0d6c856ff7f52917c725b159a7bd98 Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Mon, 23 Sep 2024 01:42:15 +0100 Subject: [PATCH 04/26] example fix Signed-off-by: Elena Kolevska fixes typing Signed-off-by: Elena Kolevska more readable example Signed-off-by: Elena Kolevska linter Signed-off-by: Elena Kolevska --- dapr/clients/grpc/subscription.py | 46 +++++++++++++------------ examples/pubsub-streaming/README.md | 16 ++++----- examples/pubsub-streaming/subscriber.py | 33 ++++++++---------- 3 files changed, 44 insertions(+), 51 deletions(-) diff --git a/dapr/clients/grpc/subscription.py b/dapr/clients/grpc/subscription.py index 1cdf1ef5..5ca30119 100644 --- a/dapr/clients/grpc/subscription.py +++ b/dapr/clients/grpc/subscription.py @@ -1,12 +1,13 @@ import json -import grpc +from grpc import StreamStreamMultiCallable, RpcError, StatusCode # type: ignore from dapr.clients.exceptions import StreamInactiveError from dapr.clients.grpc._response import TopicEventResponse from dapr.proto import api_v1, appcallback_v1 import queue import threading +from typing import Optional def success(): @@ -28,11 +29,11 @@ def __init__(self, stub, pubsub_name, topic, metadata=None, dead_letter_topic=No self.topic = topic self.metadata = metadata or {} self.dead_letter_topic = dead_letter_topic or '' - self._stream = None - self._response_thread = None - self._send_queue = queue.Queue() - self._receive_queue = queue.Queue() - self._stream_active = False + self._stream: Optional[StreamStreamMultiCallable] = None # Type annotation for gRPC stream + self._response_thread: Optional[threading.Thread] = None # Type for thread + self._send_queue: queue.Queue = queue.Queue() # Type annotation for send queue + self._receive_queue: queue.Queue = queue.Queue() # Type annotation for receive queue + self._stream_active: bool = False self._stream_lock = threading.Lock() # Protects _stream_active def start(self): @@ -55,9 +56,8 @@ def outgoing_request_iterator(): # Start sending back acknowledgement messages from the send queue while self._is_stream_active(): try: - response = self._send_queue.get() - # The above blocks until a message is available or the stream is closed - # so that's why we need to check again if the stream is still active + response = self._send_queue.get(timeout=1) + # Check again if the stream is still active if not self._is_stream_active(): break yield response @@ -76,17 +76,19 @@ def outgoing_request_iterator(): def _handle_incoming_messages(self): try: - # The first message dapr sends on the stream is for signalling only, so discard it - next(self._stream) - - # Read messages from the stream and put them in the receive queue - for message in self._stream: - if self._is_stream_active(): - self._receive_queue.put(message.event_message) - else: - break - except grpc.RpcError as e: - if e.code() != grpc.StatusCode.CANCELLED: + # Check if the stream is not None + if self._stream is not None: + # The first message dapr sends on the stream is for signalling only, so discard it + next(self._stream) + + # Read messages from the stream and put them in the receive queue + for message in self._stream: + if self._is_stream_active(): + self._receive_queue.put(message.event_message) + else: + break + except RpcError as e: + if e.code() != StatusCode.CANCELLED: print(f'gRPC error in stream: {e.details()}, Status Code: {e.code()}') except Exception as e: raise Exception(f'Error while handling responses: {e}') @@ -157,8 +159,8 @@ def close(self): if self._stream: try: self._stream.cancel() - except grpc.RpcError as e: - if e.code() != grpc.StatusCode.CANCELLED: + except RpcError as e: + if e.code() != StatusCode.CANCELLED: raise Exception(f'Error while closing stream: {e}') except Exception as e: raise Exception(f'Error while closing stream: {e}') diff --git a/examples/pubsub-streaming/README.md b/examples/pubsub-streaming/README.md index 5d80cf0b..f0fe0d93 100644 --- a/examples/pubsub-streaming/README.md +++ b/examples/pubsub-streaming/README.md @@ -27,16 +27,11 @@ Run the following command in a terminal/command prompt: diff --git a/examples/pubsub-streaming/subscriber.py b/examples/pubsub-streaming/subscriber.py index f6f9078a..701f5775 100644 --- a/examples/pubsub-streaming/subscriber.py +++ b/examples/pubsub-streaming/subscriber.py @@ -17,25 +17,20 @@ def main(): try: for i in range(5): - try: - message = subscription.next_message() - if message is None: - print('No message received within timeout period.') - continue - - # Process the message - response_status = process_message(message) - - if response_status == 'success': - subscription.respond_success(message) - elif response_status == 'retry': - subscription.respond_retry(message) - elif response_status == 'drop': - subscription.respond_drop(message) - - except Exception as e: - print(f'Error getting message: {e}') - break + message = subscription.next_message() + if message is None: + print('No message received within timeout period.') + continue + + # Process the message + response_status = process_message(message) + + if response_status == 'success': + subscription.respond_success(message) + elif response_status == 'retry': + subscription.respond_retry(message) + elif response_status == 'drop': + subscription.respond_drop(message) finally: subscription.close() From 430d9515db22edfaec75b422172a809b3e30d184 Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Mon, 23 Sep 2024 11:27:25 +0100 Subject: [PATCH 05/26] examples fix Signed-off-by: Elena Kolevska --- examples/invoke-binding/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/invoke-binding/README.md b/examples/invoke-binding/README.md index f1a44417..b95f5d7a 100644 --- a/examples/invoke-binding/README.md +++ b/examples/invoke-binding/README.md @@ -29,7 +29,7 @@ sleep: 30 1. Start the kafka containers using docker-compose ```bash -docker-compose -f ./docker-compose-single-kafka.yml up -d +docker compose -f ./docker-compose-single-kafka.yml up -d ``` @@ -91,7 +91,7 @@ dapr stop --app-id receiver For kafka cleanup, run the following code: ```bash -docker-compose -f ./docker-compose-single-kafka.yml down +docker compose -f ./docker-compose-single-kafka.yml down ``` From 9d67b7890790b7026e7882d1415fc5fa6c3fcfe7 Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Mon, 23 Sep 2024 13:40:29 +0100 Subject: [PATCH 06/26] Adds support for api token Signed-off-by: Elena Kolevska --- dapr/clients/grpc/interceptors.py | 28 ++++++++++++--- dapr/clients/grpc/subscription.py | 45 ++++++++++++------------- examples/pubsub-streaming/subscriber.py | 2 -- 3 files changed, 46 insertions(+), 29 deletions(-) diff --git a/dapr/clients/grpc/interceptors.py b/dapr/clients/grpc/interceptors.py index 22098f53..adda29c1 100644 --- a/dapr/clients/grpc/interceptors.py +++ b/dapr/clients/grpc/interceptors.py @@ -1,7 +1,7 @@ from collections import namedtuple from typing import List, Tuple -from grpc import UnaryUnaryClientInterceptor, ClientCallDetails # type: ignore +from grpc import UnaryUnaryClientInterceptor, ClientCallDetails, StreamStreamClientInterceptor # type: ignore from dapr.conf import settings @@ -38,7 +38,7 @@ def intercept_unary_unary(self, continuation, client_call_details, request): return continuation(client_call_details, request) -class DaprClientInterceptor(UnaryUnaryClientInterceptor): +class DaprClientInterceptor(UnaryUnaryClientInterceptor, StreamStreamClientInterceptor): """The class implements a UnaryUnaryClientInterceptor from grpc to add an interceptor to add additional headers to all calls as needed. @@ -91,8 +91,8 @@ def _intercept_call(self, client_call_details: ClientCallDetails) -> ClientCallD return new_call_details def intercept_unary_unary(self, continuation, client_call_details, request): - """This method intercepts a unary-unary gRPC call. This is the implementation of the - abstract method defined in UnaryUnaryClientInterceptor defined in grpc. This is invoked + """This method intercepts a unary-unary gRPC call. It is the implementation of the + abstract method defined in UnaryUnaryClientInterceptor defined in grpc. It's invoked automatically by grpc based on the order in which interceptors are added to the channel. Args: @@ -108,3 +108,23 @@ def intercept_unary_unary(self, continuation, client_call_details, request): # Call continuation response = continuation(new_call_details, request) return response + + def intercept_stream_stream(self, continuation, client_call_details, request_iterator): + """This method intercepts a stream-stream gRPC call. It is the implementation of the + abstract method defined in StreamStreamClientInterceptor defined in grpc. It's invoked + automatically by grpc based on the order in which interceptors are added to the channel. + + Args: + continuation: a callable to be invoked to continue with the RPC or next interceptor + client_call_details: a ClientCallDetails object describing the outgoing RPC + request_iterator: the request value for the RPC + + Returns: + A response object after invoking the continuation callable + """ + # Pre-process or intercept call + + new_call_details = self._intercept_call(client_call_details) + # Call continuation + response = continuation(new_call_details, request_iterator) + return response diff --git a/dapr/clients/grpc/subscription.py b/dapr/clients/grpc/subscription.py index 5ca30119..2c8c18e3 100644 --- a/dapr/clients/grpc/subscription.py +++ b/dapr/clients/grpc/subscription.py @@ -1,27 +1,16 @@ import json -from grpc import StreamStreamMultiCallable, RpcError, StatusCode # type: ignore +from grpc import RpcError, StatusCode, Call # type: ignore from dapr.clients.exceptions import StreamInactiveError from dapr.clients.grpc._response import TopicEventResponse +from dapr.clients.health import DaprHealth from dapr.proto import api_v1, appcallback_v1 import queue import threading from typing import Optional -def success(): - return appcallback_v1.TopicEventResponse.SUCCESS - - -def retry(): - return appcallback_v1.TopicEventResponse.RETRY - - -def drop(): - return appcallback_v1.TopicEventResponse.DROP - - class Subscription: def __init__(self, stub, pubsub_name, topic, metadata=None, dead_letter_topic=None): self._stub = stub @@ -29,10 +18,10 @@ def __init__(self, stub, pubsub_name, topic, metadata=None, dead_letter_topic=No self.topic = topic self.metadata = metadata or {} self.dead_letter_topic = dead_letter_topic or '' - self._stream: Optional[StreamStreamMultiCallable] = None # Type annotation for gRPC stream - self._response_thread: Optional[threading.Thread] = None # Type for thread - self._send_queue: queue.Queue = queue.Queue() # Type annotation for send queue - self._receive_queue: queue.Queue = queue.Queue() # Type annotation for receive queue + self._stream: Optional[Call] = None + self._response_thread: Optional[threading.Thread] = None + self._send_queue: queue.Queue = queue.Queue() + self._receive_queue: queue.Queue = queue.Queue() self._stream_active: bool = False self._stream_lock = threading.Lock() # Protects _stream_active @@ -56,7 +45,7 @@ def outgoing_request_iterator(): # Start sending back acknowledgement messages from the send queue while self._is_stream_active(): try: - response = self._send_queue.get(timeout=1) + response = self._send_queue.get() # Check again if the stream is still active if not self._is_stream_active(): break @@ -75,6 +64,7 @@ def outgoing_request_iterator(): self._response_thread.start() def _handle_incoming_messages(self): + reconnect = False try: # Check if the stream is not None if self._stream is not None: @@ -83,17 +73,26 @@ def _handle_incoming_messages(self): # Read messages from the stream and put them in the receive queue for message in self._stream: - if self._is_stream_active(): - self._receive_queue.put(message.event_message) - else: - break + self._receive_queue.put(message.event_message) except RpcError as e: - if e.code() != StatusCode.CANCELLED: + if e.code() == StatusCode.UNAVAILABLE: + print('Stream unavailable, attempting to reconnect...') + reconnect = True + elif e.code() != StatusCode.CANCELLED: print(f'gRPC error in stream: {e.details()}, Status Code: {e.code()}') + except Exception as e: raise Exception(f'Error while handling responses: {e}') finally: self._set_stream_inactive() + if reconnect: + self.reconnect_stream() + + def reconnect_stream(self): + DaprHealth.wait_until_ready() + print('Attempting to reconnect...') + self.close() + self.start() def next_message(self, timeout=None): msg = self.read_message_from_queue(self._receive_queue, timeout=timeout) diff --git a/examples/pubsub-streaming/subscriber.py b/examples/pubsub-streaming/subscriber.py index 701f5775..8b396281 100644 --- a/examples/pubsub-streaming/subscriber.py +++ b/examples/pubsub-streaming/subscriber.py @@ -1,6 +1,4 @@ from dapr.clients import DaprClient -from dapr.clients.grpc._response import TopicEventResponse -from dapr.clients.grpc.subscription import success, retry, drop def process_message(message): From 2a4a99be68d622540103623b5fc2bb8096cbf3a9 Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Tue, 24 Sep 2024 00:26:23 +0100 Subject: [PATCH 07/26] clean up Signed-off-by: Elena Kolevska --- dapr/clients/grpc/client.py | 12 ++++++++++++ tests/clients/fake_dapr_server.py | 2 -- tests/clients/test_dapr_grpc_client.py | 4 ++-- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/dapr/clients/grpc/client.py b/dapr/clients/grpc/client.py index 81e65c7e..63fb9a31 100644 --- a/dapr/clients/grpc/client.py +++ b/dapr/clients/grpc/client.py @@ -483,6 +483,18 @@ def publish_event( return DaprResponse(call.initial_metadata()) def subscribe(self, pubsub_name, topic, metadata=None, dead_letter_topic=None): + """ + Subscribe to a topic with a bidirectional stream + + Args: + pubsub_name (str): The name of the pubsub component. + topic (str): The name of the topic. + metadata (Optional[Dict]): Additional metadata for the subscription. + dead_letter_topic (Optional[str]): Name of the dead-letter topic. + + Returns: + Subscription: The Subscription object managing the stream. + """ subscription = Subscription(self._stub, pubsub_name, topic, metadata, dead_letter_topic) subscription.start() return subscription diff --git a/tests/clients/fake_dapr_server.py b/tests/clients/fake_dapr_server.py index 1080ab30..a910accc 100644 --- a/tests/clients/fake_dapr_server.py +++ b/tests/clients/fake_dapr_server.py @@ -205,8 +205,6 @@ def SubscribeTopicEventsAlpha1(self, request_iterator, context): spec_version='1.0', ) yield api_v1.SubscribeTopicEventsResponseAlpha1(event_message=msg3) - # Simulate the stream being closed with an error - context.abort(grpc.StatusCode.CANCELLED, 'Stream closed by server') def SaveState(self, request, context): self.check_for_exception(context) diff --git a/tests/clients/test_dapr_grpc_client.py b/tests/clients/test_dapr_grpc_client.py index 4ef9e02a..ac411745 100644 --- a/tests/clients/test_dapr_grpc_client.py +++ b/tests/clients/test_dapr_grpc_client.py @@ -266,7 +266,7 @@ def test_subscribe_topic(self): dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') subscription = dapr.subscribe(pubsub_name='pubsub', topic='example') - # First message + # First message - text message1 = subscription.next_message() subscription.respond_success(message1) @@ -281,7 +281,7 @@ def test_subscribe_topic(self): self.assertEqual('text/plain', message1.data_content_type()) self.assertEqual('hello2', message1.data()) - # Second message + # Second message - json message2 = subscription.next_message() subscription.respond_success(message2) From ecc86969da725b478c60af15e2ddacd399fa63a1 Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Tue, 24 Sep 2024 00:51:28 +0100 Subject: [PATCH 08/26] Adds docs Signed-off-by: Elena Kolevska --- .../en/python-sdk-docs/python-client.md | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/daprdocs/content/en/python-sdk-docs/python-client.md b/daprdocs/content/en/python-sdk-docs/python-client.md index 52d8b2e8..6d646ff0 100644 --- a/daprdocs/content/en/python-sdk-docs/python-client.md +++ b/daprdocs/content/en/python-sdk-docs/python-client.md @@ -255,6 +255,36 @@ def mytopic_important(event: v1.Event) -> None: - For more information about pub/sub, visit [How-To: Publish & subscribe]({{< ref howto-publish-subscribe.md >}}). - Visit [Python SDK examples](https://github.com/dapr/python-sdk/tree/master/examples/pubsub-simple) for code samples and instructions to try out pub/sub. +#### Subscribe to messages with streaming +You can subscribe to messages from a PubSub topic with streaming by using the `subscribe` method. +This method will return a `Subscription` object on which you can call the `next_message` method to +yield messages as they arrive. +When done using the subscription, you should call the `close` method to stop the subscription. + +```python + with DaprClient() as client: + subscription = client.subscribe( + pubsub_name='pubsub', topic='TOPIC_A', dead_letter_topic='TOPIC_A_DEAD' + ) + + try: + for i in range(5): + message = subscription.next_message(1) + if message is None: + print('No message received within timeout period.') + continue + + # Process the message + # ... + + # Return the status based on the processing result + subscription.respond_success(message) + # or subscription.respond_retry(message) + # or subscription.respond_drop(message) + + finally: + subscription.close() +``` ### Interact with output bindings From 2caf0720fe84986df3e2163d1fc2928a8b67e1b8 Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Tue, 24 Sep 2024 01:05:32 +0100 Subject: [PATCH 09/26] more small tweaks Signed-off-by: Elena Kolevska --- dapr/clients/exceptions.py | 4 -- dapr/clients/grpc/subscription.py | 12 ++++- .../en/python-sdk-docs/python-client.md | 48 +++++++++++-------- examples/pubsub-streaming/subscriber.py | 15 +++++- tests/clients/test_dapr_grpc_client.py | 7 ++- 5 files changed, 59 insertions(+), 27 deletions(-) diff --git a/dapr/clients/exceptions.py b/dapr/clients/exceptions.py index c872b65a..91bc04a8 100644 --- a/dapr/clients/exceptions.py +++ b/dapr/clients/exceptions.py @@ -132,7 +132,3 @@ def serialize_status_detail(status_detail): if not status_detail: return None return MessageToDict(status_detail, preserving_proto_field_name=True) - - -class StreamInactiveError(Exception): - pass diff --git a/dapr/clients/grpc/subscription.py b/dapr/clients/grpc/subscription.py index 2c8c18e3..b2a88b9d 100644 --- a/dapr/clients/grpc/subscription.py +++ b/dapr/clients/grpc/subscription.py @@ -2,7 +2,6 @@ from grpc import RpcError, StatusCode, Call # type: ignore -from dapr.clients.exceptions import StreamInactiveError from dapr.clients.grpc._response import TopicEventResponse from dapr.clients.health import DaprHealth from dapr.proto import api_v1, appcallback_v1 @@ -95,6 +94,13 @@ def reconnect_stream(self): self.start() def next_message(self, timeout=None): + """ + Get the next message from the receive queue. + @param timeout: The time in seconds to wait for a message before returning None. + If None, wait indefinitely. + @return: The next message from the queue, + or None if no message is received within the timeout. + """ msg = self.read_message_from_queue(self._receive_queue, timeout=timeout) if msg is None: @@ -241,3 +247,7 @@ def _parse_data_content(self): except Exception as e: # Log or handle any unexpected exceptions print(f'Error parsing media type: {e}') + + +class StreamInactiveError(Exception): + pass diff --git a/daprdocs/content/en/python-sdk-docs/python-client.md b/daprdocs/content/en/python-sdk-docs/python-client.md index 6d646ff0..900546ed 100644 --- a/daprdocs/content/en/python-sdk-docs/python-client.md +++ b/daprdocs/content/en/python-sdk-docs/python-client.md @@ -263,27 +263,37 @@ When done using the subscription, you should call the `close` method to stop the ```python with DaprClient() as client: - subscription = client.subscribe( - pubsub_name='pubsub', topic='TOPIC_A', dead_letter_topic='TOPIC_A_DEAD' - ) - - try: - for i in range(5): + subscription = client.subscribe( + pubsub_name='pubsub', topic='TOPIC_A', dead_letter_topic='TOPIC_A_DEAD' + ) + + try: + i = 0 + while i < 5: + try: message = subscription.next_message(1) - if message is None: - print('No message received within timeout period.') - continue - - # Process the message - # ... - - # Return the status based on the processing result + except StreamInactiveError as e: + print('Stream is inactive. Retrying...') + time.sleep(5) + continue + if message is None: + print('No message received within timeout period.') + continue + + # Process the message + response_status = process_message(message) + + if response_status == 'success': subscription.respond_success(message) - # or subscription.respond_retry(message) - # or subscription.respond_drop(message) - - finally: - subscription.close() + elif response_status == 'retry': + subscription.respond_retry(message) + elif response_status == 'drop': + subscription.respond_drop(message) + + i += 1 + + finally: + subscription.close() ``` ### Interact with output bindings diff --git a/examples/pubsub-streaming/subscriber.py b/examples/pubsub-streaming/subscriber.py index 8b396281..c8cc8205 100644 --- a/examples/pubsub-streaming/subscriber.py +++ b/examples/pubsub-streaming/subscriber.py @@ -1,4 +1,7 @@ +import time + from dapr.clients import DaprClient +from dapr.clients.grpc.subscription import StreamInactiveError def process_message(message): @@ -14,8 +17,14 @@ def main(): ) try: - for i in range(5): - message = subscription.next_message() + i = 0 + while i < 5: + try: + message = subscription.next_message(1) + except StreamInactiveError as e: + print('Stream is inactive. Retrying...') + time.sleep(5) + continue if message is None: print('No message received within timeout period.') continue @@ -30,6 +39,8 @@ def main(): elif response_status == 'drop': subscription.respond_drop(message) + i += 1 + finally: subscription.close() diff --git a/tests/clients/test_dapr_grpc_client.py b/tests/clients/test_dapr_grpc_client.py index ac411745..019ea84f 100644 --- a/tests/clients/test_dapr_grpc_client.py +++ b/tests/clients/test_dapr_grpc_client.py @@ -24,9 +24,10 @@ from google.rpc import status_pb2, code_pb2 -from dapr.clients.exceptions import DaprGrpcError, StreamInactiveError +from dapr.clients.exceptions import DaprGrpcError from dapr.clients.grpc.client import DaprGrpcClient from dapr.clients import DaprClient +from dapr.clients.grpc.subscription import StreamInactiveError from dapr.proto import common_v1 from .fake_dapr_server import FakeDaprSidecar from dapr.conf import settings @@ -295,6 +296,10 @@ def test_subscribe_topic(self): self.assertEqual('application/json', message2.data_content_type()) self.assertEqual({'a': 1}, message2.data()) + # Third call with timeout + message3 = subscription.next_message(1) + self.assertIsNone(message3) + def test_subscribe_topic_early_close(self): dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') subscription = dapr.subscribe(pubsub_name='pubsub', topic='example') From 072296ef37caa75cae8903baed12c412a5432b16 Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Sat, 28 Sep 2024 22:50:18 +0100 Subject: [PATCH 10/26] cleanups and tests Signed-off-by: Elena Kolevska --- dapr/clients/grpc/subscription.py | 10 +++- examples/pubsub-streaming/publisher.py | 1 + examples/pubsub-streaming/subscriber.py | 2 +- tests/clients/test_subscription.py | 69 +++++++++++++++++++++++++ 4 files changed, 79 insertions(+), 3 deletions(-) create mode 100644 tests/clients/test_subscription.py diff --git a/dapr/clients/grpc/subscription.py b/dapr/clients/grpc/subscription.py index b2a88b9d..efdbebfc 100644 --- a/dapr/clients/grpc/subscription.py +++ b/dapr/clients/grpc/subscription.py @@ -177,7 +177,8 @@ def close(self): class SubscriptionMessage: - def __init__(self, msg): + + def __init__(self, msg: TopicEventRequest): self._id = msg.id self._source = msg.source self._type = msg.type @@ -188,6 +189,11 @@ def __init__(self, msg): self._raw_data = msg.data self._extensions = msg.extensions self._data = None + try: + self._extensions = MessageToDict(msg.extensions) + except Exception as e: + self._extensions = {} + print(f'Error parsing extensions: {e}') # Parse the content based on its media type if self._raw_data and len(self._raw_data) > 0: @@ -235,7 +241,7 @@ def _parse_data_content(self): try: self._data = self._raw_data.decode('utf-8') except UnicodeDecodeError: - pass + print(f'Error decoding message data from topic {self._topic} as UTF-8') elif self._data_content_type.startswith( 'application/' ) and self._data_content_type.endswith('+json'): diff --git a/examples/pubsub-streaming/publisher.py b/examples/pubsub-streaming/publisher.py index f7af0f10..9c18ac3c 100644 --- a/examples/pubsub-streaming/publisher.py +++ b/examples/pubsub-streaming/publisher.py @@ -28,6 +28,7 @@ topic_name='TOPIC_A', data=json.dumps(req_data), data_content_type='application/json', + publish_metadata={'ttlInSeconds': '100', 'rawPayload': 'false'} ) # Print the request diff --git a/examples/pubsub-streaming/subscriber.py b/examples/pubsub-streaming/subscriber.py index c8cc8205..2f8fcf00 100644 --- a/examples/pubsub-streaming/subscriber.py +++ b/examples/pubsub-streaming/subscriber.py @@ -19,6 +19,7 @@ def main(): try: i = 0 while i < 5: + i += 1 try: message = subscription.next_message(1) except StreamInactiveError as e: @@ -39,7 +40,6 @@ def main(): elif response_status == 'drop': subscription.respond_drop(message) - i += 1 finally: subscription.close() diff --git a/tests/clients/test_subscription.py b/tests/clients/test_subscription.py new file mode 100644 index 00000000..253f4454 --- /dev/null +++ b/tests/clients/test_subscription.py @@ -0,0 +1,69 @@ +from dapr.clients.grpc.subscription import SubscriptionMessage +from dapr.proto.runtime.v1.appcallback_pb2 import TopicEventRequest +from google.protobuf.struct_pb2 import Struct + +import unittest + + +class SubscriptionMessageTests(unittest.TestCase): + def test_subscription_message_init_raw_text(self): + extensions = Struct() + extensions["field1"] = "value1" + extensions["field2"] = 42 + extensions["field3"] = True + + msg = TopicEventRequest(id='id', data=b'hello', data_content_type='text/plain', + topic='topicA', pubsub_name='pubsub_name', source='source', + type='type', spec_version='spec_version', path='path', + extensions=extensions) + subscription_message = SubscriptionMessage(msg=msg) + + self.assertEqual('id', subscription_message.id()) + self.assertEqual('source', subscription_message.source()) + self.assertEqual('type', subscription_message.type()) + self.assertEqual('spec_version', subscription_message.spec_version()) + self.assertEqual('text/plain', subscription_message.data_content_type()) + self.assertEqual('topicA', subscription_message.topic()) + self.assertEqual('pubsub_name', subscription_message.pubsub_name()) + self.assertEqual(b'hello', subscription_message.raw_data()) + self.assertEqual('hello', subscription_message.data()) + self.assertEqual({'field1': 'value1', "field2": 42, "field3": True}, + subscription_message.extensions()) + + def test_subscription_message_init_raw_text_non_utf(self): + msg = TopicEventRequest(id='id', data=b'\x80\x81\x82', data_content_type='text/plain', + topic='topicA', pubsub_name='pubsub_name', source='source', + type='type', spec_version='spec_version', path='path') + subscription_message = SubscriptionMessage(msg=msg) + + self.assertEqual(b'\x80\x81\x82', subscription_message.raw_data()) + self.assertIsNone(subscription_message.data()) + + def test_subscription_message_init_json(self): + msg = TopicEventRequest(id='id', data=b'{"a": 1}', data_content_type='application/json', + topic='topicA', pubsub_name='pubsub_name', source='source', + type='type', spec_version='spec_version', path='path') + subscription_message = SubscriptionMessage(msg=msg) + + self.assertEqual(b'{"a": 1}', subscription_message.raw_data()) + self.assertEqual({"a": 1}, subscription_message.data()) + print(subscription_message.data()["a"]) + + def test_subscription_message_init_json_faimly(self): + msg = TopicEventRequest(id='id', data=b'{"a": 1}', + data_content_type='application/vnd.api+json', topic='topicA', + pubsub_name='pubsub_name', source='source', type='type', + spec_version='spec_version', path='path') + subscription_message = SubscriptionMessage(msg=msg) + + self.assertEqual(b'{"a": 1}', subscription_message.raw_data()) + self.assertEqual({"a": 1}, subscription_message.data()) + + def test_subscription_message_init_unknown_content_type(self): + msg = TopicEventRequest(id='id', data=b'{"a": 1}', data_content_type='unknown/content-type', + topic='topicA', pubsub_name='pubsub_name', source='source', + type='type', spec_version='spec_version', path='path') + subscription_message = SubscriptionMessage(msg=msg) + + self.assertEqual(b'{"a": 1}', subscription_message.raw_data()) + self.assertIsNone(subscription_message.data()) From bf52eec215bc15d5a8b04b4c148d152802058e90 Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Sun, 29 Sep 2024 00:07:31 +0100 Subject: [PATCH 11/26] Removes receive queue Signed-off-by: Elena Kolevska --- dapr/clients/grpc/client.py | 16 +++- dapr/clients/grpc/subscription.py | 116 +++++++++--------------- examples/pubsub-streaming/subscriber.py | 4 +- tests/clients/fake_dapr_server.py | 28 ++++-- tests/clients/test_dapr_grpc_client.py | 26 +++++- 5 files changed, 100 insertions(+), 90 deletions(-) diff --git a/dapr/clients/grpc/client.py b/dapr/clients/grpc/client.py index 63fb9a31..590279be 100644 --- a/dapr/clients/grpc/client.py +++ b/dapr/clients/grpc/client.py @@ -482,20 +482,30 @@ def publish_event( return DaprResponse(call.initial_metadata()) - def subscribe(self, pubsub_name, topic, metadata=None, dead_letter_topic=None): + def subscribe(self, + pubsub_name: str, + topic: str, + metadata: Optional[MetadataTuple] = None, + dead_letter_topic: Optional[str] = None, + timeout: Optional[int] = None + ) -> Subscription: """ Subscribe to a topic with a bidirectional stream Args: pubsub_name (str): The name of the pubsub component. topic (str): The name of the topic. - metadata (Optional[Dict]): Additional metadata for the subscription. + metadata (Optional[MetadataTuple]): Additional metadata for the subscription. dead_letter_topic (Optional[str]): Name of the dead-letter topic. + timeout (Optional[int]): The time in seconds to wait for a message before returning None + If not set, the `next_message` method will block indefinitely + until a message is received. Returns: Subscription: The Subscription object managing the stream. """ - subscription = Subscription(self._stub, pubsub_name, topic, metadata, dead_letter_topic) + subscription = Subscription(self._stub, pubsub_name, topic, metadata, dead_letter_topic, + timeout) subscription.start() return subscription diff --git a/dapr/clients/grpc/subscription.py b/dapr/clients/grpc/subscription.py index efdbebfc..4453d7fb 100644 --- a/dapr/clients/grpc/subscription.py +++ b/dapr/clients/grpc/subscription.py @@ -1,5 +1,6 @@ import json +from google.protobuf.json_format import MessageToDict from grpc import RpcError, StatusCode, Call # type: ignore from dapr.clients.grpc._response import TopicEventResponse @@ -9,9 +10,11 @@ import threading from typing import Optional +from dapr.proto.runtime.v1.appcallback_pb2 import TopicEventRequest + class Subscription: - def __init__(self, stub, pubsub_name, topic, metadata=None, dead_letter_topic=None): + def __init__(self, stub, pubsub_name, topic, metadata=None, dead_letter_topic=None, timeout=None): self._stub = stub self.pubsub_name = pubsub_name self.topic = topic @@ -20,34 +23,30 @@ def __init__(self, stub, pubsub_name, topic, metadata=None, dead_letter_topic=No self._stream: Optional[Call] = None self._response_thread: Optional[threading.Thread] = None self._send_queue: queue.Queue = queue.Queue() - self._receive_queue: queue.Queue = queue.Queue() self._stream_active: bool = False self._stream_lock = threading.Lock() # Protects _stream_active + self._timeout = timeout def start(self): def outgoing_request_iterator(): """ - Generator function to create the request iterator for the stream + Generator function to create the request iterator for the stream. + This sends the initial request to establish the stream. """ try: # Send InitialRequest needed to establish the stream initial_request = api_v1.SubscribeTopicEventsRequestAlpha1( initial_request=api_v1.SubscribeTopicEventsRequestInitialAlpha1( - pubsub_name=self.pubsub_name, - topic=self.topic, + pubsub_name=self.pubsub_name, topic=self.topic, metadata=self.metadata or {}, - dead_letter_topic=self.dead_letter_topic or '', - ) - ) + dead_letter_topic=self.dead_letter_topic or '', )) yield initial_request # Start sending back acknowledgement messages from the send queue - while self._is_stream_active(): + while self._is_stream_active(): # TODO check if this is correct try: - response = self._send_queue.get() - # Check again if the stream is still active - if not self._is_stream_active(): - break + # Wait for responses/acknowledgements to send from the send queue. + response = self._send_queue.get() # TODO check timeout yield response except queue.Empty: continue @@ -55,58 +54,49 @@ def outgoing_request_iterator(): raise Exception(f'Error in request iterator: {e}') # Create the bidirectional stream - self._stream = self._stub.SubscribeTopicEventsAlpha1(outgoing_request_iterator()) + self._stream = self._stub.SubscribeTopicEventsAlpha1(outgoing_request_iterator(), + timeout=self._timeout) self._set_stream_active() - - # Start a thread to handle incoming messages - self._response_thread = threading.Thread(target=self._handle_incoming_messages, daemon=True) - self._response_thread.start() - - def _handle_incoming_messages(self): - reconnect = False - try: - # Check if the stream is not None - if self._stream is not None: - # The first message dapr sends on the stream is for signalling only, so discard it - next(self._stream) - - # Read messages from the stream and put them in the receive queue - for message in self._stream: - self._receive_queue.put(message.event_message) - except RpcError as e: - if e.code() == StatusCode.UNAVAILABLE: - print('Stream unavailable, attempting to reconnect...') - reconnect = True - elif e.code() != StatusCode.CANCELLED: - print(f'gRPC error in stream: {e.details()}, Status Code: {e.code()}') - - except Exception as e: - raise Exception(f'Error while handling responses: {e}') - finally: - self._set_stream_inactive() - if reconnect: - self.reconnect_stream() + next(self._stream) # discard the initial message def reconnect_stream(self): + self.close() DaprHealth.wait_until_ready() print('Attempting to reconnect...') - self.close() self.start() - def next_message(self, timeout=None): + def next_message(self): """ Get the next message from the receive queue. - @param timeout: The time in seconds to wait for a message before returning None. - If None, wait indefinitely. @return: The next message from the queue, or None if no message is received within the timeout. """ - msg = self.read_message_from_queue(self._receive_queue, timeout=timeout) + if not self._is_stream_active(): + raise StreamInactiveError("Stream is not active") - if msg is None: - return None + try: + # Read the next message from the stream directly + if self._stream is not None: + message = next(self._stream, None) + if message is None: + return None + return SubscriptionMessage(message.event_message) + except RpcError as e: + if e.code() == StatusCode.UNAVAILABLE: + print( + f'gRPC error while reading from stream: {e.details()}, Status Code: {e.code()}') + self.reconnect_stream() + elif e.code() == StatusCode.DEADLINE_EXCEEDED: + # A message hasn't been received on the stream in `self._timeout` seconds + # so return control back to app + return None + elif e.code() != StatusCode.CANCELLED: + raise Exception(f'gRPC error while reading from subscription stream: {e.details()} ' + f'Status Code: {e.code()}') + except Exception as e: + raise Exception(f'Error while fetching message: {e}') - return SubscriptionMessage(msg) + return None def _respond(self, message, status): try: @@ -115,8 +105,9 @@ def _respond(self, message, status): id=message.id(), status=status ) msg = api_v1.SubscribeTopicEventsRequestAlpha1(event_processed=response) - - self.send_message_to_queue(self._send_queue, msg) + if not self._is_stream_active(): + raise StreamInactiveError('Stream is not active') + self._send_queue.put(msg) except Exception as e: print(f'Exception in send_message: {e}') @@ -129,19 +120,6 @@ def respond_retry(self, message): def respond_drop(self, message): self._respond(message, TopicEventResponse('drop').status) - def send_message_to_queue(self, q, message): - if not self._is_stream_active(): - raise StreamInactiveError('Stream is not active') - q.put(message) - - def read_message_from_queue(self, q, timeout=None): - if not self._is_stream_active(): - raise StreamInactiveError('Stream is not active') - try: - return q.get(timeout=timeout) - except queue.Empty: - return None - def _set_stream_active(self): with self._stream_lock: self._stream_active = True @@ -160,7 +138,6 @@ def close(self): self._set_stream_inactive() - # Cancel the stream if self._stream: try: self._stream.cancel() @@ -170,11 +147,6 @@ def close(self): except Exception as e: raise Exception(f'Error while closing stream: {e}') - # Join the response-handling thread to ensure it has finished - if self._response_thread: - self._response_thread.join() - self._response_thread = None - class SubscriptionMessage: diff --git a/examples/pubsub-streaming/subscriber.py b/examples/pubsub-streaming/subscriber.py index 2f8fcf00..476da9e7 100644 --- a/examples/pubsub-streaming/subscriber.py +++ b/examples/pubsub-streaming/subscriber.py @@ -13,7 +13,7 @@ def process_message(message): def main(): with DaprClient() as client: subscription = client.subscribe( - pubsub_name='pubsub', topic='TOPIC_A', dead_letter_topic='TOPIC_A_DEAD' + pubsub_name='pubsub', topic='TOPIC_A', dead_letter_topic='TOPIC_A_DEAD', timeout=2 ) try: @@ -21,7 +21,7 @@ def main(): while i < 5: i += 1 try: - message = subscription.next_message(1) + message = subscription.next_message() except StreamInactiveError as e: print('Stream is inactive. Retrying...') time.sleep(5) diff --git a/tests/clients/fake_dapr_server.py b/tests/clients/fake_dapr_server.py index a910accc..8e75cb27 100644 --- a/tests/clients/fake_dapr_server.py +++ b/tests/clients/fake_dapr_server.py @@ -3,7 +3,8 @@ from concurrent import futures from google.protobuf.any_pb2 import Any as GrpcAny -from google.protobuf import empty_pb2 +from google.protobuf import empty_pb2, struct_pb2 +from google.rpc import status_pb2, code_pb2 from grpc_status import rpc_status from dapr.clients.grpc._helpers import to_bytes @@ -182,29 +183,38 @@ def SubscribeTopicEventsAlpha1(self, request_iterator, context): initial_response=api_v1.SubscribeTopicEventsResponseInitialAlpha1() ) - msg2 = appcallback_v1.TopicEventRequest( - id='123', + extensions = struct_pb2.Struct() + extensions["field1"] = "value1" + extensions["field2"] = 42 + extensions["field3"] = True + + msg1 = appcallback_v1.TopicEventRequest( + id='111', topic='TOPIC_A', data=b'hello2', source='app1', data_content_type='text/plain', type='com.example.type2', pubsub_name='pubsub', - spec_version='1.0', + spec_version='1.0', extensions=extensions ) - yield api_v1.SubscribeTopicEventsResponseAlpha1(event_message=msg2) + yield api_v1.SubscribeTopicEventsResponseAlpha1(event_message=msg1) - msg3 = appcallback_v1.TopicEventRequest( - id='456', + msg2 = appcallback_v1.TopicEventRequest( + id='222', topic='TOPIC_A', data=b'{"a": 1}', source='app1', data_content_type='application/json', type='com.example.type2', pubsub_name='pubsub', - spec_version='1.0', + spec_version='1.0', extensions=extensions ) - yield api_v1.SubscribeTopicEventsResponseAlpha1(event_message=msg3) + yield api_v1.SubscribeTopicEventsResponseAlpha1(event_message=msg2) + + # On the third message simulate a disconnection + status = status_pb2.Status(code=code_pb2.UNAVAILABLE, message='Simulated disconnection') + context.abort_with_status(rpc_status.to_status(status)) def SaveState(self, request, context): self.check_for_exception(context) diff --git a/tests/clients/test_dapr_grpc_client.py b/tests/clients/test_dapr_grpc_client.py index 019ea84f..6ab4861e 100644 --- a/tests/clients/test_dapr_grpc_client.py +++ b/tests/clients/test_dapr_grpc_client.py @@ -264,6 +264,9 @@ def test_publish_error(self): ) def test_subscribe_topic(self): + # The fake server we're using sends two messages and then closes the stream + # The client should be able to read both messages, handle the stream closure and reconnect + # which will result in the reading the same two messages again dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') subscription = dapr.subscribe(pubsub_name='pubsub', topic='example') @@ -271,7 +274,7 @@ def test_subscribe_topic(self): message1 = subscription.next_message() subscription.respond_success(message1) - self.assertEqual('123', message1.id()) + self.assertEqual('111', message1.id()) self.assertEqual('app1', message1.source()) self.assertEqual('com.example.type2', message1.type()) self.assertEqual('1.0', message1.spec_version()) @@ -286,7 +289,7 @@ def test_subscribe_topic(self): message2 = subscription.next_message() subscription.respond_success(message2) - self.assertEqual('456', message2.id()) + self.assertEqual('222', message2.id()) self.assertEqual('app1', message2.source()) self.assertEqual('com.example.type2', message2.type()) self.assertEqual('1.0', message2.spec_version()) @@ -296,10 +299,25 @@ def test_subscribe_topic(self): self.assertEqual('application/json', message2.data_content_type()) self.assertEqual({'a': 1}, message2.data()) - # Third call with timeout - message3 = subscription.next_message(1) + # On this call the stream will be closed and return an error, so the message will be none + # but the client will try to reconnect + message3 = subscription.next_message() self.assertIsNone(message3) + # The client already reconnected and will start reading the messages again + # Since we're working with a fake server, the messages will be the same + message4 = subscription.next_message() + self.assertEqual('111', message4.id()) + self.assertEqual('app1', message4.source()) + self.assertEqual('com.example.type2', message4.type()) + self.assertEqual('1.0', message4.spec_version()) + self.assertEqual('text/plain', message4.data_content_type()) + self.assertEqual('TOPIC_A', message4.topic()) + self.assertEqual('pubsub', message4.pubsub_name()) + self.assertEqual(b'hello2', message4.raw_data()) + self.assertEqual('text/plain', message4.data_content_type()) + self.assertEqual('hello2', message4.data()) + def test_subscribe_topic_early_close(self): dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') subscription = dapr.subscribe(pubsub_name='pubsub', topic='example') From 7945101b3332377719ccd25ed602f70de49712f8 Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Mon, 30 Sep 2024 15:55:07 +0100 Subject: [PATCH 12/26] Adds `subscribe_with_handler` Signed-off-by: Elena Kolevska --- dapr/clients/grpc/client.py | 67 ++++++-- dapr/clients/grpc/subscription.py | 87 +++++----- .../en/python-sdk-docs/python-client.md | 152 +++++++++++++----- examples/pubsub-streaming/README.md | 61 ++++++- .../pubsub-streaming/subscriber-handler.py | 36 +++++ examples/pubsub-streaming/subscriber.py | 19 ++- tests/clients/fake_dapr_server.py | 12 +- tests/clients/test_dapr_grpc_client.py | 64 +++++++- tests/clients/test_subscription.py | 90 ++++++++--- 9 files changed, 454 insertions(+), 134 deletions(-) create mode 100644 examples/pubsub-streaming/subscriber-handler.py diff --git a/dapr/clients/grpc/client.py b/dapr/clients/grpc/client.py index 590279be..1af81e90 100644 --- a/dapr/clients/grpc/client.py +++ b/dapr/clients/grpc/client.py @@ -12,7 +12,7 @@ See the License for the specific language governing permissions and limitations under the License. """ - +import threading import time import socket import json @@ -41,7 +41,7 @@ from dapr.clients.grpc._state import StateOptions, StateItem from dapr.clients.grpc._helpers import getWorkflowRuntimeStatus from dapr.clients.grpc._crypto import EncryptOptions, DecryptOptions -from dapr.clients.grpc.subscription import Subscription +from dapr.clients.grpc.subscription import Subscription, StreamInactiveError from dapr.clients.grpc.interceptors import DaprClientInterceptor, DaprClientTimeoutInterceptor from dapr.clients.health import DaprHealth from dapr.clients.retry import RetryPolicy @@ -86,6 +86,8 @@ StartWorkflowResponse, EncryptResponse, DecryptResponse, + TopicEventResponseStatus, + TopicEventResponse, ) @@ -482,12 +484,12 @@ def publish_event( return DaprResponse(call.initial_metadata()) - def subscribe(self, - pubsub_name: str, - topic: str, - metadata: Optional[MetadataTuple] = None, - dead_letter_topic: Optional[str] = None, - timeout: Optional[int] = None + def subscribe( + self, + pubsub_name: str, + topic: str, + metadata: Optional[MetadataTuple] = None, + dead_letter_topic: Optional[str] = None, ) -> Subscription: """ Subscribe to a topic with a bidirectional stream @@ -504,11 +506,56 @@ def subscribe(self, Returns: Subscription: The Subscription object managing the stream. """ - subscription = Subscription(self._stub, pubsub_name, topic, metadata, dead_letter_topic, - timeout) + subscription = Subscription(self._stub, pubsub_name, topic, metadata, dead_letter_topic) subscription.start() return subscription + def subscribe_with_handler( + self, + pubsub_name: str, + topic: str, + handler_fn: Callable[..., TopicEventResponse], + metadata: Optional[MetadataTuple] = None, + dead_letter_topic: Optional[str] = None, + ) -> Callable: + """ + Subscribe to a topic with a bidirectional stream and a message handler function + + Args: + pubsub_name (str): The name of the pubsub component. + topic (str): The name of the topic. + handler_fn (Callable[..., TopicEventResponseStatus]): The function to call when a message is received. + metadata (Optional[MetadataTuple]): Additional metadata for the subscription. + dead_letter_topic (Optional[str]): Name of the dead-letter topic. + timeout (Optional[int]): The time in seconds to wait for a message before returning None + If not set, the `next_message` method will block indefinitely + until a message is received. + """ + subscription = self.subscribe(pubsub_name, topic, metadata, dead_letter_topic) + + def stream_messages(sub): + while True: + try: + message = sub.next_message() + if message: + # Process the message + response = handler_fn(message) + if response: + subscription._respond(message, response) + else: + # No message received + continue + except StreamInactiveError: + break + + def close_subscription(): + subscription.close() + + streaming_thread = threading.Thread(target=stream_messages, args=(subscription,)) + streaming_thread.start() + + return close_subscription + def get_state( self, store_name: str, diff --git a/dapr/clients/grpc/subscription.py b/dapr/clients/grpc/subscription.py index 4453d7fb..053194ad 100644 --- a/dapr/clients/grpc/subscription.py +++ b/dapr/clients/grpc/subscription.py @@ -8,24 +8,27 @@ from dapr.proto import api_v1, appcallback_v1 import queue import threading -from typing import Optional +from typing import Optional, Union from dapr.proto.runtime.v1.appcallback_pb2 import TopicEventRequest class Subscription: - def __init__(self, stub, pubsub_name, topic, metadata=None, dead_letter_topic=None, timeout=None): + SUCCESS = TopicEventResponse('success').status + RETRY = TopicEventResponse('retry').status + DROP = TopicEventResponse('drop').status + + def __init__(self, stub, pubsub_name, topic, metadata=None, dead_letter_topic=None): self._stub = stub - self.pubsub_name = pubsub_name - self.topic = topic - self.metadata = metadata or {} - self.dead_letter_topic = dead_letter_topic or '' + self._pubsub_name = pubsub_name + self._topic = topic + self._metadata = metadata or {} + self._dead_letter_topic = dead_letter_topic or '' self._stream: Optional[Call] = None self._response_thread: Optional[threading.Thread] = None self._send_queue: queue.Queue = queue.Queue() self._stream_active: bool = False self._stream_lock = threading.Lock() # Protects _stream_active - self._timeout = timeout def start(self): def outgoing_request_iterator(): @@ -37,25 +40,27 @@ def outgoing_request_iterator(): # Send InitialRequest needed to establish the stream initial_request = api_v1.SubscribeTopicEventsRequestAlpha1( initial_request=api_v1.SubscribeTopicEventsRequestInitialAlpha1( - pubsub_name=self.pubsub_name, topic=self.topic, - metadata=self.metadata or {}, - dead_letter_topic=self.dead_letter_topic or '', )) + pubsub_name=self._pubsub_name, + topic=self._topic, + metadata=self._metadata or {}, + dead_letter_topic=self._dead_letter_topic or '', + ) + ) yield initial_request # Start sending back acknowledgement messages from the send queue - while self._is_stream_active(): # TODO check if this is correct + while self._is_stream_active(): try: # Wait for responses/acknowledgements to send from the send queue. - response = self._send_queue.get() # TODO check timeout + response = self._send_queue.get() yield response except queue.Empty: continue except Exception as e: - raise Exception(f'Error in request iterator: {e}') + raise Exception(f'Error while writing to stream: {e}') # Create the bidirectional stream - self._stream = self._stub.SubscribeTopicEventsAlpha1(outgoing_request_iterator(), - timeout=self._timeout) + self._stream = self._stub.SubscribeTopicEventsAlpha1(outgoing_request_iterator()) self._set_stream_active() next(self._stream) # discard the initial message @@ -72,7 +77,7 @@ def next_message(self): or None if no message is received within the timeout. """ if not self._is_stream_active(): - raise StreamInactiveError("Stream is not active") + raise StreamInactiveError('Stream is not active') try: # Read the next message from the stream directly @@ -84,15 +89,14 @@ def next_message(self): except RpcError as e: if e.code() == StatusCode.UNAVAILABLE: print( - f'gRPC error while reading from stream: {e.details()}, Status Code: {e.code()}') + f'gRPC error while reading from stream: {e.details()}, Status Code: {e.code()}' + ) self.reconnect_stream() - elif e.code() == StatusCode.DEADLINE_EXCEEDED: - # A message hasn't been received on the stream in `self._timeout` seconds - # so return control back to app - return None elif e.code() != StatusCode.CANCELLED: - raise Exception(f'gRPC error while reading from subscription stream: {e.details()} ' - f'Status Code: {e.code()}') + raise Exception( + f'gRPC error while reading from subscription stream: {e.details()} ' + f'Status Code: {e.code()}' + ) except Exception as e: raise Exception(f'Error while fetching message: {e}') @@ -109,16 +113,16 @@ def _respond(self, message, status): raise StreamInactiveError('Stream is not active') self._send_queue.put(msg) except Exception as e: - print(f'Exception in send_message: {e}') + print(f"Can't send message on inactive stream: {e}") def respond_success(self, message): - self._respond(message, TopicEventResponse('success').status) + self._respond(message, self.SUCCESS) def respond_retry(self, message): - self._respond(message, TopicEventResponse('retry').status) + self._respond(message, self.RETRY) def respond_drop(self, message): - self._respond(message, TopicEventResponse('drop').status) + self._respond(message, self.DROP) def _set_stream_active(self): with self._stream_lock: @@ -133,14 +137,10 @@ def _is_stream_active(self): return self._stream_active def close(self): - if not self._is_stream_active(): - return - - self._set_stream_inactive() - if self._stream: try: self._stream.cancel() + self._set_stream_inactive() except RpcError as e: if e.code() != StatusCode.CANCELLED: raise Exception(f'Error while closing stream: {e}') @@ -149,18 +149,17 @@ def close(self): class SubscriptionMessage: - def __init__(self, msg: TopicEventRequest): - self._id = msg.id - self._source = msg.source - self._type = msg.type - self._spec_version = msg.spec_version - self._data_content_type = msg.data_content_type - self._topic = msg.topic - self._pubsub_name = msg.pubsub_name - self._raw_data = msg.data - self._extensions = msg.extensions - self._data = None + self._id: str = msg.id + self._source: str = msg.source + self._type: str = msg.type + self._spec_version: str = msg.spec_version + self._data_content_type: str = msg.data_content_type + self._topic: str = msg.topic + self._pubsub_name: str = msg.pubsub_name + self._raw_data: bytes = msg.data + self._data: Optional[Union[dict, str]] = None + try: self._extensions = MessageToDict(msg.extensions) except Exception as e: @@ -207,6 +206,7 @@ def _parse_data_content(self): try: self._data = json.loads(self._raw_data) except json.JSONDecodeError: + print(f'Error parsing json message data from topic {self._topic}') pass # If JSON parsing fails, keep `data` as None elif self._data_content_type == 'text/plain': # Assume UTF-8 encoding @@ -221,6 +221,7 @@ def _parse_data_content(self): try: self._data = json.loads(self._raw_data) except json.JSONDecodeError: + print(f'Error parsing json message data from topic {self._topic}') pass # If JSON parsing fails, keep `data` as None except Exception as e: # Log or handle any unexpected exceptions diff --git a/daprdocs/content/en/python-sdk-docs/python-client.md b/daprdocs/content/en/python-sdk-docs/python-client.md index 900546ed..4f51f945 100644 --- a/daprdocs/content/en/python-sdk-docs/python-client.md +++ b/daprdocs/content/en/python-sdk-docs/python-client.md @@ -255,45 +255,125 @@ def mytopic_important(event: v1.Event) -> None: - For more information about pub/sub, visit [How-To: Publish & subscribe]({{< ref howto-publish-subscribe.md >}}). - Visit [Python SDK examples](https://github.com/dapr/python-sdk/tree/master/examples/pubsub-simple) for code samples and instructions to try out pub/sub. -#### Subscribe to messages with streaming -You can subscribe to messages from a PubSub topic with streaming by using the `subscribe` method. -This method will return a `Subscription` object on which you can call the `next_message` method to -yield messages as they arrive. -When done using the subscription, you should call the `close` method to stop the subscription. +#### Streaming message subscription + +You can create a streaming subscription to a PubSub topic using either the `subscribe` +or `subscribe_handler` methods. + +The `subscribe` method returns a `Subscription` object, which allows you to pull messages from the +stream by +calling the `next_message` method. This will block on the main thread while waiting for messages. +When done, you should call the close method to terminate the +subscription and stop receiving messages. + +The `subscribe_with_handler` method accepts a callback function that is executed for each message +received from the stream. +It runs in a separate thread, so it doesn't block the main thread. The callback should return a +`TopicEventResponseStatus`, indicating whether the message was processed successfully, should be +retried, or should be discarded. You can return these statuses using the `Subscription.SUCCESS`, +`Subscription.RETRY`, and `Subscription.DROP` class properties. The method will automatically manage +message acknowledgments based on the returned status. When done, the subscription will automatically +close, and you don't need to manually stop it. + +The call to `subscribe_with_handler` method returns a close function, which should be called to +terminate the subscription when you're done. + +Here's an example of using the `subscribe` method: ```python +import time + +from dapr.clients import DaprClient +from dapr.clients.grpc.subscription import StreamInactiveError + +counter = 0 + + +def process_message(message): + global counter + counter += 1 + # Process the message here + print(f'Processing message: {message.data()} from {message.topic()}...') + return 'success' + + +def main(): with DaprClient() as client: - subscription = client.subscribe( - pubsub_name='pubsub', topic='TOPIC_A', dead_letter_topic='TOPIC_A_DEAD' - ) - - try: - i = 0 - while i < 5: - try: - message = subscription.next_message(1) - except StreamInactiveError as e: - print('Stream is inactive. Retrying...') - time.sleep(5) - continue - if message is None: - print('No message received within timeout period.') - continue - - # Process the message - response_status = process_message(message) - - if response_status == 'success': - subscription.respond_success(message) - elif response_status == 'retry': - subscription.respond_retry(message) - elif response_status == 'drop': - subscription.respond_drop(message) - - i += 1 - - finally: - subscription.close() + global counter + + subscription = client.subscribe( + pubsub_name='pubsub', topic='TOPIC_A', dead_letter_topic='TOPIC_A_DEAD' + ) + + try: + while counter < 5: + try: + message = subscription.next_message() + + except StreamInactiveError as e: + print('Stream is inactive. Retrying...') + time.sleep(1) + continue + if message is None: + print('No message received within timeout period.') + continue + + # Process the message + response_status = process_message(message) + + if response_status == 'success': + subscription.respond_success(message) + elif response_status == 'retry': + subscription.respond_retry(message) + elif response_status == 'drop': + subscription.respond_drop(message) + + finally: + print("Closing subscription...") + subscription.close() + + +if __name__ == '__main__': + main() +``` + +And here's an example of using the `subscribe_with_handler` method: + +```python +import time + +from dapr.clients import DaprClient +from dapr.clients.grpc.subscription import Subscription + +counter = 0 + + +def process_message(message): + # Process the message here + global counter + counter += 1 + print(f'Processing message: {message.data()} from {message.topic()}...') + return Subscription.SUCCESS + + +def main(): + with (DaprClient() as client): + # This will start a new thread that will listen for messages + # and process them in the `process_message` function + close_fn = client.subscribe_with_handler( + pubsub_name='pubsub', topic='TOPIC_A', handler_fn=process_message, + dead_letter_topic='TOPIC_A_DEAD' + ) + + while counter < 5: + time.sleep(1) + + print("Closing subscription...") + close_fn() + + +if __name__ == '__main__': + main() ``` ### Interact with output bindings diff --git a/examples/pubsub-streaming/README.md b/examples/pubsub-streaming/README.md index f0fe0d93..4849e791 100644 --- a/examples/pubsub-streaming/README.md +++ b/examples/pubsub-streaming/README.md @@ -20,18 +20,19 @@ In the s`subscriber.py` file it creates a subscriber object that can call the `n pip3 install dapr ``` -## Run the example +## Run example where users control reading messages off the stream Run the following command in a terminal/command prompt: +## Run example with a handler function + +Run the following command in a terminal/command prompt: + + + +```bash +# 1. Start Subscriber +dapr run --app-id python-subscriber --app-protocol grpc python3 subscriber-handler.py +``` + + + +In another terminal/command prompt run: + + + +```bash +# 2. Start Publisher +dapr run --app-id python-publisher --app-protocol grpc --dapr-grpc-port=3500 --enable-app-health-check python3 publisher.py +``` + + + ## Cleanup diff --git a/examples/pubsub-streaming/subscriber-handler.py b/examples/pubsub-streaming/subscriber-handler.py new file mode 100644 index 00000000..896c00ac --- /dev/null +++ b/examples/pubsub-streaming/subscriber-handler.py @@ -0,0 +1,36 @@ +import time + +from dapr.clients import DaprClient +from dapr.clients.grpc.subscription import Subscription + +counter = 0 + + +def process_message(message): + # Process the message here + global counter + counter += 1 + print(f'Processing message: {message.data()} from {message.topic()}...') + return Subscription.SUCCESS + + +def main(): + with DaprClient() as client: + # This will start a new thread that will listen for messages + # and process them in the `process_message` function + close_fn = client.subscribe_with_handler( + pubsub_name='pubsub', + topic='TOPIC_A', + handler_fn=process_message, + dead_letter_topic='TOPIC_A_DEAD', + ) + + while counter < 5: + time.sleep(1) + + print('Closing subscription...') + close_fn() + + +if __name__ == '__main__': + main() diff --git a/examples/pubsub-streaming/subscriber.py b/examples/pubsub-streaming/subscriber.py index 476da9e7..5716b34c 100644 --- a/examples/pubsub-streaming/subscriber.py +++ b/examples/pubsub-streaming/subscriber.py @@ -3,28 +3,33 @@ from dapr.clients import DaprClient from dapr.clients.grpc.subscription import StreamInactiveError +counter = 0 + def process_message(message): + global counter + counter += 1 # Process the message here - print(f'Processing message: {message.data()} from {message.topic()}') + print(f'Processing message: {message.data()} from {message.topic()}...') return 'success' def main(): with DaprClient() as client: + global counter + subscription = client.subscribe( - pubsub_name='pubsub', topic='TOPIC_A', dead_letter_topic='TOPIC_A_DEAD', timeout=2 + pubsub_name='pubsub', topic='TOPIC_A', dead_letter_topic='TOPIC_A_DEAD' ) try: - i = 0 - while i < 5: - i += 1 + while counter < 5: try: message = subscription.next_message() + except StreamInactiveError as e: print('Stream is inactive. Retrying...') - time.sleep(5) + time.sleep(1) continue if message is None: print('No message received within timeout period.') @@ -40,8 +45,8 @@ def main(): elif response_status == 'drop': subscription.respond_drop(message) - finally: + print('Closing subscription...') subscription.close() diff --git a/tests/clients/fake_dapr_server.py b/tests/clients/fake_dapr_server.py index 8e75cb27..8627ab46 100644 --- a/tests/clients/fake_dapr_server.py +++ b/tests/clients/fake_dapr_server.py @@ -184,9 +184,9 @@ def SubscribeTopicEventsAlpha1(self, request_iterator, context): ) extensions = struct_pb2.Struct() - extensions["field1"] = "value1" - extensions["field2"] = 42 - extensions["field3"] = True + extensions['field1'] = 'value1' + extensions['field2'] = 42 + extensions['field3'] = True msg1 = appcallback_v1.TopicEventRequest( id='111', @@ -196,7 +196,8 @@ def SubscribeTopicEventsAlpha1(self, request_iterator, context): data_content_type='text/plain', type='com.example.type2', pubsub_name='pubsub', - spec_version='1.0', extensions=extensions + spec_version='1.0', + extensions=extensions, ) yield api_v1.SubscribeTopicEventsResponseAlpha1(event_message=msg1) @@ -208,7 +209,8 @@ def SubscribeTopicEventsAlpha1(self, request_iterator, context): data_content_type='application/json', type='com.example.type2', pubsub_name='pubsub', - spec_version='1.0', extensions=extensions + spec_version='1.0', + extensions=extensions, ) yield api_v1.SubscribeTopicEventsResponseAlpha1(event_message=msg2) diff --git a/tests/clients/test_dapr_grpc_client.py b/tests/clients/test_dapr_grpc_client.py index 6ab4861e..fee4eb2d 100644 --- a/tests/clients/test_dapr_grpc_client.py +++ b/tests/clients/test_dapr_grpc_client.py @@ -16,6 +16,7 @@ import json import socket import tempfile +import time import unittest import uuid import asyncio @@ -27,7 +28,7 @@ from dapr.clients.exceptions import DaprGrpcError from dapr.clients.grpc.client import DaprGrpcClient from dapr.clients import DaprClient -from dapr.clients.grpc.subscription import StreamInactiveError +from dapr.clients.grpc.subscription import StreamInactiveError, Subscription from dapr.proto import common_v1 from .fake_dapr_server import FakeDaprSidecar from dapr.conf import settings @@ -266,7 +267,8 @@ def test_publish_error(self): def test_subscribe_topic(self): # The fake server we're using sends two messages and then closes the stream # The client should be able to read both messages, handle the stream closure and reconnect - # which will result in the reading the same two messages again + # which will result in reading the same two messages again. + # That's why message 3 should be the same as message 1 dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') subscription = dapr.subscribe(pubsub_name='pubsub', topic='example') @@ -307,6 +309,7 @@ def test_subscribe_topic(self): # The client already reconnected and will start reading the messages again # Since we're working with a fake server, the messages will be the same message4 = subscription.next_message() + subscription.respond_success(message4) self.assertEqual('111', message4.id()) self.assertEqual('app1', message4.source()) self.assertEqual('com.example.type2', message4.type()) @@ -318,6 +321,8 @@ def test_subscribe_topic(self): self.assertEqual('text/plain', message4.data_content_type()) self.assertEqual('hello2', message4.data()) + subscription.close() + def test_subscribe_topic_early_close(self): dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') subscription = dapr.subscribe(pubsub_name='pubsub', topic='example') @@ -326,6 +331,61 @@ def test_subscribe_topic_early_close(self): with self.assertRaises(StreamInactiveError): subscription.next_message() + def test_subscribe_topic_with_handler(self): + # The fake server we're using sends two messages and then closes the stream + # The client should be able to read both messages, handle the stream closure and reconnect + # which will result in reading the same two messages again. + # That's why message 3 should be the same as message 1 + dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') + counter = 0 + + def handler(message): + nonlocal counter + if counter == 0: + self.assertEqual('111', message.id()) + self.assertEqual('app1', message.source()) + self.assertEqual('com.example.type2', message.type()) + self.assertEqual('1.0', message.spec_version()) + self.assertEqual('text/plain', message.data_content_type()) + self.assertEqual('TOPIC_A', message.topic()) + self.assertEqual('pubsub', message.pubsub_name()) + self.assertEqual(b'hello2', message.raw_data()) + self.assertEqual('text/plain', message.data_content_type()) + self.assertEqual('hello2', message.data()) + elif counter == 1: + self.assertEqual('222', message.id()) + self.assertEqual('app1', message.source()) + self.assertEqual('com.example.type2', message.type()) + self.assertEqual('1.0', message.spec_version()) + self.assertEqual('TOPIC_A', message.topic()) + self.assertEqual('pubsub', message.pubsub_name()) + self.assertEqual(b'{"a": 1}', message.raw_data()) + self.assertEqual('application/json', message.data_content_type()) + self.assertEqual({'a': 1}, message.data()) + elif counter == 2: + self.assertEqual('111', message.id()) + self.assertEqual('app1', message.source()) + self.assertEqual('com.example.type2', message.type()) + self.assertEqual('1.0', message.spec_version()) + self.assertEqual('text/plain', message.data_content_type()) + self.assertEqual('TOPIC_A', message.topic()) + self.assertEqual('pubsub', message.pubsub_name()) + self.assertEqual(b'hello2', message.raw_data()) + self.assertEqual('text/plain', message.data_content_type()) + self.assertEqual('hello2', message.data()) + + counter += 1 + + return Subscription.SUCCESS + + close_fn = dapr.subscribe_with_handler( + pubsub_name='pubsub', topic='example', handler_fn=handler + ) + + while counter < 3: + time.sleep(0.1) # Use sleep to prevent a busy-wait loop + close_fn() + @patch.object(settings, 'DAPR_API_TOKEN', 'test-token') def test_dapr_api_token_insertion(self): dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') diff --git a/tests/clients/test_subscription.py b/tests/clients/test_subscription.py index 253f4454..ed2eae3f 100644 --- a/tests/clients/test_subscription.py +++ b/tests/clients/test_subscription.py @@ -8,14 +8,22 @@ class SubscriptionMessageTests(unittest.TestCase): def test_subscription_message_init_raw_text(self): extensions = Struct() - extensions["field1"] = "value1" - extensions["field2"] = 42 - extensions["field3"] = True + extensions['field1'] = 'value1' + extensions['field2'] = 42 + extensions['field3'] = True - msg = TopicEventRequest(id='id', data=b'hello', data_content_type='text/plain', - topic='topicA', pubsub_name='pubsub_name', source='source', - type='type', spec_version='spec_version', path='path', - extensions=extensions) + msg = TopicEventRequest( + id='id', + data=b'hello', + data_content_type='text/plain', + topic='topicA', + pubsub_name='pubsub_name', + source='source', + type='type', + spec_version='spec_version', + path='path', + extensions=extensions, + ) subscription_message = SubscriptionMessage(msg=msg) self.assertEqual('id', subscription_message.id()) @@ -27,42 +35,74 @@ def test_subscription_message_init_raw_text(self): self.assertEqual('pubsub_name', subscription_message.pubsub_name()) self.assertEqual(b'hello', subscription_message.raw_data()) self.assertEqual('hello', subscription_message.data()) - self.assertEqual({'field1': 'value1', "field2": 42, "field3": True}, - subscription_message.extensions()) + self.assertEqual( + {'field1': 'value1', 'field2': 42, 'field3': True}, subscription_message.extensions() + ) def test_subscription_message_init_raw_text_non_utf(self): - msg = TopicEventRequest(id='id', data=b'\x80\x81\x82', data_content_type='text/plain', - topic='topicA', pubsub_name='pubsub_name', source='source', - type='type', spec_version='spec_version', path='path') + msg = TopicEventRequest( + id='id', + data=b'\x80\x81\x82', + data_content_type='text/plain', + topic='topicA', + pubsub_name='pubsub_name', + source='source', + type='type', + spec_version='spec_version', + path='path', + ) subscription_message = SubscriptionMessage(msg=msg) self.assertEqual(b'\x80\x81\x82', subscription_message.raw_data()) self.assertIsNone(subscription_message.data()) def test_subscription_message_init_json(self): - msg = TopicEventRequest(id='id', data=b'{"a": 1}', data_content_type='application/json', - topic='topicA', pubsub_name='pubsub_name', source='source', - type='type', spec_version='spec_version', path='path') + msg = TopicEventRequest( + id='id', + data=b'{"a": 1}', + data_content_type='application/json', + topic='topicA', + pubsub_name='pubsub_name', + source='source', + type='type', + spec_version='spec_version', + path='path', + ) subscription_message = SubscriptionMessage(msg=msg) self.assertEqual(b'{"a": 1}', subscription_message.raw_data()) - self.assertEqual({"a": 1}, subscription_message.data()) - print(subscription_message.data()["a"]) + self.assertEqual({'a': 1}, subscription_message.data()) + print(subscription_message.data()['a']) def test_subscription_message_init_json_faimly(self): - msg = TopicEventRequest(id='id', data=b'{"a": 1}', - data_content_type='application/vnd.api+json', topic='topicA', - pubsub_name='pubsub_name', source='source', type='type', - spec_version='spec_version', path='path') + msg = TopicEventRequest( + id='id', + data=b'{"a": 1}', + data_content_type='application/vnd.api+json', + topic='topicA', + pubsub_name='pubsub_name', + source='source', + type='type', + spec_version='spec_version', + path='path', + ) subscription_message = SubscriptionMessage(msg=msg) self.assertEqual(b'{"a": 1}', subscription_message.raw_data()) - self.assertEqual({"a": 1}, subscription_message.data()) + self.assertEqual({'a': 1}, subscription_message.data()) def test_subscription_message_init_unknown_content_type(self): - msg = TopicEventRequest(id='id', data=b'{"a": 1}', data_content_type='unknown/content-type', - topic='topicA', pubsub_name='pubsub_name', source='source', - type='type', spec_version='spec_version', path='path') + msg = TopicEventRequest( + id='id', + data=b'{"a": 1}', + data_content_type='unknown/content-type', + topic='topicA', + pubsub_name='pubsub_name', + source='source', + type='type', + spec_version='spec_version', + path='path', + ) subscription_message = SubscriptionMessage(msg=msg) self.assertEqual(b'{"a": 1}', subscription_message.raw_data()) From 894e523b8ea692d05cc225ab9259b6c74f69655f Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Mon, 30 Sep 2024 15:55:56 +0100 Subject: [PATCH 13/26] Fixes linter Signed-off-by: Elena Kolevska --- dapr/clients/grpc/client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dapr/clients/grpc/client.py b/dapr/clients/grpc/client.py index 1af81e90..e6469c4f 100644 --- a/dapr/clients/grpc/client.py +++ b/dapr/clients/grpc/client.py @@ -86,7 +86,6 @@ StartWorkflowResponse, EncryptResponse, DecryptResponse, - TopicEventResponseStatus, TopicEventResponse, ) From 045ca7cab6ccae29d1eee23b0af9683dd3b8882e Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Mon, 30 Sep 2024 18:32:18 +0100 Subject: [PATCH 14/26] Fixes linter Signed-off-by: Elena Kolevska --- examples/pubsub-streaming/publisher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/pubsub-streaming/publisher.py b/examples/pubsub-streaming/publisher.py index 9c18ac3c..fd797470 100644 --- a/examples/pubsub-streaming/publisher.py +++ b/examples/pubsub-streaming/publisher.py @@ -28,7 +28,7 @@ topic_name='TOPIC_A', data=json.dumps(req_data), data_content_type='application/json', - publish_metadata={'ttlInSeconds': '100', 'rawPayload': 'false'} + publish_metadata={'ttlInSeconds': '100', 'rawPayload': 'false'}, ) # Print the request From 59d3c73ad052d186b94edf8fab261cc75cc668f0 Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Wed, 9 Oct 2024 17:40:37 +0100 Subject: [PATCH 15/26] Adds async Signed-off-by: Elena Kolevska --- dapr/aio/clients/grpc/client.py | 90 +++++++++++---- dapr/aio/clients/grpc/subscription.py | 109 ++++++++++++++++++ dapr/clients/grpc/client.py | 4 +- dapr/clients/grpc/subscription.py | 102 +--------------- dapr/common/pubsub/subscription.py | 92 +++++++++++++++ .../en/python-sdk-docs/python-client.md | 24 ++-- examples/pubsub-streaming/README.md | 97 ++++++++++++++++ .../async-subscriber-handler.py | 43 +++++++ examples/pubsub-streaming/async-subscriber.py | 54 +++++++++ .../pubsub-streaming/subscriber-handler.py | 4 +- 10 files changed, 485 insertions(+), 134 deletions(-) create mode 100644 dapr/aio/clients/grpc/subscription.py create mode 100644 dapr/common/pubsub/subscription.py create mode 100644 examples/pubsub-streaming/async-subscriber-handler.py create mode 100644 examples/pubsub-streaming/async-subscriber.py diff --git a/dapr/aio/clients/grpc/client.py b/dapr/aio/clients/grpc/client.py index f9f53498..d69be23c 100644 --- a/dapr/aio/clients/grpc/client.py +++ b/dapr/aio/clients/grpc/client.py @@ -24,7 +24,7 @@ from warnings import warn -from typing import Callable, Dict, Optional, Text, Union, Sequence, List, Any +from typing import Callable, Dict, Optional, Text, Union, Sequence, List, Any, Awaitable from typing_extensions import Self from google.protobuf.message import Message as GrpcMessage @@ -39,12 +39,14 @@ AioRpcError, ) +from dapr.aio.clients.grpc.subscription import Subscription from dapr.clients.exceptions import DaprInternalError, DaprGrpcError from dapr.clients.grpc._crypto import EncryptOptions, DecryptOptions from dapr.clients.grpc._state import StateOptions, StateItem from dapr.clients.grpc._helpers import getWorkflowRuntimeStatus from dapr.clients.health import DaprHealth from dapr.clients.retry import RetryPolicy +from dapr.common.pubsub.subscription import StreamInactiveError from dapr.conf.helpers import GrpcEndpoint from dapr.conf import settings from dapr.proto import api_v1, api_service_v1, common_v1 @@ -74,27 +76,14 @@ BindingRequest, TransactionalStateOperation, ) -from dapr.clients.grpc._response import ( - BindingResponse, - DaprResponse, - GetSecretResponse, - GetBulkSecretResponse, - GetMetadataResponse, - InvokeMethodResponse, - UnlockResponseStatus, - StateResponse, - BulkStatesResponse, - BulkStateItem, - ConfigurationResponse, - QueryResponse, - QueryResponseItem, - RegisteredComponents, - ConfigurationWatcher, - TryLockResponse, - UnlockResponse, - GetWorkflowResponse, - StartWorkflowResponse, -) +from dapr.clients.grpc._response import (BindingResponse, DaprResponse, GetSecretResponse, + GetBulkSecretResponse, GetMetadataResponse, + InvokeMethodResponse, UnlockResponseStatus, StateResponse, + BulkStatesResponse, BulkStateItem, ConfigurationResponse, + QueryResponse, QueryResponseItem, RegisteredComponents, + ConfigurationWatcher, TryLockResponse, UnlockResponse, + GetWorkflowResponse, StartWorkflowResponse, + TopicEventResponse, ) class DaprGrpcClientAsync: @@ -482,6 +471,63 @@ async def publish_event( return DaprResponse(await call.initial_metadata()) + async def subscribe(self, pubsub_name: str, topic: str, metadata: Optional[dict] = None, + dead_letter_topic: Optional[str] = None, ) -> Subscription: + """ + Subscribe to a topic with a bidirectional stream + + Args: + pubsub_name (str): The name of the pubsub component. + topic (str): The name of the topic. + metadata (Optional[dict]): Additional metadata for the subscription. + dead_letter_topic (Optional[str]): Name of the dead-letter topic. + + Returns: + Subscription: The Subscription object managing the stream. + """ + subscription = Subscription(self._stub, pubsub_name, topic, metadata, + dead_letter_topic) + await subscription.start() + return subscription + + async def subscribe_with_handler(self, pubsub_name: str, topic: str, + handler_fn: Callable[..., TopicEventResponse], metadata: Optional[dict] = None, + dead_letter_topic: Optional[str] = None, ) -> Callable[[], Awaitable[None]]: + """ + Subscribe to a topic with a bidirectional stream and a message handler function + + Args: + pubsub_name (str): The name of the pubsub component. + topic (str): The name of the topic. + handler_fn (Callable[..., TopicEventResponse]): The function to call when a message is received. + metadata (Optional[dict]): Additional metadata for the subscription. + dead_letter_topic (Optional[str]): Name of the dead-letter topic. + + Returns: + Callable[[], Awaitable[None]]: An async function to close the subscription. + """ + subscription = await self.subscribe(pubsub_name, topic, metadata, dead_letter_topic) + + async def stream_messages(sub: Subscription): + while True: + try: + message = await sub.next_message() + if message: + response = await handler_fn(message) + if response: + await subscription._respond(message, response.status) + else: + continue + except StreamInactiveError: + break + + async def close_subscription(): + await subscription.close() + + asyncio.create_task(stream_messages(subscription)) + + return close_subscription + async def get_state( self, store_name: str, diff --git a/dapr/aio/clients/grpc/subscription.py b/dapr/aio/clients/grpc/subscription.py new file mode 100644 index 00000000..b4be6250 --- /dev/null +++ b/dapr/aio/clients/grpc/subscription.py @@ -0,0 +1,109 @@ +import asyncio +from grpc import StatusCode +from grpc.aio import AioRpcError + +from dapr.clients.grpc._response import TopicEventResponse +from dapr.clients.health import DaprHealth +from dapr.common.pubsub.subscription import StreamInactiveError, SubscriptionMessage +from dapr.proto import api_v1, appcallback_v1 + +class Subscription: + + def __init__(self, stub, pubsub_name, topic, metadata=None, dead_letter_topic=None): + self._stub = stub + self._pubsub_name = pubsub_name + self._topic = topic + self._metadata = metadata or {} + self._dead_letter_topic = dead_letter_topic or '' + self._stream = None + self._send_queue = asyncio.Queue() + self._stream_active = asyncio.Event() + + async def start(self): + async def outgoing_request_iterator(): + try: + initial_request = api_v1.SubscribeTopicEventsRequestAlpha1( + initial_request=api_v1.SubscribeTopicEventsRequestInitialAlpha1( + pubsub_name=self._pubsub_name, + topic=self._topic, + metadata=self._metadata, + dead_letter_topic=self._dead_letter_topic, + ) + ) + yield initial_request + + while self._stream_active.is_set(): + try: + response = await asyncio.wait_for(self._send_queue.get(), timeout=1.0) + yield response + except asyncio.TimeoutError: + continue + except Exception as e: + raise Exception(f'Error while writing to stream: {e}') + + self._stream = self._stub.SubscribeTopicEventsAlpha1(outgoing_request_iterator()) + self._stream_active.set() + await self._stream.read() # discard the initial message + + async def reconnect_stream(self): + await self.close() + DaprHealth.wait_until_ready() + print('Attempting to reconnect...') + await self.start() + + async def next_message(self): + if not self._stream_active.is_set(): + raise StreamInactiveError('Stream is not active') + + try: + if self._stream is not None: + message = await self._stream.read() + if message is None: + return None + return SubscriptionMessage(message.event_message) + except AioRpcError as e: + if e.code() == StatusCode.UNAVAILABLE: + print(f'gRPC error while reading from stream: {e.details()}, ' + f'Status Code: {e.code()}. ' + f'Attempting to reconnect...') + await self.reconnect_stream() + elif e.code() != StatusCode.CANCELLED: + raise Exception(f'gRPC error while reading from subscription stream: {e.details()} ' + f'Status Code: {e.code()}') + except Exception as e: + raise Exception(f'Error while fetching message: {e}') + + return None + + async def _respond(self, message, status): + try: + status = appcallback_v1.TopicEventResponse(status=status.value) + response = api_v1.SubscribeTopicEventsRequestProcessedAlpha1( + id=message.id(), status=status + ) + msg = api_v1.SubscribeTopicEventsRequestAlpha1(event_processed=response) + if not self._stream_active.is_set(): + raise StreamInactiveError('Stream is not active') + await self._send_queue.put(msg) + except Exception as e: + print(f"Can't send message on inactive stream: {e}") + + async def respond_success(self, message): + await self._respond(message, TopicEventResponse('success').status) + + async def respond_retry(self, message): + await self._respond(message, TopicEventResponse('retry').status) + + async def respond_drop(self, message): + await self._respond(message, TopicEventResponse('drop').status) + + async def close(self): + if self._stream: + try: + self._stream.cancel() + self._stream_active.clear() + except AioRpcError as e: + if e.code() != StatusCode.CANCELLED: + raise Exception(f'Error while closing stream: {e}') + except Exception as e: + raise Exception(f'Error while closing stream: {e}') \ No newline at end of file diff --git a/dapr/clients/grpc/client.py b/dapr/clients/grpc/client.py index e6469c4f..94793907 100644 --- a/dapr/clients/grpc/client.py +++ b/dapr/clients/grpc/client.py @@ -523,7 +523,7 @@ def subscribe_with_handler( Args: pubsub_name (str): The name of the pubsub component. topic (str): The name of the topic. - handler_fn (Callable[..., TopicEventResponseStatus]): The function to call when a message is received. + handler_fn (Callable[..., TopicEventResponse]): The function to call when a message is received. metadata (Optional[MetadataTuple]): Additional metadata for the subscription. dead_letter_topic (Optional[str]): Name of the dead-letter topic. timeout (Optional[int]): The time in seconds to wait for a message before returning None @@ -540,7 +540,7 @@ def stream_messages(sub): # Process the message response = handler_fn(message) if response: - subscription._respond(message, response) + subscription.respond(message, response.status) else: # No message received continue diff --git a/dapr/clients/grpc/subscription.py b/dapr/clients/grpc/subscription.py index 053194ad..8b99b34f 100644 --- a/dapr/clients/grpc/subscription.py +++ b/dapr/clients/grpc/subscription.py @@ -1,22 +1,16 @@ -import json - -from google.protobuf.json_format import MessageToDict from grpc import RpcError, StatusCode, Call # type: ignore from dapr.clients.grpc._response import TopicEventResponse from dapr.clients.health import DaprHealth +from dapr.common.pubsub.subscription import StreamInactiveError, SubscriptionMessage from dapr.proto import api_v1, appcallback_v1 import queue import threading -from typing import Optional, Union +from typing import Optional -from dapr.proto.runtime.v1.appcallback_pb2 import TopicEventRequest class Subscription: - SUCCESS = TopicEventResponse('success').status - RETRY = TopicEventResponse('retry').status - DROP = TopicEventResponse('drop').status def __init__(self, stub, pubsub_name, topic, metadata=None, dead_letter_topic=None): self._stub = stub @@ -102,7 +96,7 @@ def next_message(self): return None - def _respond(self, message, status): + def respond(self, message, status): try: status = appcallback_v1.TopicEventResponse(status=status.value) response = api_v1.SubscribeTopicEventsRequestProcessedAlpha1( @@ -116,13 +110,13 @@ def _respond(self, message, status): print(f"Can't send message on inactive stream: {e}") def respond_success(self, message): - self._respond(message, self.SUCCESS) + self.respond(message, TopicEventResponse('success').status) def respond_retry(self, message): - self._respond(message, self.RETRY) + self.respond(message, TopicEventResponse('retry').status) def respond_drop(self, message): - self._respond(message, self.DROP) + self.respond(message, TopicEventResponse('drop').status) def _set_stream_active(self): with self._stream_lock: @@ -146,87 +140,3 @@ def close(self): raise Exception(f'Error while closing stream: {e}') except Exception as e: raise Exception(f'Error while closing stream: {e}') - - -class SubscriptionMessage: - def __init__(self, msg: TopicEventRequest): - self._id: str = msg.id - self._source: str = msg.source - self._type: str = msg.type - self._spec_version: str = msg.spec_version - self._data_content_type: str = msg.data_content_type - self._topic: str = msg.topic - self._pubsub_name: str = msg.pubsub_name - self._raw_data: bytes = msg.data - self._data: Optional[Union[dict, str]] = None - - try: - self._extensions = MessageToDict(msg.extensions) - except Exception as e: - self._extensions = {} - print(f'Error parsing extensions: {e}') - - # Parse the content based on its media type - if self._raw_data and len(self._raw_data) > 0: - self._parse_data_content() - - def id(self): - return self._id - - def source(self): - return self._source - - def type(self): - return self._type - - def spec_version(self): - return self._spec_version - - def data_content_type(self): - return self._data_content_type - - def topic(self): - return self._topic - - def pubsub_name(self): - return self._pubsub_name - - def raw_data(self): - return self._raw_data - - def extensions(self): - return self._extensions - - def data(self): - return self._data - - def _parse_data_content(self): - try: - if self._data_content_type == 'application/json': - try: - self._data = json.loads(self._raw_data) - except json.JSONDecodeError: - print(f'Error parsing json message data from topic {self._topic}') - pass # If JSON parsing fails, keep `data` as None - elif self._data_content_type == 'text/plain': - # Assume UTF-8 encoding - try: - self._data = self._raw_data.decode('utf-8') - except UnicodeDecodeError: - print(f'Error decoding message data from topic {self._topic} as UTF-8') - elif self._data_content_type.startswith( - 'application/' - ) and self._data_content_type.endswith('+json'): - # Handle custom JSON-based media types (e.g., application/vnd.api+json) - try: - self._data = json.loads(self._raw_data) - except json.JSONDecodeError: - print(f'Error parsing json message data from topic {self._topic}') - pass # If JSON parsing fails, keep `data` as None - except Exception as e: - # Log or handle any unexpected exceptions - print(f'Error parsing media type: {e}') - - -class StreamInactiveError(Exception): - pass diff --git a/dapr/common/pubsub/subscription.py b/dapr/common/pubsub/subscription.py new file mode 100644 index 00000000..ac8db973 --- /dev/null +++ b/dapr/common/pubsub/subscription.py @@ -0,0 +1,92 @@ +import json +from google.protobuf.json_format import MessageToDict +from dapr.proto.runtime.v1.appcallback_pb2 import TopicEventRequest +from typing import Optional, Union + +class SubscriptionMessage: + def __init__(self, msg: TopicEventRequest): + self._id: str = msg.id + self._source: str = msg.source + self._type: str = msg.type + self._spec_version: str = msg.spec_version + self._data_content_type: str = msg.data_content_type + self._topic: str = msg.topic + self._pubsub_name: str = msg.pubsub_name + self._raw_data: bytes = msg.data + self._data: Optional[Union[dict, str]] = None + + try: + self._extensions = MessageToDict(msg.extensions) + except Exception as e: + self._extensions = {} + print(f'Error parsing extensions: {e}') + + # Parse the content based on its media type + if self._raw_data and len(self._raw_data) > 0: + self._parse_data_content() + + def id(self): + return self._id + + def source(self): + return self._source + + def type(self): + return self._type + + def spec_version(self): + return self._spec_version + + def data_content_type(self): + return self._data_content_type + + def topic(self): + return self._topic + + def pubsub_name(self): + return self._pubsub_name + + def raw_data(self): + return self._raw_data + + def extensions(self): + return self._extensions + + def data(self): + return self._data + + def _parse_data_content(self): + try: + if self._data_content_type == 'application/json': + try: + self._data = json.loads(self._raw_data) + except json.JSONDecodeError: + print(f'Error parsing json message data from topic {self._topic}') + pass # If JSON parsing fails, keep `data` as None + elif self._data_content_type == 'text/plain': + # Assume UTF-8 encoding + try: + self._data = self._raw_data.decode('utf-8') + except UnicodeDecodeError: + print(f'Error decoding message data from topic {self._topic} as UTF-8') + elif self._data_content_type.startswith( + 'application/' + ) and self._data_content_type.endswith('+json'): + # Handle custom JSON-based media types (e.g., application/vnd.api+json) + try: + self._data = json.loads(self._raw_data) + except json.JSONDecodeError: + print(f'Error parsing json message data from topic {self._topic}') + pass # If JSON parsing fails, keep `data` as None + except Exception as e: + # Log or handle any unexpected exceptions + print(f'Error parsing media type: {e}') + + +class StreamInactiveError(Exception): + pass + +class PubSubEventStatus: + SUCCESS = 'success' + RETRY = 'retry' + DROP = 'drop' \ No newline at end of file diff --git a/daprdocs/content/en/python-sdk-docs/python-client.md b/daprdocs/content/en/python-sdk-docs/python-client.md index 4f51f945..b4e92a9b 100644 --- a/daprdocs/content/en/python-sdk-docs/python-client.md +++ b/daprdocs/content/en/python-sdk-docs/python-client.md @@ -216,7 +216,7 @@ with DaprClient() as d: - For a full list of state store query options visit [How-To: Query state]({{< ref howto-state-query-api.md >}}). - Visit [Python SDK examples](https://github.com/dapr/python-sdk/tree/master/examples/state_store_query) for code samples and instructions to try out state store querying. -### Publish & subscribe to messages +### Publish & subscribe #### Publish messages @@ -269,14 +269,11 @@ subscription and stop receiving messages. The `subscribe_with_handler` method accepts a callback function that is executed for each message received from the stream. It runs in a separate thread, so it doesn't block the main thread. The callback should return a -`TopicEventResponseStatus`, indicating whether the message was processed successfully, should be -retried, or should be discarded. You can return these statuses using the `Subscription.SUCCESS`, -`Subscription.RETRY`, and `Subscription.DROP` class properties. The method will automatically manage -message acknowledgments based on the returned status. When done, the subscription will automatically -close, and you don't need to manually stop it. - -The call to `subscribe_with_handler` method returns a close function, which should be called to -terminate the subscription when you're done. +`TopicEventResponse` (ex. `TopicEventResponse('success')`), indicating whether the message was +processed successfully, should be retried, or should be discarded. The method will automatically +manage message acknowledgements based on the returned status. The call to `subscribe_with_handler` +method returns a close function, which should be called to terminate the subscription when you're +done. Here's an example of using the `subscribe` method: @@ -343,7 +340,7 @@ And here's an example of using the `subscribe_with_handler` method: import time from dapr.clients import DaprClient -from dapr.clients.grpc.subscription import Subscription +from dapr.clients.grpc._response import TopicEventResponse counter = 0 @@ -353,7 +350,7 @@ def process_message(message): global counter counter += 1 print(f'Processing message: {message.data()} from {message.topic()}...') - return Subscription.SUCCESS + return TopicEventResponse('success') def main(): @@ -376,6 +373,9 @@ if __name__ == '__main__': main() ``` +- For more information about pub/sub, visit [How-To: Publish & subscribe]({{< ref howto-publish-subscribe.md >}}). +- Visit [Python SDK examples](https://github.com/dapr/python-sdk/tree/main/examples/pubsub-simple) for code samples and instructions to try out streaming pub/sub. + ### Interact with output bindings ```python @@ -386,7 +386,7 @@ with DaprClient() as d: ``` - For a full guide on output bindings visit [How-To: Use bindings]({{< ref howto-bindings.md >}}). -- Visit [Python SDK examples](https://github.com/dapr/python-sdk/tree/master/examples/invoke-binding) for code samples and instructions to try out output bindings. +- Visit [Python SDK examples](https://github.com/dapr/python-sdk/tree/main/examples/invoke-binding) for code samples and instructions to try out output bindings. ### Retrieve secrets diff --git a/examples/pubsub-streaming/README.md b/examples/pubsub-streaming/README.md index 4849e791..4bad7f3c 100644 --- a/examples/pubsub-streaming/README.md +++ b/examples/pubsub-streaming/README.md @@ -116,6 +116,103 @@ dapr run --app-id python-publisher --app-protocol grpc --dapr-grpc-port=3500 --e +## Run async example where users control reading messages off the stream + +Run the following command in a terminal/command prompt: + + + +```bash +# 1. Start Subscriber +dapr run --app-id python-subscriber --app-protocol grpc python3 async-subscriber.py +``` + + + +In another terminal/command prompt run: + + + +```bash +# 2. Start Publisher +dapr run --app-id python-publisher --app-protocol grpc --dapr-grpc-port=3500 --enable-app-health-check python3 publisher.py +``` + + + +## Run async example with a handler function + +Run the following command in a terminal/command prompt: + + + +```bash +# 1. Start Subscriber +dapr run --app-id python-subscriber --app-protocol grpc python3 async-subscriber-handler.py +``` + + + +In another terminal/command prompt run: + + + +```bash +# 2. Start Publisher +dapr run --app-id python-publisher --app-protocol grpc --dapr-grpc-port=3500 --enable-app-health-check python3 publisher.py +``` + + + + ## Cleanup diff --git a/examples/pubsub-streaming/async-subscriber-handler.py b/examples/pubsub-streaming/async-subscriber-handler.py new file mode 100644 index 00000000..75aca19e --- /dev/null +++ b/examples/pubsub-streaming/async-subscriber-handler.py @@ -0,0 +1,43 @@ +import asyncio +from dapr.aio.clients import DaprClient +from dapr.clients.grpc._response import TopicEventResponse + +counter = 0 + + +async def process_message(message) -> TopicEventResponse: + """ + Asynchronously processes the message and returns a TopicEventResponse. + """ + + print(f'Processing message: {message.data()} from {message.topic()}...') + global counter + counter += 1 + return TopicEventResponse('success') + + +async def main(): + """ + Main function to subscribe to a pubsub topic and handle messages asynchronously. + """ + async with DaprClient() as client: + # Subscribe to the pubsub topic with the message handler + close_fn = await client.subscribe_with_handler( + pubsub_name='pubsub', + topic='TOPIC_A', + handler_fn=process_message, + dead_letter_topic='TOPIC_A_DEAD', + ) + + # Wait until 5 messages are processed + global counter + while counter < 5: + print("Counter: ", counter) + await asyncio.sleep(1) + + print('Closing subscription...') + await close_fn() + + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/examples/pubsub-streaming/async-subscriber.py b/examples/pubsub-streaming/async-subscriber.py new file mode 100644 index 00000000..396b3cc2 --- /dev/null +++ b/examples/pubsub-streaming/async-subscriber.py @@ -0,0 +1,54 @@ +import asyncio + +from dapr.aio.clients import DaprClient +from dapr.clients.grpc.subscription import StreamInactiveError + +counter = 0 + + +def process_message(message): + global counter + counter += 1 + # Process the message here + print(f'Processing message: {message.data()} from {message.topic()}...') + return 'success' + + +async def main(): + async with DaprClient() as client: + global counter + subscription = await client.subscribe( + pubsub_name='pubsub', topic='TOPIC_A', dead_letter_topic='TOPIC_A_DEAD' + ) + + try: + while counter < 5: + try: + message = await subscription.next_message() + + except StreamInactiveError: + print('Stream is inactive. Retrying...') + await asyncio.sleep(1) + continue + if message is None: + print('No message received within timeout period.') + continue + + # Process the message + response_status = process_message(message) + + if response_status == 'success': + await subscription.respond_success(message) + elif response_status == 'retry': + await subscription.respond_retry(message) + elif response_status == 'drop': + await subscription.respond_drop(message) + + finally: + print('Closing subscription...') + await subscription.close() + + + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/examples/pubsub-streaming/subscriber-handler.py b/examples/pubsub-streaming/subscriber-handler.py index 896c00ac..aab840a4 100644 --- a/examples/pubsub-streaming/subscriber-handler.py +++ b/examples/pubsub-streaming/subscriber-handler.py @@ -1,7 +1,7 @@ import time from dapr.clients import DaprClient -from dapr.clients.grpc.subscription import Subscription +from dapr.clients.grpc._response import TopicEventResponse counter = 0 @@ -11,7 +11,7 @@ def process_message(message): global counter counter += 1 print(f'Processing message: {message.data()} from {message.topic()}...') - return Subscription.SUCCESS + return TopicEventResponse('success') def main(): From cac1726067695d32c81fa814e3cc88cc80c20423 Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Wed, 9 Oct 2024 21:54:36 +0100 Subject: [PATCH 16/26] Adds tests for async streaming subscription Signed-off-by: Elena Kolevska --- dapr/aio/clients/grpc/client.py | 2 +- dapr/aio/clients/grpc/subscription.py | 13 +- tests/clients/test_dapr_grpc_client.py | 12 +- tests/clients/test_dapr_grpc_client_async.py | 137 +++++++++++++++++-- 4 files changed, 140 insertions(+), 24 deletions(-) diff --git a/dapr/aio/clients/grpc/client.py b/dapr/aio/clients/grpc/client.py index d69be23c..626ca697 100644 --- a/dapr/aio/clients/grpc/client.py +++ b/dapr/aio/clients/grpc/client.py @@ -515,7 +515,7 @@ async def stream_messages(sub: Subscription): if message: response = await handler_fn(message) if response: - await subscription._respond(message, response.status) + await subscription.respond(message, response.status) else: continue except StreamInactiveError: diff --git a/dapr/aio/clients/grpc/subscription.py b/dapr/aio/clients/grpc/subscription.py index b4be6250..7dc10f9a 100644 --- a/dapr/aio/clients/grpc/subscription.py +++ b/dapr/aio/clients/grpc/subscription.py @@ -68,14 +68,13 @@ async def next_message(self): f'Attempting to reconnect...') await self.reconnect_stream() elif e.code() != StatusCode.CANCELLED: - raise Exception(f'gRPC error while reading from subscription stream: {e.details()} ' - f'Status Code: {e.code()}') + raise Exception(f'gRPC error while reading from subscription stream: {e} ') except Exception as e: raise Exception(f'Error while fetching message: {e}') return None - async def _respond(self, message, status): + async def respond(self, message, status): try: status = appcallback_v1.TopicEventResponse(status=status.value) response = api_v1.SubscribeTopicEventsRequestProcessedAlpha1( @@ -86,16 +85,16 @@ async def _respond(self, message, status): raise StreamInactiveError('Stream is not active') await self._send_queue.put(msg) except Exception as e: - print(f"Can't send message on inactive stream: {e}") + print(f"Can't send message: {e}") async def respond_success(self, message): - await self._respond(message, TopicEventResponse('success').status) + await self.respond(message, TopicEventResponse('success').status) async def respond_retry(self, message): - await self._respond(message, TopicEventResponse('retry').status) + await self.respond(message, TopicEventResponse('retry').status) async def respond_drop(self, message): - await self._respond(message, TopicEventResponse('drop').status) + await self.respond(message, TopicEventResponse('drop').status) async def close(self): if self._stream: diff --git a/tests/clients/test_dapr_grpc_client.py b/tests/clients/test_dapr_grpc_client.py index fee4eb2d..6c46d5ec 100644 --- a/tests/clients/test_dapr_grpc_client.py +++ b/tests/clients/test_dapr_grpc_client.py @@ -36,13 +36,9 @@ from dapr.clients.grpc._request import TransactionalStateOperation from dapr.clients.grpc._state import StateOptions, Consistency, Concurrency, StateItem from dapr.clients.grpc._crypto import EncryptOptions, DecryptOptions -from dapr.clients.grpc._response import ( - ConfigurationItem, - ConfigurationResponse, - ConfigurationWatcher, - UnlockResponseStatus, - WorkflowRuntimeStatus, -) +from dapr.clients.grpc._response import (ConfigurationItem, ConfigurationResponse, + ConfigurationWatcher, UnlockResponseStatus, + WorkflowRuntimeStatus, TopicEventResponse, ) class DaprGrpcClientTests(unittest.TestCase): @@ -376,7 +372,7 @@ def handler(message): counter += 1 - return Subscription.SUCCESS + return TopicEventResponse("success") close_fn = dapr.subscribe_with_handler( pubsub_name='pubsub', topic='example', handler_fn=handler diff --git a/tests/clients/test_dapr_grpc_client_async.py b/tests/clients/test_dapr_grpc_client_async.py index 8099e3ab..e8f8af3c 100644 --- a/tests/clients/test_dapr_grpc_client_async.py +++ b/tests/clients/test_dapr_grpc_client_async.py @@ -12,13 +12,13 @@ See the License for the specific language governing permissions and limitations under the License. """ - +import asyncio import json import socket import tempfile import unittest import uuid - +import time from unittest.mock import patch from google.rpc import status_pb2, code_pb2 @@ -26,6 +26,7 @@ from dapr.aio.clients.grpc.client import DaprGrpcClientAsync from dapr.aio.clients import DaprClient from dapr.clients.exceptions import DaprGrpcError +from dapr.common.pubsub.subscription import StreamInactiveError from dapr.proto import common_v1 from .fake_dapr_server import FakeDaprSidecar from dapr.conf import settings @@ -33,12 +34,9 @@ from dapr.clients.grpc._request import TransactionalStateOperation from dapr.clients.grpc._state import StateOptions, Consistency, Concurrency, StateItem from dapr.clients.grpc._crypto import EncryptOptions, DecryptOptions -from dapr.clients.grpc._response import ( - ConfigurationItem, - ConfigurationWatcher, - ConfigurationResponse, - UnlockResponseStatus, -) +from dapr.clients.grpc._response import (ConfigurationItem, ConfigurationWatcher, + ConfigurationResponse, UnlockResponseStatus, + TopicEventResponse, ) class DaprGrpcClientAsyncTests(unittest.IsolatedAsyncioTestCase): @@ -262,6 +260,129 @@ async def test_publish_error(self): data=111, ) + async def test_subscribe_topic(self): + # The fake server we're using sends two messages and then closes the stream + # The client should be able to read both messages, handle the stream closure and reconnect + # which will result in reading the same two messages again. + # That's why message 3 should be the same as message 1 + dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') + subscription = await dapr.subscribe(pubsub_name='pubsub', topic='example') + + # First message - text + message1 = await subscription.next_message() + await subscription.respond_success(message1) + + self.assertEqual('111', message1.id()) + self.assertEqual('app1', message1.source()) + self.assertEqual('com.example.type2', message1.type()) + self.assertEqual('1.0', message1.spec_version()) + self.assertEqual('text/plain', message1.data_content_type()) + self.assertEqual('TOPIC_A', message1.topic()) + self.assertEqual('pubsub', message1.pubsub_name()) + self.assertEqual(b'hello2', message1.raw_data()) + self.assertEqual('text/plain', message1.data_content_type()) + self.assertEqual('hello2', message1.data()) + + # Second message - json + message2 = await subscription.next_message() + await subscription.respond_success(message2) + + self.assertEqual('222', message2.id()) + self.assertEqual('app1', message2.source()) + self.assertEqual('com.example.type2', message2.type()) + self.assertEqual('1.0', message2.spec_version()) + self.assertEqual('TOPIC_A', message2.topic()) + self.assertEqual('pubsub', message2.pubsub_name()) + self.assertEqual(b'{"a": 1}', message2.raw_data()) + self.assertEqual('application/json', message2.data_content_type()) + self.assertEqual({'a': 1}, message2.data()) + + # On this call the stream will be closed and return an error, so the message will be none + # but the client will try to reconnect + message3 = await subscription.next_message() + self.assertIsNone(message3) + + # # The client already reconnected and will start reading the messages again + # # Since we're working with a fake server, the messages will be the same + # message4 = await subscription.next_message() + # await subscription.respond_success(message4) + # self.assertEqual('111', message4.id()) + # self.assertEqual('app1', message4.source()) + # self.assertEqual('com.example.type2', message4.type()) + # self.assertEqual('1.0', message4.spec_version()) + # self.assertEqual('text/plain', message4.data_content_type()) + # self.assertEqual('TOPIC_A', message4.topic()) + # self.assertEqual('pubsub', message4.pubsub_name()) + # self.assertEqual(b'hello2', message4.raw_data()) + # self.assertEqual('text/plain', message4.data_content_type()) + # self.assertEqual('hello2', message4.data()) + + await subscription.close() + + async def test_subscribe_topic_early_close(self): + dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') + subscription = await dapr.subscribe(pubsub_name='pubsub', topic='example') + await subscription.close() + + with self.assertRaises(StreamInactiveError): + await subscription.next_message() + + # async def test_subscribe_topic_with_handler(self): + # # The fake server we're using sends two messages and then closes the stream + # # The client should be able to read both messages, handle the stream closure and reconnect + # # which will result in reading the same two messages again. + # # That's why message 3 should be the same as message 1 + # dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') + # counter = 0 + # + # async def handler(message): + # nonlocal counter + # if counter == 0: + # self.assertEqual('111', message.id()) + # self.assertEqual('app1', message.source()) + # self.assertEqual('com.example.type2', message.type()) + # self.assertEqual('1.0', message.spec_version()) + # self.assertEqual('text/plain', message.data_content_type()) + # self.assertEqual('TOPIC_A', message.topic()) + # self.assertEqual('pubsub', message.pubsub_name()) + # self.assertEqual(b'hello2', message.raw_data()) + # self.assertEqual('text/plain', message.data_content_type()) + # self.assertEqual('hello2', message.data()) + # elif counter == 1: + # self.assertEqual('222', message.id()) + # self.assertEqual('app1', message.source()) + # self.assertEqual('com.example.type2', message.type()) + # self.assertEqual('1.0', message.spec_version()) + # self.assertEqual('TOPIC_A', message.topic()) + # self.assertEqual('pubsub', message.pubsub_name()) + # self.assertEqual(b'{"a": 1}', message.raw_data()) + # self.assertEqual('application/json', message.data_content_type()) + # self.assertEqual({'a': 1}, message.data()) + # elif counter == 2: + # self.assertEqual('111', message.id()) + # self.assertEqual('app1', message.source()) + # self.assertEqual('com.example.type2', message.type()) + # self.assertEqual('1.0', message.spec_version()) + # self.assertEqual('text/plain', message.data_content_type()) + # self.assertEqual('TOPIC_A', message.topic()) + # self.assertEqual('pubsub', message.pubsub_name()) + # self.assertEqual(b'hello2', message.raw_data()) + # self.assertEqual('text/plain', message.data_content_type()) + # self.assertEqual('hello2', message.data()) + # + # counter += 1 + # + # return TopicEventResponse("success") + # + # close_fn = await dapr.subscribe_with_handler( + # pubsub_name='pubsub', topic='example', handler_fn=handler + # ) + # + # while counter < 3: + # await asyncio.sleep(0.1) # sleep to prevent a busy loop + # await close_fn() + + @patch.object(settings, 'DAPR_API_TOKEN', 'test-token') async def test_dapr_api_token_insertion(self): dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') From 0ada5ea1165bd147c251bbec4b724906f99c8557 Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Thu, 10 Oct 2024 11:02:09 +0100 Subject: [PATCH 17/26] Linter Signed-off-by: Elena Kolevska --- dapr/aio/clients/grpc/client.py | 53 +++++++++++++------ dapr/aio/clients/grpc/subscription.py | 12 +++-- dapr/clients/grpc/subscription.py | 2 - dapr/common/pubsub/subscription.py | 4 +- .../async-subscriber-handler.py | 2 +- examples/pubsub-streaming/async-subscriber.py | 1 - tests/clients/test_dapr_grpc_client.py | 15 ++++-- tests/clients/test_dapr_grpc_client_async.py | 12 ++--- 8 files changed, 65 insertions(+), 36 deletions(-) diff --git a/dapr/aio/clients/grpc/client.py b/dapr/aio/clients/grpc/client.py index 626ca697..2b40101c 100644 --- a/dapr/aio/clients/grpc/client.py +++ b/dapr/aio/clients/grpc/client.py @@ -76,14 +76,28 @@ BindingRequest, TransactionalStateOperation, ) -from dapr.clients.grpc._response import (BindingResponse, DaprResponse, GetSecretResponse, - GetBulkSecretResponse, GetMetadataResponse, - InvokeMethodResponse, UnlockResponseStatus, StateResponse, - BulkStatesResponse, BulkStateItem, ConfigurationResponse, - QueryResponse, QueryResponseItem, RegisteredComponents, - ConfigurationWatcher, TryLockResponse, UnlockResponse, - GetWorkflowResponse, StartWorkflowResponse, - TopicEventResponse, ) +from dapr.clients.grpc._response import ( + BindingResponse, + DaprResponse, + GetSecretResponse, + GetBulkSecretResponse, + GetMetadataResponse, + InvokeMethodResponse, + UnlockResponseStatus, + StateResponse, + BulkStatesResponse, + BulkStateItem, + ConfigurationResponse, + QueryResponse, + QueryResponseItem, + RegisteredComponents, + ConfigurationWatcher, + TryLockResponse, + UnlockResponse, + GetWorkflowResponse, + StartWorkflowResponse, + TopicEventResponse, +) class DaprGrpcClientAsync: @@ -471,8 +485,13 @@ async def publish_event( return DaprResponse(await call.initial_metadata()) - async def subscribe(self, pubsub_name: str, topic: str, metadata: Optional[dict] = None, - dead_letter_topic: Optional[str] = None, ) -> Subscription: + async def subscribe( + self, + pubsub_name: str, + topic: str, + metadata: Optional[dict] = None, + dead_letter_topic: Optional[str] = None, + ) -> Subscription: """ Subscribe to a topic with a bidirectional stream @@ -485,14 +504,18 @@ async def subscribe(self, pubsub_name: str, topic: str, metadata: Optional[dict] Returns: Subscription: The Subscription object managing the stream. """ - subscription = Subscription(self._stub, pubsub_name, topic, metadata, - dead_letter_topic) + subscription = Subscription(self._stub, pubsub_name, topic, metadata, dead_letter_topic) await subscription.start() return subscription - async def subscribe_with_handler(self, pubsub_name: str, topic: str, - handler_fn: Callable[..., TopicEventResponse], metadata: Optional[dict] = None, - dead_letter_topic: Optional[str] = None, ) -> Callable[[], Awaitable[None]]: + async def subscribe_with_handler( + self, + pubsub_name: str, + topic: str, + handler_fn: Callable[..., TopicEventResponse], + metadata: Optional[dict] = None, + dead_letter_topic: Optional[str] = None, + ) -> Callable[[], Awaitable[None]]: """ Subscribe to a topic with a bidirectional stream and a message handler function diff --git a/dapr/aio/clients/grpc/subscription.py b/dapr/aio/clients/grpc/subscription.py index 7dc10f9a..84542bb4 100644 --- a/dapr/aio/clients/grpc/subscription.py +++ b/dapr/aio/clients/grpc/subscription.py @@ -7,8 +7,8 @@ from dapr.common.pubsub.subscription import StreamInactiveError, SubscriptionMessage from dapr.proto import api_v1, appcallback_v1 -class Subscription: +class Subscription: def __init__(self, stub, pubsub_name, topic, metadata=None, dead_letter_topic=None): self._stub = stub self._pubsub_name = pubsub_name @@ -63,9 +63,11 @@ async def next_message(self): return SubscriptionMessage(message.event_message) except AioRpcError as e: if e.code() == StatusCode.UNAVAILABLE: - print(f'gRPC error while reading from stream: {e.details()}, ' - f'Status Code: {e.code()}. ' - f'Attempting to reconnect...') + print( + f'gRPC error while reading from stream: {e.details()}, ' + f'Status Code: {e.code()}. ' + f'Attempting to reconnect...' + ) await self.reconnect_stream() elif e.code() != StatusCode.CANCELLED: raise Exception(f'gRPC error while reading from subscription stream: {e} ') @@ -105,4 +107,4 @@ async def close(self): if e.code() != StatusCode.CANCELLED: raise Exception(f'Error while closing stream: {e}') except Exception as e: - raise Exception(f'Error while closing stream: {e}') \ No newline at end of file + raise Exception(f'Error while closing stream: {e}') diff --git a/dapr/clients/grpc/subscription.py b/dapr/clients/grpc/subscription.py index 8b99b34f..3374a121 100644 --- a/dapr/clients/grpc/subscription.py +++ b/dapr/clients/grpc/subscription.py @@ -9,9 +9,7 @@ from typing import Optional - class Subscription: - def __init__(self, stub, pubsub_name, topic, metadata=None, dead_letter_topic=None): self._stub = stub self._pubsub_name = pubsub_name diff --git a/dapr/common/pubsub/subscription.py b/dapr/common/pubsub/subscription.py index ac8db973..0f96ab6b 100644 --- a/dapr/common/pubsub/subscription.py +++ b/dapr/common/pubsub/subscription.py @@ -3,6 +3,7 @@ from dapr.proto.runtime.v1.appcallback_pb2 import TopicEventRequest from typing import Optional, Union + class SubscriptionMessage: def __init__(self, msg: TopicEventRequest): self._id: str = msg.id @@ -86,7 +87,8 @@ def _parse_data_content(self): class StreamInactiveError(Exception): pass + class PubSubEventStatus: SUCCESS = 'success' RETRY = 'retry' - DROP = 'drop' \ No newline at end of file + DROP = 'drop' diff --git a/examples/pubsub-streaming/async-subscriber-handler.py b/examples/pubsub-streaming/async-subscriber-handler.py index 75aca19e..e5f68953 100644 --- a/examples/pubsub-streaming/async-subscriber-handler.py +++ b/examples/pubsub-streaming/async-subscriber-handler.py @@ -32,7 +32,7 @@ async def main(): # Wait until 5 messages are processed global counter while counter < 5: - print("Counter: ", counter) + print('Counter: ', counter) await asyncio.sleep(1) print('Closing subscription...') diff --git a/examples/pubsub-streaming/async-subscriber.py b/examples/pubsub-streaming/async-subscriber.py index 396b3cc2..0f7da59b 100644 --- a/examples/pubsub-streaming/async-subscriber.py +++ b/examples/pubsub-streaming/async-subscriber.py @@ -49,6 +49,5 @@ async def main(): await subscription.close() - if __name__ == '__main__': asyncio.run(main()) diff --git a/tests/clients/test_dapr_grpc_client.py b/tests/clients/test_dapr_grpc_client.py index 6c46d5ec..d3eab236 100644 --- a/tests/clients/test_dapr_grpc_client.py +++ b/tests/clients/test_dapr_grpc_client.py @@ -28,7 +28,7 @@ from dapr.clients.exceptions import DaprGrpcError from dapr.clients.grpc.client import DaprGrpcClient from dapr.clients import DaprClient -from dapr.clients.grpc.subscription import StreamInactiveError, Subscription +from dapr.clients.grpc.subscription import StreamInactiveError from dapr.proto import common_v1 from .fake_dapr_server import FakeDaprSidecar from dapr.conf import settings @@ -36,9 +36,14 @@ from dapr.clients.grpc._request import TransactionalStateOperation from dapr.clients.grpc._state import StateOptions, Consistency, Concurrency, StateItem from dapr.clients.grpc._crypto import EncryptOptions, DecryptOptions -from dapr.clients.grpc._response import (ConfigurationItem, ConfigurationResponse, - ConfigurationWatcher, UnlockResponseStatus, - WorkflowRuntimeStatus, TopicEventResponse, ) +from dapr.clients.grpc._response import ( + ConfigurationItem, + ConfigurationResponse, + ConfigurationWatcher, + UnlockResponseStatus, + WorkflowRuntimeStatus, + TopicEventResponse, +) class DaprGrpcClientTests(unittest.TestCase): @@ -372,7 +377,7 @@ def handler(message): counter += 1 - return TopicEventResponse("success") + return TopicEventResponse('success') close_fn = dapr.subscribe_with_handler( pubsub_name='pubsub', topic='example', handler_fn=handler diff --git a/tests/clients/test_dapr_grpc_client_async.py b/tests/clients/test_dapr_grpc_client_async.py index e8f8af3c..42bbd830 100644 --- a/tests/clients/test_dapr_grpc_client_async.py +++ b/tests/clients/test_dapr_grpc_client_async.py @@ -12,13 +12,11 @@ See the License for the specific language governing permissions and limitations under the License. """ -import asyncio import json import socket import tempfile import unittest import uuid -import time from unittest.mock import patch from google.rpc import status_pb2, code_pb2 @@ -34,9 +32,12 @@ from dapr.clients.grpc._request import TransactionalStateOperation from dapr.clients.grpc._state import StateOptions, Consistency, Concurrency, StateItem from dapr.clients.grpc._crypto import EncryptOptions, DecryptOptions -from dapr.clients.grpc._response import (ConfigurationItem, ConfigurationWatcher, - ConfigurationResponse, UnlockResponseStatus, - TopicEventResponse, ) +from dapr.clients.grpc._response import ( + ConfigurationItem, + ConfigurationWatcher, + ConfigurationResponse, + UnlockResponseStatus, +) class DaprGrpcClientAsyncTests(unittest.IsolatedAsyncioTestCase): @@ -382,7 +383,6 @@ async def test_subscribe_topic_early_close(self): # await asyncio.sleep(0.1) # sleep to prevent a busy loop # await close_fn() - @patch.object(settings, 'DAPR_API_TOKEN', 'test-token') async def test_dapr_api_token_insertion(self): dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') From edadffe07d65d1bca504f4def31157ba97d730d8 Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Thu, 10 Oct 2024 16:25:30 +0100 Subject: [PATCH 18/26] Split sync and async examples Signed-off-by: Elena Kolevska --- examples/pubsub-streaming-async/README.md | 122 ++++++++++++++++++ examples/pubsub-streaming-async/publisher.py | 43 ++++++ .../subscriber-handler.py} | 1 - .../subscriber.py} | 0 examples/pubsub-streaming/README.md | 97 -------------- tox.ini | 1 + 6 files changed, 166 insertions(+), 98 deletions(-) create mode 100644 examples/pubsub-streaming-async/README.md create mode 100644 examples/pubsub-streaming-async/publisher.py rename examples/{pubsub-streaming/async-subscriber-handler.py => pubsub-streaming-async/subscriber-handler.py} (96%) rename examples/{pubsub-streaming/async-subscriber.py => pubsub-streaming-async/subscriber.py} (100%) diff --git a/examples/pubsub-streaming-async/README.md b/examples/pubsub-streaming-async/README.md new file mode 100644 index 00000000..dfa7d27d --- /dev/null +++ b/examples/pubsub-streaming-async/README.md @@ -0,0 +1,122 @@ +# Example - Publish and subscribe to messages + +This example utilizes a publisher and a subscriber to show the bidirectional pubsub pattern. +It creates a publisher and calls the `publish_event` method in the `DaprClient`. +In the s`subscriber.py` file it creates a subscriber object that can call the `next_message` method to get new messages from the stream. After processing the new message, it returns a status to the stream. + + +> **Note:** Make sure to use the latest proto bindings + +## Pre-requisites + +- [Dapr CLI and initialized environment](https://docs.dapr.io/getting-started) +- [Install Python 3.8+](https://www.python.org/downloads/) + +## Install Dapr python-SDK + + + +```bash +pip3 install dapr +``` + +## Run async example where users control reading messages off the stream + +Run the following command in a terminal/command prompt: + + + +```bash +# 1. Start Subscriber +dapr run --app-id python-subscriber --app-protocol grpc python3 subscriber.py +``` + + + +In another terminal/command prompt run: + + + +```bash +# 2. Start Publisher +dapr run --app-id python-publisher --app-protocol grpc --dapr-grpc-port=3500 --enable-app-health-check python3 publisher.py +``` + + + +## Run async example with a handler function + +Run the following command in a terminal/command prompt: + + + +```bash +# 1. Start Subscriber +dapr run --app-id python-subscriber --app-protocol grpc python3 subscriber-handler.py +``` + + + +In another terminal/command prompt run: + + + +```bash +# 2. Start Publisher +dapr run --app-id python-publisher --app-protocol grpc --dapr-grpc-port=3500 --enable-app-health-check python3 publisher.py +``` + + + + +## Cleanup + + diff --git a/examples/pubsub-streaming-async/publisher.py b/examples/pubsub-streaming-async/publisher.py new file mode 100644 index 00000000..7268f16a --- /dev/null +++ b/examples/pubsub-streaming-async/publisher.py @@ -0,0 +1,43 @@ +# ------------------------------------------------------------ +# Copyright 2022 The Dapr Authors +# Licensed 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. +# ------------------------------------------------------------ +import asyncio +import json + +from dapr.aio.clients import DaprClient + +async def publish_events(): + """ + Publishes events to a pubsub topic asynchronously + """ + + async with DaprClient() as d: + id = 0 + while id < 5: + id += 1 + req_data = {'id': id, 'message': 'hello world'} + + # Create a typed message with content type and body + await d.publish_event( + pubsub_name='pubsub', + topic_name='TOPIC_A', + data=json.dumps(req_data), + data_content_type='application/json', + publish_metadata={'ttlInSeconds': '100', 'rawPayload': 'false'}, + ) + + # Print the request + print(req_data, flush=True) + + await asyncio.sleep(1) + +asyncio.run(publish_events()) \ No newline at end of file diff --git a/examples/pubsub-streaming/async-subscriber-handler.py b/examples/pubsub-streaming-async/subscriber-handler.py similarity index 96% rename from examples/pubsub-streaming/async-subscriber-handler.py rename to examples/pubsub-streaming-async/subscriber-handler.py index e5f68953..f9503f06 100644 --- a/examples/pubsub-streaming/async-subscriber-handler.py +++ b/examples/pubsub-streaming-async/subscriber-handler.py @@ -32,7 +32,6 @@ async def main(): # Wait until 5 messages are processed global counter while counter < 5: - print('Counter: ', counter) await asyncio.sleep(1) print('Closing subscription...') diff --git a/examples/pubsub-streaming/async-subscriber.py b/examples/pubsub-streaming-async/subscriber.py similarity index 100% rename from examples/pubsub-streaming/async-subscriber.py rename to examples/pubsub-streaming-async/subscriber.py diff --git a/examples/pubsub-streaming/README.md b/examples/pubsub-streaming/README.md index 4bad7f3c..4849e791 100644 --- a/examples/pubsub-streaming/README.md +++ b/examples/pubsub-streaming/README.md @@ -116,103 +116,6 @@ dapr run --app-id python-publisher --app-protocol grpc --dapr-grpc-port=3500 --e -## Run async example where users control reading messages off the stream - -Run the following command in a terminal/command prompt: - - - -```bash -# 1. Start Subscriber -dapr run --app-id python-subscriber --app-protocol grpc python3 async-subscriber.py -``` - - - -In another terminal/command prompt run: - - - -```bash -# 2. Start Publisher -dapr run --app-id python-publisher --app-protocol grpc --dapr-grpc-port=3500 --enable-app-health-check python3 publisher.py -``` - - - -## Run async example with a handler function - -Run the following command in a terminal/command prompt: - - - -```bash -# 1. Start Subscriber -dapr run --app-id python-subscriber --app-protocol grpc python3 async-subscriber-handler.py -``` - - - -In another terminal/command prompt run: - - - -```bash -# 2. Start Publisher -dapr run --app-id python-publisher --app-protocol grpc --dapr-grpc-port=3500 --enable-app-health-check python3 publisher.py -``` - - - - ## Cleanup diff --git a/tox.ini b/tox.ini index 6400e329..78f23086 100644 --- a/tox.ini +++ b/tox.ini @@ -51,6 +51,7 @@ commands = ./validate.sh error_handling ./validate.sh pubsub-simple ./validate.sh pubsub-streaming + ./validate.sh pubsub-streaming-async ./validate.sh state_store ./validate.sh state_store_query ./validate.sh secret_store From baff8e737ee99653a58b29f156c975455e650b73 Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Thu, 10 Oct 2024 16:38:37 +0100 Subject: [PATCH 19/26] linter Signed-off-by: Elena Kolevska --- examples/pubsub-streaming-async/publisher.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/pubsub-streaming-async/publisher.py b/examples/pubsub-streaming-async/publisher.py index 7268f16a..b9702355 100644 --- a/examples/pubsub-streaming-async/publisher.py +++ b/examples/pubsub-streaming-async/publisher.py @@ -15,6 +15,7 @@ from dapr.aio.clients import DaprClient + async def publish_events(): """ Publishes events to a pubsub topic asynchronously @@ -40,4 +41,5 @@ async def publish_events(): await asyncio.sleep(1) -asyncio.run(publish_events()) \ No newline at end of file + +asyncio.run(publish_events()) From 2626f446cc84519daa7805d91f3bca839cf149d4 Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Fri, 11 Oct 2024 19:01:23 +0100 Subject: [PATCH 20/26] Adds interceptors to the async client for bidirectional streaming Signed-off-by: Elena Kolevska --- dapr/aio/clients/grpc/interceptors.py | 25 ++++++++++++++++++++----- dapr/clients/grpc/interceptors.py | 3 --- dapr/clients/grpc/subscription.py | 6 +++++- examples/pubsub-streaming/subscriber.py | 13 ++++++++++--- 4 files changed, 35 insertions(+), 12 deletions(-) diff --git a/dapr/aio/clients/grpc/interceptors.py b/dapr/aio/clients/grpc/interceptors.py index 55ede4b9..346214dc 100644 --- a/dapr/aio/clients/grpc/interceptors.py +++ b/dapr/aio/clients/grpc/interceptors.py @@ -16,7 +16,7 @@ from collections import namedtuple from typing import List, Tuple -from grpc.aio import UnaryUnaryClientInterceptor, ClientCallDetails # type: ignore +from grpc.aio import UnaryUnaryClientInterceptor, StreamStreamClientInterceptor, ClientCallDetails # type: ignore from dapr.conf import settings @@ -50,8 +50,7 @@ def intercept_unary_unary(self, continuation, client_call_details, request): return continuation(client_call_details, request) - -class DaprClientInterceptorAsync(UnaryUnaryClientInterceptor): +class DaprClientInterceptorAsync(UnaryUnaryClientInterceptor, StreamStreamClientInterceptor): """The class implements a UnaryUnaryClientInterceptor from grpc to add an interceptor to add additional headers to all calls as needed. @@ -115,9 +114,25 @@ async def intercept_unary_unary(self, continuation, client_call_details, request Returns: A response object after invoking the continuation callable """ - - # Pre-process or intercept call new_call_details = await self._intercept_call(client_call_details) # Call continuation response = await continuation(new_call_details, request) return response + + async def intercept_stream_stream(self, continuation, client_call_details, request): + """This method intercepts a stream-stream gRPC call. This is the implementation of the + abstract method defined in StreamStreamClientInterceptor defined in grpc. This is invoked + automatically by grpc based on the order in which interceptors are added to the channel. + + Args: + continuation: a callable to be invoked to continue with the RPC or next interceptor + client_call_details: a ClientCallDetails object describing the outgoing RPC + request: the request value for the RPC + + Returns: + A response object after invoking the continuation callable + """ + new_call_details = await self._intercept_call(client_call_details) + # Call continuation + response = await continuation(new_call_details, request) + return response \ No newline at end of file diff --git a/dapr/clients/grpc/interceptors.py b/dapr/clients/grpc/interceptors.py index adda29c1..15bde185 100644 --- a/dapr/clients/grpc/interceptors.py +++ b/dapr/clients/grpc/interceptors.py @@ -103,7 +103,6 @@ def intercept_unary_unary(self, continuation, client_call_details, request): Returns: A response object after invoking the continuation callable """ - # Pre-process or intercept call new_call_details = self._intercept_call(client_call_details) # Call continuation response = continuation(new_call_details, request) @@ -122,8 +121,6 @@ def intercept_stream_stream(self, continuation, client_call_details, request_ite Returns: A response object after invoking the continuation callable """ - # Pre-process or intercept call - new_call_details = self._intercept_call(client_call_details) # Call continuation response = continuation(new_call_details, request_iterator) diff --git a/dapr/clients/grpc/subscription.py b/dapr/clients/grpc/subscription.py index 3374a121..a69df125 100644 --- a/dapr/clients/grpc/subscription.py +++ b/dapr/clients/grpc/subscription.py @@ -54,7 +54,11 @@ def outgoing_request_iterator(): # Create the bidirectional stream self._stream = self._stub.SubscribeTopicEventsAlpha1(outgoing_request_iterator()) self._set_stream_active() - next(self._stream) # discard the initial message + try: + next(self._stream) # discard the initial message + except Exception as e: + raise Exception(f'Error while initializing stream: {e}') + def reconnect_stream(self): self.close() diff --git a/examples/pubsub-streaming/subscriber.py b/examples/pubsub-streaming/subscriber.py index 5716b34c..4af7ee0a 100644 --- a/examples/pubsub-streaming/subscriber.py +++ b/examples/pubsub-streaming/subscriber.py @@ -18,9 +18,13 @@ def main(): with DaprClient() as client: global counter - subscription = client.subscribe( - pubsub_name='pubsub', topic='TOPIC_A', dead_letter_topic='TOPIC_A_DEAD' - ) + try: + subscription = client.subscribe( + pubsub_name='pubsub', topic='TOPIC_A', dead_letter_topic='TOPIC_A_DEAD' + ) + except Exception as e: + print(f'Error occurred: {e}') + return try: while counter < 5: @@ -31,6 +35,9 @@ def main(): print('Stream is inactive. Retrying...') time.sleep(1) continue + except Exception as e: + print(f'Error occurred: {e}') + pass if message is None: print('No message received within timeout period.') continue From b04615f6cdbfdaf3d5a04eb34edd496b9ee417f1 Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Fri, 11 Oct 2024 19:05:33 +0100 Subject: [PATCH 21/26] Removes unneeded class Signed-off-by: Elena Kolevska --- dapr/aio/clients/grpc/interceptors.py | 3 ++- dapr/clients/grpc/subscription.py | 1 - dapr/common/pubsub/subscription.py | 6 ------ 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/dapr/aio/clients/grpc/interceptors.py b/dapr/aio/clients/grpc/interceptors.py index 346214dc..bf83cf56 100644 --- a/dapr/aio/clients/grpc/interceptors.py +++ b/dapr/aio/clients/grpc/interceptors.py @@ -50,6 +50,7 @@ def intercept_unary_unary(self, continuation, client_call_details, request): return continuation(client_call_details, request) + class DaprClientInterceptorAsync(UnaryUnaryClientInterceptor, StreamStreamClientInterceptor): """The class implements a UnaryUnaryClientInterceptor from grpc to add an interceptor to add additional headers to all calls as needed. @@ -135,4 +136,4 @@ async def intercept_stream_stream(self, continuation, client_call_details, reque new_call_details = await self._intercept_call(client_call_details) # Call continuation response = await continuation(new_call_details, request) - return response \ No newline at end of file + return response diff --git a/dapr/clients/grpc/subscription.py b/dapr/clients/grpc/subscription.py index a69df125..b5a87080 100644 --- a/dapr/clients/grpc/subscription.py +++ b/dapr/clients/grpc/subscription.py @@ -59,7 +59,6 @@ def outgoing_request_iterator(): except Exception as e: raise Exception(f'Error while initializing stream: {e}') - def reconnect_stream(self): self.close() DaprHealth.wait_until_ready() diff --git a/dapr/common/pubsub/subscription.py b/dapr/common/pubsub/subscription.py index 0f96ab6b..ad6f6f56 100644 --- a/dapr/common/pubsub/subscription.py +++ b/dapr/common/pubsub/subscription.py @@ -86,9 +86,3 @@ def _parse_data_content(self): class StreamInactiveError(Exception): pass - - -class PubSubEventStatus: - SUCCESS = 'success' - RETRY = 'retry' - DROP = 'drop' From ce0f78140c2c2e5c53976d2fa8dbda1ef249aa4a Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Mon, 21 Oct 2024 11:29:48 +0100 Subject: [PATCH 22/26] Removes async examples tests Signed-off-by: Elena Kolevska --- tox.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/tox.ini b/tox.ini index 78f23086..6400e329 100644 --- a/tox.ini +++ b/tox.ini @@ -51,7 +51,6 @@ commands = ./validate.sh error_handling ./validate.sh pubsub-simple ./validate.sh pubsub-streaming - ./validate.sh pubsub-streaming-async ./validate.sh state_store ./validate.sh state_store_query ./validate.sh secret_store From df2fa9f24e4ac9252d0737ae58093fa98e428f30 Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Mon, 21 Oct 2024 11:30:44 +0100 Subject: [PATCH 23/26] Revert "Removes async examples tests" This reverts commit ce0f78140c2c2e5c53976d2fa8dbda1ef249aa4a. --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 6400e329..78f23086 100644 --- a/tox.ini +++ b/tox.ini @@ -51,6 +51,7 @@ commands = ./validate.sh error_handling ./validate.sh pubsub-simple ./validate.sh pubsub-streaming + ./validate.sh pubsub-streaming-async ./validate.sh state_store ./validate.sh state_store_query ./validate.sh secret_store From fc0c5faead3339687f9a6b00c2e2af57ba5a7fb2 Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Mon, 21 Oct 2024 14:10:33 +0100 Subject: [PATCH 24/26] Split up topic names between tests Signed-off-by: Elena Kolevska --- examples/pubsub-streaming/README.md | 28 +++++++++---------- examples/pubsub-streaming/publisher.py | 10 +++++-- .../pubsub-streaming/subscriber-handler.py | 11 ++++++-- examples/pubsub-streaming/subscriber.py | 10 ++++++- 4 files changed, 40 insertions(+), 19 deletions(-) diff --git a/examples/pubsub-streaming/README.md b/examples/pubsub-streaming/README.md index 4849e791..15664522 100644 --- a/examples/pubsub-streaming/README.md +++ b/examples/pubsub-streaming/README.md @@ -27,11 +27,11 @@ Run the following command in a terminal/command prompt: @@ -63,7 +63,7 @@ sleep: 15 ```bash # 2. Start Publisher -dapr run --app-id python-publisher --app-protocol grpc --dapr-grpc-port=3500 --enable-app-health-check python3 publisher.py +dapr run --app-id python-publisher --app-protocol grpc --dapr-grpc-port=3500 --enable-app-health-check -- python3 publisher.py --topic=TOPIC_A1 ``` @@ -75,11 +75,11 @@ Run the following command in a terminal/command prompt: @@ -111,7 +111,7 @@ sleep: 15 ```bash # 2. Start Publisher -dapr run --app-id python-publisher --app-protocol grpc --dapr-grpc-port=3500 --enable-app-health-check python3 publisher.py +dapr run --app-id python-publisher --app-protocol grpc --dapr-grpc-port=3500 --enable-app-health-check -- python3 publisher.py --topic=TOPIC_A2 ``` diff --git a/examples/pubsub-streaming/publisher.py b/examples/pubsub-streaming/publisher.py index fd797470..6ae68c22 100644 --- a/examples/pubsub-streaming/publisher.py +++ b/examples/pubsub-streaming/publisher.py @@ -10,12 +10,18 @@ # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------ - +import argparse import json import time from dapr.clients import DaprClient +parser = argparse.ArgumentParser(description='Publish events to a Dapr pub/sub topic.') +parser.add_argument('--topic', type=str, required=True, help='The topic name to publish to.') +args = parser.parse_args() + +topic_name = args.topic + with DaprClient() as d: id = 0 while id < 5: @@ -25,7 +31,7 @@ # Create a typed message with content type and body resp = d.publish_event( pubsub_name='pubsub', - topic_name='TOPIC_A', + topic_name=topic_name, data=json.dumps(req_data), data_content_type='application/json', publish_metadata={'ttlInSeconds': '100', 'rawPayload': 'false'}, diff --git a/examples/pubsub-streaming/subscriber-handler.py b/examples/pubsub-streaming/subscriber-handler.py index aab840a4..4409fa5a 100644 --- a/examples/pubsub-streaming/subscriber-handler.py +++ b/examples/pubsub-streaming/subscriber-handler.py @@ -1,3 +1,4 @@ +import argparse import time from dapr.clients import DaprClient @@ -5,6 +6,12 @@ counter = 0 +parser = argparse.ArgumentParser(description='Publish events to a Dapr pub/sub topic.') +parser.add_argument('--topic', type=str, required=True, help='The topic name to publish to.') +args = parser.parse_args() + +topic_name = args.topic +dlq_topic_name = topic_name + '_DEAD' def process_message(message): # Process the message here @@ -20,9 +27,9 @@ def main(): # and process them in the `process_message` function close_fn = client.subscribe_with_handler( pubsub_name='pubsub', - topic='TOPIC_A', + topic=topic_name, handler_fn=process_message, - dead_letter_topic='TOPIC_A_DEAD', + dead_letter_topic=dlq_topic_name, ) while counter < 5: diff --git a/examples/pubsub-streaming/subscriber.py b/examples/pubsub-streaming/subscriber.py index 4af7ee0a..2c79235a 100644 --- a/examples/pubsub-streaming/subscriber.py +++ b/examples/pubsub-streaming/subscriber.py @@ -1,3 +1,4 @@ +import argparse import time from dapr.clients import DaprClient @@ -5,6 +6,13 @@ counter = 0 +parser = argparse.ArgumentParser(description='Publish events to a Dapr pub/sub topic.') +parser.add_argument('--topic', type=str, required=True, help='The topic name to publish to.') +args = parser.parse_args() + +topic_name = args.topic +dlq_topic_name = topic_name + '_DEAD' + def process_message(message): global counter @@ -20,7 +28,7 @@ def main(): try: subscription = client.subscribe( - pubsub_name='pubsub', topic='TOPIC_A', dead_letter_topic='TOPIC_A_DEAD' + pubsub_name='pubsub', topic=topic_name, dead_letter_topic=dlq_topic_name ) except Exception as e: print(f'Error occurred: {e}') From 09576c8599c38b4c92269736848d49325277a3eb Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Mon, 21 Oct 2024 14:15:06 +0100 Subject: [PATCH 25/26] Split up topic names between tests Signed-off-by: Elena Kolevska --- examples/pubsub-streaming-async/README.md | 28 +++++++++---------- examples/pubsub-streaming-async/publisher.py | 9 +++++- .../subscriber-handler.py | 12 ++++++-- examples/pubsub-streaming-async/subscriber.py | 10 ++++++- 4 files changed, 41 insertions(+), 18 deletions(-) diff --git a/examples/pubsub-streaming-async/README.md b/examples/pubsub-streaming-async/README.md index dfa7d27d..60c1cdef 100644 --- a/examples/pubsub-streaming-async/README.md +++ b/examples/pubsub-streaming-async/README.md @@ -27,11 +27,11 @@ Run the following command in a terminal/command prompt: @@ -63,7 +63,7 @@ sleep: 15 ```bash # 2. Start Publisher -dapr run --app-id python-publisher --app-protocol grpc --dapr-grpc-port=3500 --enable-app-health-check python3 publisher.py +dapr run --app-id python-publisher --app-protocol grpc --dapr-grpc-port=3500 --enable-app-health-check -- python3 publisher.py --topic=TOPIC_B1 ``` @@ -75,11 +75,11 @@ Run the following command in a terminal/command prompt: @@ -111,7 +111,7 @@ sleep: 15 ```bash # 2. Start Publisher -dapr run --app-id python-publisher --app-protocol grpc --dapr-grpc-port=3500 --enable-app-health-check python3 publisher.py +dapr run --app-id python-publisher --app-protocol grpc --dapr-grpc-port=3500 --enable-app-health-check -- python3 publisher.py --topic=TOPIC_B2 ``` diff --git a/examples/pubsub-streaming-async/publisher.py b/examples/pubsub-streaming-async/publisher.py index b9702355..e4abf359 100644 --- a/examples/pubsub-streaming-async/publisher.py +++ b/examples/pubsub-streaming-async/publisher.py @@ -10,11 +10,18 @@ # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------ +import argparse import asyncio import json from dapr.aio.clients import DaprClient +parser = argparse.ArgumentParser(description='Publish events to a Dapr pub/sub topic.') +parser.add_argument('--topic', type=str, required=True, help='The topic name to publish to.') +args = parser.parse_args() + +topic_name = args.topic + async def publish_events(): """ @@ -30,7 +37,7 @@ async def publish_events(): # Create a typed message with content type and body await d.publish_event( pubsub_name='pubsub', - topic_name='TOPIC_A', + topic_name=topic_name, data=json.dumps(req_data), data_content_type='application/json', publish_metadata={'ttlInSeconds': '100', 'rawPayload': 'false'}, diff --git a/examples/pubsub-streaming-async/subscriber-handler.py b/examples/pubsub-streaming-async/subscriber-handler.py index f9503f06..34129ee7 100644 --- a/examples/pubsub-streaming-async/subscriber-handler.py +++ b/examples/pubsub-streaming-async/subscriber-handler.py @@ -1,7 +1,15 @@ +import argparse import asyncio from dapr.aio.clients import DaprClient from dapr.clients.grpc._response import TopicEventResponse +parser = argparse.ArgumentParser(description='Publish events to a Dapr pub/sub topic.') +parser.add_argument('--topic', type=str, required=True, help='The topic name to publish to.') +args = parser.parse_args() + +topic_name = args.topic +dlq_topic_name = topic_name + '_DEAD' + counter = 0 @@ -24,9 +32,9 @@ async def main(): # Subscribe to the pubsub topic with the message handler close_fn = await client.subscribe_with_handler( pubsub_name='pubsub', - topic='TOPIC_A', + topic=topic_name, handler_fn=process_message, - dead_letter_topic='TOPIC_A_DEAD', + dead_letter_topic=dlq_topic_name, ) # Wait until 5 messages are processed diff --git a/examples/pubsub-streaming-async/subscriber.py b/examples/pubsub-streaming-async/subscriber.py index 0f7da59b..9a0d34a5 100644 --- a/examples/pubsub-streaming-async/subscriber.py +++ b/examples/pubsub-streaming-async/subscriber.py @@ -1,8 +1,16 @@ +import argparse import asyncio from dapr.aio.clients import DaprClient from dapr.clients.grpc.subscription import StreamInactiveError +parser = argparse.ArgumentParser(description='Publish events to a Dapr pub/sub topic.') +parser.add_argument('--topic', type=str, required=True, help='The topic name to publish to.') +args = parser.parse_args() + +topic_name = args.topic +dlq_topic_name = topic_name + '_DEAD' + counter = 0 @@ -18,7 +26,7 @@ async def main(): async with DaprClient() as client: global counter subscription = await client.subscribe( - pubsub_name='pubsub', topic='TOPIC_A', dead_letter_topic='TOPIC_A_DEAD' + pubsub_name='pubsub', topic=topic_name, dead_letter_topic=dlq_topic_name ) try: From 8ba8b4139f8eba9346966ffc67a9c2e766a92e56 Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Mon, 21 Oct 2024 14:19:48 +0100 Subject: [PATCH 26/26] linter Signed-off-by: Elena Kolevska --- examples/pubsub-streaming/subscriber-handler.py | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/pubsub-streaming/subscriber-handler.py b/examples/pubsub-streaming/subscriber-handler.py index 4409fa5a..3a963fd2 100644 --- a/examples/pubsub-streaming/subscriber-handler.py +++ b/examples/pubsub-streaming/subscriber-handler.py @@ -13,6 +13,7 @@ topic_name = args.topic dlq_topic_name = topic_name + '_DEAD' + def process_message(message): # Process the message here global counter