Skip to content

Commit c4da69c

Browse files
committed
Update version to 0.0.2b5 and introduce new models for graph execution
- Incremented the version in _version.py to 0.0.2b5. - Added new models in models.py for GraphNode, Unites, RetryPolicy, and StoreConfig, enhancing the structure and validation of graph execution parameters. - Refactored StateManager methods to utilize the new models, improving type safety and clarity in the trigger and upsert_graph functions. - Updated trigger_graph to include a start_delay parameter for delayed execution, enhancing flexibility in graph triggering.
1 parent 2755f33 commit c4da69c

5 files changed

Lines changed: 175 additions & 44 deletions

File tree

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
version = "0.0.2b4"
1+
version = "0.0.2b5"

python-sdk/exospherehost/models.py

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
from pydantic import BaseModel, Field, field_validator
2+
from typing import Any, Optional, List
3+
from enum import Enum
4+
5+
6+
class UnitesStrategyEnum(str, Enum):
7+
ALL_SUCCESS = "ALL_SUCCESS"
8+
ALL_DONE = "ALL_DONE"
9+
10+
11+
class UnitesModel(BaseModel):
12+
identifier: str = Field(..., description="Identifier of the node")
13+
strategy: UnitesStrategyEnum = Field(default=UnitesStrategyEnum.ALL_SUCCESS, description="Strategy of the unites")
14+
15+
16+
class GraphNodeModel(BaseModel):
17+
node_name: str = Field(..., description="Name of the node")
18+
namespace: str = Field(..., description="Namespace of the node")
19+
identifier: str = Field(..., description="Identifier of the node")
20+
inputs: dict[str, Any] = Field(..., description="Inputs of the node")
21+
next_nodes: Optional[List[str]] = Field(None, description="Next nodes to execute")
22+
unites: Optional[UnitesModel] = Field(None, description="Unites of the node")
23+
24+
@field_validator('node_name')
25+
@classmethod
26+
def validate_node_name(cls, v: str) -> str:
27+
trimmed_v = v.strip()
28+
if trimmed_v == "" or trimmed_v is None:
29+
raise ValueError("Node name cannot be empty")
30+
return trimmed_v
31+
32+
@field_validator('identifier')
33+
@classmethod
34+
def validate_identifier(cls, v: str) -> str:
35+
trimmed_v = v.strip()
36+
if trimmed_v == "" or trimmed_v is None:
37+
raise ValueError("Node identifier cannot be empty")
38+
elif trimmed_v == "store":
39+
raise ValueError("Node identifier cannot be reserved word 'store'")
40+
return trimmed_v
41+
42+
@field_validator('next_nodes')
43+
@classmethod
44+
def validate_next_nodes(cls, v: Optional[List[str]]) -> Optional[List[str]]:
45+
identifiers = set()
46+
errors = []
47+
trimmed_v = []
48+
49+
if v is not None:
50+
for next_node_identifier in v:
51+
trimmed_next_node_identifier = next_node_identifier.strip()
52+
53+
if trimmed_next_node_identifier == "" or trimmed_next_node_identifier is None:
54+
errors.append("Next node identifier cannot be empty")
55+
continue
56+
57+
if trimmed_next_node_identifier in identifiers:
58+
errors.append(f"Next node identifier {trimmed_next_node_identifier} is not unique")
59+
continue
60+
61+
identifiers.add(trimmed_next_node_identifier)
62+
trimmed_v.append(trimmed_next_node_identifier)
63+
if errors:
64+
raise ValueError("\n".join(errors))
65+
return trimmed_v
66+
67+
@field_validator('unites')
68+
@classmethod
69+
def validate_unites(cls, v: Optional[UnitesModel]) -> Optional[UnitesModel]:
70+
trimmed_v = v
71+
if v is not None:
72+
trimmed_v = UnitesModel(identifier=v.identifier.strip(), strategy=v.strategy)
73+
if trimmed_v.identifier == "" or trimmed_v.identifier is None:
74+
raise ValueError("Unites identifier cannot be empty")
75+
return trimmed_v
76+
77+
78+
class RetryStrategyEnum(str, Enum):
79+
EXPONENTIAL = "EXPONENTIAL"
80+
EXPONENTIAL_FULL_JITTER = "EXPONENTIAL_FULL_JITTER"
81+
EXPONENTIAL_EQUAL_JITTER = "EXPONENTIAL_EQUAL_JITTER"
82+
83+
LINEAR = "LINEAR"
84+
LINEAR_FULL_JITTER = "LINEAR_FULL_JITTER"
85+
LINEAR_EQUAL_JITTER = "LINEAR_EQUAL_JITTER"
86+
87+
FIXED = "FIXED"
88+
FIXED_FULL_JITTER = "FIXED_FULL_JITTER"
89+
FIXED_EQUAL_JITTER = "FIXED_EQUAL_JITTER"
90+
91+
92+
class RetryPolicyModel(BaseModel):
93+
max_retries: int = Field(default=3, description="The maximum number of retries", ge=0)
94+
strategy: RetryStrategyEnum = Field(default=RetryStrategyEnum.EXPONENTIAL, description="The method of retry")
95+
backoff_factor: int = Field(default=2000, description="The backoff factor in milliseconds (default: 2000 = 2 seconds)", gt=0)
96+
exponent: int = Field(default=2, description="The exponent for the exponential retry strategy", gt=0)
97+
max_delay: int | None = Field(default=None, description="The maximum delay in milliseconds (no default limit when None)", gt=0)
98+
99+
100+
class StoreConfigModel(BaseModel):
101+
required_keys: list[str] = Field(default_factory=list, description="Required keys of the store")
102+
default_values: dict[str, str] = Field(default_factory=dict, description="Default values of the store")
103+
104+
@field_validator("required_keys")
105+
def validate_required_keys(cls, v: list[str]) -> list[str]:
106+
errors = []
107+
keys = set()
108+
trimmed_keys = []
109+
110+
for key in v:
111+
trimmed_key = key.strip() if key is not None else ""
112+
113+
if trimmed_key == "":
114+
errors.append("Key cannot be empty or contain only whitespace")
115+
continue
116+
117+
if '.' in trimmed_key:
118+
errors.append(f"Key '{trimmed_key}' cannot contain '.' character")
119+
continue
120+
121+
if trimmed_key in keys:
122+
errors.append(f"Key '{trimmed_key}' is duplicated")
123+
continue
124+
125+
keys.add(trimmed_key)
126+
trimmed_keys.append(trimmed_key)
127+
128+
if len(errors) > 0:
129+
raise ValueError("\n".join(errors))
130+
return trimmed_keys
131+
132+
@field_validator("default_values")
133+
def validate_default_values(cls, v: dict[str, str]) -> dict[str, str]:
134+
errors = []
135+
keys = set()
136+
normalized_dict = {}
137+
138+
for key, value in v.items():
139+
trimmed_key = key.strip() if key is not None else ""
140+
141+
if trimmed_key == "":
142+
errors.append("Key cannot be empty or contain only whitespace")
143+
continue
144+
145+
if '.' in trimmed_key:
146+
errors.append(f"Key '{trimmed_key}' cannot contain '.' character")
147+
continue
148+
149+
if trimmed_key in keys:
150+
errors.append(f"Key '{trimmed_key}' is duplicated")
151+
continue
152+
153+
keys.add(trimmed_key)
154+
normalized_dict[trimmed_key] = str(value)
155+
156+
if len(errors) > 0:
157+
raise ValueError("\n".join(errors))
158+
return normalized_dict

