mirror of
https://github.com/hanzoai/python-sdk.git
synced 2026-10-23 20:23:00 +00:00
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:
@@ -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",
|
||||
]
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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()
|
||||
@@ -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]
|
||||
@@ -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"]
|
||||
@@ -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"
|
||||
Generated
+163
@@ -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" },
|
||||
]
|
||||
Reference in New Issue
Block a user