Skip to content

Commit 9f31e8d

Browse files
authored
Merge branch 'main' into plugins
2 parents a5da91d + e6f6f91 commit 9f31e8d

File tree

83 files changed

+3578
-1532
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

83 files changed

+3578
-1532
lines changed

.github/workflows/build-binaries.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,8 @@ jobs:
7474
with:
7575
name: packages-${{ matrix.package-suffix }}
7676
path: dist
77+
78+
- name: Deliberately fail to prevent releasing nexus-rpc w/ GitHub link in pyproject.toml
79+
run: |
80+
echo "This is a deliberate failure to prevent releasing nexus-rpc with a GitHub link in pyproject.toml"
81+
exit 1

README.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ informal introduction to the features and their implementation.
9494
- [Heartbeating and Cancellation](#heartbeating-and-cancellation)
9595
- [Worker Shutdown](#worker-shutdown)
9696
- [Testing](#testing-1)
97+
- [Interceptors](#interceptors)
9798
- [Nexus](#nexus)
9899
- [Plugins](#plugins)
99100
- [Client Plugins](#client-plugins)
@@ -1259,6 +1260,7 @@ calls in the `temporalio.activity` package make use of it. Specifically:
12591260

12601261
* `in_activity()` - Whether an activity context is present
12611262
* `info()` - Returns the immutable info of the currently running activity
1263+
* `client()` - Returns the Temporal client used by this worker. Only available in `async def` activities.
12621264
* `heartbeat(*details)` - Record a heartbeat
12631265
* `is_cancelled()` - Whether a cancellation has been requested on this activity
12641266
* `wait_for_cancelled()` - `async` call to wait for cancellation request
@@ -1313,6 +1315,70 @@ affect calls activity code might make to functions on the `temporalio.activity`
13131315
* `worker_shutdown()` can be invoked to simulate a worker shutdown during execution of the activity
13141316

13151317

1318+
### Interceptors
1319+
1320+
The behavior of the SDK can be customized in many useful ways by modifying inbound and outbound calls using
1321+
interceptors. This is similar to the use of middleware in other frameworks.
1322+
1323+
There are five categories of inbound and outbound calls that you can modify in this way:
1324+
1325+
1. Outbound client calls, such as `start_workflow()`, `signal_workflow()`, `list_workflows()`, `update_schedule()`, etc.
1326+
1327+
2. Inbound workflow calls: `execute_workflow()`, `handle_signal()`, `handle_update_handler()`, etc
1328+
1329+
3. Outbound workflow calls: `start_activity()`, `start_child_workflow()`, `start_nexus_operation()`, etc
1330+
1331+
4. Inbound call to execute an activity: `execute_activity()`
1332+
1333+
5. Outbound activity calls: `info()` and `heartbeat()`
1334+
1335+
1336+
To modify outbound client calls, define a class inheriting from
1337+
[`client.Interceptor`](https://python.temporal.io/temporalio.client.Interceptor.html), and implement the method
1338+
`intercept_client()` to return an instance of
1339+
[`OutboundInterceptor`](https://python.temporal.io/temporalio.client.OutboundInterceptor.html) that implements the
1340+
subset of outbound client calls that you wish to modify.
1341+
1342+
Then, pass a list containing an instance of your `client.Interceptor` class as the
1343+
`interceptors` argument of [`Client.connect()`](https://python.temporal.io/temporalio.client.Client.html#connect).
1344+
1345+
The purpose of the interceptor framework is that the methods you implement on your interceptor classes can perform
1346+
arbitrary side effects and/or arbitrary modifications to the data, before it is received by the SDK's "real"
1347+
implementation. The `interceptors` list can contain multiple interceptors. In this case they form a chain: a method
1348+
implemented on an interceptor instance in the list can perform side effects, and modify the data, before passing it on
1349+
to the corresponding method on the next interceptor in the list. Your interceptor classes need not implement every
1350+
method; the default implementation is always to pass the data on to the next method in the interceptor chain.
1351+
1352+
The remaining four categories are worker calls. To modify these, define a class inheriting from
1353+
[`worker.Interceptor`](https://python.temporal.io/temporalio.worker.Interceptor.html) and implement methods on that
1354+
class to define the
1355+
[`ActivityInboundInterceptor`](https://python.temporal.io/temporalio.worker.ActivityInboundInterceptor.html),
1356+
[`ActivityOutboundInterceptor`](https://python.temporal.io/temporalio.worker.ActivityOutboundInterceptor.html),
1357+
[`WorkflowInboundInterceptor`](https://python.temporal.io/temporalio.worker.WorkflowInboundInterceptor.html), and
1358+
[`WorkflowOutboundInterceptor`](https://python.temporal.io/temporalio.worker.WorkflowOutboundInterceptor.html) classes
1359+
that you wish to use to effect your modifications. Then, pass a list containing an instance of your `worker.Interceptor`
1360+
class as the `interceptors` argument of the [`Worker()`](https://python.temporal.io/temporalio.worker.Worker.html)
1361+
constructor.
1362+
1363+
It often happens that your worker and client interceptors will share code because they implement closely related logic.
1364+
For convenience, you can create an interceptor class that inherits from _both_ `client.Interceptor` and
1365+
`worker.Interceptor` (their method sets do not overlap). You can then pass this in the `interceptors` argument of
1366+
`Client.connect()` when starting your worker _as well as_ in your client/starter code. If you do this, your worker will
1367+
automatically pick up the interceptors from its underlying client (and you should not pass them directly to the
1368+
`Worker()` constructor).
1369+
1370+
This is best explained by example. The [Context Propagation Interceptor
1371+
Sample](https://github.com/temporalio/samples-python/tree/main/context_propagation) is a good starting point. In
1372+
[context_propagation/interceptor.py](https://github.com/temporalio/samples-python/blob/main/context_propagation/interceptor.py)
1373+
a class is defined that inherits from both `client.Interceptor` and `worker.Interceptor`. It implements the various
1374+
methods such that the outbound client and workflow calls set a certain key in the outbound `headers` field, and the
1375+
inbound workflow and activity calls retrieve the header value from the inbound workflow/activity input data. An instance
1376+
of this interceptor class is passed to `Client.connect()` when [starting the
1377+
worker](https://github.com/temporalio/samples-python/blob/main/context_propagation/worker.py) and when connecting the
1378+
client in the [workflow starter
1379+
code](https://github.com/temporalio/samples-python/blob/main/context_propagation/starter.py).
1380+
1381+
13161382
### Nexus
13171383

13181384
⚠️ **Nexus support is currently at an experimental release stage. Backwards-incompatible changes are anticipated until a stable release is announced.** ⚠️

pyproject.toml

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
[project]
22
name = "temporalio"
3-
version = "1.14.0"
3+
version = "1.14.1"
44
description = "Temporal.io Python SDK"
55
authors = [{ name = "Temporal Technologies Inc", email = "[email protected]" }]
6-
requires-python = "~=3.9"
6+
requires-python = ">=3.9"
77
readme = "README.md"
88
license = { file = "LICENSE" }
99
keywords = [
@@ -45,7 +45,7 @@ dev = [
4545
"psutil>=5.9.3,<6",
4646
"pydocstyle>=6.3.0,<7",
4747
"pydoctor>=24.11.1,<25",
48-
"pyright==1.1.402",
48+
"pyright==1.1.403",
4949
"pytest~=7.4",
5050
"pytest-asyncio>=0.21,<0.22",
5151
"pytest-timeout~=2.2",
@@ -69,14 +69,16 @@ lint = [
6969
{cmd = "uv run ruff check --select I"},
7070
{cmd = "uv run ruff format --check"},
7171
{ref = "lint-types"},
72-
{cmd = "uv run pyright"},
7372
{ref = "lint-docs"},
7473
]
7574
bridge-lint = { cmd = "cargo clippy -- -D warnings", cwd = "temporalio/bridge" }
7675
# TODO(cretz): Why does pydocstyle complain about @overload missing docs after
7776
# https://github.com/PyCQA/pydocstyle/pull/511?
7877
lint-docs = "uv run pydocstyle --ignore-decorators=overload"
79-
lint-types = "uv run mypy --namespace-packages --check-untyped-defs ."
78+
lint-types = [
79+
{ cmd = "uv run pyright"},
80+
{ cmd = "uv run mypy --namespace-packages --check-untyped-defs ."},
81+
]
8082
run-bench = "uv run python scripts/run_bench.py"
8183
test = "uv run pytest"
8284

@@ -100,7 +102,7 @@ filterwarnings = [
100102
[tool.cibuildwheel]
101103
before-all = "pip install protoc-wheel-0"
102104
build = "cp39-win_amd64 cp39-manylinux_x86_64 cp39-manylinux_aarch64 cp39-macosx_x86_64 cp39-macosx_arm64"
103-
build-verbosity = "1"
105+
build-verbosity = 1
104106

105107
[tool.cibuildwheel.macos]
106108
environment = { MACOSX_DEPLOYMENT_TARGET = "10.12" }
@@ -158,6 +160,24 @@ project-name = "Temporal Python"
158160
sidebar-expand-depth = 2
159161

160162
[tool.pyright]
163+
enableTypeIgnoreComments = true
164+
reportAny = "none"
165+
reportCallInDefaultInitializer = "none"
166+
reportExplicitAny = "none"
167+
reportIgnoreCommentWithoutRule = "none"
168+
reportImplicitOverride = "none"
169+
reportImplicitStringConcatenation = "none"
170+
reportImportCycles = "none"
171+
reportMissingTypeArgument = "none"
172+
reportPrivateUsage = "none"
173+
reportUnannotatedClassAttribute = "none"
174+
reportUnknownArgumentType = "none"
175+
reportUnknownMemberType = "none"
176+
reportUnknownParameterType = "none"
177+
reportUnknownVariableType = "none"
178+
reportUnnecessaryIsInstance = "none"
179+
reportUnnecessaryTypeIgnoreComment = "none"
180+
reportUnusedCallResult = "none"
161181
include = ["temporalio", "tests"]
162182
exclude = [
163183
"temporalio/api",
@@ -216,3 +236,6 @@ exclude = [
216236
[tool.uv]
217237
# Prevent uv commands from building the package by default
218238
package = false
239+
240+
[tool.uv.sources]
241+
nexus-rpc = { git = "https://github.com/nexus-rpc/sdk-python.git", rev = "35f574c711193a6e2560d3e6665732a5bb7ae92c" }

temporalio/activity.py

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from dataclasses import dataclass
2020
from datetime import datetime, timedelta
2121
from typing import (
22+
TYPE_CHECKING,
2223
Any,
2324
Callable,
2425
Iterator,
@@ -42,6 +43,9 @@
4243

4344
from .types import CallableType
4445

46+
if TYPE_CHECKING:
47+
from temporalio.client import Client
48+
4549

4650
@overload
4751
def defn(fn: CallableType) -> CallableType: ...
@@ -179,6 +183,7 @@ class _Context:
179183
temporalio.converter.PayloadConverter,
180184
]
181185
runtime_metric_meter: Optional[temporalio.common.MetricMeter]
186+
client: Optional[Client]
182187
cancellation_details: _ActivityCancellationDetailsHolder
183188
_logger_details: Optional[Mapping[str, Any]] = None
184189
_payload_converter: Optional[temporalio.converter.PayloadConverter] = None
@@ -271,13 +276,37 @@ def wait_sync(self, timeout: Optional[float] = None) -> None:
271276
self.thread_event.wait(timeout)
272277

273278

279+
def client() -> Client:
280+
"""Return a Temporal Client for use in the current activity.
281+
282+
The client is only available in `async def` activities.
283+
284+
In tests it is not available automatically, but you can pass a client when creating a
285+
:py:class:`temporalio.testing.ActivityEnvironment`.
286+
287+
Returns:
288+
:py:class:`temporalio.client.Client` for use in the current activity.
289+
290+
Raises:
291+
RuntimeError: When the client is not available.
292+
"""
293+
client = _Context.current().client
294+
if not client:
295+
raise RuntimeError(
296+
"No client available. The client is only available in `async def` "
297+
"activities; not in `def` activities. In tests you can pass a "
298+
"client when creating ActivityEnvironment."
299+
)
300+
return client
301+
302+
274303
def in_activity() -> bool:
275304
"""Whether the current code is inside an activity.
276305
277306
Returns:
278307
True if in an activity, False otherwise.
279308
"""
280-
return not _current_context.get(None) is None
309+
return _current_context.get(None) is not None
281310

282311

283312
def info() -> Info:
@@ -574,8 +603,10 @@ def _apply_to_callable(
574603
fn=fn,
575604
# iscoroutinefunction does not return true for async __call__
576605
# TODO(cretz): Why can't MyPy handle this?
577-
is_async=inspect.iscoroutinefunction(fn)
578-
or inspect.iscoroutinefunction(fn.__call__), # type: ignore
606+
is_async=(
607+
inspect.iscoroutinefunction(fn)
608+
or inspect.iscoroutinefunction(fn.__call__) # type: ignore
609+
),
579610
no_thread_cancel_exception=no_thread_cancel_exception,
580611
),
581612
)

temporalio/api/batch/v1/message_pb2.py

Lines changed: 14 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

temporalio/api/batch/v1/message_pb2.pyi

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -219,9 +219,9 @@ class BatchOperationReset(google.protobuf.message.Message):
219219
def options(self) -> temporalio.api.common.v1.message_pb2.ResetOptions:
220220
"""Describes what to reset to and how. If set, `reset_type` and `reset_reapply_type` are ignored."""
221221
reset_type: temporalio.api.enums.v1.reset_pb2.ResetType.ValueType
222-
"""Reset type (deprecated, use `options`)."""
222+
"""Deprecated. Use `options`."""
223223
reset_reapply_type: temporalio.api.enums.v1.reset_pb2.ResetReapplyType.ValueType
224-
"""History event reapply options (deprecated, use `options`)."""
224+
"""Deprecated. Use `options`."""
225225
@property
226226
def post_reset_operations(
227227
self,

0 commit comments

Comments
 (0)