From ce19b350d833da72ab3cd4c1b63d452d9af74f99 Mon Sep 17 00:00:00 2001 From: Mohammad Dashti Date: Sat, 3 Oct 2026 02:16:36 -0700 Subject: [PATCH 1/3] Vendored the server's stream service protos. The stream service is still defined in the server, so its protos are copied in and compiled by a script rather than taken from the api submodule. --- scripts/gen_stream_protos.py | 193 ++ temporalio/api/streamservice/__init__.py | 0 temporalio/api/streamservice/v1/__init__.py | 158 ++ .../api/streamservice/v1/message_pb2.py | 79 + .../api/streamservice/v1/message_pb2.pyi | 174 ++ .../api/streamservice/v1/message_pb2_grpc.py | 4 + .../api/streamservice/v1/message_pb2_grpc.pyi | 4 + .../streamservice/v1/request_response_pb2.py | 908 ++++++++ .../streamservice/v1/request_response_pb2.pyi | 1979 +++++++++++++++++ .../v1/request_response_pb2_grpc.py | 4 + .../v1/request_response_pb2_grpc.pyi | 4 + .../api/streamservice/v1/service_pb2.py | 34 + .../api/streamservice/v1/service_pb2.pyi | 8 + .../api/streamservice/v1/service_pb2_grpc.py | 719 ++++++ .../api/streamservice/v1/service_pb2_grpc.pyi | 199 ++ .../api/streamservice/v1/stream_state_pb2.py | 148 ++ .../api/streamservice/v1/stream_state_pb2.pyi | 479 ++++ .../streamservice/v1/stream_state_pb2_grpc.py | 4 + .../v1/stream_state_pb2_grpc.pyi | 4 + 19 files changed, 5102 insertions(+) create mode 100644 scripts/gen_stream_protos.py create mode 100644 temporalio/api/streamservice/__init__.py create mode 100644 temporalio/api/streamservice/v1/__init__.py create mode 100644 temporalio/api/streamservice/v1/message_pb2.py create mode 100644 temporalio/api/streamservice/v1/message_pb2.pyi create mode 100644 temporalio/api/streamservice/v1/message_pb2_grpc.py create mode 100644 temporalio/api/streamservice/v1/message_pb2_grpc.pyi create mode 100644 temporalio/api/streamservice/v1/request_response_pb2.py create mode 100644 temporalio/api/streamservice/v1/request_response_pb2.pyi create mode 100644 temporalio/api/streamservice/v1/request_response_pb2_grpc.py create mode 100644 temporalio/api/streamservice/v1/request_response_pb2_grpc.pyi create mode 100644 temporalio/api/streamservice/v1/service_pb2.py create mode 100644 temporalio/api/streamservice/v1/service_pb2.pyi create mode 100644 temporalio/api/streamservice/v1/service_pb2_grpc.py create mode 100644 temporalio/api/streamservice/v1/service_pb2_grpc.pyi create mode 100644 temporalio/api/streamservice/v1/stream_state_pb2.py create mode 100644 temporalio/api/streamservice/v1/stream_state_pb2.pyi create mode 100644 temporalio/api/streamservice/v1/stream_state_pb2_grpc.py create mode 100644 temporalio/api/streamservice/v1/stream_state_pb2_grpc.pyi diff --git a/scripts/gen_stream_protos.py b/scripts/gen_stream_protos.py new file mode 100644 index 000000000..faf5453d5 --- /dev/null +++ b/scripts/gen_stream_protos.py @@ -0,0 +1,193 @@ +"""Regenerate the vendored server-side stream protos. + +The stream service is still defined in the server rather than in the public +API repo, so its protos are copied in and compiled here. Run this whenever the +server-side definitions change: + + uv run python scripts/gen_stream_protos.py /path/to/temporal + +Run it with an interpreter whose ``grpcio-tools`` matches the oldest protobuf +runtime this package has to load on. Generated code refuses to load on a +runtime older than the one it was built against, and these modules end up in +applications that pin protobuf themselves. The stubs come from +``mypy-protobuf`` rather than protoc's own ``--pyi_out``, which that +generation of protoc does not have. + +Two rewrites make the copies compile against this SDK. The server's own +routing and API-category options are dropped, because they mean nothing to a +client and their proto files are not vendored. Paths move under +``temporalio/`` so the generated modules import each other and the public API +messages by the names this package actually publishes. +""" + +from __future__ import annotations + +import os +import re +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path + +BASE = Path(__file__).parent.parent +OUT = BASE / "temporalio" / "api" / "streamservice" / "v1" +# The stream protos reference the public API messages. They normally come from +# the sdk-core submodule; TEMPORAL_API_PROTOS points at a `temporalio/api` +# checkout instead when the submodule is not initialized. +UPSTREAM_API = ( + BASE + / "temporalio" + / "bridge" + / "sdk-core" + / "crates" + / "protos" + / "protos" + / "api_upstream" +) + + +def api_proto_path() -> Path: + override = os.environ.get("TEMPORAL_API_PROTOS") + if override: + return Path(override).resolve() + if ( + UPSTREAM_API / "temporal" / "api" / "common" / "v1" / "message.proto" + ).is_file(): + return UPSTREAM_API + raise SystemExit( + "no api protos found: initialize the sdk-core submodule or set " + "TEMPORAL_API_PROTOS to a temporalio/api checkout" + ) + + +SERVER_PROTO_DIR = Path("chasm/lib/stream/proto/v1") +STAGE_PROTO_DIR = Path("temporalio/api/stream/v1") +FILES = [ + "message.proto", + "stream_state.proto", + "request_response.proto", + "service.proto", +] + +# Server-only options. They carry shard routing and rate-limit category, which +# a client neither reads nor can resolve, since the files defining them are not +# vendored. +DROP_IMPORT = re.compile(r'^import "temporal/server/.*?";\n', re.M) +DROP_OPTION = re.compile(r"^\s*option \(temporal\.server\.api\..*?\n", re.M) + +# protoc names a python module after the proto path. These put the generated +# modules where this package publishes them. +FIX_PY = [ + ( + re.compile(r"from temporalio\.api\.stream\.v1 import"), + "from temporalio.api.streamservice.v1 import", + ), + (re.compile(r"temporalio\.api\.stream\.v1\."), "temporalio.api.streamservice.v1."), + (re.compile(r"from temporal\.api\."), "from temporalio.api."), + (re.compile(r"import temporal\.api\."), "import temporalio.api."), + # mypy-protobuf also writes the public API types fully qualified in the + # stubs, `temporal.api.common.v1.message_pb2.Payload`, which the import + # rewrites above do not reach. The `_pb2` suffix keeps this off the proto + # package names inside the serialized descriptors, which stay `temporal.api`. + # After the streamservice rewrite on purpose, so the public + # `temporal.api.stream.v1` package is not sent to the vendored one. + ( + re.compile(r"\btemporal\.api\.(\w+)\.v1\.(\w+_pb2)\b"), + r"temporalio.api.\1.v1.\2", + ), +] + + +def stage(server: Path, into: Path) -> list[str]: + dest = into / STAGE_PROTO_DIR + dest.mkdir(parents=True) + for name in FILES: + text = (server / SERVER_PROTO_DIR / name).read_text() + text = text.replace(str(SERVER_PROTO_DIR) + "/", str(STAGE_PROTO_DIR) + "/") + text = DROP_IMPORT.sub("", text) + text = DROP_OPTION.sub("", text) + (dest / name).write_text(text) + return [str(STAGE_PROTO_DIR / name) for name in FILES] + + +def main() -> None: + if len(sys.argv) != 2: + sys.exit("usage: gen_stream_protos.py /path/to/temporal-server-checkout") + server = Path(sys.argv[1]).resolve() + if not (server / SERVER_PROTO_DIR).is_dir(): + sys.exit(f"no stream protos under {server / SERVER_PROTO_DIR}") + + with tempfile.TemporaryDirectory() as tmp: + work = Path(tmp) + targets = stage(server, work) + out = work / "out" + out.mkdir() + subprocess.run( + [ + sys.executable, + "-m", + "grpc_tools.protoc", + f"--proto_path={work}", + f"--proto_path={api_proto_path()}", + f"--python_out={out}", + f"--grpc_python_out={out}", + f"--mypy_out={out}", + f"--mypy_grpc_out={out}", + *targets, + ], + check=True, + cwd=work, + ) + + generated = out / STAGE_PROTO_DIR + for path in sorted(generated.iterdir()): + if path.name == "__init__.py": + continue + text = path.read_text() + for pattern, repl in FIX_PY: + text = pattern.sub(repl, text) + (OUT / path.name).write_text(text) + print(f"wrote {OUT / path.name}") + + shutil.rmtree(OUT / "__pycache__", ignore_errors=True) + write_init() + + +MODULES = ["message_pb2", "stream_state_pb2", "request_response_pb2"] + +INIT_HEADER = """\ +# Generated by scripts/gen_stream_protos.py. Do not edit. +# +# Vendored rather than taken from the api submodule, because the stream service +# is still defined in the server. See temporalio/client_stream.py. +""" + + +def write_init() -> None: + """Re-export every generated message and enum, so the package surface + cannot drift from the protos it was built from.""" + import importlib + + sys.path.insert(0, str(BASE)) + blocks, exported = [], [] + for module in MODULES: + mod = importlib.import_module(f"temporalio.api.streamservice.v1.{module}") + names = list(mod.DESCRIPTOR.message_types_by_name) + for enum in mod.DESCRIPTOR.enum_types_by_name.values(): + names.append(enum.name) + names.extend(value.name for value in enum.values) + names.sort() + exported.extend(names) + joined = "".join(f" {name},\n" for name in names) + blocks.append(f"from .{module} import (\n{joined})\n") + + listed = "".join(f' "{name}",\n' for name in sorted(exported)) + (OUT / "__init__.py").write_text( + INIT_HEADER + "\n" + "\n".join(blocks) + f"\n__all__ = [\n{listed}]\n" + ) + print(f"wrote {OUT / '__init__.py'}") + + +if __name__ == "__main__": + main() diff --git a/temporalio/api/streamservice/__init__.py b/temporalio/api/streamservice/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/temporalio/api/streamservice/v1/__init__.py b/temporalio/api/streamservice/v1/__init__.py new file mode 100644 index 000000000..f63562365 --- /dev/null +++ b/temporalio/api/streamservice/v1/__init__.py @@ -0,0 +1,158 @@ +# Generated by scripts/gen_stream_protos.py. Do not edit. +# +# Vendored rather than taken from the api submodule, because the stream service +# is still defined in the server. See temporalio/client_stream.py. + +from .message_pb2 import ( + StreamRecord, + StreamRecordBatch, +) +from .request_response_pb2 import ( + STREAM_OWNER_KIND_ACTIVITY, + STREAM_OWNER_KIND_UNSPECIFIED, + STREAM_OWNER_KIND_WORKFLOW, + STREAM_OWNER_KIND_WORKFLOW_ACTIVITY, + AddMessagesInput, + AddMessagesOutput, + AddMessagesRequest, + AddMessagesResponse, + AddWorkflowMessagesInput, + AddWorkflowMessagesRequest, + AddWorkflowMessagesResponse, + AdvanceConsumerHeadInput, + AdvanceConsumerHeadOutput, + AdvanceConsumerHeadRequest, + AdvanceConsumerHeadResponse, + CloseStreamInput, + CloseStreamOutput, + CloseStreamRequest, + CloseStreamResponse, + CreateStreamInput, + CreateStreamOutput, + CreateStreamRequest, + CreateStreamResponse, + DeleteStreamInput, + DeleteStreamOutput, + DeleteStreamRequest, + DeleteStreamResponse, + DescribeStreamInput, + DescribeStreamOutput, + DescribeStreamRequest, + DescribeStreamResponse, + DescribeWorkflowStreamInput, + DescribeWorkflowStreamRequest, + DescribeWorkflowStreamResponse, + FinishWritingInput, + FinishWritingOutput, + FinishWritingRequest, + FinishWritingResponse, + ListStreamsInput, + ListStreamsOutput, + ListStreamsRequest, + ListStreamsResponse, + PollMessagesInput, + PollMessagesOutput, + PollMessagesRequest, + PollMessagesResponse, + PollWorkflowMessagesInput, + PollWorkflowMessagesRequest, + PollWorkflowMessagesResponse, + RegisterStreamConsumerInput, + RegisterStreamConsumerOutput, + RegisterStreamConsumerRequest, + RegisterStreamConsumerResponse, + StreamListEntry, + StreamOwner, + StreamOwnerKind, + SubscribeWorkflowInput, + SubscribeWorkflowOutput, + SubscribeWorkflowRequest, + SubscribeWorkflowResponse, + TruncateStreamInput, + TruncateStreamOutput, + TruncateStreamRequest, + TruncateStreamResponse, +) +from .stream_state_pb2 import ( + ConsumerCursor, + ProducerCursor, + StreamBudget, + StreamLifecycle, + StreamState, + WorkflowStreamCursor, +) + +__all__ = [ + "AddMessagesInput", + "AddMessagesOutput", + "AddMessagesRequest", + "AddMessagesResponse", + "AddWorkflowMessagesInput", + "AddWorkflowMessagesRequest", + "AddWorkflowMessagesResponse", + "AdvanceConsumerHeadInput", + "AdvanceConsumerHeadOutput", + "AdvanceConsumerHeadRequest", + "AdvanceConsumerHeadResponse", + "CloseStreamInput", + "CloseStreamOutput", + "CloseStreamRequest", + "CloseStreamResponse", + "ConsumerCursor", + "CreateStreamInput", + "CreateStreamOutput", + "CreateStreamRequest", + "CreateStreamResponse", + "DeleteStreamInput", + "DeleteStreamOutput", + "DeleteStreamRequest", + "DeleteStreamResponse", + "DescribeStreamInput", + "DescribeStreamOutput", + "DescribeStreamRequest", + "DescribeStreamResponse", + "DescribeWorkflowStreamInput", + "DescribeWorkflowStreamRequest", + "DescribeWorkflowStreamResponse", + "FinishWritingInput", + "FinishWritingOutput", + "FinishWritingRequest", + "FinishWritingResponse", + "ListStreamsInput", + "ListStreamsOutput", + "ListStreamsRequest", + "ListStreamsResponse", + "PollMessagesInput", + "PollMessagesOutput", + "PollMessagesRequest", + "PollMessagesResponse", + "PollWorkflowMessagesInput", + "PollWorkflowMessagesRequest", + "PollWorkflowMessagesResponse", + "ProducerCursor", + "RegisterStreamConsumerInput", + "RegisterStreamConsumerOutput", + "RegisterStreamConsumerRequest", + "RegisterStreamConsumerResponse", + "STREAM_OWNER_KIND_ACTIVITY", + "STREAM_OWNER_KIND_UNSPECIFIED", + "STREAM_OWNER_KIND_WORKFLOW", + "STREAM_OWNER_KIND_WORKFLOW_ACTIVITY", + "StreamBudget", + "StreamLifecycle", + "StreamListEntry", + "StreamOwner", + "StreamOwnerKind", + "StreamRecord", + "StreamRecordBatch", + "StreamState", + "SubscribeWorkflowInput", + "SubscribeWorkflowOutput", + "SubscribeWorkflowRequest", + "SubscribeWorkflowResponse", + "TruncateStreamInput", + "TruncateStreamOutput", + "TruncateStreamRequest", + "TruncateStreamResponse", + "WorkflowStreamCursor", +] diff --git a/temporalio/api/streamservice/v1/message_pb2.py b/temporalio/api/streamservice/v1/message_pb2.py new file mode 100644 index 000000000..cd10df58f --- /dev/null +++ b/temporalio/api/streamservice/v1/message_pb2.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: temporalio/api/stream/v1/message.proto +"""Generated protocol buffer code.""" + +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database + +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 + +from temporalio.api.common.v1 import ( + message_pb2 as temporal_dot_api_dot_common_dot_v1_dot_message__pb2, +) +from temporalio.api.stream.v1 import ( + message_pb2 as temporal_dot_api_dot_stream_dot_v1_dot_message__pb2, +) + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b'\n&temporalio/api/stream/v1/message.proto\x12)temporal.server.chasm.lib.stream.proto.v1\x1a\x1fgoogle/protobuf/timestamp.proto\x1a$temporal/api/common/v1/message.proto\x1a$temporal/api/stream/v1/message.proto"\xf7\x02\n\x0cStreamRecord\x12-\n\x04\x62ody\x18\x01 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12W\n\x08metadata\x18\x02 \x03(\x0b\x32\x45.temporal.server.chasm.lib.stream.proto.v1.StreamRecord.MetadataEntry\x12\r\n\x05topic\x18\x03 \x01(\t\x12\x10\n\x08sequence\x18\x04 \x01(\x03\x12\x36\n\x04kind\x18\x05 \x01(\x0e\x32(.temporal.api.stream.v1.StreamRecordKind\x12\x0e\n\x06offset\x18\x06 \x01(\x03\x12\x13\n\x0bproducer_id\x18\x07 \x01(\t\x12\x0f\n\x07\x61ttempt\x18\x08 \x01(\x03\x1aP\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12.\n\x05value\x18\x02 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload:\x02\x38\x01"\x8e\x01\n\x11StreamRecordBatch\x12H\n\x07records\x18\x01 \x03(\x0b\x32\x37.temporal.server.chasm.lib.stream.proto.v1.StreamRecord\x12/\n\x0b\x61ppended_at\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.TimestampB>Z= (3, 8): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class StreamRecord(google.protobuf.message.Message): + """The stored shape of temporal.api.stream.v1.StreamRecord plus the offset a + read assigns. Field for field the public record, so one crosses the frontend + in either direction without translation. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class MetadataEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + @property + def value(self) -> temporalio.api.common.v1.message_pb2.Payload: ... + def __init__( + self, + *, + key: builtins.str = ..., + value: temporalio.api.common.v1.message_pb2.Payload | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["value", b"value"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal["key", b"key", "value", b"value"], + ) -> None: ... + + BODY_FIELD_NUMBER: builtins.int + METADATA_FIELD_NUMBER: builtins.int + TOPIC_FIELD_NUMBER: builtins.int + SEQUENCE_FIELD_NUMBER: builtins.int + KIND_FIELD_NUMBER: builtins.int + OFFSET_FIELD_NUMBER: builtins.int + PRODUCER_ID_FIELD_NUMBER: builtins.int + ATTEMPT_FIELD_NUMBER: builtins.int + @property + def body(self) -> temporalio.api.common.v1.message_pb2.Payload: ... + @property + def metadata( + self, + ) -> google.protobuf.internal.containers.MessageMap[ + builtins.str, temporalio.api.common.v1.message_pb2.Payload + ]: + """Producer-supplied provenance, stored as sent.""" + topic: builtins.str + sequence: builtins.int + """The producer's position within its attempt, zero when it does not number + its records. Stored as sent; the global offset is what orders the stream. + """ + kind: temporalio.api.stream.v1.message_pb2.StreamRecordKind.ValueType + """Settled to DATA on append when left unspecified, so a retry hashes the + same bytes and a reader never sees the zero value. + """ + offset: builtins.int + """Position in the whole stream, set on read and never stored. A consumer + that resumes at record granularity needs it, and a topic-filtered read + leaves gaps that make it underivable from the response alone. + """ + producer_id: builtins.str + """Who wrote the record. Empty when the owning Workflow did: its publish + command clears the field, whatever the worker sent. + """ + attempt: builtins.int + """The producer's attempt, stored as sent. Readers treat a later attempt by + the same producer as superseding what the earlier one wrote. + """ + def __init__( + self, + *, + body: temporalio.api.common.v1.message_pb2.Payload | None = ..., + metadata: collections.abc.Mapping[ + builtins.str, temporalio.api.common.v1.message_pb2.Payload + ] + | None = ..., + topic: builtins.str = ..., + sequence: builtins.int = ..., + kind: temporalio.api.stream.v1.message_pb2.StreamRecordKind.ValueType = ..., + offset: builtins.int = ..., + producer_id: builtins.str = ..., + attempt: builtins.int = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["body", b"body"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "attempt", + b"attempt", + "body", + b"body", + "kind", + b"kind", + "metadata", + b"metadata", + "offset", + b"offset", + "producer_id", + b"producer_id", + "sequence", + b"sequence", + "topic", + b"topic", + ], + ) -> None: ... + +global___StreamRecord = StreamRecord + +class StreamRecordBatch(google.protobuf.message.Message): + """One append is one batch, and one batch is one data node. The server stores + this serialized and opaque; it decodes only to trim a partial first page, to + apply a topic filter, or to learn the batch's age. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + RECORDS_FIELD_NUMBER: builtins.int + APPENDED_AT_FIELD_NUMBER: builtins.int + @property + def records( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + global___StreamRecord + ]: ... + @property + def appended_at(self) -> google.protobuf.timestamp_pb2.Timestamp: + """When the batch was appended, for the lifecycle's retention age. Kept here + rather than in the stream's state so the state stays free of a per-batch + index; the age check reads the oldest batches to find it. + """ + def __init__( + self, + *, + records: collections.abc.Iterable[global___StreamRecord] | None = ..., + appended_at: google.protobuf.timestamp_pb2.Timestamp | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["appended_at", b"appended_at"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "appended_at", b"appended_at", "records", b"records" + ], + ) -> None: ... + +global___StreamRecordBatch = StreamRecordBatch diff --git a/temporalio/api/streamservice/v1/message_pb2_grpc.py b/temporalio/api/streamservice/v1/message_pb2_grpc.py new file mode 100644 index 000000000..bf947056a --- /dev/null +++ b/temporalio/api/streamservice/v1/message_pb2_grpc.py @@ -0,0 +1,4 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" + +import grpc diff --git a/temporalio/api/streamservice/v1/message_pb2_grpc.pyi b/temporalio/api/streamservice/v1/message_pb2_grpc.pyi new file mode 100644 index 000000000..f3a5a087e --- /dev/null +++ b/temporalio/api/streamservice/v1/message_pb2_grpc.pyi @@ -0,0 +1,4 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" diff --git a/temporalio/api/streamservice/v1/request_response_pb2.py b/temporalio/api/streamservice/v1/request_response_pb2.py new file mode 100644 index 000000000..0ecfa75c6 --- /dev/null +++ b/temporalio/api/streamservice/v1/request_response_pb2.py @@ -0,0 +1,908 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: temporalio/api/stream/v1/request_response.proto +"""Generated protocol buffer code.""" + +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import enum_type_wrapper + +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from temporalio.api.common.v1 import ( + message_pb2 as temporal_dot_api_dot_common_dot_v1_dot_message__pb2, +) +from temporalio.api.stream.v1 import ( + message_pb2 as temporal_dot_api_dot_stream_dot_v1_dot_message__pb2, +) +from temporalio.api.streamservice.v1 import ( + message_pb2 as temporalio_dot_api_dot_stream_dot_v1_dot_message__pb2, +) +from temporalio.api.streamservice.v1 import ( + stream_state_pb2 as temporalio_dot_api_dot_stream_dot_v1_dot_stream__state__pb2, +) + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b'\n/temporalio/api/stream/v1/request_response.proto\x12)temporal.server.chasm.lib.stream.proto.v1\x1a&temporalio/api/stream/v1/message.proto\x1a+temporalio/api/stream/v1/stream_state.proto\x1a$temporal/api/common/v1/message.proto\x1a$temporal/api/stream/v1/message.proto"\x88\x01\n\x11\x43reateStreamInput\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x11\n\tstream_id\x18\x02 \x01(\t\x12M\n\tlifecycle\x18\x03 \x01(\x0b\x32:.temporal.server.chasm.lib.stream.proto.v1.StreamLifecycle"$\n\x12\x43reateStreamOutput\x12\x0e\n\x06run_id\x18\x01 \x01(\t"\xf5\x01\n\x10\x41\x64\x64MessagesInput\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x11\n\tstream_id\x18\x02 \x01(\t\x12\x0e\n\x06run_id\x18\t \x01(\t\x12H\n\x07records\x18\x03 \x03(\x0b\x32\x37.temporal.server.chasm.lib.stream.proto.v1.StreamRecord\x12\x13\n\x0bproducer_id\x18\x04 \x01(\t\x12\x10\n\x08sequence\x18\x05 \x01(\x03\x12\x17\n\x0f\x65xpected_offset\x18\x06 \x01(\x03\x12\x1b\n\x13use_expected_offset\x18\x08 \x01(\x08J\x04\x08\x07\x10\x08"c\n\x11\x41\x64\x64MessagesOutput\x12\x14\n\x0c\x66irst_offset\x18\x01 \x01(\x03\x12\x13\n\x0bnext_offset\x18\x02 \x01(\x03\x12\r\n\x05\x63ount\x18\x03 \x01(\x03\x12\x14\n\x0c\x64\x65\x64uplicated\x18\x04 \x01(\x08"O\n\x12\x46inishWritingInput\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x11\n\tstream_id\x18\x02 \x01(\t\x12\x13\n\x0bproducer_id\x18\x03 \x01(\t"\x15\n\x13\x46inishWritingOutput"\xd9\x01\n\x16SubscribeWorkflowInput\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x13\n\x0bworkflow_id\x18\x02 \x01(\t\x12\x14\n\x0cowner_run_id\x18\x06 \x01(\t\x12\x13\n\x0bstream_name\x18\x03 \x01(\t\x12\x11\n\tstream_id\x18\x05 \x01(\t\x12\x14\n\x0cstart_offset\x18\x04 \x01(\x03\x12\x43\n\x0estart_position\x18\x07 \x01(\x0b\x32+.temporal.api.stream.v1.StreamStartPosition"/\n\x17SubscribeWorkflowOutput\x12\x14\n\x0cstart_offset\x18\x01 \x01(\x03"\xe4\x01\n\x11PollMessagesInput\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x11\n\tstream_id\x18\x02 \x01(\t\x12\x0e\n\x06run_id\x18\x07 \x01(\t\x12\x13\n\x0b\x66rom_offset\x18\x03 \x01(\x03\x12\x43\n\x0estart_position\x18\x08 \x01(\x0b\x32+.temporal.api.stream.v1.StreamStartPosition\x12\x14\n\x0cmax_messages\x18\x04 \x01(\x05\x12\x0e\n\x06topics\x18\x05 \x03(\t\x12\x19\n\x11wait_new_messages\x18\x06 \x01(\x08"\xdf\x01\n\x12PollMessagesOutput\x12H\n\x07records\x18\x01 \x03(\x0b\x32\x37.temporal.server.chasm.lib.stream.proto.v1.StreamRecord\x12\x13\n\x0bnext_offset\x18\x02 \x01(\x03\x12\x13\n\x0bhead_offset\x18\x03 \x01(\x03\x12\x0e\n\x06\x63losed\x18\x04 \x01(\x08\x12\x35\n\x0c\x63lose_reason\x18\x05 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12\x0e\n\x06run_id\x18\x06 \x01(\t";\n\x13\x44\x65scribeStreamInput\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x11\n\tstream_id\x18\x02 \x01(\t"\x88\x01\n\x0bStreamOwner\x12H\n\x04kind\x18\x01 \x01(\x0e\x32:.temporal.server.chasm.lib.stream.proto.v1.StreamOwnerKind\x12\n\n\x02id\x18\x02 \x01(\t\x12\x0e\n\x06run_id\x18\x03 \x01(\t\x12\x13\n\x0b\x61\x63tivity_id\x18\x04 \x01(\t"\xd0\x02\n\x19PollWorkflowMessagesInput\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x45\n\x05owner\x18\t \x01(\x0b\x32\x36.temporal.server.chasm.lib.stream.proto.v1.StreamOwner\x12\x13\n\x0bworkflow_id\x18\x02 \x01(\t\x12\x14\n\x0cowner_run_id\x18\x08 \x01(\t\x12\x13\n\x0bstream_name\x18\x03 \x01(\t\x12\x13\n\x0b\x66rom_offset\x18\x04 \x01(\x03\x12\x43\n\x0estart_position\x18\n \x01(\x0b\x32+.temporal.api.stream.v1.StreamStartPosition\x12\x14\n\x0cmax_messages\x18\x05 \x01(\x05\x12\x0e\n\x06topics\x18\x06 \x03(\t\x12\x19\n\x11wait_new_messages\x18\x07 \x01(\x08"\xb7\x01\n\x1b\x44\x65scribeWorkflowStreamInput\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x45\n\x05owner\x18\x05 \x01(\x0b\x32\x36.temporal.server.chasm.lib.stream.proto.v1.StreamOwner\x12\x13\n\x0bworkflow_id\x18\x02 \x01(\t\x12\x14\n\x0cowner_run_id\x18\x04 \x01(\t\x12\x13\n\x0bstream_name\x18\x03 \x01(\t"\xa5\x02\n\x18\x41\x64\x64WorkflowMessagesInput\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x45\n\x05owner\x18\x08 \x01(\x0b\x32\x36.temporal.server.chasm.lib.stream.proto.v1.StreamOwner\x12\x13\n\x0bworkflow_id\x18\x02 \x01(\t\x12\x14\n\x0cowner_run_id\x18\x07 \x01(\t\x12\x13\n\x0bstream_name\x18\x03 \x01(\t\x12H\n\x07records\x18\x04 \x03(\x0b\x32\x37.temporal.server.chasm.lib.stream.proto.v1.StreamRecord\x12\x13\n\x0bproducer_id\x18\x05 \x01(\t\x12\x10\n\x08sequence\x18\x06 \x01(\x03"]\n\x14\x44\x65scribeStreamOutput\x12\x45\n\x05state\x18\x01 \x01(\x0b\x32\x36.temporal.server.chasm.lib.stream.proto.v1.StreamState"i\n\x10\x43loseStreamInput\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x11\n\tstream_id\x18\x02 \x01(\t\x12/\n\x06reason\x18\x03 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload"\x13\n\x11\x43loseStreamOutput"T\n\x13TruncateStreamInput\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x11\n\tstream_id\x18\x02 \x01(\t\x12\x17\n\x0fnew_base_offset\x18\x03 \x01(\x03"\x16\n\x14TruncateStreamOutput"H\n\x11\x44\x65leteStreamInput\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x11\n\tstream_id\x18\x02 \x01(\t\x12\r\n\x05\x66orce\x18\x03 \x01(\x08"\x14\n\x12\x44\x65leteStreamOutput"\x83\x01\n\x13\x43reateStreamRequest\x12\x14\n\x0cnamespace_id\x18\x01 \x01(\t\x12V\n\x10\x66rontend_request\x18\x02 \x01(\x0b\x32<.temporal.server.chasm.lib.stream.proto.v1.CreateStreamInput"p\n\x14\x43reateStreamResponse\x12X\n\x11\x66rontend_response\x18\x01 \x01(\x0b\x32=.temporal.server.chasm.lib.stream.proto.v1.CreateStreamOutput"\x81\x01\n\x12\x41\x64\x64MessagesRequest\x12\x14\n\x0cnamespace_id\x18\x01 \x01(\t\x12U\n\x10\x66rontend_request\x18\x02 \x01(\x0b\x32;.temporal.server.chasm.lib.stream.proto.v1.AddMessagesInput"n\n\x13\x41\x64\x64MessagesResponse\x12W\n\x11\x66rontend_response\x18\x01 \x01(\x0b\x32<.temporal.server.chasm.lib.stream.proto.v1.AddMessagesOutput"\x85\x01\n\x14\x46inishWritingRequest\x12\x14\n\x0cnamespace_id\x18\x01 \x01(\t\x12W\n\x10\x66rontend_request\x18\x02 \x01(\x0b\x32=.temporal.server.chasm.lib.stream.proto.v1.FinishWritingInput"r\n\x15\x46inishWritingResponse\x12Y\n\x11\x66rontend_response\x18\x01 \x01(\x0b\x32>.temporal.server.chasm.lib.stream.proto.v1.FinishWritingOutput"\x8d\x01\n\x18SubscribeWorkflowRequest\x12\x14\n\x0cnamespace_id\x18\x01 \x01(\t\x12[\n\x10\x66rontend_request\x18\x02 \x01(\x0b\x32\x41.temporal.server.chasm.lib.stream.proto.v1.SubscribeWorkflowInput"z\n\x19SubscribeWorkflowResponse\x12]\n\x11\x66rontend_response\x18\x01 \x01(\x0b\x32\x42.temporal.server.chasm.lib.stream.proto.v1.SubscribeWorkflowOutput"\x83\x01\n\x13PollMessagesRequest\x12\x14\n\x0cnamespace_id\x18\x01 \x01(\t\x12V\n\x10\x66rontend_request\x18\x02 \x01(\x0b\x32<.temporal.server.chasm.lib.stream.proto.v1.PollMessagesInput"p\n\x14PollMessagesResponse\x12X\n\x11\x66rontend_response\x18\x01 \x01(\x0b\x32=.temporal.server.chasm.lib.stream.proto.v1.PollMessagesOutput"\x87\x01\n\x15\x44\x65scribeStreamRequest\x12\x14\n\x0cnamespace_id\x18\x01 \x01(\t\x12X\n\x10\x66rontend_request\x18\x02 \x01(\x0b\x32>.temporal.server.chasm.lib.stream.proto.v1.DescribeStreamInput"t\n\x16\x44\x65scribeStreamResponse\x12Z\n\x11\x66rontend_response\x18\x01 \x01(\x0b\x32?.temporal.server.chasm.lib.stream.proto.v1.DescribeStreamOutput"\x93\x01\n\x1bPollWorkflowMessagesRequest\x12\x14\n\x0cnamespace_id\x18\x01 \x01(\t\x12^\n\x10\x66rontend_request\x18\x02 \x01(\x0b\x32\x44.temporal.server.chasm.lib.stream.proto.v1.PollWorkflowMessagesInput"x\n\x1cPollWorkflowMessagesResponse\x12X\n\x11\x66rontend_response\x18\x01 \x01(\x0b\x32=.temporal.server.chasm.lib.stream.proto.v1.PollMessagesOutput"\x97\x01\n\x1d\x44\x65scribeWorkflowStreamRequest\x12\x14\n\x0cnamespace_id\x18\x01 \x01(\t\x12`\n\x10\x66rontend_request\x18\x02 \x01(\x0b\x32\x46.temporal.server.chasm.lib.stream.proto.v1.DescribeWorkflowStreamInput"|\n\x1e\x44\x65scribeWorkflowStreamResponse\x12Z\n\x11\x66rontend_response\x18\x01 \x01(\x0b\x32?.temporal.server.chasm.lib.stream.proto.v1.DescribeStreamOutput"\xd5\x01\n\x1bRegisterStreamConsumerInput\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x11\n\tstream_id\x18\x02 \x01(\t\x12\x1c\n\x14\x63onsumer_workflow_id\x18\x03 \x01(\t\x12\x17\n\x0f\x63onsumer_run_id\x18\x05 \x01(\t\x12\x14\n\x0cstart_offset\x18\x04 \x01(\x03\x12\x43\n\x0estart_position\x18\x06 \x01(\x0b\x32+.temporal.api.stream.v1.StreamStartPosition"k\n\x1cRegisterStreamConsumerOutput\x12\x14\n\x0cstart_offset\x18\x01 \x01(\x03\x12\x12\n\nknown_head\x18\x04 \x01(\x03\x12\x15\n\rstream_absent\x18\x05 \x01(\x08J\x04\x08\x02\x10\x03J\x04\x08\x03\x10\x04"\x80\x01\n\x18\x41\x64vanceConsumerHeadInput\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x13\n\x0bworkflow_id\x18\x02 \x01(\t\x12\x14\n\x0cowner_run_id\x18\x05 \x01(\t\x12\x11\n\tstream_id\x18\x03 \x01(\t\x12\x13\n\x0bhead_offset\x18\x04 \x01(\x03"n\n\x19\x41\x64vanceConsumerHeadOutput\x12\x17\n\x0f\x63onsumer_closed\x18\x01 \x01(\x08\x12\x18\n\x10successor_run_id\x18\x02 \x01(\t\x12\x1e\n\x16successor_start_offset\x18\x03 \x01(\x03"\x91\x01\n\x1a\x41\x64\x64WorkflowMessagesRequest\x12\x14\n\x0cnamespace_id\x18\x01 \x01(\t\x12]\n\x10\x66rontend_request\x18\x02 \x01(\x0b\x32\x43.temporal.server.chasm.lib.stream.proto.v1.AddWorkflowMessagesInput"v\n\x1b\x41\x64\x64WorkflowMessagesResponse\x12W\n\x11\x66rontend_response\x18\x01 \x01(\x0b\x32<.temporal.server.chasm.lib.stream.proto.v1.AddMessagesOutput"\x97\x01\n\x1dRegisterStreamConsumerRequest\x12\x14\n\x0cnamespace_id\x18\x01 \x01(\t\x12`\n\x10\x66rontend_request\x18\x02 \x01(\x0b\x32\x46.temporal.server.chasm.lib.stream.proto.v1.RegisterStreamConsumerInput"\x84\x01\n\x1eRegisterStreamConsumerResponse\x12\x62\n\x11\x66rontend_response\x18\x01 \x01(\x0b\x32G.temporal.server.chasm.lib.stream.proto.v1.RegisterStreamConsumerOutput"\x91\x01\n\x1a\x41\x64vanceConsumerHeadRequest\x12\x14\n\x0cnamespace_id\x18\x01 \x01(\t\x12]\n\x10\x66rontend_request\x18\x02 \x01(\x0b\x32\x43.temporal.server.chasm.lib.stream.proto.v1.AdvanceConsumerHeadInput"~\n\x1b\x41\x64vanceConsumerHeadResponse\x12_\n\x11\x66rontend_response\x18\x01 \x01(\x0b\x32\x44.temporal.server.chasm.lib.stream.proto.v1.AdvanceConsumerHeadOutput"\x81\x01\n\x12\x43loseStreamRequest\x12\x14\n\x0cnamespace_id\x18\x01 \x01(\t\x12U\n\x10\x66rontend_request\x18\x02 \x01(\x0b\x32;.temporal.server.chasm.lib.stream.proto.v1.CloseStreamInput"n\n\x13\x43loseStreamResponse\x12W\n\x11\x66rontend_response\x18\x01 \x01(\x0b\x32<.temporal.server.chasm.lib.stream.proto.v1.CloseStreamOutput"\x87\x01\n\x15TruncateStreamRequest\x12\x14\n\x0cnamespace_id\x18\x01 \x01(\t\x12X\n\x10\x66rontend_request\x18\x02 \x01(\x0b\x32>.temporal.server.chasm.lib.stream.proto.v1.TruncateStreamInput"t\n\x16TruncateStreamResponse\x12Z\n\x11\x66rontend_response\x18\x01 \x01(\x0b\x32?.temporal.server.chasm.lib.stream.proto.v1.TruncateStreamOutput"`\n\x10ListStreamsInput\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x11\n\tpage_size\x18\x02 \x01(\x05\x12\x17\n\x0fnext_page_token\x18\x03 \x01(\x0c\x12\r\n\x05query\x18\x04 \x01(\t"4\n\x0fStreamListEntry\x12\x11\n\tstream_id\x18\x01 \x01(\t\x12\x0e\n\x06run_id\x18\x02 \x01(\t"y\n\x11ListStreamsOutput\x12K\n\x07streams\x18\x01 \x03(\x0b\x32:.temporal.server.chasm.lib.stream.proto.v1.StreamListEntry\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\x0c"\x81\x01\n\x12ListStreamsRequest\x12\x14\n\x0cnamespace_id\x18\x01 \x01(\t\x12U\n\x10\x66rontend_request\x18\x02 \x01(\x0b\x32;.temporal.server.chasm.lib.stream.proto.v1.ListStreamsInput"n\n\x13ListStreamsResponse\x12W\n\x11\x66rontend_response\x18\x01 \x01(\x0b\x32<.temporal.server.chasm.lib.stream.proto.v1.ListStreamsOutput"\x83\x01\n\x13\x44\x65leteStreamRequest\x12\x14\n\x0cnamespace_id\x18\x01 \x01(\t\x12V\n\x10\x66rontend_request\x18\x02 \x01(\x0b\x32<.temporal.server.chasm.lib.stream.proto.v1.DeleteStreamInput"p\n\x14\x44\x65leteStreamResponse\x12X\n\x11\x66rontend_response\x18\x01 \x01(\x0b\x32=.temporal.server.chasm.lib.stream.proto.v1.DeleteStreamOutput*\x9d\x01\n\x0fStreamOwnerKind\x12!\n\x1dSTREAM_OWNER_KIND_UNSPECIFIED\x10\x00\x12\x1e\n\x1aSTREAM_OWNER_KIND_WORKFLOW\x10\x01\x12\x1e\n\x1aSTREAM_OWNER_KIND_ACTIVITY\x10\x02\x12\'\n#STREAM_OWNER_KIND_WORKFLOW_ACTIVITY\x10\x03\x42>Z= (3, 10): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class _StreamOwnerKind: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _StreamOwnerKindEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[ + _StreamOwnerKind.ValueType + ], + builtins.type, +): # noqa: F821 + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + STREAM_OWNER_KIND_UNSPECIFIED: _StreamOwnerKind.ValueType # 0 + STREAM_OWNER_KIND_WORKFLOW: _StreamOwnerKind.ValueType # 1 + STREAM_OWNER_KIND_ACTIVITY: _StreamOwnerKind.ValueType # 2 + """A standalone activity, which is an execution of its own.""" + STREAM_OWNER_KIND_WORKFLOW_ACTIVITY: _StreamOwnerKind.ValueType # 3 + """An activity a workflow scheduled. It is not an execution of its own, so + its streams are reached through the workflow and routed on it. + """ + +class StreamOwnerKind(_StreamOwnerKind, metaclass=_StreamOwnerKindEnumTypeWrapper): + """The kind of execution an owned stream lives in.""" + +STREAM_OWNER_KIND_UNSPECIFIED: StreamOwnerKind.ValueType # 0 +STREAM_OWNER_KIND_WORKFLOW: StreamOwnerKind.ValueType # 1 +STREAM_OWNER_KIND_ACTIVITY: StreamOwnerKind.ValueType # 2 +"""A standalone activity, which is an execution of its own.""" +STREAM_OWNER_KIND_WORKFLOW_ACTIVITY: StreamOwnerKind.ValueType # 3 +"""An activity a workflow scheduled. It is not an execution of its own, so +its streams are reached through the workflow and routed on it. +""" +global___StreamOwnerKind = StreamOwnerKind + +class CreateStreamInput(google.protobuf.message.Message): + """The frontend-facing shapes are defined here rather than in the public API + because streams have no public API yet. Keeping them in a nested + frontend_request mirrors the other CHASM libraries, so promoting them later + is a package move rather than a redesign. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_FIELD_NUMBER: builtins.int + STREAM_ID_FIELD_NUMBER: builtins.int + LIFECYCLE_FIELD_NUMBER: builtins.int + namespace: builtins.str + stream_id: builtins.str + @property + def lifecycle( + self, + ) -> temporalio.api.streamservice.v1.stream_state_pb2.StreamLifecycle: ... + def __init__( + self, + *, + namespace: builtins.str = ..., + stream_id: builtins.str = ..., + lifecycle: temporalio.api.streamservice.v1.stream_state_pb2.StreamLifecycle + | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["lifecycle", b"lifecycle"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "lifecycle", + b"lifecycle", + "namespace", + b"namespace", + "stream_id", + b"stream_id", + ], + ) -> None: ... + +global___CreateStreamInput = CreateStreamInput + +class CreateStreamOutput(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + RUN_ID_FIELD_NUMBER: builtins.int + run_id: builtins.str + def __init__( + self, + *, + run_id: builtins.str = ..., + ) -> None: ... + def ClearField( + self, field_name: typing_extensions.Literal["run_id", b"run_id"] + ) -> None: ... + +global___CreateStreamOutput = CreateStreamOutput + +class AddMessagesInput(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_FIELD_NUMBER: builtins.int + STREAM_ID_FIELD_NUMBER: builtins.int + RUN_ID_FIELD_NUMBER: builtins.int + RECORDS_FIELD_NUMBER: builtins.int + PRODUCER_ID_FIELD_NUMBER: builtins.int + SEQUENCE_FIELD_NUMBER: builtins.int + EXPECTED_OFFSET_FIELD_NUMBER: builtins.int + USE_EXPECTED_OFFSET_FIELD_NUMBER: builtins.int + namespace: builtins.str + stream_id: builtins.str + run_id: builtins.str + """Optional. Supplying it skips resolving the stream's current run, which is + a persistence lookup on every call. CreateStream returns it. + """ + @property + def records( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + temporalio.api.streamservice.v1.message_pb2.StreamRecord + ]: ... + producer_id: builtins.str + """Idempotency, all optional. Supply a producer identity and sequence, or an + expected offset, or neither and accept at-least-once. + """ + sequence: builtins.int + expected_offset: builtins.int + """Guarded by use_expected_offset because proto3 optional is not supported + by this repo's helper generator. + """ + use_expected_offset: builtins.bool + def __init__( + self, + *, + namespace: builtins.str = ..., + stream_id: builtins.str = ..., + run_id: builtins.str = ..., + records: collections.abc.Iterable[ + temporalio.api.streamservice.v1.message_pb2.StreamRecord + ] + | None = ..., + producer_id: builtins.str = ..., + sequence: builtins.int = ..., + expected_offset: builtins.int = ..., + use_expected_offset: builtins.bool = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "expected_offset", + b"expected_offset", + "namespace", + b"namespace", + "producer_id", + b"producer_id", + "records", + b"records", + "run_id", + b"run_id", + "sequence", + b"sequence", + "stream_id", + b"stream_id", + "use_expected_offset", + b"use_expected_offset", + ], + ) -> None: ... + +global___AddMessagesInput = AddMessagesInput + +class AddMessagesOutput(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + FIRST_OFFSET_FIELD_NUMBER: builtins.int + NEXT_OFFSET_FIELD_NUMBER: builtins.int + COUNT_FIELD_NUMBER: builtins.int + DEDUPLICATED_FIELD_NUMBER: builtins.int + first_offset: builtins.int + next_offset: builtins.int + count: builtins.int + deduplicated: builtins.bool + """True when a retry matched a recorded sequence and nothing was appended.""" + def __init__( + self, + *, + first_offset: builtins.int = ..., + next_offset: builtins.int = ..., + count: builtins.int = ..., + deduplicated: builtins.bool = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "count", + b"count", + "deduplicated", + b"deduplicated", + "first_offset", + b"first_offset", + "next_offset", + b"next_offset", + ], + ) -> None: ... + +global___AddMessagesOutput = AddMessagesOutput + +class FinishWritingInput(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_FIELD_NUMBER: builtins.int + STREAM_ID_FIELD_NUMBER: builtins.int + PRODUCER_ID_FIELD_NUMBER: builtins.int + namespace: builtins.str + stream_id: builtins.str + producer_id: builtins.str + def __init__( + self, + *, + namespace: builtins.str = ..., + stream_id: builtins.str = ..., + producer_id: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "namespace", + b"namespace", + "producer_id", + b"producer_id", + "stream_id", + b"stream_id", + ], + ) -> None: ... + +global___FinishWritingInput = FinishWritingInput + +class FinishWritingOutput(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + def __init__( + self, + ) -> None: ... + +global___FinishWritingOutput = FinishWritingOutput + +class SubscribeWorkflowInput(google.protobuf.message.Message): + """Registers a Workflow as a consumer of a stream it owns. The cursor lands in + the Workflow's own state, so from then on each of its Workflow Tasks carries + the next range and records what it consumed. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_FIELD_NUMBER: builtins.int + WORKFLOW_ID_FIELD_NUMBER: builtins.int + OWNER_RUN_ID_FIELD_NUMBER: builtins.int + STREAM_NAME_FIELD_NUMBER: builtins.int + STREAM_ID_FIELD_NUMBER: builtins.int + START_OFFSET_FIELD_NUMBER: builtins.int + START_POSITION_FIELD_NUMBER: builtins.int + namespace: builtins.str + workflow_id: builtins.str + owner_run_id: builtins.str + """Optional. Pins to one run, so a caller that has continued as new is not + silently redirected to the successor's stream, which starts empty and at + offset zero. Empty means whichever run is current. + """ + stream_name: builtins.str + """Name of the stream within the Workflow, for a stream it owns.""" + stream_id: builtins.str + """Id of a standalone stream in another execution. Exactly one of this and + stream_name is set. + """ + start_offset: builtins.int + """Where to start, as an absolute offset. Read only when start_position is + unset. A negative value is refused. + """ + @property + def start_position( + self, + ) -> temporalio.api.stream.v1.message_pb2.StreamStartPosition: + """Where to start. Resolved here rather than at delivery, so the first + recorded range starts from a fact instead of a reading. Refused alongside a + non-zero start_offset. + """ + def __init__( + self, + *, + namespace: builtins.str = ..., + workflow_id: builtins.str = ..., + owner_run_id: builtins.str = ..., + stream_name: builtins.str = ..., + stream_id: builtins.str = ..., + start_offset: builtins.int = ..., + start_position: temporalio.api.stream.v1.message_pb2.StreamStartPosition + | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["start_position", b"start_position"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "namespace", + b"namespace", + "owner_run_id", + b"owner_run_id", + "start_offset", + b"start_offset", + "start_position", + b"start_position", + "stream_id", + b"stream_id", + "stream_name", + b"stream_name", + "workflow_id", + b"workflow_id", + ], + ) -> None: ... + +global___SubscribeWorkflowInput = SubscribeWorkflowInput + +class SubscribeWorkflowOutput(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + START_OFFSET_FIELD_NUMBER: builtins.int + start_offset: builtins.int + def __init__( + self, + *, + start_offset: builtins.int = ..., + ) -> None: ... + def ClearField( + self, field_name: typing_extensions.Literal["start_offset", b"start_offset"] + ) -> None: ... + +global___SubscribeWorkflowOutput = SubscribeWorkflowOutput + +class PollMessagesInput(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_FIELD_NUMBER: builtins.int + STREAM_ID_FIELD_NUMBER: builtins.int + RUN_ID_FIELD_NUMBER: builtins.int + FROM_OFFSET_FIELD_NUMBER: builtins.int + START_POSITION_FIELD_NUMBER: builtins.int + MAX_MESSAGES_FIELD_NUMBER: builtins.int + TOPICS_FIELD_NUMBER: builtins.int + WAIT_NEW_MESSAGES_FIELD_NUMBER: builtins.int + namespace: builtins.str + stream_id: builtins.str + run_id: builtins.str + """Optional, as on AddMessagesInput.""" + from_offset: builtins.int + @property + def start_position( + self, + ) -> temporalio.api.stream.v1.message_pb2.StreamStartPosition: + """Where a first poll starts, when the reader has no offset yet. Resolved in + the same read that serves the records, so it cannot race with truncation, + and next_offset carries the result for the polls after it. Refused + alongside a non-zero from_offset. + """ + max_messages: builtins.int + @property + def topics( + self, + ) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: + """Filters by exact topic. Offsets are assigned over the unfiltered stream, so + next_offset advances past filtered-out records too. + """ + wait_new_messages: builtins.bool + """When set and the reader is caught up, block until something arrives, the + stream closes, or the server's long-poll timeout elapses. A timeout returns + an empty response rather than an error, so the caller simply polls again. + """ + def __init__( + self, + *, + namespace: builtins.str = ..., + stream_id: builtins.str = ..., + run_id: builtins.str = ..., + from_offset: builtins.int = ..., + start_position: temporalio.api.stream.v1.message_pb2.StreamStartPosition + | None = ..., + max_messages: builtins.int = ..., + topics: collections.abc.Iterable[builtins.str] | None = ..., + wait_new_messages: builtins.bool = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["start_position", b"start_position"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "from_offset", + b"from_offset", + "max_messages", + b"max_messages", + "namespace", + b"namespace", + "run_id", + b"run_id", + "start_position", + b"start_position", + "stream_id", + b"stream_id", + "topics", + b"topics", + "wait_new_messages", + b"wait_new_messages", + ], + ) -> None: ... + +global___PollMessagesInput = PollMessagesInput + +class PollMessagesOutput(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + RECORDS_FIELD_NUMBER: builtins.int + NEXT_OFFSET_FIELD_NUMBER: builtins.int + HEAD_OFFSET_FIELD_NUMBER: builtins.int + CLOSED_FIELD_NUMBER: builtins.int + CLOSE_REASON_FIELD_NUMBER: builtins.int + RUN_ID_FIELD_NUMBER: builtins.int + @property + def records( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + temporalio.api.streamservice.v1.message_pb2.StreamRecord + ]: ... + next_offset: builtins.int + head_offset: builtins.int + closed: builtins.bool + @property + def close_reason(self) -> temporalio.api.common.v1.message_pb2.Payload: ... + run_id: builtins.str + """The execution holding the stream. A workflow task slice built from this + read names the run it came from. + """ + def __init__( + self, + *, + records: collections.abc.Iterable[ + temporalio.api.streamservice.v1.message_pb2.StreamRecord + ] + | None = ..., + next_offset: builtins.int = ..., + head_offset: builtins.int = ..., + closed: builtins.bool = ..., + close_reason: temporalio.api.common.v1.message_pb2.Payload | None = ..., + run_id: builtins.str = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["close_reason", b"close_reason"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "close_reason", + b"close_reason", + "closed", + b"closed", + "head_offset", + b"head_offset", + "next_offset", + b"next_offset", + "records", + b"records", + "run_id", + b"run_id", + ], + ) -> None: ... + +global___PollMessagesOutput = PollMessagesOutput + +class DescribeStreamInput(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_FIELD_NUMBER: builtins.int + STREAM_ID_FIELD_NUMBER: builtins.int + namespace: builtins.str + stream_id: builtins.str + def __init__( + self, + *, + namespace: builtins.str = ..., + stream_id: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "namespace", b"namespace", "stream_id", b"stream_id" + ], + ) -> None: ... + +global___DescribeStreamInput = DescribeStreamInput + +class StreamOwner(google.protobuf.message.Message): + """The execution that owns a stream. An attached stream has no id of its own, + so it is named by this and a stream name, and routed on id. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KIND_FIELD_NUMBER: builtins.int + ID_FIELD_NUMBER: builtins.int + RUN_ID_FIELD_NUMBER: builtins.int + ACTIVITY_ID_FIELD_NUMBER: builtins.int + kind: global___StreamOwnerKind.ValueType + id: builtins.str + """The workflow id for WORKFLOW and WORKFLOW_ACTIVITY, the activity id for + ACTIVITY. + """ + run_id: builtins.str + """Optional. Pins to one run of the execution named by id, so a caller is not + silently redirected to a later run's stream, which starts empty and at + offset zero. Empty means whichever run is current. + """ + activity_id: builtins.str + """The activity within the workflow, for WORKFLOW_ACTIVITY only.""" + def __init__( + self, + *, + kind: global___StreamOwnerKind.ValueType = ..., + id: builtins.str = ..., + run_id: builtins.str = ..., + activity_id: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "activity_id", + b"activity_id", + "id", + b"id", + "kind", + b"kind", + "run_id", + b"run_id", + ], + ) -> None: ... + +global___StreamOwner = StreamOwner + +class PollWorkflowMessagesInput(google.protobuf.message.Message): + """A stream an execution owns lives inside it, so it has no standalone id to + address it by. It is named by its owner and its name instead, and routed on + the owner. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_FIELD_NUMBER: builtins.int + OWNER_FIELD_NUMBER: builtins.int + WORKFLOW_ID_FIELD_NUMBER: builtins.int + OWNER_RUN_ID_FIELD_NUMBER: builtins.int + STREAM_NAME_FIELD_NUMBER: builtins.int + FROM_OFFSET_FIELD_NUMBER: builtins.int + START_POSITION_FIELD_NUMBER: builtins.int + MAX_MESSAGES_FIELD_NUMBER: builtins.int + TOPICS_FIELD_NUMBER: builtins.int + WAIT_NEW_MESSAGES_FIELD_NUMBER: builtins.int + namespace: builtins.str + @property + def owner(self) -> global___StreamOwner: ... + workflow_id: builtins.str + """Shorthand for an owner of kind WORKFLOW, and refused alongside owner.""" + owner_run_id: builtins.str + stream_name: builtins.str + """Empty means the owner's default output stream.""" + from_offset: builtins.int + @property + def start_position( + self, + ) -> temporalio.api.stream.v1.message_pb2.StreamStartPosition: + """As on PollMessagesInput.""" + max_messages: builtins.int + @property + def topics( + self, + ) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: + """Filters as on PollMessagesInput.""" + wait_new_messages: builtins.bool + def __init__( + self, + *, + namespace: builtins.str = ..., + owner: global___StreamOwner | None = ..., + workflow_id: builtins.str = ..., + owner_run_id: builtins.str = ..., + stream_name: builtins.str = ..., + from_offset: builtins.int = ..., + start_position: temporalio.api.stream.v1.message_pb2.StreamStartPosition + | None = ..., + max_messages: builtins.int = ..., + topics: collections.abc.Iterable[builtins.str] | None = ..., + wait_new_messages: builtins.bool = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "owner", b"owner", "start_position", b"start_position" + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "from_offset", + b"from_offset", + "max_messages", + b"max_messages", + "namespace", + b"namespace", + "owner", + b"owner", + "owner_run_id", + b"owner_run_id", + "start_position", + b"start_position", + "stream_name", + b"stream_name", + "topics", + b"topics", + "wait_new_messages", + b"wait_new_messages", + "workflow_id", + b"workflow_id", + ], + ) -> None: ... + +global___PollWorkflowMessagesInput = PollWorkflowMessagesInput + +class DescribeWorkflowStreamInput(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_FIELD_NUMBER: builtins.int + OWNER_FIELD_NUMBER: builtins.int + WORKFLOW_ID_FIELD_NUMBER: builtins.int + OWNER_RUN_ID_FIELD_NUMBER: builtins.int + STREAM_NAME_FIELD_NUMBER: builtins.int + namespace: builtins.str + @property + def owner(self) -> global___StreamOwner: ... + workflow_id: builtins.str + """Shorthand for an owner of kind WORKFLOW, and refused alongside owner.""" + owner_run_id: builtins.str + stream_name: builtins.str + def __init__( + self, + *, + namespace: builtins.str = ..., + owner: global___StreamOwner | None = ..., + workflow_id: builtins.str = ..., + owner_run_id: builtins.str = ..., + stream_name: builtins.str = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["owner", b"owner"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "namespace", + b"namespace", + "owner", + b"owner", + "owner_run_id", + b"owner_run_id", + "stream_name", + b"stream_name", + "workflow_id", + b"workflow_id", + ], + ) -> None: ... + +global___DescribeWorkflowStreamInput = DescribeWorkflowStreamInput + +class AddWorkflowMessagesInput(google.protobuf.message.Message): + """Appending to an owned stream from outside its owner. A workflow's own + publishes ride its Workflow Task instead; an activity has no other path. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_FIELD_NUMBER: builtins.int + OWNER_FIELD_NUMBER: builtins.int + WORKFLOW_ID_FIELD_NUMBER: builtins.int + OWNER_RUN_ID_FIELD_NUMBER: builtins.int + STREAM_NAME_FIELD_NUMBER: builtins.int + RECORDS_FIELD_NUMBER: builtins.int + PRODUCER_ID_FIELD_NUMBER: builtins.int + SEQUENCE_FIELD_NUMBER: builtins.int + namespace: builtins.str + @property + def owner(self) -> global___StreamOwner: ... + workflow_id: builtins.str + """Shorthand for an owner of kind WORKFLOW, and refused alongside owner.""" + owner_run_id: builtins.str + stream_name: builtins.str + """Empty means the owner's default output stream.""" + @property + def records( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + temporalio.api.streamservice.v1.message_pb2.StreamRecord + ]: ... + producer_id: builtins.str + """Optional idempotency, as on AddMessagesInput.""" + sequence: builtins.int + def __init__( + self, + *, + namespace: builtins.str = ..., + owner: global___StreamOwner | None = ..., + workflow_id: builtins.str = ..., + owner_run_id: builtins.str = ..., + stream_name: builtins.str = ..., + records: collections.abc.Iterable[ + temporalio.api.streamservice.v1.message_pb2.StreamRecord + ] + | None = ..., + producer_id: builtins.str = ..., + sequence: builtins.int = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["owner", b"owner"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "namespace", + b"namespace", + "owner", + b"owner", + "owner_run_id", + b"owner_run_id", + "producer_id", + b"producer_id", + "records", + b"records", + "sequence", + b"sequence", + "stream_name", + b"stream_name", + "workflow_id", + b"workflow_id", + ], + ) -> None: ... + +global___AddWorkflowMessagesInput = AddWorkflowMessagesInput + +class DescribeStreamOutput(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + STATE_FIELD_NUMBER: builtins.int + @property + def state(self) -> temporalio.api.streamservice.v1.stream_state_pb2.StreamState: ... + def __init__( + self, + *, + state: temporalio.api.streamservice.v1.stream_state_pb2.StreamState + | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["state", b"state"] + ) -> builtins.bool: ... + def ClearField( + self, field_name: typing_extensions.Literal["state", b"state"] + ) -> None: ... + +global___DescribeStreamOutput = DescribeStreamOutput + +class CloseStreamInput(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_FIELD_NUMBER: builtins.int + STREAM_ID_FIELD_NUMBER: builtins.int + REASON_FIELD_NUMBER: builtins.int + namespace: builtins.str + stream_id: builtins.str + @property + def reason(self) -> temporalio.api.common.v1.message_pb2.Payload: ... + def __init__( + self, + *, + namespace: builtins.str = ..., + stream_id: builtins.str = ..., + reason: temporalio.api.common.v1.message_pb2.Payload | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["reason", b"reason"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "namespace", b"namespace", "reason", b"reason", "stream_id", b"stream_id" + ], + ) -> None: ... + +global___CloseStreamInput = CloseStreamInput + +class CloseStreamOutput(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + def __init__( + self, + ) -> None: ... + +global___CloseStreamOutput = CloseStreamOutput + +class TruncateStreamInput(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_FIELD_NUMBER: builtins.int + STREAM_ID_FIELD_NUMBER: builtins.int + NEW_BASE_OFFSET_FIELD_NUMBER: builtins.int + namespace: builtins.str + stream_id: builtins.str + new_base_offset: builtins.int + def __init__( + self, + *, + namespace: builtins.str = ..., + stream_id: builtins.str = ..., + new_base_offset: builtins.int = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "namespace", + b"namespace", + "new_base_offset", + b"new_base_offset", + "stream_id", + b"stream_id", + ], + ) -> None: ... + +global___TruncateStreamInput = TruncateStreamInput + +class TruncateStreamOutput(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + def __init__( + self, + ) -> None: ... + +global___TruncateStreamOutput = TruncateStreamOutput + +class DeleteStreamInput(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_FIELD_NUMBER: builtins.int + STREAM_ID_FIELD_NUMBER: builtins.int + FORCE_FIELD_NUMBER: builtins.int + namespace: builtins.str + stream_id: builtins.str + force: builtins.bool + """Delete even while a workflow consumer is active. Without it the call is + refused, because the consumer's History depends on ranges the deletion + takes with it. + """ + def __init__( + self, + *, + namespace: builtins.str = ..., + stream_id: builtins.str = ..., + force: builtins.bool = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "force", b"force", "namespace", b"namespace", "stream_id", b"stream_id" + ], + ) -> None: ... + +global___DeleteStreamInput = DeleteStreamInput + +class DeleteStreamOutput(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + def __init__( + self, + ) -> None: ... + +global___DeleteStreamOutput = DeleteStreamOutput + +class CreateStreamRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_ID_FIELD_NUMBER: builtins.int + FRONTEND_REQUEST_FIELD_NUMBER: builtins.int + namespace_id: builtins.str + @property + def frontend_request(self) -> global___CreateStreamInput: ... + def __init__( + self, + *, + namespace_id: builtins.str = ..., + frontend_request: global___CreateStreamInput | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal["frontend_request", b"frontend_request"], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "frontend_request", b"frontend_request", "namespace_id", b"namespace_id" + ], + ) -> None: ... + +global___CreateStreamRequest = CreateStreamRequest + +class CreateStreamResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + FRONTEND_RESPONSE_FIELD_NUMBER: builtins.int + @property + def frontend_response(self) -> global___CreateStreamOutput: ... + def __init__( + self, + *, + frontend_response: global___CreateStreamOutput | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "frontend_response", b"frontend_response" + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "frontend_response", b"frontend_response" + ], + ) -> None: ... + +global___CreateStreamResponse = CreateStreamResponse + +class AddMessagesRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_ID_FIELD_NUMBER: builtins.int + FRONTEND_REQUEST_FIELD_NUMBER: builtins.int + namespace_id: builtins.str + @property + def frontend_request(self) -> global___AddMessagesInput: ... + def __init__( + self, + *, + namespace_id: builtins.str = ..., + frontend_request: global___AddMessagesInput | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal["frontend_request", b"frontend_request"], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "frontend_request", b"frontend_request", "namespace_id", b"namespace_id" + ], + ) -> None: ... + +global___AddMessagesRequest = AddMessagesRequest + +class AddMessagesResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + FRONTEND_RESPONSE_FIELD_NUMBER: builtins.int + @property + def frontend_response(self) -> global___AddMessagesOutput: ... + def __init__( + self, + *, + frontend_response: global___AddMessagesOutput | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "frontend_response", b"frontend_response" + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "frontend_response", b"frontend_response" + ], + ) -> None: ... + +global___AddMessagesResponse = AddMessagesResponse + +class FinishWritingRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_ID_FIELD_NUMBER: builtins.int + FRONTEND_REQUEST_FIELD_NUMBER: builtins.int + namespace_id: builtins.str + @property + def frontend_request(self) -> global___FinishWritingInput: ... + def __init__( + self, + *, + namespace_id: builtins.str = ..., + frontend_request: global___FinishWritingInput | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal["frontend_request", b"frontend_request"], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "frontend_request", b"frontend_request", "namespace_id", b"namespace_id" + ], + ) -> None: ... + +global___FinishWritingRequest = FinishWritingRequest + +class FinishWritingResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + FRONTEND_RESPONSE_FIELD_NUMBER: builtins.int + @property + def frontend_response(self) -> global___FinishWritingOutput: ... + def __init__( + self, + *, + frontend_response: global___FinishWritingOutput | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "frontend_response", b"frontend_response" + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "frontend_response", b"frontend_response" + ], + ) -> None: ... + +global___FinishWritingResponse = FinishWritingResponse + +class SubscribeWorkflowRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_ID_FIELD_NUMBER: builtins.int + FRONTEND_REQUEST_FIELD_NUMBER: builtins.int + namespace_id: builtins.str + @property + def frontend_request(self) -> global___SubscribeWorkflowInput: ... + def __init__( + self, + *, + namespace_id: builtins.str = ..., + frontend_request: global___SubscribeWorkflowInput | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal["frontend_request", b"frontend_request"], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "frontend_request", b"frontend_request", "namespace_id", b"namespace_id" + ], + ) -> None: ... + +global___SubscribeWorkflowRequest = SubscribeWorkflowRequest + +class SubscribeWorkflowResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + FRONTEND_RESPONSE_FIELD_NUMBER: builtins.int + @property + def frontend_response(self) -> global___SubscribeWorkflowOutput: ... + def __init__( + self, + *, + frontend_response: global___SubscribeWorkflowOutput | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "frontend_response", b"frontend_response" + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "frontend_response", b"frontend_response" + ], + ) -> None: ... + +global___SubscribeWorkflowResponse = SubscribeWorkflowResponse + +class PollMessagesRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_ID_FIELD_NUMBER: builtins.int + FRONTEND_REQUEST_FIELD_NUMBER: builtins.int + namespace_id: builtins.str + @property + def frontend_request(self) -> global___PollMessagesInput: ... + def __init__( + self, + *, + namespace_id: builtins.str = ..., + frontend_request: global___PollMessagesInput | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal["frontend_request", b"frontend_request"], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "frontend_request", b"frontend_request", "namespace_id", b"namespace_id" + ], + ) -> None: ... + +global___PollMessagesRequest = PollMessagesRequest + +class PollMessagesResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + FRONTEND_RESPONSE_FIELD_NUMBER: builtins.int + @property + def frontend_response(self) -> global___PollMessagesOutput: ... + def __init__( + self, + *, + frontend_response: global___PollMessagesOutput | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "frontend_response", b"frontend_response" + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "frontend_response", b"frontend_response" + ], + ) -> None: ... + +global___PollMessagesResponse = PollMessagesResponse + +class DescribeStreamRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_ID_FIELD_NUMBER: builtins.int + FRONTEND_REQUEST_FIELD_NUMBER: builtins.int + namespace_id: builtins.str + @property + def frontend_request(self) -> global___DescribeStreamInput: ... + def __init__( + self, + *, + namespace_id: builtins.str = ..., + frontend_request: global___DescribeStreamInput | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal["frontend_request", b"frontend_request"], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "frontend_request", b"frontend_request", "namespace_id", b"namespace_id" + ], + ) -> None: ... + +global___DescribeStreamRequest = DescribeStreamRequest + +class DescribeStreamResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + FRONTEND_RESPONSE_FIELD_NUMBER: builtins.int + @property + def frontend_response(self) -> global___DescribeStreamOutput: ... + def __init__( + self, + *, + frontend_response: global___DescribeStreamOutput | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "frontend_response", b"frontend_response" + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "frontend_response", b"frontend_response" + ], + ) -> None: ... + +global___DescribeStreamResponse = DescribeStreamResponse + +class PollWorkflowMessagesRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_ID_FIELD_NUMBER: builtins.int + FRONTEND_REQUEST_FIELD_NUMBER: builtins.int + namespace_id: builtins.str + @property + def frontend_request(self) -> global___PollWorkflowMessagesInput: ... + def __init__( + self, + *, + namespace_id: builtins.str = ..., + frontend_request: global___PollWorkflowMessagesInput | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal["frontend_request", b"frontend_request"], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "frontend_request", b"frontend_request", "namespace_id", b"namespace_id" + ], + ) -> None: ... + +global___PollWorkflowMessagesRequest = PollWorkflowMessagesRequest + +class PollWorkflowMessagesResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + FRONTEND_RESPONSE_FIELD_NUMBER: builtins.int + @property + def frontend_response(self) -> global___PollMessagesOutput: ... + def __init__( + self, + *, + frontend_response: global___PollMessagesOutput | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "frontend_response", b"frontend_response" + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "frontend_response", b"frontend_response" + ], + ) -> None: ... + +global___PollWorkflowMessagesResponse = PollWorkflowMessagesResponse + +class DescribeWorkflowStreamRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_ID_FIELD_NUMBER: builtins.int + FRONTEND_REQUEST_FIELD_NUMBER: builtins.int + namespace_id: builtins.str + @property + def frontend_request(self) -> global___DescribeWorkflowStreamInput: ... + def __init__( + self, + *, + namespace_id: builtins.str = ..., + frontend_request: global___DescribeWorkflowStreamInput | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal["frontend_request", b"frontend_request"], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "frontend_request", b"frontend_request", "namespace_id", b"namespace_id" + ], + ) -> None: ... + +global___DescribeWorkflowStreamRequest = DescribeWorkflowStreamRequest + +class DescribeWorkflowStreamResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + FRONTEND_RESPONSE_FIELD_NUMBER: builtins.int + @property + def frontend_response(self) -> global___DescribeStreamOutput: ... + def __init__( + self, + *, + frontend_response: global___DescribeStreamOutput | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "frontend_response", b"frontend_response" + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "frontend_response", b"frontend_response" + ], + ) -> None: ... + +global___DescribeWorkflowStreamResponse = DescribeWorkflowStreamResponse + +class RegisterStreamConsumerInput(google.protobuf.message.Message): + """Registering a consumer on a stream in another execution. Split out from + SubscribeWorkflow because the two halves live on different shards: the pin + goes on the stream, the cursor goes on the consuming workflow, and a handler + can only reach the shard it was routed to. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_FIELD_NUMBER: builtins.int + STREAM_ID_FIELD_NUMBER: builtins.int + CONSUMER_WORKFLOW_ID_FIELD_NUMBER: builtins.int + CONSUMER_RUN_ID_FIELD_NUMBER: builtins.int + START_OFFSET_FIELD_NUMBER: builtins.int + START_POSITION_FIELD_NUMBER: builtins.int + namespace: builtins.str + stream_id: builtins.str + consumer_workflow_id: builtins.str + """The workflow that will consume. Together with the run it names the pin, + so a later run of the same workflow id registers fresh rather than + inheriting a closed run's floor. + """ + consumer_run_id: builtins.str + start_offset: builtins.int + """An absolute offset, read only when start_position is unset. A negative + value is refused. + """ + @property + def start_position( + self, + ) -> temporalio.api.stream.v1.message_pb2.StreamStartPosition: + """Resolved here, where the frontier is, and returned as start_offset on the + output so the cursor records a fact. + """ + def __init__( + self, + *, + namespace: builtins.str = ..., + stream_id: builtins.str = ..., + consumer_workflow_id: builtins.str = ..., + consumer_run_id: builtins.str = ..., + start_offset: builtins.int = ..., + start_position: temporalio.api.stream.v1.message_pb2.StreamStartPosition + | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["start_position", b"start_position"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "consumer_run_id", + b"consumer_run_id", + "consumer_workflow_id", + b"consumer_workflow_id", + "namespace", + b"namespace", + "start_offset", + b"start_offset", + "start_position", + b"start_position", + "stream_id", + b"stream_id", + ], + ) -> None: ... + +global___RegisterStreamConsumerInput = RegisterStreamConsumerInput + +class RegisterStreamConsumerOutput(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + START_OFFSET_FIELD_NUMBER: builtins.int + KNOWN_HEAD_FIELD_NUMBER: builtins.int + STREAM_ABSENT_FIELD_NUMBER: builtins.int + start_offset: builtins.int + known_head: builtins.int + """The frontier at registration, so the cursor starts with a known head + instead of waiting for the first push. + """ + stream_absent: builtins.bool + """No execution in this namespace holds a stream with that id, so nothing + was registered. An answer rather than a NotFound because the caller acts + on it: a subscribe command falls back to a stream of the workflow's own by + that name. Every other NotFound, from a registry miss to a shard that has + moved, stays an error, since binding the workflow to different data on one + of those would be silent and permanent. + """ + def __init__( + self, + *, + start_offset: builtins.int = ..., + known_head: builtins.int = ..., + stream_absent: builtins.bool = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "known_head", + b"known_head", + "start_offset", + b"start_offset", + "stream_absent", + b"stream_absent", + ], + ) -> None: ... + +global___RegisterStreamConsumerOutput = RegisterStreamConsumerOutput + +class AdvanceConsumerHeadInput(google.protobuf.message.Message): + """Telling one consumer that the frontier moved. Routed to the consumer, which + is not where the stream lives. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_FIELD_NUMBER: builtins.int + WORKFLOW_ID_FIELD_NUMBER: builtins.int + OWNER_RUN_ID_FIELD_NUMBER: builtins.int + STREAM_ID_FIELD_NUMBER: builtins.int + HEAD_OFFSET_FIELD_NUMBER: builtins.int + namespace: builtins.str + workflow_id: builtins.str + owner_run_id: builtins.str + """The run that subscribed. A successor from continue-as-new holds no cursor + for this stream, so pushing the frontier at it would land nowhere. + """ + stream_id: builtins.str + head_offset: builtins.int + def __init__( + self, + *, + namespace: builtins.str = ..., + workflow_id: builtins.str = ..., + owner_run_id: builtins.str = ..., + stream_id: builtins.str = ..., + head_offset: builtins.int = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "head_offset", + b"head_offset", + "namespace", + b"namespace", + "owner_run_id", + b"owner_run_id", + "stream_id", + b"stream_id", + "workflow_id", + b"workflow_id", + ], + ) -> None: ... + +global___AdvanceConsumerHeadInput = AdvanceConsumerHeadInput + +class AdvanceConsumerHeadOutput(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + CONSUMER_CLOSED_FIELD_NUMBER: builtins.int + SUCCESSOR_RUN_ID_FIELD_NUMBER: builtins.int + SUCCESSOR_START_OFFSET_FIELD_NUMBER: builtins.int + consumer_closed: builtins.bool + """The run that held the pin is closed and no current run of the workflow + consumes the stream, so the stream can release the pin. + """ + successor_run_id: builtins.str + """The run that held the pin is closed but a successor carries the + subscription, so the stream re-keys the pin to it, with the floor the + successor's cursor started from. + """ + successor_start_offset: builtins.int + def __init__( + self, + *, + consumer_closed: builtins.bool = ..., + successor_run_id: builtins.str = ..., + successor_start_offset: builtins.int = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "consumer_closed", + b"consumer_closed", + "successor_run_id", + b"successor_run_id", + "successor_start_offset", + b"successor_start_offset", + ], + ) -> None: ... + +global___AdvanceConsumerHeadOutput = AdvanceConsumerHeadOutput + +class AddWorkflowMessagesRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_ID_FIELD_NUMBER: builtins.int + FRONTEND_REQUEST_FIELD_NUMBER: builtins.int + namespace_id: builtins.str + @property + def frontend_request(self) -> global___AddWorkflowMessagesInput: ... + def __init__( + self, + *, + namespace_id: builtins.str = ..., + frontend_request: global___AddWorkflowMessagesInput | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal["frontend_request", b"frontend_request"], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "frontend_request", b"frontend_request", "namespace_id", b"namespace_id" + ], + ) -> None: ... + +global___AddWorkflowMessagesRequest = AddWorkflowMessagesRequest + +class AddWorkflowMessagesResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + FRONTEND_RESPONSE_FIELD_NUMBER: builtins.int + @property + def frontend_response(self) -> global___AddMessagesOutput: ... + def __init__( + self, + *, + frontend_response: global___AddMessagesOutput | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "frontend_response", b"frontend_response" + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "frontend_response", b"frontend_response" + ], + ) -> None: ... + +global___AddWorkflowMessagesResponse = AddWorkflowMessagesResponse + +class RegisterStreamConsumerRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_ID_FIELD_NUMBER: builtins.int + FRONTEND_REQUEST_FIELD_NUMBER: builtins.int + namespace_id: builtins.str + @property + def frontend_request(self) -> global___RegisterStreamConsumerInput: ... + def __init__( + self, + *, + namespace_id: builtins.str = ..., + frontend_request: global___RegisterStreamConsumerInput | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal["frontend_request", b"frontend_request"], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "frontend_request", b"frontend_request", "namespace_id", b"namespace_id" + ], + ) -> None: ... + +global___RegisterStreamConsumerRequest = RegisterStreamConsumerRequest + +class RegisterStreamConsumerResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + FRONTEND_RESPONSE_FIELD_NUMBER: builtins.int + @property + def frontend_response(self) -> global___RegisterStreamConsumerOutput: ... + def __init__( + self, + *, + frontend_response: global___RegisterStreamConsumerOutput | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "frontend_response", b"frontend_response" + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "frontend_response", b"frontend_response" + ], + ) -> None: ... + +global___RegisterStreamConsumerResponse = RegisterStreamConsumerResponse + +class AdvanceConsumerHeadRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_ID_FIELD_NUMBER: builtins.int + FRONTEND_REQUEST_FIELD_NUMBER: builtins.int + namespace_id: builtins.str + @property + def frontend_request(self) -> global___AdvanceConsumerHeadInput: ... + def __init__( + self, + *, + namespace_id: builtins.str = ..., + frontend_request: global___AdvanceConsumerHeadInput | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal["frontend_request", b"frontend_request"], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "frontend_request", b"frontend_request", "namespace_id", b"namespace_id" + ], + ) -> None: ... + +global___AdvanceConsumerHeadRequest = AdvanceConsumerHeadRequest + +class AdvanceConsumerHeadResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + FRONTEND_RESPONSE_FIELD_NUMBER: builtins.int + @property + def frontend_response(self) -> global___AdvanceConsumerHeadOutput: ... + def __init__( + self, + *, + frontend_response: global___AdvanceConsumerHeadOutput | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "frontend_response", b"frontend_response" + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "frontend_response", b"frontend_response" + ], + ) -> None: ... + +global___AdvanceConsumerHeadResponse = AdvanceConsumerHeadResponse + +class CloseStreamRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_ID_FIELD_NUMBER: builtins.int + FRONTEND_REQUEST_FIELD_NUMBER: builtins.int + namespace_id: builtins.str + @property + def frontend_request(self) -> global___CloseStreamInput: ... + def __init__( + self, + *, + namespace_id: builtins.str = ..., + frontend_request: global___CloseStreamInput | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal["frontend_request", b"frontend_request"], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "frontend_request", b"frontend_request", "namespace_id", b"namespace_id" + ], + ) -> None: ... + +global___CloseStreamRequest = CloseStreamRequest + +class CloseStreamResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + FRONTEND_RESPONSE_FIELD_NUMBER: builtins.int + @property + def frontend_response(self) -> global___CloseStreamOutput: ... + def __init__( + self, + *, + frontend_response: global___CloseStreamOutput | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "frontend_response", b"frontend_response" + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "frontend_response", b"frontend_response" + ], + ) -> None: ... + +global___CloseStreamResponse = CloseStreamResponse + +class TruncateStreamRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_ID_FIELD_NUMBER: builtins.int + FRONTEND_REQUEST_FIELD_NUMBER: builtins.int + namespace_id: builtins.str + @property + def frontend_request(self) -> global___TruncateStreamInput: ... + def __init__( + self, + *, + namespace_id: builtins.str = ..., + frontend_request: global___TruncateStreamInput | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal["frontend_request", b"frontend_request"], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "frontend_request", b"frontend_request", "namespace_id", b"namespace_id" + ], + ) -> None: ... + +global___TruncateStreamRequest = TruncateStreamRequest + +class TruncateStreamResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + FRONTEND_RESPONSE_FIELD_NUMBER: builtins.int + @property + def frontend_response(self) -> global___TruncateStreamOutput: ... + def __init__( + self, + *, + frontend_response: global___TruncateStreamOutput | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "frontend_response", b"frontend_response" + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "frontend_response", b"frontend_response" + ], + ) -> None: ... + +global___TruncateStreamResponse = TruncateStreamResponse + +class ListStreamsInput(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_FIELD_NUMBER: builtins.int + PAGE_SIZE_FIELD_NUMBER: builtins.int + NEXT_PAGE_TOKEN_FIELD_NUMBER: builtins.int + QUERY_FIELD_NUMBER: builtins.int + namespace: builtins.str + page_size: builtins.int + next_page_token: builtins.bytes + query: builtins.str + def __init__( + self, + *, + namespace: builtins.str = ..., + page_size: builtins.int = ..., + next_page_token: builtins.bytes = ..., + query: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "namespace", + b"namespace", + "next_page_token", + b"next_page_token", + "page_size", + b"page_size", + "query", + b"query", + ], + ) -> None: ... + +global___ListStreamsInput = ListStreamsInput + +class StreamListEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + STREAM_ID_FIELD_NUMBER: builtins.int + RUN_ID_FIELD_NUMBER: builtins.int + stream_id: builtins.str + run_id: builtins.str + def __init__( + self, + *, + stream_id: builtins.str = ..., + run_id: builtins.str = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "run_id", b"run_id", "stream_id", b"stream_id" + ], + ) -> None: ... + +global___StreamListEntry = StreamListEntry + +class ListStreamsOutput(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + STREAMS_FIELD_NUMBER: builtins.int + NEXT_PAGE_TOKEN_FIELD_NUMBER: builtins.int + @property + def streams( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + global___StreamListEntry + ]: ... + next_page_token: builtins.bytes + def __init__( + self, + *, + streams: collections.abc.Iterable[global___StreamListEntry] | None = ..., + next_page_token: builtins.bytes = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "next_page_token", b"next_page_token", "streams", b"streams" + ], + ) -> None: ... + +global___ListStreamsOutput = ListStreamsOutput + +class ListStreamsRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_ID_FIELD_NUMBER: builtins.int + FRONTEND_REQUEST_FIELD_NUMBER: builtins.int + namespace_id: builtins.str + @property + def frontend_request(self) -> global___ListStreamsInput: ... + def __init__( + self, + *, + namespace_id: builtins.str = ..., + frontend_request: global___ListStreamsInput | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal["frontend_request", b"frontend_request"], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "frontend_request", b"frontend_request", "namespace_id", b"namespace_id" + ], + ) -> None: ... + +global___ListStreamsRequest = ListStreamsRequest + +class ListStreamsResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + FRONTEND_RESPONSE_FIELD_NUMBER: builtins.int + @property + def frontend_response(self) -> global___ListStreamsOutput: ... + def __init__( + self, + *, + frontend_response: global___ListStreamsOutput | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "frontend_response", b"frontend_response" + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "frontend_response", b"frontend_response" + ], + ) -> None: ... + +global___ListStreamsResponse = ListStreamsResponse + +class DeleteStreamRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAMESPACE_ID_FIELD_NUMBER: builtins.int + FRONTEND_REQUEST_FIELD_NUMBER: builtins.int + namespace_id: builtins.str + @property + def frontend_request(self) -> global___DeleteStreamInput: ... + def __init__( + self, + *, + namespace_id: builtins.str = ..., + frontend_request: global___DeleteStreamInput | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal["frontend_request", b"frontend_request"], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "frontend_request", b"frontend_request", "namespace_id", b"namespace_id" + ], + ) -> None: ... + +global___DeleteStreamRequest = DeleteStreamRequest + +class DeleteStreamResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + FRONTEND_RESPONSE_FIELD_NUMBER: builtins.int + @property + def frontend_response(self) -> global___DeleteStreamOutput: ... + def __init__( + self, + *, + frontend_response: global___DeleteStreamOutput | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "frontend_response", b"frontend_response" + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "frontend_response", b"frontend_response" + ], + ) -> None: ... + +global___DeleteStreamResponse = DeleteStreamResponse diff --git a/temporalio/api/streamservice/v1/request_response_pb2_grpc.py b/temporalio/api/streamservice/v1/request_response_pb2_grpc.py new file mode 100644 index 000000000..bf947056a --- /dev/null +++ b/temporalio/api/streamservice/v1/request_response_pb2_grpc.py @@ -0,0 +1,4 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" + +import grpc diff --git a/temporalio/api/streamservice/v1/request_response_pb2_grpc.pyi b/temporalio/api/streamservice/v1/request_response_pb2_grpc.pyi new file mode 100644 index 000000000..f3a5a087e --- /dev/null +++ b/temporalio/api/streamservice/v1/request_response_pb2_grpc.pyi @@ -0,0 +1,4 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" diff --git a/temporalio/api/streamservice/v1/service_pb2.py b/temporalio/api/streamservice/v1/service_pb2.py new file mode 100644 index 000000000..432abfc5c --- /dev/null +++ b/temporalio/api/streamservice/v1/service_pb2.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: temporalio/api/stream/v1/service.proto +"""Generated protocol buffer code.""" + +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database + +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from temporalio.api.streamservice.v1 import ( + request_response_pb2 as temporalio_dot_api_dot_stream_dot_v1_dot_request__response__pb2, +) + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b'\n&temporalio/api/stream/v1/service.proto\x12)temporal.server.chasm.lib.stream.proto.v1\x1a/temporalio/api/stream/v1/request_response.proto2\xce\x12\n\rStreamService\x12\x91\x01\n\x0c\x43reateStream\x12>.temporal.server.chasm.lib.stream.proto.v1.CreateStreamRequest\x1a?.temporal.server.chasm.lib.stream.proto.v1.CreateStreamResponse"\x00\x12\x8e\x01\n\x0b\x41\x64\x64Messages\x12=.temporal.server.chasm.lib.stream.proto.v1.AddMessagesRequest\x1a>.temporal.server.chasm.lib.stream.proto.v1.AddMessagesResponse"\x00\x12\x94\x01\n\rFinishWriting\x12?.temporal.server.chasm.lib.stream.proto.v1.FinishWritingRequest\x1a@.temporal.server.chasm.lib.stream.proto.v1.FinishWritingResponse"\x00\x12\xa0\x01\n\x11SubscribeWorkflow\x12\x43.temporal.server.chasm.lib.stream.proto.v1.SubscribeWorkflowRequest\x1a\x44.temporal.server.chasm.lib.stream.proto.v1.SubscribeWorkflowResponse"\x00\x12\x91\x01\n\x0cPollMessages\x12>.temporal.server.chasm.lib.stream.proto.v1.PollMessagesRequest\x1a?.temporal.server.chasm.lib.stream.proto.v1.PollMessagesResponse"\x00\x12\x97\x01\n\x0e\x44\x65scribeStream\x12@.temporal.server.chasm.lib.stream.proto.v1.DescribeStreamRequest\x1a\x41.temporal.server.chasm.lib.stream.proto.v1.DescribeStreamResponse"\x00\x12\xa9\x01\n\x14PollWorkflowMessages\x12\x46.temporal.server.chasm.lib.stream.proto.v1.PollWorkflowMessagesRequest\x1aG.temporal.server.chasm.lib.stream.proto.v1.PollWorkflowMessagesResponse"\x00\x12\xaf\x01\n\x16\x44\x65scribeWorkflowStream\x12H.temporal.server.chasm.lib.stream.proto.v1.DescribeWorkflowStreamRequest\x1aI.temporal.server.chasm.lib.stream.proto.v1.DescribeWorkflowStreamResponse"\x00\x12\xa6\x01\n\x13\x41\x64\x64WorkflowMessages\x12\x45.temporal.server.chasm.lib.stream.proto.v1.AddWorkflowMessagesRequest\x1a\x46.temporal.server.chasm.lib.stream.proto.v1.AddWorkflowMessagesResponse"\x00\x12\xaf\x01\n\x16RegisterStreamConsumer\x12H.temporal.server.chasm.lib.stream.proto.v1.RegisterStreamConsumerRequest\x1aI.temporal.server.chasm.lib.stream.proto.v1.RegisterStreamConsumerResponse"\x00\x12\xa6\x01\n\x13\x41\x64vanceConsumerHead\x12\x45.temporal.server.chasm.lib.stream.proto.v1.AdvanceConsumerHeadRequest\x1a\x46.temporal.server.chasm.lib.stream.proto.v1.AdvanceConsumerHeadResponse"\x00\x12\x8e\x01\n\x0b\x43loseStream\x12=.temporal.server.chasm.lib.stream.proto.v1.CloseStreamRequest\x1a>.temporal.server.chasm.lib.stream.proto.v1.CloseStreamResponse"\x00\x12\x97\x01\n\x0eTruncateStream\x12@.temporal.server.chasm.lib.stream.proto.v1.TruncateStreamRequest\x1a\x41.temporal.server.chasm.lib.stream.proto.v1.TruncateStreamResponse"\x00\x12\x8e\x01\n\x0bListStreams\x12=.temporal.server.chasm.lib.stream.proto.v1.ListStreamsRequest\x1a>.temporal.server.chasm.lib.stream.proto.v1.ListStreamsResponse"\x00\x12\x91\x01\n\x0c\x44\x65leteStream\x12>.temporal.server.chasm.lib.stream.proto.v1.DeleteStreamRequest\x1a?.temporal.server.chasm.lib.stream.proto.v1.DeleteStreamResponse"\x00\x42>Z None: ... + CreateStream: grpc.UnaryUnaryMultiCallable[ + temporalio.api.streamservice.v1.request_response_pb2.CreateStreamRequest, + temporalio.api.streamservice.v1.request_response_pb2.CreateStreamResponse, + ] + AddMessages: grpc.UnaryUnaryMultiCallable[ + temporalio.api.streamservice.v1.request_response_pb2.AddMessagesRequest, + temporalio.api.streamservice.v1.request_response_pb2.AddMessagesResponse, + ] + FinishWriting: grpc.UnaryUnaryMultiCallable[ + temporalio.api.streamservice.v1.request_response_pb2.FinishWritingRequest, + temporalio.api.streamservice.v1.request_response_pb2.FinishWritingResponse, + ] + SubscribeWorkflow: grpc.UnaryUnaryMultiCallable[ + temporalio.api.streamservice.v1.request_response_pb2.SubscribeWorkflowRequest, + temporalio.api.streamservice.v1.request_response_pb2.SubscribeWorkflowResponse, + ] + PollMessages: grpc.UnaryUnaryMultiCallable[ + temporalio.api.streamservice.v1.request_response_pb2.PollMessagesRequest, + temporalio.api.streamservice.v1.request_response_pb2.PollMessagesResponse, + ] + DescribeStream: grpc.UnaryUnaryMultiCallable[ + temporalio.api.streamservice.v1.request_response_pb2.DescribeStreamRequest, + temporalio.api.streamservice.v1.request_response_pb2.DescribeStreamResponse, + ] + PollWorkflowMessages: grpc.UnaryUnaryMultiCallable[ + temporalio.api.streamservice.v1.request_response_pb2.PollWorkflowMessagesRequest, + temporalio.api.streamservice.v1.request_response_pb2.PollWorkflowMessagesResponse, + ] + """The owned-stream calls are routed on the owner, because the stream has no + id of its own. A workflow's activity is routed on the workflow. + """ + DescribeWorkflowStream: grpc.UnaryUnaryMultiCallable[ + temporalio.api.streamservice.v1.request_response_pb2.DescribeWorkflowStreamRequest, + temporalio.api.streamservice.v1.request_response_pb2.DescribeWorkflowStreamResponse, + ] + AddWorkflowMessages: grpc.UnaryUnaryMultiCallable[ + temporalio.api.streamservice.v1.request_response_pb2.AddWorkflowMessagesRequest, + temporalio.api.streamservice.v1.request_response_pb2.AddWorkflowMessagesResponse, + ] + RegisterStreamConsumer: grpc.UnaryUnaryMultiCallable[ + temporalio.api.streamservice.v1.request_response_pb2.RegisterStreamConsumerRequest, + temporalio.api.streamservice.v1.request_response_pb2.RegisterStreamConsumerResponse, + ] + """Internal. History calls these on itself to reach a shard it does not own, + which is the only way a step that spans two executions can work on a + cluster with more than one history host. + """ + AdvanceConsumerHead: grpc.UnaryUnaryMultiCallable[ + temporalio.api.streamservice.v1.request_response_pb2.AdvanceConsumerHeadRequest, + temporalio.api.streamservice.v1.request_response_pb2.AdvanceConsumerHeadResponse, + ] + CloseStream: grpc.UnaryUnaryMultiCallable[ + temporalio.api.streamservice.v1.request_response_pb2.CloseStreamRequest, + temporalio.api.streamservice.v1.request_response_pb2.CloseStreamResponse, + ] + TruncateStream: grpc.UnaryUnaryMultiCallable[ + temporalio.api.streamservice.v1.request_response_pb2.TruncateStreamRequest, + temporalio.api.streamservice.v1.request_response_pb2.TruncateStreamResponse, + ] + ListStreams: grpc.UnaryUnaryMultiCallable[ + temporalio.api.streamservice.v1.request_response_pb2.ListStreamsRequest, + temporalio.api.streamservice.v1.request_response_pb2.ListStreamsResponse, + ] + """Served on the frontend only: it queries visibility rather than a stream, + so there is no business ID to route on and nothing for a shard to answer. + """ + DeleteStream: grpc.UnaryUnaryMultiCallable[ + temporalio.api.streamservice.v1.request_response_pb2.DeleteStreamRequest, + temporalio.api.streamservice.v1.request_response_pb2.DeleteStreamResponse, + ] + +class StreamServiceServicer(metaclass=abc.ABCMeta): + @abc.abstractmethod + def CreateStream( + self, + request: temporalio.api.streamservice.v1.request_response_pb2.CreateStreamRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.streamservice.v1.request_response_pb2.CreateStreamResponse: ... + @abc.abstractmethod + def AddMessages( + self, + request: temporalio.api.streamservice.v1.request_response_pb2.AddMessagesRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.streamservice.v1.request_response_pb2.AddMessagesResponse: ... + @abc.abstractmethod + def FinishWriting( + self, + request: temporalio.api.streamservice.v1.request_response_pb2.FinishWritingRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.streamservice.v1.request_response_pb2.FinishWritingResponse: ... + @abc.abstractmethod + def SubscribeWorkflow( + self, + request: temporalio.api.streamservice.v1.request_response_pb2.SubscribeWorkflowRequest, + context: grpc.ServicerContext, + ) -> ( + temporalio.api.streamservice.v1.request_response_pb2.SubscribeWorkflowResponse + ): ... + @abc.abstractmethod + def PollMessages( + self, + request: temporalio.api.streamservice.v1.request_response_pb2.PollMessagesRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.streamservice.v1.request_response_pb2.PollMessagesResponse: ... + @abc.abstractmethod + def DescribeStream( + self, + request: temporalio.api.streamservice.v1.request_response_pb2.DescribeStreamRequest, + context: grpc.ServicerContext, + ) -> ( + temporalio.api.streamservice.v1.request_response_pb2.DescribeStreamResponse + ): ... + @abc.abstractmethod + def PollWorkflowMessages( + self, + request: temporalio.api.streamservice.v1.request_response_pb2.PollWorkflowMessagesRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.streamservice.v1.request_response_pb2.PollWorkflowMessagesResponse: + """The owned-stream calls are routed on the owner, because the stream has no + id of its own. A workflow's activity is routed on the workflow. + """ + @abc.abstractmethod + def DescribeWorkflowStream( + self, + request: temporalio.api.streamservice.v1.request_response_pb2.DescribeWorkflowStreamRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.streamservice.v1.request_response_pb2.DescribeWorkflowStreamResponse: ... + @abc.abstractmethod + def AddWorkflowMessages( + self, + request: temporalio.api.streamservice.v1.request_response_pb2.AddWorkflowMessagesRequest, + context: grpc.ServicerContext, + ) -> ( + temporalio.api.streamservice.v1.request_response_pb2.AddWorkflowMessagesResponse + ): ... + @abc.abstractmethod + def RegisterStreamConsumer( + self, + request: temporalio.api.streamservice.v1.request_response_pb2.RegisterStreamConsumerRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.streamservice.v1.request_response_pb2.RegisterStreamConsumerResponse: + """Internal. History calls these on itself to reach a shard it does not own, + which is the only way a step that spans two executions can work on a + cluster with more than one history host. + """ + @abc.abstractmethod + def AdvanceConsumerHead( + self, + request: temporalio.api.streamservice.v1.request_response_pb2.AdvanceConsumerHeadRequest, + context: grpc.ServicerContext, + ) -> ( + temporalio.api.streamservice.v1.request_response_pb2.AdvanceConsumerHeadResponse + ): ... + @abc.abstractmethod + def CloseStream( + self, + request: temporalio.api.streamservice.v1.request_response_pb2.CloseStreamRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.streamservice.v1.request_response_pb2.CloseStreamResponse: ... + @abc.abstractmethod + def TruncateStream( + self, + request: temporalio.api.streamservice.v1.request_response_pb2.TruncateStreamRequest, + context: grpc.ServicerContext, + ) -> ( + temporalio.api.streamservice.v1.request_response_pb2.TruncateStreamResponse + ): ... + @abc.abstractmethod + def ListStreams( + self, + request: temporalio.api.streamservice.v1.request_response_pb2.ListStreamsRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.streamservice.v1.request_response_pb2.ListStreamsResponse: + """Served on the frontend only: it queries visibility rather than a stream, + so there is no business ID to route on and nothing for a shard to answer. + """ + @abc.abstractmethod + def DeleteStream( + self, + request: temporalio.api.streamservice.v1.request_response_pb2.DeleteStreamRequest, + context: grpc.ServicerContext, + ) -> temporalio.api.streamservice.v1.request_response_pb2.DeleteStreamResponse: ... + +def add_StreamServiceServicer_to_server( + servicer: StreamServiceServicer, server: grpc.Server +) -> None: ... diff --git a/temporalio/api/streamservice/v1/stream_state_pb2.py b/temporalio/api/streamservice/v1/stream_state_pb2.py new file mode 100644 index 000000000..2b4eeafa3 --- /dev/null +++ b/temporalio/api/streamservice/v1/stream_state_pb2.py @@ -0,0 +1,148 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: temporalio/api/stream/v1/stream_state.proto +"""Generated protocol buffer code.""" + +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database + +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import duration_pb2 as google_dot_protobuf_dot_duration__pb2 +from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 + +from temporalio.api.common.v1 import ( + message_pb2 as temporal_dot_api_dot_common_dot_v1_dot_message__pb2, +) + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b'\n+temporalio/api/stream/v1/stream_state.proto\x12)temporal.server.chasm.lib.stream.proto.v1\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a$temporal/api/common/v1/message.proto"\xe3\x06\n\x0bStreamState\x12\x13\n\x0bhead_offset\x18\x01 \x01(\x03\x12\x13\n\x0b\x62\x61se_offset\x18\x02 \x01(\x03\x12\x0e\n\x06\x63losed\x18\x04 \x01(\x08\x12\x35\n\x0c\x63lose_reason\x18\x05 \x01(\x0b\x32\x1f.temporal.api.common.v1.Payload\x12X\n\tproducers\x18\t \x03(\x0b\x32\x45.temporal.server.chasm.lib.stream.proto.v1.StreamState.ProducersEntry\x12X\n\tconsumers\x18\n \x03(\x0b\x32\x45.temporal.server.chasm.lib.stream.proto.v1.StreamState.ConsumersEntry\x12M\n\tlifecycle\x18\x0b \x01(\x0b\x32:.temporal.server.chasm.lib.stream.proto.v1.StreamLifecycle\x12\x17\n\x0fredirect_run_id\x18\x0c \x01(\t\x12.\n\nclose_time\x18\r \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12G\n\x06\x62udget\x18\x0e \x01(\x0b\x32\x37.temporal.server.chasm.lib.stream.proto.v1.StreamBudget\x12\x16\n\x0e\x61ppended_bytes\x18\x0f \x01(\x03\x12\x16\n\x0enotify_pending\x18\x10 \x01(\x08\x12\x12\n\nheld_bytes\x18\x11 \x01(\x03\x12\x18\n\x10\x61ge_task_pending\x18\x12 \x01(\x08\x1ak\n\x0eProducersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12H\n\x05value\x18\x02 \x01(\x0b\x32\x39.temporal.server.chasm.lib.stream.proto.v1.ProducerCursor:\x02\x38\x01\x1ak\n\x0e\x43onsumersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12H\n\x05value\x18\x02 \x01(\x0b\x32\x39.temporal.server.chasm.lib.stream.proto.v1.ConsumerCursor:\x02\x38\x01J\x04\x08\x03\x10\x04J\x04\x08\x06\x10\x07J\x04\x08\x07\x10\x08J\x04\x08\x08\x10\t"4\n\x0cStreamBudget\x12\x11\n\tmax_items\x18\x01 \x01(\x03\x12\x11\n\tmax_bytes\x18\x02 \x01(\x03"h\n\x0eProducerCursor\x12\x0b\n\x03seq\x18\x01 \x01(\x03\x12\x14\n\x0c\x66irst_offset\x18\x02 \x01(\x03\x12\r\n\x05\x63ount\x18\x03 \x01(\x03\x12\x14\n\x0c\x63ontent_hash\x18\x04 \x01(\x0c\x12\x0e\n\x06\x66\x65nced\x18\x05 \x01(\x08"}\n\x0e\x43onsumerCursor\x12\x13\n\x0bworkflow_id\x18\x01 \x01(\t\x12\x0e\n\x06run_id\x18\x02 \x01(\t\x12\x0e\n\x06offset\x18\x03 \x01(\x03\x12\x0e\n\x06\x61\x63tive\x18\x04 \x01(\x08\x12\x10\n\x08\x65xternal\x18\x05 \x01(\x08\x12\x14\n\x0creplay_floor\x18\x06 \x01(\x03"\xc0\x01\n\x14WorkflowStreamCursor\x12\x11\n\tstream_id\x18\x01 \x01(\t\x12\x0e\n\x06offset\x18\x04 \x01(\x03\x12\x12\n\nknown_head\x18\x08 \x01(\x03\x12\x10\n\x08\x65xternal\x18\t \x01(\x08\x12\x14\n\x0cpending_from\x18\x05 \x01(\x03\x12\x12\n\npending_to\x18\x06 \x01(\x03\x12\x13\n\x0bhas_pending\x18\x07 \x01(\x08\x12\x14\n\x0cstart_offset\x18\n \x01(\x03J\x04\x08\x02\x10\x03J\x04\x08\x03\x10\x04"e\n\x0fStreamLifecycle\x12,\n\tretention\x18\x01 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x11\n\tmax_items\x18\x02 \x01(\x03\x12\x11\n\tmax_bytes\x18\x03 \x01(\x03\x42>Z= (3, 8): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class StreamState(google.protobuf.message.Message): + """Size is O(producers + consumers), never O(messages). Payload bytes live in + the component's data nodes, not here, which keeps this off the CHASM + partial-read path. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class ProducersEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + @property + def value(self) -> global___ProducerCursor: ... + def __init__( + self, + *, + key: builtins.str = ..., + value: global___ProducerCursor | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["value", b"value"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal["key", b"key", "value", b"value"], + ) -> None: ... + + class ConsumersEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + @property + def value(self) -> global___ConsumerCursor: ... + def __init__( + self, + *, + key: builtins.str = ..., + value: global___ConsumerCursor | None = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["value", b"value"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal["key", b"key", "value", b"value"], + ) -> None: ... + + HEAD_OFFSET_FIELD_NUMBER: builtins.int + BASE_OFFSET_FIELD_NUMBER: builtins.int + CLOSED_FIELD_NUMBER: builtins.int + CLOSE_REASON_FIELD_NUMBER: builtins.int + PRODUCERS_FIELD_NUMBER: builtins.int + CONSUMERS_FIELD_NUMBER: builtins.int + LIFECYCLE_FIELD_NUMBER: builtins.int + REDIRECT_RUN_ID_FIELD_NUMBER: builtins.int + CLOSE_TIME_FIELD_NUMBER: builtins.int + BUDGET_FIELD_NUMBER: builtins.int + APPENDED_BYTES_FIELD_NUMBER: builtins.int + NOTIFY_PENDING_FIELD_NUMBER: builtins.int + HELD_BYTES_FIELD_NUMBER: builtins.int + AGE_TASK_PENDING_FIELD_NUMBER: builtins.int + head_offset: builtins.int + """Visibility frontier. Readers never observe an offset at or past this.""" + base_offset: builtins.int + """Truncation floor. Offsets below this are gone.""" + closed: builtins.bool + @property + def close_reason(self) -> temporalio.api.common.v1.message_pb2.Payload: ... + @property + def producers( + self, + ) -> google.protobuf.internal.containers.MessageMap[ + builtins.str, global___ProducerCursor + ]: ... + @property + def consumers( + self, + ) -> google.protobuf.internal.containers.MessageMap[ + builtins.str, global___ConsumerCursor + ]: ... + @property + def lifecycle(self) -> global___StreamLifecycle: ... + redirect_run_id: builtins.str + """Set when a successor run takes ownership, so an in-flight poll can follow + the chain instead of stalling on a superseded run. + """ + @property + def close_time(self) -> google.protobuf.timestamp_pb2.Timestamp: + """Wall-clock close time, used to schedule retention deletion.""" + @property + def budget(self) -> global___StreamBudget: + """Set on a stream a workflow owns. Its batches are the owning execution's + mutable state, so appends past the budget are refused rather than left to + the execution size limit, which terminates the workflow. + """ + appended_bytes: builtins.int + """Bytes appended over the stream's life, kept for the budget check. A stream + with a budget carries no lifecycle cap and is never truncated, so nothing + reclaims behind it and this is also what it holds. The item side of the + budget measures head minus base for the same reason: offsets are global + and a stream can begin above zero. + """ + notify_pending: builtins.bool + """A notify task is scheduled and has not run yet. Appends while it is set + schedule none of their own; the task reads the head when it runs, so it + carries every append that landed before it. + """ + held_bytes: builtins.int + """Bytes of the batches the stream still holds, from the floor to the head. + Kept for the lifecycle's byte cap, which the appended total cannot serve + once truncation has reclaimed behind the floor. A batch straddling the + floor counts whole, since it is held whole. + """ + age_task_pending: builtins.bool + """An age check is scheduled and has not run yet. Appends while it is set + schedule none of their own; the check re-arms itself while the stream + holds records and lowers the flag when it holds none. + """ + def __init__( + self, + *, + head_offset: builtins.int = ..., + base_offset: builtins.int = ..., + closed: builtins.bool = ..., + close_reason: temporalio.api.common.v1.message_pb2.Payload | None = ..., + producers: collections.abc.Mapping[builtins.str, global___ProducerCursor] + | None = ..., + consumers: collections.abc.Mapping[builtins.str, global___ConsumerCursor] + | None = ..., + lifecycle: global___StreamLifecycle | None = ..., + redirect_run_id: builtins.str = ..., + close_time: google.protobuf.timestamp_pb2.Timestamp | None = ..., + budget: global___StreamBudget | None = ..., + appended_bytes: builtins.int = ..., + notify_pending: builtins.bool = ..., + held_bytes: builtins.int = ..., + age_task_pending: builtins.bool = ..., + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "budget", + b"budget", + "close_reason", + b"close_reason", + "close_time", + b"close_time", + "lifecycle", + b"lifecycle", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "age_task_pending", + b"age_task_pending", + "appended_bytes", + b"appended_bytes", + "base_offset", + b"base_offset", + "budget", + b"budget", + "close_reason", + b"close_reason", + "close_time", + b"close_time", + "closed", + b"closed", + "consumers", + b"consumers", + "head_offset", + b"head_offset", + "held_bytes", + b"held_bytes", + "lifecycle", + b"lifecycle", + "notify_pending", + b"notify_pending", + "producers", + b"producers", + "redirect_run_id", + b"redirect_run_id", + ], + ) -> None: ... + +global___StreamState = StreamState + +class StreamBudget(google.protobuf.message.Message): + """Hard bounds on what a stream may hold. Distinct from StreamLifecycle.max_items, + which reclaims the oldest messages: a budget refuses the newest. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + MAX_ITEMS_FIELD_NUMBER: builtins.int + MAX_BYTES_FIELD_NUMBER: builtins.int + max_items: builtins.int + max_bytes: builtins.int + def __init__( + self, + *, + max_items: builtins.int = ..., + max_bytes: builtins.int = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "max_bytes", b"max_bytes", "max_items", b"max_items" + ], + ) -> None: ... + +global___StreamBudget = StreamBudget + +class ProducerCursor(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + SEQ_FIELD_NUMBER: builtins.int + FIRST_OFFSET_FIELD_NUMBER: builtins.int + COUNT_FIELD_NUMBER: builtins.int + CONTENT_HASH_FIELD_NUMBER: builtins.int + FENCED_FIELD_NUMBER: builtins.int + seq: builtins.int + first_offset: builtins.int + count: builtins.int + content_hash: builtins.bytes + """Distinguishes a genuine retry from a client reusing a sequence with + different content, which must be rejected rather than deduplicated. + """ + fenced: builtins.bool + """Set by FinishWriting. Ends this producer's writes without closing the + stream for anyone else. + """ + def __init__( + self, + *, + seq: builtins.int = ..., + first_offset: builtins.int = ..., + count: builtins.int = ..., + content_hash: builtins.bytes = ..., + fenced: builtins.bool = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "content_hash", + b"content_hash", + "count", + b"count", + "fenced", + b"fenced", + "first_offset", + b"first_offset", + "seq", + b"seq", + ], + ) -> None: ... + +global___ProducerCursor = ProducerCursor + +class ConsumerCursor(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + WORKFLOW_ID_FIELD_NUMBER: builtins.int + RUN_ID_FIELD_NUMBER: builtins.int + OFFSET_FIELD_NUMBER: builtins.int + ACTIVE_FIELD_NUMBER: builtins.int + EXTERNAL_FIELD_NUMBER: builtins.int + REPLAY_FLOOR_FIELD_NUMBER: builtins.int + workflow_id: builtins.str + run_id: builtins.str + offset: builtins.int + """How far this consumer has read. Used to decide whether it needs waking, + not to decide what the stream may drop. + """ + active: builtins.bool + external: builtins.bool + """Set when the consumer is a workflow in another execution, which is the + only case that has to be told the frontier moved. A workflow consuming a + stream it owns sees that while closing its own transaction. + """ + replay_floor: builtins.int + """The oldest offset this consumer's History still depends on. + + It is where the subscription started, not where it has read to. A range + this consumer already consumed is recorded in its History and has to be + re-readable for the workflow to replay, so bytes below the read position + are exactly the ones a replay needs most. It stays put while the consumer + is active and is released when it deregisters. + """ + def __init__( + self, + *, + workflow_id: builtins.str = ..., + run_id: builtins.str = ..., + offset: builtins.int = ..., + active: builtins.bool = ..., + external: builtins.bool = ..., + replay_floor: builtins.int = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "active", + b"active", + "external", + b"external", + "offset", + b"offset", + "replay_floor", + b"replay_floor", + "run_id", + b"run_id", + "workflow_id", + b"workflow_id", + ], + ) -> None: ... + +global___ConsumerCursor = ConsumerCursor + +class WorkflowStreamCursor(google.protobuf.message.Message): + """A consuming Workflow's position in a stream. This lives in the consuming + Workflow's own state rather than on the stream, so advancing it commits in + the same transaction as the WorkflowTaskCompleted event that records the + range. Keeping it on the stream would make the advance a cross-execution + write, and a crash between the two would either redeliver or skip. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + STREAM_ID_FIELD_NUMBER: builtins.int + OFFSET_FIELD_NUMBER: builtins.int + KNOWN_HEAD_FIELD_NUMBER: builtins.int + EXTERNAL_FIELD_NUMBER: builtins.int + PENDING_FROM_FIELD_NUMBER: builtins.int + PENDING_TO_FIELD_NUMBER: builtins.int + HAS_PENDING_FIELD_NUMBER: builtins.int + START_OFFSET_FIELD_NUMBER: builtins.int + stream_id: builtins.str + offset: builtins.int + """Next offset to deliver.""" + known_head: builtins.int + """The stream's frontier as of the last delivery. A workflow consuming a + stream in another execution cannot read the real frontier while closing its + own transaction, so this is what tells it a capped slice left more behind. + """ + external: builtins.bool + """Set when the stream lives in another execution, which decides whether the + frontier is read locally or taken from known_head. + """ + pending_from: builtins.int + """The range attached to the Workflow Task currently in flight. Recorded on + the event that closes that task, then folded into offset. An empty range + is still recorded: a task where the subscription saw nothing is a fact + replay has to reproduce. + """ + pending_to: builtins.int + has_pending: builtins.bool + start_offset: builtins.int + """Where this subscription began reading. Retained separately from offset, + which advances, because replay has to tell "committed nothing yet" apart + from "the events recording what was committed are not in this page". + """ + def __init__( + self, + *, + stream_id: builtins.str = ..., + offset: builtins.int = ..., + known_head: builtins.int = ..., + external: builtins.bool = ..., + pending_from: builtins.int = ..., + pending_to: builtins.int = ..., + has_pending: builtins.bool = ..., + start_offset: builtins.int = ..., + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "external", + b"external", + "has_pending", + b"has_pending", + "known_head", + b"known_head", + "offset", + b"offset", + "pending_from", + b"pending_from", + "pending_to", + b"pending_to", + "start_offset", + b"start_offset", + "stream_id", + b"stream_id", + ], + ) -> None: ... + +global___WorkflowStreamCursor = WorkflowStreamCursor + +class StreamLifecycle(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + RETENTION_FIELD_NUMBER: builtins.int + MAX_ITEMS_FIELD_NUMBER: builtins.int + MAX_BYTES_FIELD_NUMBER: builtins.int + @property + def retention(self) -> google.protobuf.duration_pb2.Duration: + """How long a record stays readable, and how long a closed stream stays + readable before it is deleted. On an open stream batches older than this + are reclaimed behind the floor, never past an active consumer's floor; on + a closed one it is the time to deletion. + """ + max_items: builtins.int + """Cap on readable messages. Whole batches are reclaimed once the floor + passes them, so a capped stream has bounded storage. + + While a workflow consumer is registered the cap stops being a rolling + window: the consumer's floor sits at the offset it subscribed from, + because replay re-reads every range its History recorded, so the cap + behaves as a lifetime quota measured from that subscription and appends + past it are refused rather than reclaiming behind the consumer. + """ + max_bytes: builtins.int + """Cap on held bytes. Unlike max_items it reclaims nothing: an append that + would take the held bytes past it is refused, and room comes back only as + the record cap, an explicit truncation or the retention age reclaims + batches behind the floor. + """ + def __init__( + self, + *, + retention: google.protobuf.duration_pb2.Duration | None = ..., + max_items: builtins.int = ..., + max_bytes: builtins.int = ..., + ) -> None: ... + def HasField( + self, field_name: typing_extensions.Literal["retention", b"retention"] + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "max_bytes", + b"max_bytes", + "max_items", + b"max_items", + "retention", + b"retention", + ], + ) -> None: ... + +global___StreamLifecycle = StreamLifecycle diff --git a/temporalio/api/streamservice/v1/stream_state_pb2_grpc.py b/temporalio/api/streamservice/v1/stream_state_pb2_grpc.py new file mode 100644 index 000000000..bf947056a --- /dev/null +++ b/temporalio/api/streamservice/v1/stream_state_pb2_grpc.py @@ -0,0 +1,4 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" + +import grpc diff --git a/temporalio/api/streamservice/v1/stream_state_pb2_grpc.pyi b/temporalio/api/streamservice/v1/stream_state_pb2_grpc.pyi new file mode 100644 index 000000000..f3a5a087e --- /dev/null +++ b/temporalio/api/streamservice/v1/stream_state_pb2_grpc.pyi @@ -0,0 +1,4 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" From 6f2f9ad4e4d146d1a8216f2faba031b3481b21c8 Mon Sep 17 00:00:00 2001 From: Mohammad Dashti Date: Sat, 3 Oct 2026 02:16:36 -0700 Subject: [PATCH 2/3] Added a client for the server-side stream service. temporalio.client_stream opens, appends to, reads and closes a native stream on the server, with the service's errors mapped onto the stream error family. --- temporalio/client_stream.py | 760 ++++++++++++++++++++++++++++++++++++ 1 file changed, 760 insertions(+) create mode 100644 temporalio/client_stream.py diff --git a/temporalio/client_stream.py b/temporalio/client_stream.py new file mode 100644 index 000000000..1f02ab128 --- /dev/null +++ b/temporalio/client_stream.py @@ -0,0 +1,760 @@ +"""Client for Temporal server-side streams. + +A stream is a durable, offset-addressed append-only sequence that lives beside +Workflow History rather than inside it. Appending schedules no Workflow Task, +and each reader holds its own cursor, so adding a reader costs nothing on the +write side. The record is ``temporal.api.stream.v1.StreamRecord`` on the wire +and in the store, so a reader in any language decodes the same bytes. + +Prototype support for AI-198. Four things about it are temporary and will +change before this is a real feature: + +- **Requires the ``grpc`` extra.** The rest of this SDK reaches the server + through sdk-core, which does not know about this service yet, so the client + here opens its own channel with ``grpcio``. +- **The protos are vendored** under ``temporalio.api.streamservice.v1`` instead of + coming from the api submodule, because the service is still defined in the + server. That is why the wire names read as server-internal. +- **This client is for use outside a Workflow.** Workflow code publishes and + consumes with ``workflow.append_stream_records`` and + ``workflow.read_stream_records`` instead. +- **No TLS or API-key support**, for the same reason: the channel is built + here rather than by the machinery that normally handles that. + +A failed call raises :class:`temporalio.streams.StreamNotFoundError` when the +server answers ``NOT_FOUND``, +:class:`temporalio.streams.StreamProducerError` when it refuses a producer +sequence it already holds, and :class:`temporalio.service.RPCError` +otherwise, never the transport's own exception type. +""" + +from __future__ import annotations + +import asyncio +import weakref +from collections.abc import AsyncIterator, Awaitable, Callable, Sequence +from dataclasses import dataclass +from typing import Any, TypeVar + +import google.protobuf.duration_pb2 +import grpc +import grpc.aio +from google.protobuf.message import Message + +import temporalio.api.streamservice.v1 as stream +from temporalio.api.stream.v1 import StreamRecord, StreamStartPosition +from temporalio.api.streamservice.v1 import service_pb2_grpc +from temporalio.service import RPCError, RPCStatusCode +from temporalio.streams import StreamNotFoundError, StreamProducerError + +__all__ = [ + "Appended", + "Page", + "StreamClient", + "StreamEntry", + "StreamHandle", + "WorkflowStreamHandle", + "close_shared_clients", + "shared_client", +] + +_T = TypeVar("_T") + +# The server refuses a producer sequence it already holds with a message and +# no typed detail, so the phrase is the only thing to match on. Both refusals +# it sends carry it: a repeat with different content, and one behind the +# sequence it accepted last. +_PRODUCER_CONFLICT = "producer sequence" +# The status the server puts on a producer conflict; the older one is kept +# so a server built before the reason tokens still gets the typed error. +_PRODUCER_CONFLICT_CODES = ( + grpc.StatusCode.FAILED_PRECONDITION, + grpc.StatusCode.INVALID_ARGUMENT, +) + + +@dataclass(frozen=True) +class Appended: + """Where one append landed. + + ``deduplicated`` says the server already held this producer's batch at + this sequence and wrote nothing; the offsets are then the original ones. + """ + + first_offset: int + next_offset: int + count: int + deduplicated: bool = False + + +@dataclass(frozen=True) +class StreamEntry: + """One record read from a stream, with where it sits. + + Offsets are assigned over the unfiltered stream, so a topic-filtered read + hands back entries whose offsets are not contiguous. A reader that resumes + between records takes ``offset`` rather than counting what it received. + """ + + record: StreamRecord + offset: int = 0 + + +@dataclass(frozen=True) +class Page: + """One read of a stream. + + ``closed`` with ``next_offset >= head_offset`` is the end: nothing more can + be added and this reader has everything. ``run_id`` names the execution + holding the stream, which is how a reader of a workflow's stream learns + which run it is on when it did not pin one. + """ + + entries: list[StreamEntry] + next_offset: int + head_offset: int + closed: bool + run_id: str = "" + + +def _copy_by_name(source: Message, target: Message, *, skip: frozenset[str]) -> None: + """Copy every field ``source`` has set onto the field of ``target`` with that name. + + By descriptor rather than field by field, so a field added to + ``StreamRecord`` crosses in both directions without anybody remembering to + add a line here. A field the target does not have raises, which is the + answer a reader wants: better a loud failure than a body that arrives + without the thing that described it. + """ + fields = target.DESCRIPTOR.fields_by_name + for descriptor, value in source.ListFields(): + if descriptor.name in skip: + continue + if descriptor.name not in fields: + raise ValueError( + f"{source.DESCRIPTOR.full_name}.{descriptor.name} has no counterpart " + f"on {target.DESCRIPTOR.full_name}" + ) + field = getattr(target, descriptor.name) + if descriptor.message_type is not None and ( + descriptor.message_type.GetOptions().map_entry + ): + holds_message = ( + descriptor.message_type.fields_by_name["value"].message_type is not None + ) + for key, item in value.items(): + if holds_message: + field[key].CopyFrom(item) + else: + field[key] = item + elif hasattr(field, "extend"): + # A plain repeated field; maps answered above and everything else + # takes an assignment or a CopyFrom. + field.extend(value) + elif descriptor.type == descriptor.TYPE_MESSAGE: + field.CopyFrom(value) + else: + setattr(target, descriptor.name, value) + + +def _to_service(record: StreamRecord) -> stream.StreamRecord: + # The stored shape is the public record plus the offset a read assigns. + # An unset body stays unset, so a FINISH record reads back as one. + out = stream.StreamRecord() + _copy_by_name(record, out, skip=frozenset()) + return out + + +def _to_public(record: stream.StreamRecord) -> StreamEntry: + out = StreamRecord() + # The offset is the store's, not the record's; it rides on the entry. + _copy_by_name(record, out, skip=frozenset({"offset"})) + return StreamEntry(record=out, offset=record.offset) + + +def _translate(error: grpc.aio.AioRpcError) -> Exception: + code = error.code() + details = error.details() or code.name + if code is grpc.StatusCode.NOT_FOUND: + return StreamNotFoundError(details) + if code in _PRODUCER_CONFLICT_CODES and _PRODUCER_CONFLICT in details: + # A producer sequence the store already holds, either with different + # content or behind the one it accepted last. The caller asked to be + # deduplicated and could not be, which is a condition of its own. The + # server reports it as a failed precondition; one built before the + # reason tokens existed called it an invalid argument. + return StreamProducerError(details) + raw = b"" + # The aio metadata iterates as (key, value) pairs at runtime, whatever + # shape the stubs give its items. + trailing: Any = error.trailing_metadata() + for item in trailing or (): + key, value = item[0], item[1] + if key == "grpc-status-details-bin" and isinstance(value, bytes): + raw = value + return RPCError(details, RPCStatusCode(code.value[0]), raw) + + +async def _call(method: Callable[[Any], Awaitable[_T]], request: Any) -> _T: + """Make one stub call, translating the transport's failure to the SDK's.""" + try: + return await method(request) + except grpc.aio.AioRpcError as error: + raise _translate(error) from error + + +class StreamClient: + """Creates and opens streams on a namespace.""" + + def __init__(self, channel: Any, namespace: str) -> None: + """Wrap an existing ``grpc.aio`` channel. Prefer :meth:`connect`.""" + self._channel = channel + self._namespace = namespace + # The generated stub is typed for a synchronous channel. This client + # drives it over ``grpc.aio``, where every call is awaited. + self._stub: Any = service_pb2_grpc.StreamServiceStub(channel) + + @staticmethod + def connect(target_host: str, namespace: str = "default") -> StreamClient: + """Open a channel to a frontend. + + Separate from ``Client.connect`` because this does not share the + connection the rest of the SDK uses. + """ + return StreamClient(grpc.aio.insecure_channel(target_host), namespace) + + async def close(self) -> None: + """Close the underlying channel.""" + await self._channel.close() + + async def create( + self, + stream_id: str, + *, + retention: float | None = None, + max_items: int | None = None, + ) -> StreamHandle: + """Create a stream and return a handle to it. + + ``retention`` is how long a closed stream stays readable, in seconds. + ``max_items`` caps how many records remain readable, dropping the + oldest, which bounds storage for a stream nobody truncates. + """ + lifecycle = stream.StreamLifecycle() + if retention is not None: + lifecycle.retention.CopyFrom( + google.protobuf.duration_pb2.Duration(seconds=int(retention)) + ) + if max_items is not None: + lifecycle.max_items = max_items + + response = await _call( + self._stub.CreateStream, + stream.CreateStreamRequest( + frontend_request=stream.CreateStreamInput( + namespace=self._namespace, + stream_id=stream_id, + lifecycle=lifecycle, + ) + ), + ) + return StreamHandle( + self._stub, + self._namespace, + stream_id, + run_id=response.frontend_response.run_id, + ) + + def get(self, stream_id: str) -> StreamHandle: + """Open an existing stream without a round trip.""" + return StreamHandle(self._stub, self._namespace, stream_id) + + def workflow_stream( + self, workflow_id: str, name: str = "", *, owner_run_id: str = "" + ) -> WorkflowStreamHandle: + """Open a stream a workflow owns. + + A stream a workflow owns lives inside that workflow's execution and has + no id of its own, so it is named by its owner and its name. An empty + name is the workflow's default output stream, which is what + ``workflow.append_stream_records`` writes to when it is given none. + + The stream is created by the workflow's first publish or subscription, + or by the first append from outside. Reading one that does not exist + yet is not an error: it reads as empty and a + :meth:`WorkflowStreamHandle.follow` parked on it wakes when something + is written. + + ``owner_run_id`` pins the handle to one run of the workflow; see + :meth:`WorkflowStreamHandle.pin` for why a follower wants that. + """ + return WorkflowStreamHandle( + self._stub, self._namespace, workflow_id, name, owner_run_id + ) + + def activity_stream( + self, + activity_id: str, + name: str = "", + *, + workflow_id: str = "", + run_id: str = "", + ) -> WorkflowStreamHandle: + """Open a stream an activity owns. + + Without ``workflow_id`` the activity is a standalone one, an execution + of its own, and ``run_id`` pins one run of it. With ``workflow_id`` it + is an activity that workflow scheduled, reached through the workflow, + and ``run_id`` pins the workflow's run. Either way the stream is apart + from the workflow's streams, one per activity execution rather than + per attempt, and it reads as closed once the activity reaches a + terminal status. An empty name is the activity's default stream. + """ + return WorkflowStreamHandle( + self._stub, + self._namespace, + workflow_id, + name, + run_id, + activity_id=activity_id, + ) + + +class StreamHandle: + """A handle to one standalone stream.""" + + def __init__( + self, stub: Any, namespace: str, stream_id: str, run_id: str = "" + ) -> None: + """Prefer :meth:`StreamClient.get` or :meth:`StreamClient.create`.""" + self._stub = stub + self._namespace = namespace + self._id = stream_id + # Passing this back saves the server resolving the current run on every + # call, which is otherwise a persistence lookup per request. + self._run_id = run_id + + @property + def id(self) -> str: + """Id of the stream this handle points at.""" + return self._id + + async def append( + self, + *records: StreamRecord, + producer_id: str = "", + sequence: int = 0, + ) -> Appended: + """Append records and return where they landed. + + Supplying ``producer_id`` and ``sequence`` makes the append idempotent: + a retry with the same pair returns the original offsets rather than + appending twice, and says so. Without them the append is + at-least-once, which is only the right trade when duplicates are + harmless. + """ + response = await _call( + self._stub.AddMessages, + stream.AddMessagesRequest( + frontend_request=stream.AddMessagesInput( + namespace=self._namespace, + stream_id=self._id, + run_id=self._run_id, + records=[_to_service(record) for record in records], + producer_id=producer_id, + sequence=sequence, + ) + ), + ) + return _appended(response.frontend_response) + + async def read( + self, + *, + from_offset: int = 0, + start: StreamStartPosition | None = None, + max_records: int = 0, + topics: Sequence[str] = (), + wait: bool = False, + ) -> tuple[list[StreamEntry], int]: + """Read once from ``from_offset``, returning the entries and the + offset to read from next. + + A reader with no offset yet passes ``start`` instead: the oldest + record held, the tail, or the last N records. The server resolves it + in the same read, so it cannot race with truncation, and the offset + returned is where to continue. Passing both is refused. + + With ``wait`` set, blocks until something arrives, the stream closes, + or the server's long-poll window elapses. A window that elapses returns + an empty list rather than raising, so the caller just reads again. + """ + page = await self.poll( + from_offset=from_offset, + start=start, + max_records=max_records, + topics=topics, + wait=wait, + ) + return page.entries, page.next_offset + + async def follow( + self, + *, + from_offset: int = 0, + start: StreamStartPosition | None = None, + topics: Sequence[str] = (), + ) -> AsyncIterator[StreamEntry]: + """Yield entries as they arrive, starting at ``from_offset`` or ``start``. + + Ends once the stream is closed and this reader has drained it. A closed + stream stays readable until its retention expires, so a reader that + starts late still sees everything instead of having to coordinate a + shutdown with the producer. + """ + offset = from_offset + while True: + page = await self.poll(from_offset=offset, start=start, topics=topics) + # The first page carries where the start resolved to, and every + # later poll continues from the offset it handed back. + start = None + for entry in page.entries: + yield entry + offset = page.next_offset + if page.closed and offset >= page.head_offset: + return + + async def poll( + self, + *, + from_offset: int = 0, + start: StreamStartPosition | None = None, + topics: Sequence[str] = (), + max_records: int = 0, + wait: bool = True, + ) -> Page: + """One read, with everything the caller needs to decide what to do next.""" + response = await _call( + self._stub.PollMessages, + stream.PollMessagesRequest( + frontend_request=stream.PollMessagesInput( + namespace=self._namespace, + stream_id=self._id, + run_id=self._run_id, + from_offset=from_offset, + start_position=start, + max_messages=max_records, + topics=list(topics), + wait_new_messages=wait, + ) + ), + ) + return _page(response.frontend_response) + + async def truncate(self, new_base_offset: int) -> None: + """Drop every record below ``new_base_offset``. + + A reader that asks for an offset below it is refused. One that has no + offset yet and wants the oldest record left asks for + ``StreamStartPosition(earliest=True)`` rather than offset zero. + """ + await _call( + self._stub.TruncateStream, + stream.TruncateStreamRequest( + frontend_request=stream.TruncateStreamInput( + namespace=self._namespace, + stream_id=self._id, + new_base_offset=new_base_offset, + ) + ), + ) + + async def finish_writing(self, producer_id: str) -> None: + """Declare one producer done without ending the stream for others.""" + await _call( + self._stub.FinishWriting, + stream.FinishWritingRequest( + frontend_request=stream.FinishWritingInput( + namespace=self._namespace, + stream_id=self._id, + producer_id=producer_id, + ) + ), + ) + + async def close(self) -> None: + """Seal the stream. Readers can still drain what is already there.""" + await _call( + self._stub.CloseStream, + stream.CloseStreamRequest( + frontend_request=stream.CloseStreamInput( + namespace=self._namespace, stream_id=self._id + ) + ), + ) + + async def describe(self) -> stream.StreamState: + """Read the stream's current frontier, floor and closed state.""" + response = await _call( + self._stub.DescribeStream, + stream.DescribeStreamRequest( + frontend_request=stream.DescribeStreamInput( + namespace=self._namespace, stream_id=self._id + ) + ), + ) + return response.frontend_response.state + + +class WorkflowStreamHandle: + """A handle to a stream a workflow or an activity owns. + + A workflow writes to its own from inside its Workflow Task, which costs it + no transition of its own. Anything else writes through :meth:`append`, + which costs one transition on the owning execution per batch. Both land in + the same log in the order the server accepted them. An activity has only + the second path. + """ + + def __init__( + self, + stub: Any, + namespace: str, + workflow_id: str, + name: str = "", + owner_run_id: str = "", + *, + activity_id: str = "", + ) -> None: + """Prefer :meth:`StreamClient.workflow_stream` or :meth:`StreamClient.activity_stream`.""" + self._stub = stub + self._namespace = namespace + self._workflow_id = workflow_id + self._name = name + self._owner_run_id = owner_run_id + self._activity_id = activity_id + + @property + def workflow_id(self) -> str: + """Id of the workflow that owns this stream, or that scheduled the activity that does.""" + return self._workflow_id + + @property + def activity_id(self) -> str: + """Id of the activity that owns this stream, empty when a workflow does.""" + return self._activity_id + + def _owner(self) -> dict[str, Any]: + # A workflow owner goes out in the workflow fields, which a server + # without owner support still routes on; only an activity needs the + # owner reference. + if not self._activity_id: + return { + "workflow_id": self._workflow_id, + "owner_run_id": self._owner_run_id, + } + if self._workflow_id: + owner = stream.StreamOwner( + kind=stream.STREAM_OWNER_KIND_WORKFLOW_ACTIVITY, + id=self._workflow_id, + run_id=self._owner_run_id, + activity_id=self._activity_id, + ) + else: + owner = stream.StreamOwner( + kind=stream.STREAM_OWNER_KIND_ACTIVITY, + id=self._activity_id, + run_id=self._owner_run_id, + ) + return {"owner": owner} + + @property + def name(self) -> str: + """Name the owner publishes under, empty for its default stream.""" + return self._name + + @property + def owner_run_id(self) -> str: + """The run this handle is pinned to, empty while it follows the current run.""" + return self._owner_run_id + + def pin(self, run_id: str) -> None: + """Address one run of the workflow from now on. + + Unpinned, every call resolves to whichever run is current. A follower + holding an offset from one run would then be redirected when the + workflow continues as new: the successor's stream starts empty at + offset zero, so the server reads the follower's offset as "caught up", + parks it until the successor has published that many records, and + then skips exactly that many. Pinned, the run's end shows up as + ``closed`` on the page, and the caller decides whether to follow the + successor from the beginning. + """ + self._owner_run_id = run_id + + async def append( + self, + *records: StreamRecord, + producer_id: str = "", + sequence: int = 0, + ) -> Appended: + """Append from outside the workflow, as :meth:`StreamHandle.append`. + + Batch where you can. The append costs one transition on the owning + execution whatever its size, so a batch of a hundred costs what a batch + of one does. + """ + response = await _call( + self._stub.AddWorkflowMessages, + stream.AddWorkflowMessagesRequest( + frontend_request=stream.AddWorkflowMessagesInput( + namespace=self._namespace, + **self._owner(), + stream_name=self._name, + records=[_to_service(record) for record in records], + producer_id=producer_id, + sequence=sequence, + ) + ), + ) + return _appended(response.frontend_response) + + async def read( + self, + *, + from_offset: int = 0, + start: StreamStartPosition | None = None, + max_records: int = 0, + topics: Sequence[str] = (), + wait: bool = False, + ) -> tuple[list[StreamEntry], int]: + """Read once from ``from_offset``, as :meth:`StreamHandle.read`.""" + page = await self.poll( + from_offset=from_offset, + start=start, + max_records=max_records, + topics=topics, + wait=wait, + ) + return page.entries, page.next_offset + + async def follow( + self, + *, + from_offset: int = 0, + start: StreamStartPosition | None = None, + topics: Sequence[str] = (), + ) -> AsyncIterator[StreamEntry]: + """Yield entries as they arrive, as :meth:`StreamHandle.follow`.""" + offset = from_offset + while True: + page = await self.poll(from_offset=offset, start=start, topics=topics) + # The first page carries where the start resolved to, and every + # later poll continues from the offset it handed back. + start = None + for entry in page.entries: + yield entry + offset = page.next_offset + if page.closed and offset >= page.head_offset: + return + + async def poll( + self, + *, + from_offset: int = 0, + start: StreamStartPosition | None = None, + topics: Sequence[str] = (), + max_records: int = 0, + wait: bool = True, + ) -> Page: + """One read, with everything the caller needs to decide what to do next. + + :meth:`read` is the same call without the frontier and the closed flag. + A caller that has to tell "nothing yet" from "nothing ever" needs both. + On a pinned handle ``closed`` also says the run has ended. + """ + response = await _call( + self._stub.PollWorkflowMessages, + stream.PollWorkflowMessagesRequest( + frontend_request=stream.PollWorkflowMessagesInput( + namespace=self._namespace, + **self._owner(), + stream_name=self._name, + from_offset=from_offset, + start_position=start, + max_messages=max_records, + topics=list(topics), + wait_new_messages=wait, + ) + ), + ) + return _page(response.frontend_response) + + async def describe(self) -> stream.StreamState: + """Read the stream's current frontier, floor and closed state. + + A reader that wants only what comes next starts from the head this + reports rather than from zero. + """ + response = await _call( + self._stub.DescribeWorkflowStream, + stream.DescribeWorkflowStreamRequest( + frontend_request=stream.DescribeWorkflowStreamInput( + namespace=self._namespace, + **self._owner(), + stream_name=self._name, + ) + ), + ) + return response.frontend_response.state + + +def _appended(out: stream.AddMessagesOutput) -> Appended: + return Appended( + first_offset=out.first_offset, + next_offset=out.next_offset, + count=out.count, + deduplicated=out.deduplicated, + ) + + +def _page(out: stream.PollMessagesOutput) -> Page: + return Page( + entries=[_to_public(record) for record in out.records], + next_offset=out.next_offset, + head_offset=out.head_offset, + closed=out.closed, + run_id=out.run_id, + ) + + +# One channel per loop, target and namespace, shared by every handle in the +# process. A channel is multiplexed and long lived, and callers open a handle +# per subscription, which would otherwise be a connection per subscription. The +# loop is the key because a grpc.aio channel belongs to the loop that made it, +# and it is held weakly so a loop that is gone cannot lend its channel to a +# successor that happens to reuse its id. +_shared: weakref.WeakKeyDictionary[ + asyncio.AbstractEventLoop, dict[tuple[str, str], StreamClient] +] = weakref.WeakKeyDictionary() + + +def shared_client(target_host: str, namespace: str) -> StreamClient: + """The process-wide client for ``target_host`` and ``namespace`` on this loop.""" + per_loop = _shared.setdefault(asyncio.get_running_loop(), {}) + key = (target_host, namespace) + existing = per_loop.get(key) + if existing is None: + existing = per_loop[key] = StreamClient.connect(target_host, namespace) + return existing + + +async def close_shared_clients() -> None: + """Close every shared client this loop opened. + + For a process that is done with streams, and for tests, which open a + loop per case and would otherwise leave a channel behind on each. + """ + per_loop = _shared.pop(asyncio.get_running_loop(), {}) + for client in per_loop.values(): + await client.close() From 52203691d099f4ffb6a8951bef07df8ff156f891 Mon Sep 17 00:00:00 2001 From: Mohammad Dashti Date: Sat, 3 Oct 2026 02:16:36 -0700 Subject: [PATCH 3/3] Covered the stream service client. Live cases run when TEMPORAL_STREAM_TARGET names a server with the stream service, and skip otherwise. --- tests/test_client_stream.py | 266 ++++++++++++++++++++++++++++ tests/test_client_stream_owner.py | 89 ++++++++++ tests/test_client_stream_records.py | 50 ++++++ 3 files changed, 405 insertions(+) create mode 100644 tests/test_client_stream.py create mode 100644 tests/test_client_stream_owner.py create mode 100644 tests/test_client_stream_records.py diff --git a/tests/test_client_stream.py b/tests/test_client_stream.py new file mode 100644 index 000000000..89abb0559 --- /dev/null +++ b/tests/test_client_stream.py @@ -0,0 +1,266 @@ +"""End-to-end tests for the server-side stream client. + +These need a Temporal server built from the AI-198 branch, because the stream +service does not exist on a released server. Point them at one: + + TEMPORAL_STREAM_TARGET=127.0.0.1:7333 uv run pytest tests/test_client_stream.py + +Skipped otherwise, rather than silently passing against a server that has no +idea what a stream is. +""" + +from __future__ import annotations + +import asyncio +import os +import uuid +from collections.abc import AsyncIterator + +import pytest + +from temporalio.api.common.v1 import Payload +from temporalio.api.stream.v1 import ( + StreamRecord, + StreamRecordKind, + StreamStartPosition, +) +from temporalio.client_stream import StreamClient, StreamHandle +from temporalio.service import RPCError +from temporalio.streams import StreamNotFoundError, StreamProducerError + +TARGET = os.environ.get("TEMPORAL_STREAM_TARGET") + +pytestmark = pytest.mark.skipif( + not TARGET, + reason="set TEMPORAL_STREAM_TARGET to a server with the stream service", +) + + +def rec(body: bytes, topic: str = "") -> StreamRecord: + return StreamRecord( + body=Payload(data=body, metadata={"encoding": b"binary/plain"}), topic=topic + ) + + +def data(entries: list) -> list[bytes]: + return [entry.record.body.data for entry in entries] + + +@pytest.fixture +async def streams() -> AsyncIterator[StreamClient]: + client = StreamClient.connect( + TARGET or "", os.environ.get("TEMPORAL_NAMESPACE", "default") + ) + try: + yield client + finally: + await client.close() + + +@pytest.fixture +async def stream(streams: StreamClient) -> StreamHandle: + return await streams.create("py-test-" + uuid.uuid4().hex[:8], max_items=1000) + + +async def test_append_returns_the_offset_it_landed_at(stream: StreamHandle) -> None: + first = await stream.append(rec(b"alpha"), rec(b"beta")) + assert (first.first_offset, first.next_offset, first.count) == (0, 2, 2) + assert not first.deduplicated + assert (await stream.append(rec(b"gamma"))).first_offset == 2 + + +async def test_read_from_an_offset(stream: StreamHandle) -> None: + await stream.append(rec(b"alpha"), rec(b"beta"), rec(b"gamma")) + + entries, next_offset = await stream.read() + assert data(entries) == [b"alpha", b"beta", b"gamma"] + assert [entry.offset for entry in entries] == [0, 1, 2] + assert next_offset == 3 + + entries, next_offset = await stream.read(from_offset=1) + assert data(entries) == [b"beta", b"gamma"] + assert next_offset == 3 + + +async def test_read_past_the_end_returns_nothing(stream: StreamHandle) -> None: + await stream.append(rec(b"alpha")) + + entries, next_offset = await stream.read(from_offset=1) + assert entries == [] + assert next_offset == 1, "a caught-up reader keeps its cursor" + + +# Without an identity the append is at-least-once, so this is the only way a +# producer that retries can avoid writing the same record twice. +async def test_append_is_idempotent_for_a_named_producer(stream: StreamHandle) -> None: + first = await stream.append(rec(b"once"), producer_id="p1", sequence=1) + retry = await stream.append(rec(b"once"), producer_id="p1", sequence=1) + assert first.first_offset == retry.first_offset == 0 + assert not first.deduplicated + # The retry is told it wrote nothing and where the original landed. + assert retry.deduplicated + assert retry.next_offset == first.next_offset + + entries, _ = await stream.read() + assert data(entries) == [b"once"] + + +async def test_topics_filter_a_read(stream: StreamHandle) -> None: + await stream.append(rec(b"tok", topic="tokens")) + await stream.append(rec(b"prog", topic="progress")) + await stream.append(rec(b"tok2", topic="tokens")) + + entries, next_offset = await stream.read(topics=["tokens"]) + assert data(entries) == [b"tok", b"tok2"] + assert [entry.offset for entry in entries] == [0, 2] + assert next_offset == 3, "the cursor advances past filtered-out offsets too" + + +# The record comes back as it went in: kind, producer identity and sequence +# are the server's to store, not to interpret. +async def test_the_record_roundtrips_field_for_field(stream: StreamHandle) -> None: + sent = rec(b"x", topic="t") + sent.producer_id = "model" + sent.attempt = 2 + sent.sequence = 7 + sent.kind = StreamRecordKind.STREAM_RECORD_KIND_DATA + sent.metadata["trace"].CopyFrom(Payload(data=b"abc")) + finish = StreamRecord( + topic="t", kind=StreamRecordKind.STREAM_RECORD_KIND_FINISH, producer_id="model" + ) + await stream.append(sent, finish) + + entries, _ = await stream.read() + got = entries[0].record + assert (got.topic, got.producer_id, got.attempt, got.sequence) == ( + "t", + "model", + 2, + 7, + ) + assert got.kind == StreamRecordKind.STREAM_RECORD_KIND_DATA + assert got.metadata["trace"].data == b"abc" + assert entries[1].record.kind == StreamRecordKind.STREAM_RECORD_KIND_FINISH + assert not entries[1].record.HasField("body") + + +async def test_an_unset_kind_reads_back_as_data(stream: StreamHandle) -> None: + await stream.append(rec(b"x")) + entries, _ = await stream.read() + assert entries[0].record.kind == StreamRecordKind.STREAM_RECORD_KIND_DATA + + +# A closed stream stays readable, which is what removes the shutdown handshake +# between producer and consumer. +async def test_follow_drains_then_ends_when_the_stream_closes( + stream: StreamHandle, +) -> None: + received: list[bytes] = [] + + async def follower() -> None: + async for entry in stream.follow(): + received.append(entry.record.body.data) + + task = asyncio.create_task(follower()) + await stream.append(rec(b"one")) + await asyncio.sleep(0.2) + await stream.append(rec(b"two")) + await asyncio.sleep(0.2) + await stream.close() + + await asyncio.wait_for(task, timeout=30) + assert received == [b"one", b"two"] + + +async def test_follow_started_late_still_sees_everything( + stream: StreamHandle, +) -> None: + await stream.append(rec(b"one"), rec(b"two")) + await stream.close() + + received = [entry.record.body.data async for entry in stream.follow()] + assert received == [b"one", b"two"] + + +async def test_describe_reports_the_frontier(stream: StreamHandle) -> None: + await stream.append(rec(b"one"), rec(b"two")) + + state = await stream.describe() + assert state.head_offset == 2 + assert not state.closed + + await stream.close() + assert (await stream.describe()).closed + + +# The transport's own exception never escapes: a missing stream is the SDK's +# not-found error and anything else is its RPC error. +async def test_failures_surface_as_sdk_errors(streams: StreamClient) -> None: + missing = streams.get("py-test-missing-" + uuid.uuid4().hex[:8]) + with pytest.raises(StreamNotFoundError): + await missing.describe() + with pytest.raises((StreamNotFoundError, RPCError)): + await missing.append(rec(b"x")) + + +# A producer that asked to be deduplicated and could not be is a condition of +# its own, not a bare argument error: the store already holds that sequence. +async def test_a_producer_conflict_is_a_stream_producer_error( + stream: StreamHandle, +) -> None: + await stream.append(rec(b"one"), producer_id="p1", sequence=0) + # Same producer and sequence, different content. The server cannot know + # which of the two the reader was meant to see. + with pytest.raises(StreamProducerError, match="different content"): + await stream.append(rec(b"other"), producer_id="p1", sequence=0) + # And a sequence behind the one it accepted last. + await stream.append(rec(b"two"), producer_id="p1", sequence=1) + with pytest.raises(StreamProducerError, match="stale producer sequence"): + await stream.append(rec(b"three"), producer_id="p1", sequence=0) + + entries, _ = await stream.read() + assert data(entries) == [b"one", b"two"] + + +async def test_earliest_reads_from_the_floor_of_a_truncated_stream( + stream: StreamHandle, +) -> None: + await stream.append(rec(b"a"), rec(b"b"), rec(b"c"), rec(b"d")) + await stream.truncate(2) + + # Offset zero is what a reader with no position used to send, and a + # truncated stream no longer holds it. + with pytest.raises(RPCError, match="truncated"): + await stream.read(from_offset=0) + entries, next_offset = await stream.read(start=StreamStartPosition(earliest=True)) + assert data(entries) == [b"c", b"d"] + assert [entry.offset for entry in entries] == [2, 3] + assert next_offset == 4 + + +async def test_last_n_and_tail_resolve_on_the_first_read(stream: StreamHandle) -> None: + await stream.append(rec(b"a"), rec(b"b"), rec(b"c")) + + entries, _ = await stream.read(start=StreamStartPosition(last_n=2)) + assert data(entries) == [b"b", b"c"] + entries, next_offset = await stream.read(start=StreamStartPosition(tail=True)) + assert entries == [] + assert next_offset == 3 + with pytest.raises(RPCError): + await stream.read(from_offset=1, start=StreamStartPosition(earliest=True)) + + +async def test_follow_from_the_tail_skips_what_was_there(stream: StreamHandle) -> None: + await stream.append(rec(b"old")) + following = stream.follow(start=StreamStartPosition(tail=True)) + first = asyncio.ensure_future(following.__anext__()) + try: + for _ in range(100): + await stream.append(rec(b"new")) + done, _ = await asyncio.wait({first}, timeout=0.1) + if done: + break + entry = await asyncio.wait_for(first, 10) + finally: + first.cancel() + assert entry.record.body.data == b"new" diff --git a/tests/test_client_stream_owner.py b/tests/test_client_stream_owner.py new file mode 100644 index 000000000..a87f9d782 --- /dev/null +++ b/tests/test_client_stream_owner.py @@ -0,0 +1,89 @@ +"""Which owner an owned-stream call names on the wire. + +A workflow owner goes out in the workflow fields, which a server without +owner support still routes on. An activity owner needs the owner reference: +a standalone activity is routed on its own id, and an activity a workflow +scheduled is routed on the workflow. +""" + +from __future__ import annotations + +from typing import Any + +import temporalio.api.streamservice.v1 as stream +from temporalio.api.stream.v1 import StreamRecord, StreamStartPosition +from temporalio.client_stream import StreamClient + + +class _Recorder: + """Stands in for the stub and answers every call with an empty response.""" + + def __init__(self) -> None: + self.requests: list[Any] = [] + + def __getattr__(self, method: str) -> Any: + responses = { + "AddWorkflowMessages": stream.AddWorkflowMessagesResponse, + "PollWorkflowMessages": stream.PollWorkflowMessagesResponse, + "DescribeWorkflowStream": stream.DescribeWorkflowStreamResponse, + } + + async def call(request: Any, **_: Any) -> Any: + self.requests.append(request.frontend_request) + return responses[method]() + + return call + + +def _client(recorder: _Recorder) -> StreamClient: + client = StreamClient.__new__(StreamClient) + client._stub = recorder + client._namespace = "ns" + return client + + +async def _every_call(handle: Any) -> None: + await handle.append(StreamRecord()) + await handle.poll(wait=False) + await handle.describe() + + +async def test_a_workflow_owner_uses_the_workflow_fields(): + recorder = _Recorder() + await _every_call(_client(recorder).workflow_stream("wf", owner_run_id="run")) + for request in recorder.requests: + assert (request.workflow_id, request.owner_run_id) == ("wf", "run") + assert not request.HasField("owner") + + +async def test_a_standalone_activity_is_its_own_owner(): + recorder = _Recorder() + await _every_call(_client(recorder).activity_stream("act", run_id="run")) + for request in recorder.requests: + assert request.owner == stream.StreamOwner( + kind=stream.STREAM_OWNER_KIND_ACTIVITY, id="act", run_id="run" + ) + assert request.workflow_id == "" + + +async def test_a_workflow_activity_is_reached_through_its_workflow(): + recorder = _Recorder() + await _every_call( + _client(recorder).activity_stream("act", "reasoning", workflow_id="wf") + ) + for request in recorder.requests: + assert request.owner == stream.StreamOwner( + kind=stream.STREAM_OWNER_KIND_WORKFLOW_ACTIVITY, id="wf", activity_id="act" + ) + assert request.stream_name == "reasoning" + + +async def test_a_first_read_carries_its_start_position(): + recorder = _Recorder() + handle = _client(recorder).workflow_stream("wf") + await handle.read(start=StreamStartPosition(last_n=3)) + await handle.read(from_offset=4) + first, later = recorder.requests + assert first.start_position == StreamStartPosition(last_n=3) + assert first.from_offset == 0 + assert not later.HasField("start_position") diff --git a/tests/test_client_stream_records.py b/tests/test_client_stream_records.py new file mode 100644 index 000000000..261d0b74f --- /dev/null +++ b/tests/test_client_stream_records.py @@ -0,0 +1,50 @@ +"""How a record crosses between the public shape and the stored one. + +No server: what this pins is that the two converters carry every field the +record declares, so a field added to ``StreamRecord`` is not dropped in +silence on the way out or the way back. +""" + +from __future__ import annotations + +import pytest + +from temporalio.api.common.v1 import Payload +from temporalio.api.stream.v1 import StreamRecord, StreamRecordKind +from temporalio.client_stream import _copy_by_name, _to_public, _to_service + + +def test_a_record_crosses_both_ways_without_dropping_a_field() -> None: + # Copied by descriptor rather than field by field, so a field added to + # StreamRecord crosses without anybody adding a line to the converters. + sent = StreamRecord( + topic="t", + kind=StreamRecordKind.STREAM_RECORD_KIND_FINISH, + producer_id="model", + attempt=3, + sequence=7, + ) + sent.body.CopyFrom(Payload(data=b"x", metadata={"encoding": b"binary/plain"})) + sent.metadata["trace"].CopyFrom(Payload(data=b"abc")) + + entry = _to_public(_to_service(sent)) + assert entry.record == sent + assert {f.name for f, _ in sent.ListFields()} == { + f.name for f, _ in entry.record.ListFields() + } + # Every field the public record declares is carried, not only the ones + # the converters happened to name. + crossed = {f.name for f, _ in _to_service(sent).ListFields()} - {"offset"} + assert crossed == {f.name for f in StreamRecord.DESCRIPTOR.fields} + + +def test_a_field_with_no_counterpart_is_refused_rather_than_dropped() -> None: + # The stored shape carries an offset the public record has no room for. + # Copying it across has to say so rather than leave it behind, because + # the same silence is what loses a field somebody adds later. + stored = _to_service(StreamRecord(topic="t")) + stored.offset = 12 + with pytest.raises(ValueError, match="offset"): + _copy_by_name(stored, StreamRecord(), skip=frozenset()) + # Named explicitly, the offset is the store's and rides on the entry. + assert _to_public(stored).offset == 12