python-sdk/exospherehost/statemanager.py

Lines changed: 12 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -3,40 +3,7 @@
33
import asyncio
44
import time
55

6-
from typing import Any
7-
from pydantic import BaseModel
8-
9-
10-
class TriggerState(BaseModel):
11-
"""
12-
Represents a trigger state for graph execution.
13-
14-
A trigger state contains an identifier and a set of input parameters that
15-
will be passed to the graph when it is triggered for execution.
16-
17-
Attributes:
18-
identifier (str): A unique identifier for this trigger state. This is used
19-
to distinguish between different trigger states and may be used by the
20-
graph to determine how to process the trigger.
21-
inputs (dict[str, str]): A dictionary of input parameters that will be
22-
passed to the graph. The keys are parameter names and values are
23-
parameter values, both as strings.
24-
25-
Example:
26-
```python
27-
# Create a trigger state with identifier and inputs
28-
trigger_state = TriggerState(
29-
identifier="user-login",
30-
inputs={
31-
"user_id": "12345",
32-
"session_token": "abc123def456",
33-
"timestamp": "2024-01-15T10:30:00Z"
34-
}
35-
)
36-
```
37-
"""
38-
identifier: str
39-
inputs: dict[str, str]
6+
from .models import GraphNodeModel, RetryPolicyModel, StoreConfigModel
407

418

429
class StateManager:
@@ -67,7 +34,7 @@ def _get_upsert_graph_endpoint(self, graph_name: str):
6734
def _get_get_graph_endpoint(self, graph_name: str):
6835
return f"{self._state_manager_uri}/{self._state_manager_version}/namespace/{self._namespace}/graph/{graph_name}"
6936

