Skip to content

Commit 81e17c0

Browse files
authored
More tests, docs, and minor things (#41)
1 parent 962a5e8 commit 81e17c0

15 files changed

+1001
-195
lines changed

.github/workflows/ci.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ jobs:
3636
with:
3737
go-version: "1.17"
3838
- run: python -m pip install --upgrade wheel poetry poethepoet
39-
- run: poetry install
39+
- run: poetry install --no-root
4040
- run: poe lint
4141
- run: poe build-develop
4242
- run: poe test -s -o log_cli_level=DEBUG
@@ -90,7 +90,7 @@ jobs:
9090
with:
9191
go-version: "1.17"
9292
- run: python -m pip install --upgrade wheel poetry poethepoet
93-
- run: poetry install
93+
- run: poetry install --no-root
9494
- run: poe gen-protos
9595
- run: poetry build
9696
- run: poe fix-wheel

.gitmodules

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
[submodule "sdk-core"]
22
path = temporalio/bridge/sdk-core
3-
url = git@github.com:temporalio/sdk-core.git
3+
url = https://github.com/temporalio/sdk-core.git

README.md

+170-28
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,36 @@ execute asynchronous long-running business logic in a scalable and resilient way
99

1010
"Temporal Python SDK" is the framework for authoring workflows and activities using the Python programming language.
1111

12-
In addition to this documentation, see the [samples](https://github.com/temporalio/samples-python) repository for code
13-
examples.
12+
Also see:
13+
14+
* [Code Samples](https://github.com/temporalio/samples-python)
15+
* [API Documentation](https://python.temporal.io)
16+
17+
In addition to features common across all Temporal SDKs, the Python SDK also has the following interesting features:
18+
19+
**Type Safe**
20+
21+
This library uses the latest typing and MyPy support with generics to ensure all calls can be typed. For example,
22+
starting a workflow with an `int` parameter when it accepts a `str` parameter would cause MyPy to fail.
23+
24+
**Different Activity Types**
25+
26+
The activity worker has been developed to work with `async def`, threaded, and multiprocess activities. While
27+
`async def` activities are the easiest and recommended, care has been taken to make heartbeating and cancellation also
28+
work across threads/processes.
29+
30+
**Custom `asyncio` Event Loop**
31+
32+
The workflow implementation basically turns `async def` functions into workflows backed by a distributed, fault-tolerant
33+
event loop. This means task management, sleep, cancellation, etc have all been developed to seamlessly integrate with
34+
`asyncio` concepts.
1435

1536
**⚠️ UNDER DEVELOPMENT**
1637

1738
The Python SDK is under development. There are no compatibility guarantees nor proper documentation pages at this time.
1839

1940
Currently missing features:
2041

21-
* Async activity support (in client or worker)
2242
* Support for Windows arm, macOS arm (i.e. M1), Linux arm, and Linux x64 glibc < 2.31.
2343
* Full documentation
2444

@@ -35,7 +55,7 @@ These steps can be followed to use with a virtual environment and `pip`:
3555
* Needed because older versions of `pip` may not pick the right wheel
3656
* Install Temporal SDK - `python -m pip install temporalio`
3757

38-
The SDK is now ready for use.
58+
The SDK is now ready for use. To build from source, see "Building" near the end of this documentation.
3959

4060
### Implementing a Workflow
4161

@@ -142,6 +162,15 @@ Some things to note about the above code:
142162
* Clients can have many more options not shown here (e.g. data converters and interceptors)
143163
* A string can be used instead of the method reference to call a workflow by name (e.g. if defined in another language)
144164

165+
Clients also provide a shallow copy of their config for use in making slightly different clients backed by the same
166+
connection. For instance, given the `client` above, this is how to have a client in another namespace:
167+
168+
```python
169+
config = client.config()
170+
config["namespace"] = "my-other-namespace"
171+
other_ns_client = Client(**config)
172+
```
173+
145174
#### Data Conversion
146175

147176
Data converters are used to convert raw Temporal payloads to/from actual Python types. A custom data converter of type
@@ -393,6 +422,16 @@ protect against cancellation. The following tasks, when cancelled, perform a Tem
393422
When the workflow itself is requested to cancel, `Task.cancel` is called on the main workflow task. Therefore,
394423
`asyncio.CancelledError` can be caught in order to handle the cancel gracefully.
395424

425+
Workflows follow `asyncio` cancellation rules exactly which can cause confusion among Python developers. Cancelling a
426+
task doesn't always cancel the thing it created. For example, given
427+
`task = asyncio.create_task(workflow.start_child_workflow(...`, calling `task.cancel` does not cancel the child
428+
workflow, it only cancels the starting of it, which has no effect if it has already started. However, cancelling the
429+
result of `handle = await workflow.start_child_workflow(...` or
430+
`task = asyncio.create_task(workflow.execute_child_workflow(...` _does_ cancel the child workflow.
431+
432+
Also, due to Temporal rules, a cancellation request is a state not an event. Therefore, repeated cancellation requests
433+
are not delivered, only the first. If the workflow chooses swallow a cancellation, it cannot be requested again.
434+
396435
#### Workflow Utilities
397436

398437
While running in a workflow, in addition to features documented elsewhere, the following items are available from the
@@ -405,11 +444,15 @@ While running in a workflow, in addition to features documented elsewhere, the f
405444

406445
#### Exceptions
407446

408-
TODO
447+
* Workflows can raise exceptions to fail the workflow
448+
* Using `temporalio.exceptions.ApplicationError`, exceptions can be marked as non-retryable or include details
409449

410450
#### External Workflows
411451

412-
TODO
452+
* `workflow.get_external_workflow_handle()` inside a workflow returns a handle to interact with another workflow
453+
* `workflow.get_external_workflow_handle_for()` can be used instead for a type safe handle
454+
* `await handle.signal()` can be called on the handle to signal the external workflow
455+
* `await handle.cancel()` can be called on the handle to send a cancel to the external workflow
413456

414457
### Activities
415458

@@ -527,38 +570,137 @@ respect cancellation, the shutdown may never complete.
527570
The Python SDK is built to work with Python 3.7 and newer. It is built using
528571
[SDK Core](https://github.com/temporalio/sdk-core/) which is written in Rust.
529572

530-
### Local development environment
573+
### Building
574+
575+
#### Prepare
576+
577+
To build the SDK from source for use as a dependency, the following prerequisites are required:
578+
579+
* [Python](https://www.python.org/) >= 3.7
580+
* [Rust](https://www.rust-lang.org/)
581+
* [poetry](https://github.com/python-poetry/poetry) (e.g. `python -m pip install poetry`)
582+
* [poe](https://github.com/nat-n/poethepoet) (e.g. `python -m pip install poethepoet`)
583+
584+
With the prerequisites installed, first clone the SDK repository recursively:
585+
586+
```bash
587+
git clone --recursive https://github.com/temporalio/sdk-python.git
588+
cd sdk-python
589+
```
590+
591+
Use `poetry` to install the dependencies with `--no-root` to not install this package (because we still need to build
592+
it):
593+
594+
```bash
595+
poetry install --no-root
596+
```
597+
598+
Now generate the protobuf code:
599+
600+
```bash
601+
poe gen-protos
602+
```
603+
604+
#### Build
605+
606+
Now perform the release build:
607+
608+
```bash
609+
poetry build
610+
```
611+
612+
This will take a while because Rust will compile the core project in release mode (see "Local SDK development
613+
environment" for the quicker approach to local development).
614+
615+
The compiled wheel doesn't have the exact right tags yet for use, so run this script to fix it:
616+
617+
```bash
618+
poe fix-wheel
619+
```
620+
621+
The `whl` wheel file in `dist/` is now ready to use.
622+
623+
#### Use
624+
625+
The wheel can now be installed into any virtual environment.
626+
627+
For example,
628+
[create a virtual environment](https://packaging.python.org/en/latest/tutorials/installing-packages/#creating-virtual-environments)
629+
somewhere and then run the following inside the virtual environment:
630+
631+
```bash
632+
pip install /path/to/cloned/sdk-python/dist/*.whl
633+
```
634+
635+
Create this Python file at `example.py`:
636+
637+
```python
638+
import asyncio
639+
from temporalio import workflow, activity
640+
from temporalio.client import Client
641+
from temporalio.worker import Worker
642+
643+
@workflow.defn
644+
class SayHello:
645+
@workflow.run
646+
async def run(self, name: str) -> str:
647+
return f"Hello, {name}!"
648+
649+
async def main():
650+
client = await Client.connect("http://localhost:7233")
651+
async with Worker(client, task_queue="my-task-queue", workflows=[SayHello]):
652+
result = await client.execute_workflow(SayHello.run, "Temporal",
653+
id="my-workflow-id", task_queue="my-task-queue")
654+
print(f"Result: {result}")
531655

532-
- Install the system dependencies:
656+
if __name__ == "__main__":
657+
asyncio.run(main())
658+
```
533659

534-
- Python >=3.7
535-
- [pipx](https://github.com/pypa/pipx#install-pipx) (only needed for installing the two dependencies below)
536-
- [poetry](https://github.com/python-poetry/poetry) `pipx install poetry`
537-
- [poe](https://github.com/nat-n/poethepoet) `pipx install poethepoet`
660+
Assuming there is a [local Temporal server](https://docs.temporal.io/docs/server/quick-install/) running, executing the
661+
file with `python` (or `python3` if necessary) will give:
538662

539-
- Use a local virtual env environment (helps IDEs and Windows):
663+
Result: Hello, Temporal!
540664

541-
```bash
542-
poetry config virtualenvs.in-project true
543-
```
665+
### Local SDK development environment
544666

545-
- Install the package dependencies (requires Rust):
667+
For local development, it is often quicker to use debug builds and a local virtual environment.
546668

547-
```bash
548-
poetry install
549-
```
669+
While not required, it often helps IDEs if we put the virtual environment `.venv` directory in the project itself. This
670+
can be configured system-wide via:
550671

551-
- Build the project (requires Rust):
672+
```bash
673+
poetry config virtualenvs.in-project true
674+
```
552675

553-
```bash
554-
poe build-develop
555-
```
676+
Now perform the same steps as the "Prepare" section above by installing the prerequisites, cloning the project,
677+
installing dependencies, and generating the protobuf code:
556678

557-
- Run the tests (requires Go):
679+
```bash
680+
git clone --recursive https://github.com/temporalio/sdk-python.git
681+
cd sdk-python
682+
poetry install --no-root
683+
poe gen-protos
684+
```
558685

559-
```bash
560-
poe test
561-
```
686+
Now compile the Rust extension in develop mode which is quicker than release mode:
687+
688+
```bash
689+
poe build-develop
690+
```
691+
692+
That step can be repeated for any Rust changes made.
693+
694+
The environment is now ready to develop in.
695+
696+
#### Testing
697+
698+
Tests currently require [Go](https://go.dev/) to be installed since they use an embedded Temporal server as a library.
699+
With `Go` installed, run the following to execute tests:
700+
701+
```bash
702+
poe test
703+
```
562704

563705
### Style
564706

temporalio/common.py

+39
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Common code used in the Temporal SDK."""
22

3+
from __future__ import annotations
4+
35
from dataclasses import dataclass
46
from datetime import timedelta
57
from enum import IntEnum
@@ -35,8 +37,26 @@ class RetryPolicy:
3537
non_retryable_error_types: Optional[Iterable[str]] = None
3638
"""List of error types that are not retryable."""
3739

40+
@staticmethod
41+
def from_proto(proto: temporalio.api.common.v1.RetryPolicy) -> RetryPolicy:
42+
"""Create a retry policy from the proto object."""
43+
return RetryPolicy(
44+
initial_interval=proto.initial_interval.ToTimedelta(),
45+
backoff_coefficient=proto.backoff_coefficient,
46+
maximum_interval=proto.maximum_interval.ToTimedelta()
47+
if proto.HasField("maximum_interval")
48+
else None,
49+
maximum_attempts=proto.maximum_attempts,
50+
non_retryable_error_types=proto.non_retryable_error_types
51+
if proto.non_retryable_error_types
52+
else None,
53+
)
54+
3855
def apply_to_proto(self, proto: temporalio.api.common.v1.RetryPolicy) -> None:
3956
"""Apply the fields in this policy to the given proto object."""
57+
# Do validation before converting
58+
self._validate()
59+
# Convert
4060
proto.initial_interval.FromTimedelta(self.initial_interval)
4161
proto.backoff_coefficient = self.backoff_coefficient
4262
proto.maximum_interval.FromTimedelta(
@@ -46,6 +66,25 @@ def apply_to_proto(self, proto: temporalio.api.common.v1.RetryPolicy) -> None:
4666
if self.non_retryable_error_types:
4767
proto.non_retryable_error_types.extend(self.non_retryable_error_types)
4868

69+
def _validate(self) -> None:
70+
# Validation taken from Go SDK's test suite
71+
if self.maximum_attempts == 1:
72+
# Ignore other validation if disabling retries
73+
return
74+
if self.initial_interval.total_seconds() < 0:
75+
raise ValueError("Initial interval cannot be negative")
76+
if self.backoff_coefficient < 1:
77+
raise ValueError("Backoff coefficient cannot be less than 1")
78+
if self.maximum_interval:
79+
if self.maximum_interval.total_seconds() < 0:
80+
raise ValueError("Maximum interval cannot be negative")
81+
if self.maximum_interval < self.initial_interval:
82+
raise ValueError(
83+
"Maximum interval cannot be less than initial interval"
84+
)
85+
if self.maximum_attempts < 0:
86+
raise ValueError("Maximum attempts cannot be negative")
87+
4988

5089
class WorkflowIDReusePolicy(IntEnum):
5190
"""How already-in-use workflow IDs are handled on start.

temporalio/worker/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@
77
ContinueAsNewInput,
88
ExecuteActivityInput,
99
ExecuteWorkflowInput,
10+
GetExternalWorkflowHandleInput,
1011
HandleQueryInput,
1112
HandleSignalInput,
1213
Interceptor,
1314
StartActivityInput,
1415
StartChildWorkflowInput,
1516
StartLocalActivityInput,
1617
WorkflowInboundInterceptor,
18+
WorkflowOutboundInterceptor,
1719
)
1820
from .worker import Worker, WorkerConfig
1921
from .workflow_instance import (
@@ -42,6 +44,7 @@
4244
"StartActivityInput",
4345
"StartChildWorkflowInput",
4446
"StartLocalActivityInput",
47+
"GetExternalWorkflowHandleInput",
4548
# Advanced activity classes
4649
"SharedStateManager",
4750
"SharedHeartbeatSender",

0 commit comments

Comments
 (0)