feat(hanzo-tasks): Python SDK for durable task execution

New hanzo-tasks package wrapping temporalio SDK with Hanzo conventions:
- Client: connect, submit, get_result, cancel, signal, query
- Worker: register workflows/activities, poll and execute
- Pre-built workflows: AgentTaskWorkflow, PipelineWorkflow, FanOutWorkflow
- Pluggable agent executor via set_agent_executor()
- 28 tests passing
This commit is contained in:
2026-03-19 10:29:31 -07:00
parent 867bda8090
commit 7a5eba2dad
10 changed files with 836 additions and 0 deletions
+43
View File
@@ -0,0 +1,43 @@
"""
hanzo-tasks — Durable workflow execution for AI agents.
Wraps the Temporal Python SDK with Hanzo conventions for agent task
orchestration, including pre-built workflows for pipelines and fan-out.
Example:
>>> from hanzo_tasks import Client, TasksConfig
>>> client = await Client.connect(TasksConfig(namespace="hanzo"))
>>> handle = await client.submit(AgentTaskWorkflow.run, task_input, queue="agents")
>>> result = await handle.result()
"""
from .activities import execute_agent_task, send_notification, set_agent_executor
from .client import Client, TasksConfig, WorkflowHandle
from .worker import Worker
from .workflows import (
AgentTaskInput,
AgentTaskOutput,
AgentTaskWorkflow,
FanOutWorkflow,
PipelineWorkflow,
)
__version__ = "0.1.0"
__all__ = [
# Client
"Client",
"TasksConfig",
"WorkflowHandle",
# Worker
"Worker",
# Workflows
"AgentTaskWorkflow",
"PipelineWorkflow",
"FanOutWorkflow",
"AgentTaskInput",
"AgentTaskOutput",
# Activities
"execute_agent_task",
"send_notification",
"set_agent_executor",
]
+35
View File
@@ -0,0 +1,35 @@
"""Activity definitions for agent task execution."""
from __future__ import annotations
from typing import Any, Callable
from temporalio import activity
# Activity executor — pluggable. The playground sets this to call the ZAP sidecar.
_agent_executor: Callable[..., Any] | None = None
def set_agent_executor(fn: Callable[..., Any]) -> None:
"""Set the function that executes agent tasks (called by playground)."""
global _agent_executor
_agent_executor = fn
@activity.defn
async def execute_agent_task(input: Any) -> Any:
"""Execute an agent task. Delegates to the registered executor."""
if _agent_executor is None:
raise RuntimeError(
"No agent executor registered. Call set_agent_executor() first."
)
return await _agent_executor(input)
@activity.defn
async def send_notification(input: dict[str, Any]) -> None:
"""Send a notification (webhook, SSE, etc)."""
import httpx
async with httpx.AsyncClient() as client:
await client.post(input.get("url", ""), json=input)
+100
View File
@@ -0,0 +1,100 @@
"""Hanzo Tasks client — wraps Temporal client with Hanzo conventions."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
from uuid import uuid4
from temporalio.client import Client as TemporalClient
@dataclass
class TasksConfig:
"""Configuration for connecting to Temporal."""
address: str = "localhost:7233"
namespace: str = "hanzo"
tls: bool = False
class WorkflowHandle:
"""Handle to a running workflow."""
def __init__(self, handle: Any) -> None:
self._handle = handle
@property
def id(self) -> str:
return self._handle.id
@property
def run_id(self) -> str:
return self._handle.result_run_id
async def result(self, result_type: type | None = None) -> Any:
return await self._handle.result(result_type=result_type)
async def cancel(self) -> None:
await self._handle.cancel()
async def signal(self, name: str, data: Any = None) -> None:
await self._handle.signal(name, data)
class Client:
"""Hanzo Tasks client for submitting and managing workflows."""
def __init__(self, temporal: TemporalClient) -> None:
self._temporal = temporal
@classmethod
async def connect(cls, config: TasksConfig | None = None) -> Client:
"""Connect to Temporal server."""
cfg = config or TasksConfig()
temporal = await TemporalClient.connect(cfg.address, namespace=cfg.namespace)
return cls(temporal)
async def submit(
self,
workflow: Any,
input: Any,
*,
id: str | None = None,
queue: str = "default",
**kwargs: Any,
) -> WorkflowHandle:
"""Submit a workflow for execution."""
handle = await self._temporal.start_workflow(
workflow,
input,
id=id or str(uuid4()),
task_queue=queue,
**kwargs,
)
return WorkflowHandle(handle)
async def get_result(self, workflow_id: str, result_type: type | None = None) -> Any:
"""Get the result of a completed workflow."""
handle = self._temporal.get_workflow_handle(workflow_id)
return await handle.result(result_type=result_type)
async def cancel(self, workflow_id: str) -> None:
"""Cancel a running workflow."""
handle = self._temporal.get_workflow_handle(workflow_id)
await handle.cancel()
async def signal(self, workflow_id: str, signal_name: str, data: Any = None) -> None:
"""Send a signal to a running workflow."""
handle = self._temporal.get_workflow_handle(workflow_id)
await handle.signal(signal_name, data)
async def query(self, workflow_id: str, query_name: str) -> Any:
"""Query a running workflow."""
handle = self._temporal.get_workflow_handle(workflow_id)
return await handle.query(query_name)
@property
def temporal(self) -> TemporalClient:
"""Access the underlying Temporal client."""
return self._temporal
+1
View File
@@ -0,0 +1 @@
+54
View File
@@ -0,0 +1,54 @@
"""Hanzo Tasks worker — polls and executes activities."""
from __future__ import annotations
from typing import Any
from temporalio.worker import Worker as TemporalWorker
class Worker:
"""Hanzo Tasks worker that polls a queue and executes workflows/activities."""
def __init__(
self,
client: Any,
queue: str = "default",
workflows: list[Any] | None = None,
activities: list[Any] | None = None,
) -> None:
self._client = client
self._queue = queue
self._workflows: list[Any] = workflows or []
self._activities: list[Any] = activities or []
self._worker: TemporalWorker | None = None
def register_workflow(self, workflow_cls: Any) -> Any:
"""Register a workflow class. Can be used as a decorator."""
self._workflows.append(workflow_cls)
return workflow_cls
def register_activity(self, activity_fn: Any) -> Any:
"""Register an activity function. Can be used as a decorator."""
self._activities.append(activity_fn)
return activity_fn
async def run(self) -> None:
"""Start the worker. Blocks until shutdown."""
temporal_client = (
self._client.temporal
if hasattr(self._client, "temporal")
else self._client
)
self._worker = TemporalWorker(
temporal_client,
task_queue=self._queue,
workflows=self._workflows,
activities=self._activities,
)
await self._worker.run()
async def shutdown(self) -> None:
"""Gracefully shutdown the worker."""
if self._worker:
await self._worker.shutdown()
+80
View File
@@ -0,0 +1,80 @@
"""Pre-built workflows for agent task orchestration."""
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import timedelta
from temporalio import workflow
from temporalio.common import RetryPolicy
@dataclass
class AgentTaskInput:
"""Input for a single agent task."""
space_id: str
agent_id: str
task_title: str
task_prompt: str
timeout_seconds: int = 3600
max_retries: int = 3
@dataclass
class AgentTaskOutput:
"""Output from an agent task execution."""
result: str = ""
error: str = ""
elapsed_seconds: float = 0.0
@workflow.defn
class AgentTaskWorkflow:
"""Execute a single agent task with retries and timeout."""
@workflow.run
async def run(self, input: AgentTaskInput) -> AgentTaskOutput:
return await workflow.execute_activity(
"execute_agent_task",
input,
start_to_close_timeout=timedelta(seconds=input.timeout_seconds),
retry_policy=RetryPolicy(maximum_attempts=input.max_retries),
)
@workflow.defn
class PipelineWorkflow:
"""Run agent tasks sequentially (pipeline)."""
@workflow.run
async def run(self, tasks: list[AgentTaskInput]) -> list[AgentTaskOutput]:
results: list[AgentTaskOutput] = []
for task in tasks:
result = await workflow.execute_activity(
"execute_agent_task",
task,
start_to_close_timeout=timedelta(seconds=task.timeout_seconds),
retry_policy=RetryPolicy(maximum_attempts=task.max_retries),
)
results.append(result)
return results
@workflow.defn
class FanOutWorkflow:
"""Run agent tasks in parallel (fan-out/fan-in)."""
@workflow.run
async def run(self, tasks: list[AgentTaskInput]) -> list[AgentTaskOutput]:
handles = []
for task in tasks:
handle = workflow.start_activity(
"execute_agent_task",
task,
start_to_close_timeout=timedelta(seconds=task.timeout_seconds),
retry_policy=RetryPolicy(maximum_attempts=task.max_retries),
)
handles.append(handle)
return [await h for h in handles]
+43
View File
@@ -0,0 +1,43 @@
[build-system]
requires = ["setuptools>=61.0.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "hanzo-tasks"
version = "0.1.0"
description = "Hanzo Tasks SDK — Durable workflow execution for AI agents (powered by Temporal)"
readme = "README.md"
requires-python = ">=3.12"
license = { text = "MIT" }
authors = [{ name = "Hanzo AI", email = "[email protected]" }]
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.12",
"Topic :: Software Development :: Libraries :: Python Modules",
"Typing :: Typed",
]
keywords = ["hanzo", "temporal", "tasks", "workflow", "durable", "agents"]
dependencies = [
"temporalio>=1.9.0",
]
[project.urls]
Homepage = "https://github.com/hanzoai/python-sdk"
Repository = "https://github.com/hanzoai/python-sdk/tree/main/pkg/hanzo-tasks"
Documentation = "https://hanzo.ai/docs/tasks"
[project.optional-dependencies]
dev = [
"pytest>=7.0.0",
"pytest-asyncio>=0.26.0",
]
[tool.setuptools.packages.find]
where = ["."]
include = ["hanzo_tasks*"]
[tool.setuptools.package-data]
hanzo_tasks = ["py.typed"]
View File
+317
View File
@@ -0,0 +1,317 @@
"""Hanzo Tasks test suite.
Unit tests for client config, worker registration, dataclasses,
workflow decorators, and activity executor wiring. No Temporal
server required.
"""
import inspect
from dataclasses import asdict
import pytest
import pytest_asyncio
from hanzo_tasks import (
AgentTaskInput,
AgentTaskOutput,
AgentTaskWorkflow,
Client,
FanOutWorkflow,
PipelineWorkflow,
TasksConfig,
Worker,
WorkflowHandle,
execute_agent_task,
set_agent_executor,
)
# -- config ------------------------------------------------------------------
class TestTasksConfig:
def test_defaults(self):
cfg = TasksConfig()
assert cfg.address == "localhost:7233"
assert cfg.namespace == "hanzo"
assert cfg.tls is False
def test_custom(self):
cfg = TasksConfig(address="temporal.prod:7233", namespace="prod", tls=True)
assert cfg.address == "temporal.prod:7233"
assert cfg.namespace == "prod"
assert cfg.tls is True
# -- dataclasses -------------------------------------------------------------
class TestAgentTaskInput:
def test_defaults(self):
inp = AgentTaskInput(
space_id="sp-1",
agent_id="ag-1",
task_title="Fix bug",
task_prompt="Fix the null pointer in main.go",
)
assert inp.space_id == "sp-1"
assert inp.agent_id == "ag-1"
assert inp.task_title == "Fix bug"
assert inp.task_prompt == "Fix the null pointer in main.go"
assert inp.timeout_seconds == 3600
assert inp.max_retries == 3
def test_custom_timeout(self):
inp = AgentTaskInput(
space_id="sp-2",
agent_id="ag-2",
task_title="Deploy",
task_prompt="Deploy to prod",
timeout_seconds=600,
max_retries=1,
)
assert inp.timeout_seconds == 600
assert inp.max_retries == 1
def test_serializes_to_dict(self):
inp = AgentTaskInput(
space_id="sp-1",
agent_id="ag-1",
task_title="T",
task_prompt="P",
)
d = asdict(inp)
assert d["space_id"] == "sp-1"
assert d["agent_id"] == "ag-1"
assert d["task_title"] == "T"
assert d["task_prompt"] == "P"
assert d["timeout_seconds"] == 3600
assert d["max_retries"] == 3
assert len(d) == 6
class TestAgentTaskOutput:
def test_defaults(self):
out = AgentTaskOutput()
assert out.result == ""
assert out.error == ""
assert out.elapsed_seconds == 0.0
def test_success(self):
out = AgentTaskOutput(result="done", elapsed_seconds=1.5)
assert out.result == "done"
assert out.error == ""
assert out.elapsed_seconds == 1.5
def test_error(self):
out = AgentTaskOutput(error="timeout", elapsed_seconds=3600.0)
assert out.error == "timeout"
assert out.result == ""
def test_serializes_to_dict(self):
out = AgentTaskOutput(result="ok", elapsed_seconds=0.1)
d = asdict(out)
assert d == {"result": "ok", "error": "", "elapsed_seconds": 0.1}
# -- workflow decorators -----------------------------------------------------
class TestWorkflowDecorators:
def test_agent_task_workflow_has_run(self):
assert hasattr(AgentTaskWorkflow, "run")
assert inspect.iscoroutinefunction(AgentTaskWorkflow.run)
def test_pipeline_workflow_has_run(self):
assert hasattr(PipelineWorkflow, "run")
assert inspect.iscoroutinefunction(PipelineWorkflow.run)
def test_fanout_workflow_has_run(self):
assert hasattr(FanOutWorkflow, "run")
assert inspect.iscoroutinefunction(FanOutWorkflow.run)
def test_workflow_classes_are_distinct(self):
assert AgentTaskWorkflow is not PipelineWorkflow
assert PipelineWorkflow is not FanOutWorkflow
# -- worker ------------------------------------------------------------------
class TestWorker:
def test_init_defaults(self):
w = Worker(client=None, queue="test-q")
assert w._queue == "test-q"
assert w._workflows == []
assert w._activities == []
assert w._worker is None
def test_register_workflow(self):
w = Worker(client=None)
class MyWorkflow:
pass
result = w.register_workflow(MyWorkflow)
assert result is MyWorkflow
assert MyWorkflow in w._workflows
def test_register_activity(self):
w = Worker(client=None)
async def my_activity(input):
return "ok"
result = w.register_activity(my_activity)
assert result is my_activity
assert my_activity in w._activities
def test_register_multiple(self):
w = Worker(client=None)
for i in range(5):
w.register_workflow(type(f"Wf{i}", (), {}))
assert len(w._workflows) == 5
def test_init_with_preloaded(self):
workflows = [AgentTaskWorkflow, PipelineWorkflow]
activities = [execute_agent_task]
w = Worker(client=None, workflows=workflows, activities=activities)
assert len(w._workflows) == 2
assert len(w._activities) == 1
def test_does_not_mutate_caller_list(self):
workflows: list = []
w = Worker(client=None, workflows=workflows)
w.register_workflow(AgentTaskWorkflow)
# The caller's original list should not be modified since we
# pass a new list via `or []`, but if caller passes a list,
# it IS the same reference. That's expected Python behavior.
# Just verify worker has the workflow.
assert AgentTaskWorkflow in w._workflows
# -- executor wiring ---------------------------------------------------------
class TestSetAgentExecutor:
def test_set_and_reset(self):
import hanzo_tasks.activities as act
original = act._agent_executor
async def my_exec(input):
return "executed"
set_agent_executor(my_exec)
assert act._agent_executor is my_exec
# Restore
act._agent_executor = original
@pytest.mark.asyncio
async def test_execute_without_executor_raises(self):
import hanzo_tasks.activities as act
saved = act._agent_executor
act._agent_executor = None
try:
with pytest.raises(RuntimeError, match="No agent executor registered"):
await execute_agent_task(None)
finally:
act._agent_executor = saved
@pytest.mark.asyncio
async def test_execute_with_executor(self):
import hanzo_tasks.activities as act
saved = act._agent_executor
async def mock_exec(input):
return AgentTaskOutput(result=f"done:{input.task_title}", elapsed_seconds=0.01)
set_agent_executor(mock_exec)
try:
inp = AgentTaskInput(
space_id="sp-1",
agent_id="ag-1",
task_title="test",
task_prompt="do it",
)
out = await execute_agent_task(inp)
assert out.result == "done:test"
assert out.elapsed_seconds == 0.01
finally:
act._agent_executor = saved
# -- workflow handle ---------------------------------------------------------
class TestWorkflowHandle:
def test_id(self):
class FakeHandle:
id = "wf-123"
result_run_id = "run-456"
h = WorkflowHandle(FakeHandle())
assert h.id == "wf-123"
assert h.run_id == "run-456"
@pytest.mark.asyncio
async def test_cancel(self):
cancelled = False
class FakeHandle:
id = "wf-1"
result_run_id = "run-1"
async def cancel(self):
nonlocal cancelled
cancelled = True
h = WorkflowHandle(FakeHandle())
await h.cancel()
assert cancelled
@pytest.mark.asyncio
async def test_signal(self):
signals = []
class FakeHandle:
id = "wf-1"
result_run_id = "run-1"
async def signal(self, name, data=None):
signals.append((name, data))
h = WorkflowHandle(FakeHandle())
await h.signal("pause", {"reason": "lunch"})
assert signals == [("pause", {"reason": "lunch"})]
@pytest.mark.asyncio
async def test_result(self):
class FakeHandle:
id = "wf-1"
result_run_id = "run-1"
async def result(self, result_type=None):
return "the-result"
h = WorkflowHandle(FakeHandle())
assert await h.result() == "the-result"
# -- __init__ exports --------------------------------------------------------
class TestExports:
def test_version(self):
import hanzo_tasks
assert hanzo_tasks.__version__ == "0.1.0"
def test_all_exports_importable(self):
import hanzo_tasks
for name in hanzo_tasks.__all__:
assert hasattr(hanzo_tasks, name), f"{name} not found in hanzo_tasks"
+163
View File
@@ -0,0 +1,163 @@
version = 1
revision = 3
requires-python = ">=3.12"
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "hanzo-tasks"
version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "temporalio" },
]
[package.optional-dependencies]
dev = [
{ name = "pytest" },
{ name = "pytest-asyncio" },
]
[package.metadata]
requires-dist = [
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0.0" },
{ name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.26.0" },
{ name = "temporalio", specifier = ">=1.9.0" },
]
provides-extras = ["dev"]
[[package]]
name = "iniconfig"
version = "2.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
]
[[package]]
name = "nexus-rpc"
version = "1.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/2e/f2/d54f5c03d8f4672ccc0875787a385f53dcb61f98a8ae594b5620e85b9cb3/nexus_rpc-1.3.0.tar.gz", hash = "sha256:e56d3b57b60d707ce7a72f83f23f106b86eca1043aa658e44582ab5ff30ab9ad", size = 75650, upload-time = "2025-12-08T22:59:13.002Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d6/74/0afd841de3199c148146c1d43b4bfb5605b2f1dc4c9a9087fe395091ea5a/nexus_rpc-1.3.0-py3-none-any.whl", hash = "sha256:aee0707b4861b22d8124ecb3f27d62dafbe8777dc50c66c91e49c006f971b92d", size = 28873, upload-time = "2025-12-08T22:59:12.024Z" },
]
[[package]]
name = "packaging"
version = "26.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
]
[[package]]
name = "pluggy"
version = "1.6.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
]
[[package]]
name = "protobuf"
version = "6.33.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/66/70/e908e9c5e52ef7c3a6c7902c9dfbb34c7e29c25d2f81ade3856445fd5c94/protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135", size = 444531, upload-time = "2026-03-18T19:05:00.988Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fc/9f/2f509339e89cfa6f6a4c4ff50438db9ca488dec341f7e454adad60150b00/protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3", size = 425739, upload-time = "2026-03-18T19:04:48.373Z" },
{ url = "https://files.pythonhosted.org/packages/76/5d/683efcd4798e0030c1bab27374fd13a89f7c2515fb1f3123efdfaa5eab57/protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326", size = 437089, upload-time = "2026-03-18T19:04:50.381Z" },
{ url = "https://files.pythonhosted.org/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a", size = 427737, upload-time = "2026-03-18T19:04:51.866Z" },
{ url = "https://files.pythonhosted.org/packages/ee/90/b3c01fdec7d2f627b3a6884243ba328c1217ed2d978def5c12dc50d328a3/protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2", size = 324610, upload-time = "2026-03-18T19:04:53.096Z" },
{ url = "https://files.pythonhosted.org/packages/9b/ca/25afc144934014700c52e05103c2421997482d561f3101ff352e1292fb81/protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3", size = 339381, upload-time = "2026-03-18T19:04:54.616Z" },
{ url = "https://files.pythonhosted.org/packages/16/92/d1e32e3e0d894fe00b15ce28ad4944ab692713f2e7f0a99787405e43533a/protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593", size = 323436, upload-time = "2026-03-18T19:04:55.768Z" },
{ url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" },
]
[[package]]
name = "pygments"
version = "2.19.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
]
[[package]]
name = "pytest"
version = "9.0.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "iniconfig" },
{ name = "packaging" },
{ name = "pluggy" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
]
[[package]]
name = "pytest-asyncio"
version = "1.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pytest" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" },
]
[[package]]
name = "temporalio"
version = "1.23.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nexus-rpc" },
{ name = "protobuf" },
{ name = "types-protobuf" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/67/48/ba7413e2fab8dcd277b9df00bafa572da24e9ca32de2f38d428dc3a2825c/temporalio-1.23.0.tar.gz", hash = "sha256:72750494b00eb73ded9db76195e3a9b53ff548780f73d878ec3f807ee3191410", size = 1933051, upload-time = "2026-02-18T17:48:22.353Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6f/71/26c8f21dca9092201b3b9cb7aff42460b4864b5999aa4c6a4343ac66f1fd/temporalio-1.23.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:6b69ac8d75f2d90e66f4edce4316f6a33badc4a30b22efc50e9eddaa9acdc216", size = 12311037, upload-time = "2026-02-18T17:47:47.628Z" },
{ url = "https://files.pythonhosted.org/packages/ec/47/43102816139f2d346680cb7cc1e53da5f6968355ac65b4d35d4edbfca896/temporalio-1.23.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:1bbbb2f9c3cdd09451565163f6d741e51f109694c49435d475fdfa42b597219d", size = 11821906, upload-time = "2026-02-18T17:47:55.314Z" },
{ url = "https://files.pythonhosted.org/packages/00/b0/899ff28464a0e17adf17476bdfac8faf4ea41870358ff2d14737e43f9e66/temporalio-1.23.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf6570e0ee696f99a38d855da4441a890c7187357c16505ed458ac9ef274ed70", size = 12063601, upload-time = "2026-02-18T17:48:03.994Z" },
{ url = "https://files.pythonhosted.org/packages/ed/17/b8c6d2ec3e113c6a788322513a5ff635bdd54b3791d092ed0e273467748a/temporalio-1.23.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b82d6cca54c9f376b50e941dd10d12f7fe5b692a314fb087be72cd2898646a79", size = 12394579, upload-time = "2026-02-18T17:48:11.65Z" },
{ url = "https://files.pythonhosted.org/packages/b4/b7/f9ef7fd5ee65aef7d59ab1e95cb1b45df2fe49c17e3aa4d650ae3322f015/temporalio-1.23.0-cp310-abi3-win_amd64.whl", hash = "sha256:43c3b99a46dd329761a256f3855710c4a5b322afc879785e468bdd0b94faace6", size = 12834494, upload-time = "2026-02-18T17:48:19.071Z" },
]
[[package]]
name = "types-protobuf"
version = "6.32.1.20260221"
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" }
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" },
]
[[package]]
name = "typing-extensions"
version = "4.15.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
]