From 5babae703a04419fb3d2d238f43d3a51abff56d0 Mon Sep 17 00:00:00 2001 From: Tim Conley Date: Thu, 18 Jun 2026 11:18:33 -0700 Subject: [PATCH 1/2] Allow protobuf 7 --- .github/workflows/ci.yml | 1 + CHANGELOG.md | 2 ++ pyproject.toml | 6 ++--- scripts/_proto/Dockerfile | 1 + scripts/gen_payload_visitor.py | 29 ++++++++++++++++------ temporalio/converter/_failure_converter.py | 4 ++- tests/conftest.py | 5 ++-- tests/nexus/test_temporal_system_nexus.py | 27 +++++++++++++++----- tests/worker/test_command_aware_visitor.py | 4 +-- uv.lock | 20 +++++++-------- 10 files changed, 67 insertions(+), 32 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2bdc06191..9218d2901 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -123,6 +123,7 @@ jobs: - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8 - run: uv tool install poethepoet - run: uv remove google-adk --optional google-adk + - run: uv add --dev --python 3.10 "googleapis-common-protos==1.70.0" - run: uv add --python 3.10 "protobuf<4" - run: uv sync --all-extras - run: poe build-develop diff --git a/CHANGELOG.md b/CHANGELOG.md index 0183a74a4..fc6e34c44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,8 @@ to docs, or any other relevant information. per-invocation of the worker instead of only at startup. It is advised that any shared, heavy-weight operations are performed outside of the callback before `run_worker` is invoked. +- Relaxed the protobuf dependency bounds to allow protobuf 7 where compatible + with the selected optional dependencies. ## [1.29.0] - 2026-06-17 diff --git a/pyproject.toml b/pyproject.toml index 299c03df9..6b1e9b736 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,9 +10,9 @@ license-files = ["LICENSE"] keywords = ["temporal", "workflow"] dependencies = [ "nexus-rpc==1.4.0", - "protobuf>=3.20,<7.0.0", + "protobuf>=3.20,<8.0.0", "python-dateutil>=2.8.2,<3 ; python_version < '3.11'", - "types-protobuf>=3.20,<7.0.0", + "types-protobuf>=3.20,<8.0.0", "typing-extensions>=4.2.0,<5", ] classifiers = [ @@ -74,7 +74,7 @@ dev = [ "openai-agents[litellm]>=0.14.0; python_version < '3.14'", "litellm>=1.83.0", "openinference-instrumentation-google-adk>=0.1.11", - "googleapis-common-protos==1.70.0", + "googleapis-common-protos>=1.75.0,<2", "pytest-rerunfailures>=16.1", "pytest-xdist>=3.6,<4", "moto[s3,server]>=5", diff --git a/scripts/_proto/Dockerfile b/scripts/_proto/Dockerfile index 2e2f58391..0bbe18bb3 100644 --- a/scripts/_proto/Dockerfile +++ b/scripts/_proto/Dockerfile @@ -9,6 +9,7 @@ COPY ./ ./ RUN mkdir -p ./temporalio/api RUN uv remove google-adk --optional google-adk +RUN uv add --dev "googleapis-common-protos==1.70.0" RUN uv add "protobuf<4" RUN uv sync --all-extras RUN uv run scripts/gen_protos.py diff --git a/scripts/gen_payload_visitor.py b/scripts/gen_payload_visitor.py index e3b988ca9..da1be23ea 100644 --- a/scripts/gen_payload_visitor.py +++ b/scripts/gen_payload_visitor.py @@ -56,6 +56,16 @@ def name_for(desc: Descriptor) -> str: return desc.full_name.replace(".", "_") +def field_is_repeated(field: FieldDescriptor) -> bool: + return bool( + getattr( + field, + "is_repeated", + getattr(field, "label") == FieldDescriptor.LABEL_REPEATED, + ) + ) + + def emit_loop( field_name: str, iter_expr: str, @@ -290,17 +300,16 @@ def walk(self, desc: Descriptor) -> bool: continue # Repeated fields (including maps which are represented as repeated messages) - if field.label == FieldDescriptor.LABEL_REPEATED: - if ( - field.message_type is not None - and field.message_type.GetOptions().map_entry - ): - val_fd = field.message_type.fields_by_name.get("value") + if field_is_repeated(field): + message_type = field.message_type + if message_type is not None and message_type.GetOptions().map_entry: + val_fd = message_type.fields_by_name.get("value") if ( val_fd is not None and val_fd.type == FieldDescriptor.TYPE_MESSAGE ): child_desc = val_fd.message_type + assert child_desc is not None child_needed = self.walk(child_desc) if child_needed: has_payload = True @@ -313,12 +322,13 @@ def walk(self, desc: Descriptor) -> bool: ) ) - key_fd = field.message_type.fields_by_name.get("key") + key_fd = message_type.fields_by_name.get("key") if ( key_fd is not None and key_fd.type == FieldDescriptor.TYPE_MESSAGE ): child_desc = key_fd.message_type + assert child_desc is not None child_needed = self.walk(child_desc) if child_needed: has_payload = True @@ -331,14 +341,16 @@ def walk(self, desc: Descriptor) -> bool: ) ) else: + assert message_type is not None item = self._collect_repeated( - field.message_type, field, f"o.{field.name}" + message_type, field, f"o.{field.name}" ) if item is not None: has_payload = True emit_items.append(item) else: child_desc = field.message_type + assert child_desc is not None child_has_payload = self.walk(child_desc) has_payload |= child_has_payload if child_has_payload: @@ -358,6 +370,7 @@ def walk(self, desc: Descriptor) -> bool: first = True for field in fields: child_desc = field.message_type + assert child_desc is not None child_has_payload = self.walk(child_desc) has_payload |= child_has_payload if child_has_payload: diff --git a/temporalio/converter/_failure_converter.py b/temporalio/converter/_failure_converter.py index b1511b0b0..c76f95c23 100644 --- a/temporalio/converter/_failure_converter.py +++ b/temporalio/converter/_failure_converter.py @@ -283,7 +283,9 @@ def _nexus_failure_to_temporal_failure( failure.metadata and failure.metadata.get("type") == _TEMPORAL_FAILURE_PROTO_TYPE ): - google.protobuf.json_format.ParseDict(failure.details, temporal_failure) + google.protobuf.json_format.ParseDict( + dict(failure.details or {}), temporal_failure + ) else: temporal_failure.application_failure_info.SetInParent() temporal_failure.application_failure_info.type = "NexusFailure" diff --git a/tests/conftest.py b/tests/conftest.py index 9eaa1ff47..19ac8721f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -30,7 +30,7 @@ f"Expected {temporalio.__file__} to be in {sys.prefix}" ) -# Unless specifically overridden, we expect tests to run under protobuf 4.x/5.x lib +# Unless specifically overridden, we expect tests to run under protobuf 4.x/5.x/6.x/7.x lib import google.protobuf protobuf_version = google.protobuf.__version__ @@ -43,7 +43,8 @@ protobuf_version.startswith("4.") or protobuf_version.startswith("5.") or protobuf_version.startswith("6.") - ), f"Expected protobuf 4.x/5.x/6.x, got {protobuf_version}" + or protobuf_version.startswith("7.") + ), f"Expected protobuf 4.x/5.x/6.x/7.x, got {protobuf_version}" def pytest_runtest_setup(item): # type: ignore[reportMissingParameterType] diff --git a/tests/nexus/test_temporal_system_nexus.py b/tests/nexus/test_temporal_system_nexus.py index c7d9319ca..532cd4974 100644 --- a/tests/nexus/test_temporal_system_nexus.py +++ b/tests/nexus/test_temporal_system_nexus.py @@ -147,12 +147,13 @@ def _build_proto_sample(message_type: type[Message]) -> Message: def _populate_proto_sample(message: Message, *, path: str = "value") -> None: seen_oneofs: set[str] = set() - for field in message.DESCRIPTOR.fields: + for raw_field in message.DESCRIPTOR.fields: + field = cast(FieldDescriptor, raw_field) if field.containing_oneof is not None: if field.containing_oneof.name in seen_oneofs: continue seen_oneofs.add(field.containing_oneof.name) - if field.label == FieldDescriptor.LABEL_REPEATED: + if _field_is_repeated(field): if ( field.message_type is not None and field.message_type.GetOptions().map_entry @@ -186,8 +187,10 @@ def _populate_proto_map_entry( *, path: str, ) -> None: - key_field = field.message_type.fields_by_name["key"] - value_field = field.message_type.fields_by_name["value"] + message_type = field.message_type + assert message_type is not None + key_field = message_type.fields_by_name["key"] + value_field = message_type.fields_by_name["value"] key = _proto_scalar_sample(key_field, path=f"{path}.{field.name}.key") container = getattr(message, field.name) if value_field.cpp_type == FieldDescriptor.CPPTYPE_MESSAGE: @@ -222,13 +225,25 @@ def _proto_scalar_sample(field: FieldDescriptor, *, path: str) -> Any: ): return 1.5 if field.cpp_type == FieldDescriptor.CPPTYPE_ENUM: - for enum_value in field.enum_type.values: + enum_type = field.enum_type + assert enum_type is not None + for enum_value in enum_type.values: if enum_value.number != 0: return enum_value.number - return field.enum_type.values[0].number + return enum_type.values[0].number raise TypeError(f"Unhandled proto scalar sample at {path}: {field!r}") +def _field_is_repeated(field: FieldDescriptor) -> bool: + return bool( + getattr( + field, + "is_repeated", + getattr(field, "label") == FieldDescriptor.LABEL_REPEATED, + ) + ) + + @pytest.mark.parametrize( "message_type", [ diff --git a/tests/worker/test_command_aware_visitor.py b/tests/worker/test_command_aware_visitor.py index f354c8614..6e7da7963 100644 --- a/tests/worker/test_command_aware_visitor.py +++ b/tests/worker/test_command_aware_visitor.py @@ -78,11 +78,11 @@ def _get_workflow_command_protos_with_seq() -> Iterator[type[Any]]: """Get concrete classes of all workflow command protos with a seq field.""" for descriptor in workflow_commands_pb2.DESCRIPTOR.message_types_by_name.values(): if "seq" in descriptor.fields_by_name: - yield descriptor._concrete_class + yield getattr(descriptor, "_concrete_class") def _get_workflow_activation_job_protos_with_seq() -> Iterator[type[Any]]: """Get concrete classes of all workflow activation job protos with a seq field.""" for descriptor in workflow_activation_pb2.DESCRIPTOR.message_types_by_name.values(): if "seq" in descriptor.fields_by_name: - yield descriptor._concrete_class + yield getattr(descriptor, "_concrete_class") diff --git a/uv.lock b/uv.lock index 3e48e3df7..04fdec20e 100644 --- a/uv.lock +++ b/uv.lock @@ -9,7 +9,7 @@ resolution-markers = [ ] [options] -exclude-newer = "2026-06-01T18:36:48.998335583Z" +exclude-newer = "2026-06-04T17:08:08.645499Z" exclude-newer-span = "P2W" [options.exclude-newer-package] @@ -1812,14 +1812,14 @@ wheels = [ [[package]] name = "googleapis-common-protos" -version = "1.70.0" +version = "1.75.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/39/24/33db22342cf4a2ea27c9955e6713140fedd51e8b141b5ce5260897020f1a/googleapis_common_protos-1.70.0.tar.gz", hash = "sha256:0e1b44e0ea153e6594f9f394fef15193a68aaaea2d843f83e2742717ca753257", size = 145903, upload-time = "2025-04-14T10:17:02.924Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/c8/f439cffde755cffa462bfbb156278fa6f9d09119719af9814b858fd4f81f/googleapis_common_protos-1.75.0.tar.gz", hash = "sha256:53a062ff3c32552fbd62c11fe23768b78e4ddf0494d5e5fd97d3f4689c75fbbd", size = 151035, upload-time = "2026-05-07T08:04:49.423Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/86/f1/62a193f0227cf15a920390abe675f386dec35f7ae3ffe6da582d3ade42c7/googleapis_common_protos-1.70.0-py3-none-any.whl", hash = "sha256:b8bfcca8c25a2bb253e0e0b0adaf8c00773e5e6af6fd92397576680b807e0fd8", size = 294530, upload-time = "2025-04-14T10:17:01.271Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl", hash = "sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed", size = 300631, upload-time = "2026-05-07T08:03:30.345Z" }, ] [package.optional-dependencies] @@ -5520,12 +5520,12 @@ requires-dist = [ { name = "opentelemetry-sdk", marker = "extra == 'opentelemetry'", specifier = ">=1.11.1,<2" }, { name = "opentelemetry-sdk-extension-aws", marker = "extra == 'lambda-worker-otel'", specifier = ">=2.0.0,<3" }, { name = "opentelemetry-semantic-conventions", marker = "extra == 'lambda-worker-otel'", specifier = ">=0.40b0,<1" }, - { name = "protobuf", specifier = ">=3.20,<7.0.0" }, + { name = "protobuf", specifier = ">=3.20,<8.0.0" }, { name = "pydantic", marker = "extra == 'pydantic'", specifier = ">=2.0.0,<3" }, { name = "python-dateutil", marker = "python_full_version < '3.11'", specifier = ">=2.8.2,<3" }, { name = "strands-agents", marker = "extra == 'strands-agents'", specifier = ">=1.39.0" }, { name = "types-aioboto3", extras = ["s3"], marker = "extra == 'aioboto3'", specifier = ">=10.4.0" }, - { name = "types-protobuf", specifier = ">=3.20,<7.0.0" }, + { name = "types-protobuf", specifier = ">=3.20,<8.0.0" }, { name = "typing-extensions", specifier = ">=4.2.0,<5" }, ] provides-extras = ["grpc", "opentelemetry", "pydantic", "openai-agents", "google-adk", "langgraph", "langsmith", "lambda-worker-otel", "aioboto3", "strands-agents"] @@ -5535,7 +5535,7 @@ dev = [ { name = "async-timeout", marker = "python_full_version < '3.11'", specifier = ">=4.0,<6" }, { name = "basedpyright", specifier = "==1.34.0" }, { name = "cibuildwheel", specifier = ">=2.22.0,<3" }, - { name = "googleapis-common-protos", specifier = "==1.70.0" }, + { name = "googleapis-common-protos", specifier = ">=1.75.0,<2" }, { name = "grpcio-tools", specifier = ">=1.48.2,<2" }, { name = "httpx", specifier = ">=0.28.1" }, { name = "langgraph", specifier = ">=1.1.0" }, @@ -5856,11 +5856,11 @@ wheels = [ [[package]] name = "types-protobuf" -version = "6.32.1.20260221" +version = "7.34.1.20260518" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5f/e2/9aa4a3b2469508bd7b4e2ae11cbedaf419222a09a1b94daffcd5efca4023/types_protobuf-6.32.1.20260221.tar.gz", hash = "sha256:6d5fb060a616bfb076cbb61b4b3c3969f5fc8bec5810f9a2f7e648ee5cbcbf6e", size = 64408, upload-time = "2026-02-21T03:55:13.916Z" } +sdist = { url = "https://files.pythonhosted.org/packages/29/59/e2b13b499d15e6720150c4b1a8d91e31fcacf716b432397475b3151ff7e4/types_protobuf-7.34.1.20260518.tar.gz", hash = "sha256:28cfaded25889cb83ebfb63cfb0a43628f0b6f3785767bec17287dc6468795f2", size = 68936, upload-time = "2026-05-18T06:01:47.332Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/e8/1fd38926f9cf031188fbc5a96694203ea6f24b0e34bd64a225ec6f6291ba/types_protobuf-6.32.1.20260221-py3-none-any.whl", hash = "sha256:da7cdd947975964a93c30bfbcc2c6841ee646b318d3816b033adc2c4eb6448e4", size = 77956, upload-time = "2026-02-21T03:55:12.894Z" }, + { url = "https://files.pythonhosted.org/packages/2a/1f/ec5caf72c2e3b688ca3927e0979a04ddad19e1afc4bf1c199bd743e0f419/types_protobuf-7.34.1.20260518-py3-none-any.whl", hash = "sha256:a0a5337413347166439c0e07cbc26c6164d091401c6f01b1dfd8cdb966c4dd8f", size = 85992, upload-time = "2026-05-18T06:01:45.696Z" }, ] [[package]] From 4d4998eebbaf8264b1e52c61d12b2f7cc75d68d1 Mon Sep 17 00:00:00 2001 From: Tim Conley Date: Thu, 18 Jun 2026 12:17:53 -0700 Subject: [PATCH 2/2] Fix changelog placement --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc6e34c44..e76cfe412 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,8 @@ to docs, or any other relevant information. - AWS Lambda worker `configure` parameter supports sync, async, and async generator style functions. This callback is invoked on the asyncio event loop. +- Relaxed the protobuf dependency bounds to allow protobuf 7 where compatible + with the selected optional dependencies. ### Breaking Changes @@ -31,8 +33,6 @@ to docs, or any other relevant information. per-invocation of the worker instead of only at startup. It is advised that any shared, heavy-weight operations are performed outside of the callback before `run_worker` is invoked. -- Relaxed the protobuf dependency bounds to allow protobuf 7 where compatible - with the selected optional dependencies. ## [1.29.0] - 2026-06-17