70-
async def trigger(self, graph_name: str, inputs: dict[str, str] | None = None, store: dict[str, str] | None = None):
37+
async def trigger(self, graph_name: str, inputs: dict[str, str] | None = None, store: dict[str, str] | None = None, start_delay: int = 0):
7138
"""
7239
Trigger execution of a graph.
7340
@@ -82,7 +49,8 @@ async def trigger(self, graph_name: str, inputs: dict[str, str] | None = None, s
8249
graph. Strings only.
8350
store (dict[str, str] | None): Optional key-value store that will be merged
8451
into the graph-level store before execution (beta).
85-
52+
start_delay (int): Optional delay in milliseconds before the graph starts execution.
53+
8654
Returns:
8755
dict: JSON payload returned by the state-manager API.
8856
@@ -108,6 +76,7 @@ async def trigger(self, graph_name: str, inputs: dict[str, str] | None = None, s
10876
store = {}
10977

11078
body = {
79+
"start_delay": start_delay,
11180
"inputs": inputs,
11281
"store": store
11382
}
@@ -156,7 +125,7 @@ async def get_graph(self, graph_name: str):
156125
raise Exception(f"Failed to get graph: {response.status} {await response.text()}")
157126
return await response.json()
158127

159-
async def upsert_graph(self, graph_name: str, graph_nodes: list[dict[str, Any]], secrets: dict[str, str], retry_policy: dict[str, Any] | None = None, store_config: dict[str, Any] | None = None, validation_timeout: int = 60, polling_interval: int = 1):
128+
async def upsert_graph(self, graph_name: str, graph_nodes: list[GraphNodeModel], secrets: dict[str, str], retry_policy: RetryPolicyModel | None = None, store_config: StoreConfigModel | None = None, validation_timeout: int = 60, polling_interval: int = 1):
160129
"""
161130
Create or update a graph definition.
162131
@@ -169,10 +138,10 @@ async def upsert_graph(self, graph_name: str, graph_nodes: list[dict[str, Any]],
169138
170139
Args:
171140
graph_name (str): Graph identifier.
172-
graph_nodes (list[dict[str, Any]]): Graph node list.
141+
graph_nodes (list[GraphNodeModel]): List of graph node models defining the workflow.
173142
secrets (dict[str, str]): Secrets available to all nodes.
174-
retry_policy (dict[str, Any] | None): Optional per-node retry policy.
175-
store_config (dict[str, Any] | None): Beta configuration for the
143+
retry_policy (RetryPolicyModel | None): Optional per-node retry policy configuration.
144+
store_config (StoreConfigModel | None): Beta configuration for the
176145
graph-level store (schema is subject to change).
177146
validation_timeout (int): Seconds to wait for validation (default 60).
178147
polling_interval (int): Polling interval in seconds (default 1).
@@ -189,13 +158,13 @@ async def upsert_graph(self, graph_name: str, graph_nodes: list[dict[str, Any]],
189158
}
190159
body = {
191160
"secrets": secrets,
192-
"nodes": graph_nodes
161+
"nodes": [node.model_dump() for node in graph_nodes]
193162
}
194163

195164
if retry_policy is not None:
196-
body["retry_policy"] = retry_policy
165+
body["retry_policy"] = retry_policy.model_dump()
197166
if store_config is not None:
198-
body["store_config"] = store_config
167+
body["store_config"] = store_config.model_dump()
199168

200169
async with aiohttp.ClientSession() as session:
201170
async with session.put(endpoint, json=body, headers=headers) as response: # type: ignore

state-manager/app/controller/trigger_graph.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
from app.models.db.graph_template_model import GraphTemplate
1010
from app.models.node_template_model import NodeTemplate
1111
from app.models.dependent_string import DependentString
12+
1213
import uuid
14+
import time
1315

1416
logger = LogsManager().get_logger()
1517

@@ -96,6 +98,7 @@ async def trigger_graph(namespace_name: str, graph_name: str, body: TriggerGraph
9698
graph_name=graph_name,
9799
run_id=run_id,
98100
status=StateStatusEnum.CREATED,
101+
enqueue_after=int(time.time() * 1000) + body.start_delay,
99102
inputs=inputs,
100103
outputs={},
101104
error=None

state-manager/app/models/trigger_model.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
class TriggerGraphRequestModel(BaseModel):
55
store: dict[str, str] = Field(default_factory=dict, description="Store for the runtime")
66
inputs: dict[str, str] = Field(default_factory=dict, description="Inputs for the graph execution")
7+
start_delay: int = Field(default=0, ge=0, description="Start delay in milliseconds")
78

89
class TriggerGraphResponseModel(BaseModel):
910
status: StateStatusEnum = Field(..., description="Status of the states")

0 commit comments

Comments
 (0)