Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions docs/modules/temporal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Temporal

## Introduction

The Testcontainers module for [Temporal](https://temporal.io/) — a durable execution platform for running reliable, long-running workflows.

This module spins up the Temporal dev server (`temporalio/temporal`) which includes the Temporal server, a preconfigured `default` namespace, and the Web UI.

## Adding this module to your project dependencies

Please run the following command to add the Temporal module to your python dependencies:

```bash
pip install testcontainers[temporal]
```

To interact with the server you will also need a Temporal SDK, for example:

```bash
pip install temporalio
```

## Usage example

<!--codeinclude-->

[Creating a Temporal container](../../modules/temporal/example_basic.py)

<!--/codeinclude-->
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ nav:
- modules/registry.md
- modules/selenium.md
- modules/sftp.md
- modules/temporal.md
- modules/test_module_import.md
- modules/vault.md
- System Requirements:
Expand Down
2 changes: 2 additions & 0 deletions modules/temporal/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.. autoclass:: testcontainers.temporal.TemporalContainer
.. title:: testcontainers.temporal.TemporalContainer
40 changes: 40 additions & 0 deletions modules/temporal/example_basic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import asyncio
from datetime import timedelta

from temporalio.api.workflowservice.v1 import ListNamespacesRequest
from temporalio.client import Client

from testcontainers.temporal import TemporalContainer


async def main():
with TemporalContainer() as temporal:
print(f"Temporal gRPC address: {temporal.get_grpc_address()}")
print(f"Temporal Web UI: {temporal.get_web_ui_url()}")

# Connect a Temporal client
client = await Client.connect(temporal.get_grpc_address())

# List available namespaces
resp = await client.service_client.workflow_service.list_namespaces(ListNamespacesRequest())
for ns in resp.namespaces:
print(f"Namespace: {ns.namespace_info.name}")

# Start a workflow (untyped — no workflow definition class needed)
handle = await client.start_workflow(
"GreetingWorkflow",
id="greeting-wf-1",
task_queue="greeting-queue",
execution_timeout=timedelta(seconds=10),
memo={"env": "example"},
)
print(f"Started workflow: {handle.id}")

# Describe the workflow
desc = await handle.describe()
print(f"Workflow type: {desc.workflow_type}")
print(f"Task queue: {desc.task_queue}")


if __name__ == "__main__":
asyncio.run(main())
54 changes: 54 additions & 0 deletions modules/temporal/testcontainers/temporal/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import urllib.error
import urllib.parse
import urllib.request

from testcontainers.core.container import DockerContainer
from testcontainers.core.waiting_utils import wait_container_is_ready


class TemporalContainer(DockerContainer):
"""Temporal dev server container for integration testing.

Example:

The example spins up a Temporal dev server and connects to it using the
``temporalio`` Python SDK.

.. doctest::

>>> from testcontainers.temporal import TemporalContainer
>>> with TemporalContainer() as temporal:
... address = temporal.get_grpc_address()
"""

GRPC_PORT = 7233
HTTP_PORT = 8233

def __init__(self, image: str = "temporalio/temporal:1.5.1", **kwargs) -> None:
super().__init__(image, **kwargs)
self.with_exposed_ports(self.GRPC_PORT, self.HTTP_PORT)
self.with_command("server start-dev --ip 0.0.0.0")

@wait_container_is_ready(urllib.error.URLError, ConnectionError)
def _healthcheck(self) -> None:
host = self.get_container_host_ip()
port = self.get_exposed_port(self.HTTP_PORT)
url = urllib.parse.urlunsplit(("http", f"{host}:{port}", "/api/v1/namespaces", "", ""))
urllib.request.urlopen(url, timeout=1)

def start(self) -> "TemporalContainer":
super().start()
self._healthcheck()
return self

def get_grpc_address(self) -> str:
"""Returns ``host:port`` for the Temporal gRPC frontend.

The address intentionally omits a scheme because the Temporal SDKs
expect a plain ``host:port`` string.
"""
return f"{self.get_container_host_ip()}:{self.get_exposed_port(self.GRPC_PORT)}"

def get_web_ui_url(self) -> str:
"""Returns the base URL for the Temporal Web UI / HTTP API."""
return f"http://{self.get_container_host_ip()}:{self.get_exposed_port(self.HTTP_PORT)}"
42 changes: 42 additions & 0 deletions modules/temporal/tests/test_temporal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from datetime import timedelta
from uuid import uuid4

import pytest
from temporalio.api.workflowservice.v1 import ListNamespacesRequest
from temporalio.client import Client

from testcontainers.temporal import TemporalContainer


@pytest.fixture(scope="module")
def temporal_container():
with TemporalContainer() as container:
yield container


@pytest.mark.asyncio
async def test_default_namespace_exists(temporal_container):
client = await Client.connect(temporal_container.get_grpc_address())
resp = await client.service_client.workflow_service.list_namespaces(ListNamespacesRequest())
names = [ns.namespace_info.name for ns in resp.namespaces]
assert "default" in names


@pytest.mark.asyncio
async def test_start_and_describe_workflow(temporal_container):
client = await Client.connect(temporal_container.get_grpc_address())
workflow_id = str(uuid4())

handle = await client.start_workflow(
"MyWorkflow",
id=workflow_id,
task_queue="my-task-queue",
execution_timeout=timedelta(seconds=10),
memo={"env": "test"},
)
desc = await handle.describe()
assert desc.id == workflow_id
assert desc.workflow_type == "MyWorkflow"
assert desc.task_queue == "my-task-queue"
memo = await desc.memo()
assert memo is not None
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ registry = ["bcrypt>=5"]
selenium = ["selenium>=4"]
scylla = ["cassandra-driver>=3"]
sftp = ["cryptography"]
temporal = []
vault = []
weaviate = ["weaviate-client>=4"]
chroma = ["chromadb-client>=1"]
Expand Down Expand Up @@ -215,6 +216,7 @@ packages = [
"modules/registry/testcontainers",
"modules/sftp/testcontainers",
"modules/selenium/testcontainers",
"modules/temporal/testcontainers",
"modules/scylla/testcontainers",
"modules/trino/testcontainers",
"modules/vault/testcontainers",
Expand Down Expand Up @@ -264,6 +266,7 @@ dev-mode-dirs = [
"modules/registry",
"modules/sftp",
"modules/selenium",
"modules/temporal",
"modules/scylla",
"modules/trino",
"modules/vault",
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.