Source code for pynenc.workflow.workflow_context
from __future__ import annotations
import datetime
from typing import TYPE_CHECKING, Any, NoReturn, TypeVar
from pynenc.workflow.workflow_exceptions import (
DeterministicOperationScopeError,
WorkflowMembershipError,
)
if TYPE_CHECKING:
from pynenc.app import Pynenc
from pynenc.invocation.dist_invocation import DistributedInvocation
from pynenc.task import Task
from pynenc.workflow.workflow_deterministic import DeterministicExecutor
from pynenc.workflow.workflow_identity import WorkflowIdentity
S = TypeVar("S")
[docs]
class WorkflowContext:
"""
Provides workflow capabilities for tasks within Pynenc.
This class exposes shared workflow state for any task that is running inside
a workflow. Root-only orchestration lives on ``WorkflowRootContext.root``.
:param task: The task this helper is attached to
"""
def __init__(self, task: Task):
"""Initialize the workflow helper with its associated task."""
self.task = task
@property
def app(self) -> Pynenc:
"""Get the Pynenc app instance associated with this workflow."""
return self.task.app
@property
def identity(self) -> WorkflowIdentity:
"""Get the workflow identity for the current execution."""
invocation = self.task.invocation
if invocation.workflow is None:
raise WorkflowMembershipError(
operation_name="workflow",
task_id=self.task.task_id.key,
invocation_id=str(invocation.invocation_id),
)
return invocation.workflow
[docs]
def get_data(self, key: str, default: Any = None) -> Any:
"""
Get a value from the workflow's data store.
Any task within the workflow can read data.
:param key: The data key to retrieve
:param default: Default value if the key doesn't exist
:return: The stored value or default
"""
return self.app.state_backend.get_workflow_data(self.identity, key, default)
[docs]
def set_data(self, key: str, value: Any) -> None:
"""
Set a value in the workflow's data store.
Any task within the workflow can write data.
:param key: The data key to set
:param value: The value to store
"""
self.app.state_backend.set_workflow_data(self.identity, key, value)
[docs]
class WorkflowRootOps:
"""
Root-only workflow orchestration operations.
The public ``wf.root`` boundary makes it explicit that these methods are
only valid on the workflow-defining invocation. The deterministic replay
cursor itself is stored on the active invocation object, not on the cached
task-level workflow context.
"""
def __init__(self, task: Task):
self.task = task
@property
def _invocation(self) -> DistributedInvocation:
from pynenc.invocation.dist_invocation import DistributedInvocation
invocation = self.task.invocation
if not isinstance(invocation, DistributedInvocation):
self._raise_scope_error("workflow orchestration")
return invocation
[docs]
def _raise_scope_error(self, operation_name: str) -> NoReturn:
invocation = None
workflow = None
invocation_id = None
is_workflow_defining = False
try:
invocation = self.task.invocation
invocation_id = str(invocation.invocation_id)
workflow = invocation.workflow
is_workflow_defining = bool(invocation.task.is_workflow_root_task)
except Exception:
pass
raise DeterministicOperationScopeError(
operation_name=operation_name,
task_id=self.task.task_id.key,
invocation_id=invocation_id,
workflow_id=str(workflow.workflow_id) if workflow else None,
workflow_type=workflow.workflow_type.key if workflow else None,
parent_workflow_id=str(workflow.parent_workflow_id)
if workflow and workflow.parent_workflow_id
else None,
is_workflow_defining_invocation=is_workflow_defining,
)
[docs]
def _ensure_root_workflow_operation_allowed(
self, operation_name: str
) -> DistributedInvocation:
invocation = self._invocation
if (
not invocation.task.is_workflow_root_task
or invocation.task.task_id != self.task.task_id
):
self._raise_scope_error(operation_name)
return invocation
[docs]
def _deterministic(self, operation_name: str) -> DeterministicExecutor:
invocation = self._ensure_root_workflow_operation_allowed(operation_name)
return invocation._get_deterministic_runtime()
[docs]
def random(self) -> float:
"""
Generate a deterministic random number in the workflow context.
Returns the same sequence of "random" numbers on workflow replay.
:return: A random float between 0.0 and 1.0
"""
return self._deterministic("random").random()
[docs]
def utc_now(self) -> datetime.datetime:
"""
Get the current time deterministically in the workflow context.
Returns deterministic timestamps that advance consistently on workflow replay.
:return: Current datetime with UTC timezone
"""
return self._deterministic("utc_now").utc_now()
[docs]
def uuid(self) -> str:
"""
Generate a deterministic UUID in the workflow context.
Returns the same sequence of UUIDs on workflow replay.
:return: UUID string
"""
return self._deterministic("uuid").uuid()
# CHILD WORKFLOW/TASK EXECUTION
[docs]
def execute_task(self, task: Task, *args: Any, **kwargs: Any) -> Any:
"""
Execute a task as part of this workflow with deterministic replay.
On workflow replay, returns the recorded result without re-execution.
:param task: The task to execute
:param args: Positional arguments for the task
:param kwargs: Keyword arguments for the task
:return: Task result
"""
return self._deterministic("execute_task").execute_task(task, *args, **kwargs)
[docs]
class WorkflowRootContext(WorkflowContext):
"""Workflow context for workflow-defining tasks."""
def __init__(self, task: Task):
super().__init__(task)
self.root = WorkflowRootOps(task)