The Python API¶
This page lists every public name of the stateous package and of stateous.client. The text comes from the docstrings in the code.
Import each name from stateous, for example from stateous import Workflow. Import the client's errors from stateous.client.
Write a workflow¶
You write a workflow in flow.py, with Workflow, START and END. stateous compile reads it. See Write a workflow.
Workflow ¶
Workflow(
name: str, *, context: type[BaseModel] | None = None
)
A process: its states, its events, and the transitions between them.
You write it in flow.py. stateous compile checks it and writes the graph
file that a control plane serves. Each method refuses a mistake at once, with
DefinitionError.
flow = Workflow("sdlc", context=Ticket)
coding = flow.state("coding", timeout="6h", retries=2)
testing = flow.state("testing", timeout="1h")
failed = flow.event("failed", payload=Tests)
flow.transition(testing, coding, on=failed, when="count('failed') < 3")
Make an empty workflow.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The workflow's name: |
required |
context
|
type[BaseModel] | None
|
The Pydantic model of each token's context. A token starts with
it, and its routines read and write it as |
None
|
Raises:
| Type | Description |
|---|---|
DefinitionError
|
The name is not valid, or |
states
instance-attribute
¶
states: dict[str, State] = {}
The declared states, by name, in declaration order.
events
instance-attribute
¶
events: dict[str, Event] = {}
The declared events, by name. Stateous's own events are not among them.
transitions
instance-attribute
¶
transitions: list[Transition] = []
The declared transitions, in declaration order.
state ¶
state(
name: str,
*,
timeout: str | None = None,
retries: int = 0,
parallel: Sequence[Workflow] = (),
join: str = "all",
inputs: Sequence[str] = (),
into: str | None = None,
) -> State
Add a state to the workflow.
When a token enters the state, the control plane makes one task. With
parallel, it is a parallel state: it starts one child token for each
workflow, and it has no task. See the docs, Concepts > Parallel and nested
work.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The state's name: |
required |
timeout
|
str | None
|
How long a token can stay in the state, for example |
None
|
retries
|
int
|
How many more times a failed task runs. Then the token gets
the event |
0
|
parallel
|
Sequence[Workflow]
|
The child workflows of a parallel state. When the join is
met, the token gets the event |
()
|
join
|
str
|
|
'all'
|
inputs
|
Sequence[str]
|
The context keys that each child's context starts with. They must give each field that a child's context requires. |
()
|
into
|
str | None
|
For a parallel state: the context field that keeps the join's
|
None
|
Returns:
| Type | Description |
|---|---|
State
|
The state, to use in transitions. |
Raises:
| Type | Description |
|---|---|
DefinitionError
|
The name is not valid, is used, or is Stateous's own.
Or the timeout is not a duration, |
event ¶
event(
name: str,
*,
payload: type[BaseModel] | None = None,
into: str | None = None,
) -> Event
Add an event to the workflow. An event is an outcome: only an event moves a token.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The event's name, unique in the workflow. Do not declare
Stateous's own events, |
required |
payload
|
type[BaseModel] | None
|
The Pydantic model of the data that the event carries. None: no data. Guards read it; routines do not. |
None
|
into
|
str | None
|
The context field that keeps the payload when the event is accepted, so that routines can read it. Its type must take the payload model. |
None
|
Returns:
| Type | Description |
|---|---|
Event
|
The event, to use in transitions. |
Raises:
| Type | Description |
|---|---|
DefinitionError
|
The name is not valid, is used, or is Stateous's own.
Or the payload is not a Pydantic model class, or |
transition ¶
transition(
source: State,
target: State,
*,
on: Event | str | None = None,
when: str | None = None,
otherwise: bool = False,
) -> Transition
Add a transition: the event on moves a token from source to
target. The events that a state can emit are the events of its
transitions.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
State
|
The state that the token is in, or |
required |
target
|
State
|
The state that the token moves to, or |
required |
on
|
Event | str | None
|
The event, or the name of one of Stateous's own events:
|
None
|
when
|
str | None
|
A guard that reads |
None
|
otherwise
|
bool
|
True for the fallback: the control plane takes it when no
guard on the same state and event is true. It has no |
False
|
Returns:
| Type | Description |
|---|---|
Transition
|
The transition. |
Raises:
| Type | Description |
|---|---|
DefinitionError
|
A state is not of this workflow, or the event is missing or not allowed here. Or the guard does not parse, or it reads a field that the payload does not have. Or two transitions have no guard on the same state and event. |
START
module-attribute
¶
START = State('__start__', workflow='')
Where every token begins. Import it; do not declare it.
A transition from START takes no on=. The control plane leaves START at once, on
__started__. When START has more than one transition, their guards on the starting
context choose one.
END
module-attribute
¶
END = State('__end__', workflow='')
Where a token completes. Import it; do not declare it.
A token that enters END is completed. END has no task, and no transition leaves it.
A parallel state with into= keeps the data of its join in the context, as Branches. See Parallel and nested work.
Branches ¶
Bases: BaseModel
The payload of joined: with join="all", one entry for each child; with
join="any", the child that ended first.
Branch ¶
Bases: BaseModel
One ended child of a parallel state.
Write a routine¶
A routine is a function in the supervisor's folder. @on_state marks it, ctx is its Context, and it returns an Occurrence. The generated contract.py gives the StateRef of each state. See Write a routine.
on_state ¶
on_state(state: StateRef[Any]) -> Callable[[F], F]
Mark a function as the routine of a state. The supervisor runs it for each task of the state.
@on_state(states.testing)
def testing(ctx: TicketContext) -> events.passed | events.failed:
return events.passed(Tests(passed=12, failed=0))
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
state
|
StateRef[Any]
|
The state, from |
required |
Returns:
| Type | Description |
|---|---|
Callable[[F], F]
|
A decorator. It returns the function unchanged, with a mark. |
Raises:
| Type | Description |
|---|---|
TypeError
|
|
Context ¶
Bases: Generic[C]
What a routine gets as ctx: the token's context C, and the task's facts.
The generated contract gives the type, for example
TicketContext = Context[Ticket]. ctx["key"] reads a copy of a context
value. ctx["key"] = value writes it. The control plane checks the writes
against the context model, and saves them with the outcome.
A change in place, such as ctx["items"].append(2), changes nothing. Write
the new value with ctx["items"] = ....
@on_state(states.coding)
async def coding(ctx: TicketContext) -> events.done | events.stuck:
ctx.note(f"coding {ctx['repo']}, attempt {ctx.attempt}")
ctx["branch"] = "fix-1"
return events.done()
visit
instance-attribute
¶
visit: int
1 the first time the token is in this state, 2 the next time, and so on.
entered_by
instance-attribute
¶
entered_by: EnteredBy | None
The name of the event that brought the token here.
memory
instance-attribute
¶
memory: Memory
The routine's own memory, for this token and this state: a mapping of
keys to JSON values. Each change is saved at once. del ctx.memory[key]
deletes a key.
get ¶
get(key: str, default: Any = None) -> Any
Read a context value, with a default.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
str
|
The context key. |
required |
default
|
Any
|
The value to return when the context has no |
None
|
Returns:
| Type | Description |
|---|---|
Any
|
A copy of the value at |
note ¶
note(text: str) -> None
Add one line of progress to the token's history. The supervisor sends it at once.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
text
|
str
|
The line. The spaces at its two ends are removed. Empty text does nothing. |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
The text has more than 500 characters. |
schedule ¶
schedule(
event: type[Occurrence[Any]] | Occurrence[Any],
*,
after: str,
) -> None
Set a timer. The clock fires event on this token after the time
after, if the token is still in this state.
The timer is set when the control plane saves the report.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
event
|
type[Occurrence[Any]] | Occurrence[Any]
|
The event: a class of |
required |
after
|
str
|
A duration from now, for example |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
|
writes ¶
writes() -> dict[str, Any]
The context keys that this routine wrote, with their values. The supervisor sends them with the report.
schedules ¶
schedules() -> list[Schedule]
The timers that this routine set. The supervisor sends them with the report.
StateRef
dataclass
¶
StateRef(name: str, emits: tuple[type[O], ...] = ())
Bases: Generic[O]
A state as a supervisor sees it. The generated contract.py has one for
each state, as states.<name>. Give it to @on_state.
You do not write a StateRef: stateous init supervisor writes them. The
type argument is the union of the state's events. For example,
StateRef("coding", emits=(done, stuck)) is a StateRef[done | stuck]. So
outcome() returns a done | stuck.
emits
class-attribute
instance-attribute
¶
emits: tuple[type[O], ...] = ()
The events that the state can emit: the events of its transitions.
may_emit ¶
may_emit(occurrence: Occurrence[Any]) -> bool
Tell if the state can emit an occurrence.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
occurrence
|
Occurrence[Any]
|
An occurrence of an event. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True when the event of |
outcome_schema ¶
outcome_schema() -> dict[str, Any]
The events that this state can emit, with their payloads, as one JSON Schema. Give it to an agent as the form of its final answer.
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
A JSON Schema with one branch for each event. An answer that agrees with it names one event and its payload. |
Raises:
| Type | Description |
|---|---|
OutcomeError
|
The state can emit no event, so no answer agrees. |
outcome ¶
outcome(data: Any) -> O
Make the occurrence that an agent's answer names.
Pydantic checks the payload as JSON, not in strict mode. So the string
"3" agrees with an int field.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
Any
|
The answer: |
required |
Returns:
| Type | Description |
|---|---|
O
|
The occurrence of the event, with its payload. |
Raises:
| Type | Description |
|---|---|
OutcomeError
|
|
Occurrence ¶
Occurrence()
Occurrence(payload: P)
Occurrence(payload: P | None = None)
Bases: Generic[P]
One occurrence of an event, with its payload. A routine returns one.
The generated contract has one subclass for each event, in the class events.
The type argument is the payload model, or None. For example,
events.failed(Tests(passed=3, failed=1)) is an Occurrence[Tests], and
events.done() is an Occurrence[None].
payload
instance-attribute
¶
payload: P = cast('P', payload)
The payload, or None for an event without a payload.
payload_model
classmethod
¶
payload_model() -> type[BaseModel] | None
The payload model of the event.
Returns:
| Type | Description |
|---|---|
type[BaseModel] | None
|
The model of the class's |
OutcomeError ¶
Bases: ValueError
An outcome does not fit its state. The state cannot emit the event, or the
payload does not agree with the event's model. StateRef.outcome() raises it.
Talk to the control plane¶
A program outside uses Client to start, fire, read and steer tokens. Each method sends one request of the HTTP API. See Steer tokens.
Client ¶
Client(url: str, *, timeout: float = 30.0)
The control plane's HTTP API at one URL, for a program. Synchronous.
Each method sends one request, and returns the JSON answer. A failure raises
a ClientError. Use the client in a with block, or call close() at the
end.
from stateous import Client
with Client("http://127.0.0.1:8080") as client:
client.start("TICKET-1", {"repo": "acme/api"})
client.fire("TICKET-1", "done", key="gh-delivery-81231")
Make a client for one control plane.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
url
|
str
|
The control plane's URL, for example |
required |
timeout
|
float
|
How long to wait for each answer, in seconds. |
30.0
|
Raises:
| Type | Description |
|---|---|
ClientError
|
|
start ¶
start(
token: str, context: dict[str, Any] | None = None
) -> dict[str, Any]
Start a token. If the token exists, it is returned unchanged.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
token
|
str
|
The token's id, for example |
required |
context
|
dict[str, Any] | None
|
The token's context. None: |
None
|
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
The token. |
get ¶
get(token: str) -> dict[str, Any]
Read one token.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
token
|
str
|
The token's id. |
required |
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
The token: its state, status, visit, context and the time it entered the state. |
Raises:
| Type | Description |
|---|---|
NotFound
|
No token has this id. |
tokens ¶
tokens(
*,
status: str | None = None,
parent: str | None = None,
limit: int | None = None,
) -> list[dict[str, Any]]
List the tokens, the most recently moved first.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
status
|
str | None
|
Only the tokens with this status, for example |
None
|
parent
|
str | None
|
Only the children of this token. |
None
|
limit
|
int | None
|
The most tokens to return: 1 to 1000. None: 100. |
None
|
Returns:
| Type | Description |
|---|---|
list[dict[str, Any]]
|
The tokens. |
history ¶
history(token: str) -> list[dict[str, Any]]
Read the history of a token, in order.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
token
|
str
|
The token's id. |
required |
Returns:
| Type | Description |
|---|---|
list[dict[str, Any]]
|
The entries: each has |
Raises:
| Type | Description |
|---|---|
NotFound
|
No token has this id. |
fire ¶
fire(
token: str,
event: str,
payload: dict[str, Any] | None = None,
*,
key: str | None = None,
) -> dict[str, Any]
Fire an event on a token.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
token
|
str
|
The token's id. |
required |
event
|
str
|
The event's name. |
required |
payload
|
dict[str, Any] | None
|
The event's payload. None: no payload. |
None
|
key
|
str | None
|
An idempotency key, from 1 to 256 characters. The same key fires one time. |
None
|
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
The token after the event. When |
Raises:
| Type | Description |
|---|---|
Refused
|
The event cannot move the token, or the payload is not valid. |
pause ¶
pause(token: str) -> dict[str, Any]
Pause an active token. Its events are held, its tasks wait, and its timers stand still.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
token
|
str
|
The token's id. A child token cannot be paused: pause its top token. |
required |
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
The token. |
resume ¶
resume(token: str) -> dict[str, Any]
Resume a paused token. The held events apply in the order that they arrived.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
token
|
str
|
The token's id. |
required |
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
The token. |
cancel ¶
cancel(token: str) -> dict[str, Any]
Cancel an active or a paused token for good.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
token
|
str
|
The token's id. |
required |
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
The token. |
report ¶
report(
task: int,
event: str,
payload: dict[str, Any] | None = None,
*,
attempt: int | None = None,
) -> dict[str, Any]
Report the outcome of a task that waits. Its routine returned None, and the work went on outside.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
task
|
int
|
The task's id: |
required |
event
|
str
|
The event's name. |
required |
payload
|
dict[str, Any] | None
|
The event's payload. None: no payload. |
None
|
attempt
|
int | None
|
The attempt that the report is for. The control plane refuses the report (409) when the task is on a different attempt. |
None
|
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
The token after the event. |
workflow ¶
workflow() -> dict[str, Any]
Read the graph file that the control plane serves.
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
The graph file, as JSON. |
ClientError ¶
Bases: Exception
The base of every error of the client. The control plane could not do the request, or the client could not send it. The message is one plain sentence.
NotFound ¶
NotFound(
status: int,
message: str,
*,
url: str,
code: str | None = None,
)
Refused ¶
Refused(
status: int,
message: str,
*,
url: str,
code: str | None = None,
)
Bases: ClientError
The control plane refused the request.
Use code in a program, for example attempt_over. The codes are in the docs,
Reference > The HTTP API. message is for people, and it can change.
Attributes:
| Name | Type | Description |
|---|---|---|
status |
The HTTP status, for example 409. |
|
message |
The reason that the control plane gave, as one sentence. |
|
code |
The stable code of the refusal. None when the answer has no code, for example an answer from a proxy. |
The client makes it from the control plane's answer.
Unreachable ¶
Bases: ClientError
The control plane did not answer: the network failed, or no server listens at the URL.