py-serverless-kit 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- py_serverless_kit/__init__.py +47 -0
- py_serverless_kit/__main__.py +3 -0
- py_serverless_kit/_version.py +2 -0
- py_serverless_kit/cli/__init__.py +0 -0
- py_serverless_kit/cli/main.py +491 -0
- py_serverless_kit/cli/templates.py +212 -0
- py_serverless_kit/config/__init__.py +12 -0
- py_serverless_kit/config/loader.py +316 -0
- py_serverless_kit/config/schema.py +457 -0
- py_serverless_kit/core/__init__.py +0 -0
- py_serverless_kit/core/deployer.py +180 -0
- py_serverless_kit/core/describe.py +112 -0
- py_serverless_kit/core/project.py +147 -0
- py_serverless_kit/dashboard/__init__.py +3 -0
- py_serverless_kit/dashboard/generator.py +65 -0
- py_serverless_kit/dashboard/template.html +248 -0
- py_serverless_kit/local/__init__.py +3 -0
- py_serverless_kit/local/runner.py +234 -0
- py_serverless_kit/local/server.py +200 -0
- py_serverless_kit/packaging/__init__.py +3 -0
- py_serverless_kit/packaging/builder.py +252 -0
- py_serverless_kit/providers/__init__.py +15 -0
- py_serverless_kit/providers/alibaba.py +268 -0
- py_serverless_kit/providers/aws.py +508 -0
- py_serverless_kit/providers/azure.py +417 -0
- py_serverless_kit/providers/base.py +310 -0
- py_serverless_kit/providers/cloudflare.py +264 -0
- py_serverless_kit/providers/digitalocean.py +211 -0
- py_serverless_kit/providers/gcp.py +480 -0
- py_serverless_kit/providers/huawei.py +292 -0
- py_serverless_kit/providers/ibm.py +132 -0
- py_serverless_kit/providers/oracle.py +304 -0
- py_serverless_kit/providers/registry.py +60 -0
- py_serverless_kit/providers/schedule.py +153 -0
- py_serverless_kit/providers/tencent.py +275 -0
- py_serverless_kit/providers/terraform_doc.py +125 -0
- py_serverless_kit/runtime/__init__.py +6 -0
- py_serverless_kit/runtime/adapters/__init__.py +18 -0
- py_serverless_kit/runtime/adapters/_httpfn.py +91 -0
- py_serverless_kit/runtime/adapters/alibaba.py +147 -0
- py_serverless_kit/runtime/adapters/aws.py +201 -0
- py_serverless_kit/runtime/adapters/azure.py +185 -0
- py_serverless_kit/runtime/adapters/base.py +86 -0
- py_serverless_kit/runtime/adapters/cloudflare.py +126 -0
- py_serverless_kit/runtime/adapters/digitalocean.py +74 -0
- py_serverless_kit/runtime/adapters/gcp.py +176 -0
- py_serverless_kit/runtime/adapters/huawei.py +173 -0
- py_serverless_kit/runtime/adapters/ibm.py +91 -0
- py_serverless_kit/runtime/adapters/oracle.py +161 -0
- py_serverless_kit/runtime/adapters/tencent.py +151 -0
- py_serverless_kit/runtime/batch.py +47 -0
- py_serverless_kit/runtime/context.py +45 -0
- py_serverless_kit/runtime/dispatch.py +277 -0
- py_serverless_kit/runtime/errors.py +83 -0
- py_serverless_kit/runtime/event.py +267 -0
- py_serverless_kit/runtime/invoke.py +119 -0
- py_serverless_kit/runtime/log.py +69 -0
- py_serverless_kit/runtime/manifest.py +121 -0
- py_serverless_kit/runtime/response.py +177 -0
- py_serverless_kit/runtime/routing.py +74 -0
- py_serverless_kit/runtime/workflow.py +312 -0
- py_serverless_kit/terraform/__init__.py +3 -0
- py_serverless_kit/terraform/runner.py +104 -0
- py_serverless_kit/workflows/__init__.py +35 -0
- py_serverless_kit/workflows/asl.py +170 -0
- py_serverless_kit/workflows/gcp.py +328 -0
- py_serverless_kit-0.1.0.dist-info/METADATA +300 -0
- py_serverless_kit-0.1.0.dist-info/RECORD +71 -0
- py_serverless_kit-0.1.0.dist-info/WHEEL +4 -0
- py_serverless_kit-0.1.0.dist-info/entry_points.txt +3 -0
- py_serverless_kit-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Workflow (state machine) compilers.
|
|
2
|
+
|
|
3
|
+
One portable definition in ``pskit.yml`` -> AWS Step Functions (ASL), Google
|
|
4
|
+
Cloud Workflows, or the embedded engine in :mod:`py_serverless_kit.runtime.workflow`.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any, Dict
|
|
10
|
+
|
|
11
|
+
from ..config.schema import WorkflowDefinition
|
|
12
|
+
|
|
13
|
+
_CONFIG_ONLY = {"description", "engine", "timeout", "options"}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def definition_dict(wf: WorkflowDefinition) -> Dict[str, Any]:
|
|
17
|
+
"""Plain-dict form consumed by the engine and the compilers."""
|
|
18
|
+
raw = wf.model_dump(mode="json", exclude=_CONFIG_ONLY & set(type(wf).model_fields))
|
|
19
|
+
return _clean(raw)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _clean(value: Any) -> Any:
|
|
23
|
+
if isinstance(value, dict):
|
|
24
|
+
out = {}
|
|
25
|
+
for k, v in value.items():
|
|
26
|
+
if k == "result_path": # None is meaningful ("discard result")
|
|
27
|
+
out[k] = v
|
|
28
|
+
elif v is None or v == [] or (k == "end" and v is False):
|
|
29
|
+
continue
|
|
30
|
+
else:
|
|
31
|
+
out[k] = _clean(v)
|
|
32
|
+
return out
|
|
33
|
+
if isinstance(value, list):
|
|
34
|
+
return [_clean(v) for v in value]
|
|
35
|
+
return value
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Compile a portable workflow definition to Amazon States Language."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Callable, Dict, List
|
|
6
|
+
|
|
7
|
+
ArnResolver = Callable[[str], str]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _errors(errors: List[str]) -> List[str]:
|
|
11
|
+
return ["States.ALL" if e in ("*", "States.ALL") else e for e in (errors or ["*"])]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _typed(op: str, value: Any) -> str:
|
|
15
|
+
if isinstance(value, bool):
|
|
16
|
+
return "Boolean" + op
|
|
17
|
+
if isinstance(value, (int, float)):
|
|
18
|
+
return "Numeric" + op
|
|
19
|
+
return "String" + op
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
_CMP = {"gt": "GreaterThan", "gte": "GreaterThanEquals", "lt": "LessThan", "lte": "LessThanEquals"}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def compile_rule(rule: Dict[str, Any]) -> Dict[str, Any]:
|
|
26
|
+
if "and" in rule:
|
|
27
|
+
return {"And": [compile_rule(r) for r in rule["and"]]}
|
|
28
|
+
if "or" in rule:
|
|
29
|
+
return {"Or": [compile_rule(r) for r in rule["or"]]}
|
|
30
|
+
if "not" in rule:
|
|
31
|
+
return {"Not": compile_rule(rule["not"])}
|
|
32
|
+
var = rule.get("variable", "$")
|
|
33
|
+
if "equals" in rule:
|
|
34
|
+
value = rule["equals"]
|
|
35
|
+
if value is None:
|
|
36
|
+
return {"Variable": var, "IsNull": True}
|
|
37
|
+
return {"Variable": var, _typed("Equals", value): value}
|
|
38
|
+
if "not_equals" in rule:
|
|
39
|
+
return {"Not": compile_rule({"variable": var, "equals": rule["not_equals"]})}
|
|
40
|
+
for op, name in _CMP.items():
|
|
41
|
+
if op in rule:
|
|
42
|
+
value = rule[op]
|
|
43
|
+
if isinstance(value, bool):
|
|
44
|
+
raise ValueError(f"operator {op} cannot compare booleans")
|
|
45
|
+
return {"Variable": var, _typed(name, value): value}
|
|
46
|
+
if "in" in rule:
|
|
47
|
+
return {"Or": [compile_rule({"variable": var, "equals": v}) for v in rule["in"]]}
|
|
48
|
+
if "contains" in rule:
|
|
49
|
+
needle = str(rule["contains"]).replace("*", "\\*")
|
|
50
|
+
return {"Variable": var, "StringMatches": f"*{needle}*"}
|
|
51
|
+
if "matches" in rule:
|
|
52
|
+
return {"Variable": var, "StringMatches": rule["matches"]}
|
|
53
|
+
if "is_null" in rule:
|
|
54
|
+
return {"Variable": var, "IsNull": bool(rule["is_null"])}
|
|
55
|
+
if "is_present" in rule:
|
|
56
|
+
return {"Variable": var, "IsPresent": bool(rule["is_present"])}
|
|
57
|
+
raise ValueError(f"unsupported choice rule: {rule}")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _flow(state: Dict[str, Any], out: Dict[str, Any]) -> None:
|
|
61
|
+
if state.get("next"):
|
|
62
|
+
out["Next"] = state["next"]
|
|
63
|
+
else:
|
|
64
|
+
out["End"] = True
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _io(state: Dict[str, Any], out: Dict[str, Any]) -> None:
|
|
68
|
+
if state.get("input_path"):
|
|
69
|
+
out["InputPath"] = state["input_path"]
|
|
70
|
+
if "result_path" in state and state["result_path"] != "$":
|
|
71
|
+
out["ResultPath"] = state["result_path"]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def compile_machine(machine: Dict[str, Any], arn_of: ArnResolver) -> Dict[str, Any]:
|
|
75
|
+
states: Dict[str, Any] = {}
|
|
76
|
+
for name, st in machine["states"].items():
|
|
77
|
+
kind = st["type"]
|
|
78
|
+
out: Dict[str, Any] = {}
|
|
79
|
+
if st.get("comment"):
|
|
80
|
+
out["Comment"] = st["comment"]
|
|
81
|
+
if kind == "task":
|
|
82
|
+
out["Type"] = "Task"
|
|
83
|
+
out["Resource"] = arn_of(st["function"])
|
|
84
|
+
out["Parameters"] = {"__pskit": "workflow", "data.$": "$"}
|
|
85
|
+
_io(st, out)
|
|
86
|
+
if st.get("timeout_seconds"):
|
|
87
|
+
out["TimeoutSeconds"] = int(st["timeout_seconds"])
|
|
88
|
+
_flow(st, out)
|
|
89
|
+
elif kind == "pass":
|
|
90
|
+
out["Type"] = "Pass"
|
|
91
|
+
if "result" in st:
|
|
92
|
+
out["Result"] = st["result"]
|
|
93
|
+
_io(st, out)
|
|
94
|
+
_flow(st, out)
|
|
95
|
+
elif kind == "wait":
|
|
96
|
+
out["Type"] = "Wait"
|
|
97
|
+
if st.get("seconds_path"):
|
|
98
|
+
out["SecondsPath"] = st["seconds_path"]
|
|
99
|
+
else:
|
|
100
|
+
out["Seconds"] = int(st.get("seconds", 0))
|
|
101
|
+
if st.get("input_path"):
|
|
102
|
+
out["InputPath"] = st["input_path"]
|
|
103
|
+
_flow(st, out)
|
|
104
|
+
elif kind == "choice":
|
|
105
|
+
out["Type"] = "Choice"
|
|
106
|
+
out["Choices"] = [dict(compile_rule(r), Next=r["next"]) for r in st["choices"]]
|
|
107
|
+
if st.get("default"):
|
|
108
|
+
out["Default"] = st["default"]
|
|
109
|
+
elif kind == "parallel":
|
|
110
|
+
out["Type"] = "Parallel"
|
|
111
|
+
out["Branches"] = [compile_machine(b, arn_of) for b in st["branches"]]
|
|
112
|
+
_io(st, out)
|
|
113
|
+
_flow(st, out)
|
|
114
|
+
elif kind == "map":
|
|
115
|
+
out["Type"] = "Map"
|
|
116
|
+
if st.get("items_path"):
|
|
117
|
+
out["ItemsPath"] = st["items_path"]
|
|
118
|
+
out["ItemProcessor"] = {
|
|
119
|
+
"ProcessorConfig": {"Mode": "INLINE"},
|
|
120
|
+
**compile_machine(st["iterator"], arn_of),
|
|
121
|
+
}
|
|
122
|
+
if st.get("max_concurrency"):
|
|
123
|
+
out["MaxConcurrency"] = int(st["max_concurrency"])
|
|
124
|
+
_io(st, out)
|
|
125
|
+
_flow(st, out)
|
|
126
|
+
elif kind == "succeed":
|
|
127
|
+
out["Type"] = "Succeed"
|
|
128
|
+
elif kind == "fail":
|
|
129
|
+
out["Type"] = "Fail"
|
|
130
|
+
if st.get("error"):
|
|
131
|
+
out["Error"] = st["error"]
|
|
132
|
+
if st.get("cause"):
|
|
133
|
+
out["Cause"] = st["cause"]
|
|
134
|
+
else: # pragma: no cover - schema prevents this
|
|
135
|
+
raise ValueError(f"unknown state type {kind}")
|
|
136
|
+
|
|
137
|
+
if kind in ("task", "parallel", "map"):
|
|
138
|
+
if st.get("retry"):
|
|
139
|
+
out["Retry"] = []
|
|
140
|
+
for r in st["retry"]:
|
|
141
|
+
rr: Dict[str, Any] = {
|
|
142
|
+
"ErrorEquals": _errors(r.get("errors", ["*"])),
|
|
143
|
+
"IntervalSeconds": max(1, int(round(r.get("interval_seconds", 1)))),
|
|
144
|
+
"MaxAttempts": int(r.get("max_attempts", 3)),
|
|
145
|
+
"BackoffRate": float(r.get("backoff_rate", 2.0)),
|
|
146
|
+
}
|
|
147
|
+
if r.get("max_delay_seconds"):
|
|
148
|
+
rr["MaxDelaySeconds"] = int(r["max_delay_seconds"])
|
|
149
|
+
out["Retry"].append(rr)
|
|
150
|
+
if st.get("catch"):
|
|
151
|
+
out["Catch"] = [
|
|
152
|
+
{
|
|
153
|
+
"ErrorEquals": _errors(c.get("errors", ["*"])),
|
|
154
|
+
"Next": c["next"],
|
|
155
|
+
"ResultPath": c.get("result_path", "$.error"),
|
|
156
|
+
}
|
|
157
|
+
for c in st["catch"]
|
|
158
|
+
]
|
|
159
|
+
states[name] = out
|
|
160
|
+
return {"StartAt": machine["start_at"], "States": states}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def compile_asl(definition: Dict[str, Any], arn_of: ArnResolver, comment: str = "", timeout: int = 0) -> Dict[str, Any]:
|
|
164
|
+
asl: Dict[str, Any] = {}
|
|
165
|
+
if comment:
|
|
166
|
+
asl["Comment"] = comment
|
|
167
|
+
asl.update(compile_machine(definition, arn_of))
|
|
168
|
+
if timeout:
|
|
169
|
+
asl["TimeoutSeconds"] = int(timeout)
|
|
170
|
+
return asl
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
"""Compile a portable workflow definition to Google Cloud Workflows (JSON syntax).
|
|
2
|
+
|
|
3
|
+
Each task state becomes an authenticated ``http.post`` to the Cloud Function's
|
|
4
|
+
URL with the ``{"__pskit": "workflow", "data": ...}`` envelope; the GCP adapter
|
|
5
|
+
answers with the handler's JSON result or a 500 ``{"errorType", "cause"}``.
|
|
6
|
+
|
|
7
|
+
Limitations compared to Step Functions (validated, reported as warnings):
|
|
8
|
+
|
|
9
|
+
* only the *first* retry rule of a task is used (Cloud Workflows allows one policy);
|
|
10
|
+
* retry / catch are honoured on ``task`` states only.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import re
|
|
17
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
18
|
+
|
|
19
|
+
UrlResolver = Callable[[str], str]
|
|
20
|
+
_MAX_HTTP_TIMEOUT = 1800
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _sid(*parts: str) -> str:
|
|
24
|
+
return re.sub(r"[^A-Za-z0-9_]", "_", "_".join(p for p in parts if p))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _keys(path: Optional[str]) -> List[Any]:
|
|
28
|
+
if not path or path == "$":
|
|
29
|
+
return []
|
|
30
|
+
from ..runtime.workflow import _tokens # same JSON-path dialect as the engine
|
|
31
|
+
|
|
32
|
+
return _tokens(path)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def read_expr(path: Optional[str], var: str = "data") -> str:
|
|
36
|
+
keys = _keys(path)
|
|
37
|
+
if not keys:
|
|
38
|
+
return var
|
|
39
|
+
if all(isinstance(k, str) for k in keys):
|
|
40
|
+
return f"map.get({var}, {json.dumps(keys)})"
|
|
41
|
+
expr = var
|
|
42
|
+
for k in keys:
|
|
43
|
+
expr += f"[{json.dumps(k)}]"
|
|
44
|
+
return expr
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def assign_target(path: Optional[str], var: str = "data") -> Optional[str]:
|
|
48
|
+
if path is None:
|
|
49
|
+
return None
|
|
50
|
+
keys = _keys(path)
|
|
51
|
+
target = var
|
|
52
|
+
for k in keys:
|
|
53
|
+
target += f"[{json.dumps(k)}]" if isinstance(k, int) else f".{k}"
|
|
54
|
+
return target
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _lit(value: Any) -> str:
|
|
58
|
+
return json.dumps(value)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def rule_expr(rule: Dict[str, Any]) -> str:
|
|
62
|
+
if "and" in rule:
|
|
63
|
+
return "(" + " and ".join(rule_expr(r) for r in rule["and"]) + ")"
|
|
64
|
+
if "or" in rule:
|
|
65
|
+
return "(" + " or ".join(rule_expr(r) for r in rule["or"]) + ")"
|
|
66
|
+
if "not" in rule:
|
|
67
|
+
return f"(not {rule_expr(rule['not'])})"
|
|
68
|
+
v = read_expr(rule.get("variable", "$"))
|
|
69
|
+
simple = {"equals": "==", "not_equals": "!=", "gt": ">", "gte": ">=", "lt": "<", "lte": "<="}
|
|
70
|
+
for op, sym in simple.items():
|
|
71
|
+
if op in rule:
|
|
72
|
+
return f"({v} {sym} {_lit(rule[op])})"
|
|
73
|
+
if "in" in rule:
|
|
74
|
+
return f"({v} in {_lit(rule['in'])})"
|
|
75
|
+
if "contains" in rule:
|
|
76
|
+
return f"(len(text.find_all({v}, {_lit(str(rule['contains']))})) > 0)"
|
|
77
|
+
if "matches" in rule:
|
|
78
|
+
regex = "^" + re.escape(rule["matches"]).replace("\\*", ".*").replace("\\?", ".") + "$"
|
|
79
|
+
return f"text.match_regex({v}, {_lit(regex)})"
|
|
80
|
+
if "is_null" in rule:
|
|
81
|
+
return f"({v} == null)" if rule["is_null"] else f"({v} != null)"
|
|
82
|
+
if "is_present" in rule:
|
|
83
|
+
return f"({v} != null)" if rule["is_present"] else f"({v} == null)"
|
|
84
|
+
raise ValueError(f"unsupported choice rule: {rule}")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _errors_condition(errors: List[str]) -> str:
|
|
88
|
+
if any(e in ("*", "States.ALL") for e in errors or ["*"]):
|
|
89
|
+
return "${true}"
|
|
90
|
+
return '${map.get(e, ["body", "errorType"]) in ' + _lit(list(errors)) + "}"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class _Compiler:
|
|
94
|
+
def __init__(self, url_of: UrlResolver, workflow: str) -> None:
|
|
95
|
+
self.url_of = url_of
|
|
96
|
+
self.workflow = workflow
|
|
97
|
+
self.subworkflows: Dict[str, Any] = {}
|
|
98
|
+
self.warnings: List[str] = []
|
|
99
|
+
|
|
100
|
+
# ------------------------------------------------------------------
|
|
101
|
+
def machine(self, machine: Dict[str, Any], prefix: str) -> List[Dict[str, Any]]:
|
|
102
|
+
states: Dict[str, Dict[str, Any]] = machine["states"]
|
|
103
|
+
entry = {name: self._entry_step(prefix, name, st) for name, st in states.items()}
|
|
104
|
+
steps: List[Dict[str, Any]] = [
|
|
105
|
+
{
|
|
106
|
+
_sid(prefix, "init"): {
|
|
107
|
+
"assign": [{"data": "${input}"}, {"_caught": ""}],
|
|
108
|
+
"next": entry[machine["start_at"]],
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
]
|
|
112
|
+
for name, st in states.items():
|
|
113
|
+
steps.extend(self.state(prefix, name, st, entry))
|
|
114
|
+
return steps
|
|
115
|
+
|
|
116
|
+
def _entry_step(self, prefix: str, name: str, st: Dict[str, Any]) -> str:
|
|
117
|
+
if st["type"] == "task" and st.get("catch"):
|
|
118
|
+
return _sid(prefix, name, "pre")
|
|
119
|
+
if st["type"] in ("parallel", "map"):
|
|
120
|
+
return _sid(prefix, name, "init")
|
|
121
|
+
return _sid(prefix, name)
|
|
122
|
+
|
|
123
|
+
def _after(self, st: Dict[str, Any], entry: Dict[str, str]) -> Dict[str, Any]:
|
|
124
|
+
if st.get("next"):
|
|
125
|
+
return {"next": entry[st["next"]]}
|
|
126
|
+
return {"return": "${data}"}
|
|
127
|
+
|
|
128
|
+
def _store(
|
|
129
|
+
self, prefix: str, name: str, st: Dict[str, Any], value_expr: str, entry: Dict[str, str]
|
|
130
|
+
) -> Dict[str, Any]:
|
|
131
|
+
target = assign_target(st.get("result_path", "$"))
|
|
132
|
+
body: Dict[str, Any] = {}
|
|
133
|
+
if target is not None:
|
|
134
|
+
body["assign"] = [{target: value_expr}]
|
|
135
|
+
else:
|
|
136
|
+
body["assign"] = [{"_discard": value_expr}]
|
|
137
|
+
body.update(self._after(st, entry))
|
|
138
|
+
return {_sid(prefix, name, "out"): body}
|
|
139
|
+
|
|
140
|
+
def _sub(self, machine: Dict[str, Any], name: str) -> str:
|
|
141
|
+
sub_name = _sid(self.workflow, name)
|
|
142
|
+
self.subworkflows[sub_name] = {"params": ["input"], "steps": self.machine(machine, sub_name)}
|
|
143
|
+
return sub_name
|
|
144
|
+
|
|
145
|
+
# ------------------------------------------------------------------
|
|
146
|
+
def state(self, prefix: str, name: str, st: Dict[str, Any], entry: Dict[str, str]) -> List[Dict[str, Any]]:
|
|
147
|
+
kind = st["type"]
|
|
148
|
+
sid = _sid(prefix, name)
|
|
149
|
+
input_expr = "${" + read_expr(st.get("input_path")) + "}"
|
|
150
|
+
if kind == "task":
|
|
151
|
+
return self.task(prefix, name, st, entry, input_expr)
|
|
152
|
+
if kind == "pass":
|
|
153
|
+
value = _lit(st["result"]) if "result" in st else read_expr(st.get("input_path"))
|
|
154
|
+
return [
|
|
155
|
+
{sid: {"assign": [{"_pass": "${" + value + "}"}], "next": _sid(prefix, name, "out")}},
|
|
156
|
+
self._store(prefix, name, st, "${_pass}", entry),
|
|
157
|
+
]
|
|
158
|
+
if kind == "wait":
|
|
159
|
+
seconds: Any = int(st.get("seconds", 0))
|
|
160
|
+
if st.get("seconds_path"):
|
|
161
|
+
seconds = "${" + read_expr(st["seconds_path"]) + "}"
|
|
162
|
+
return [{sid: {"call": "sys.sleep", "args": {"seconds": seconds}, **self._after(st, entry)}}]
|
|
163
|
+
if kind == "choice":
|
|
164
|
+
conds: List[Dict[str, Any]] = [
|
|
165
|
+
{"condition": "${" + rule_expr(r) + "}", "next": entry[r["next"]]} for r in st["choices"]
|
|
166
|
+
]
|
|
167
|
+
body: Dict[str, Any] = {"switch": conds}
|
|
168
|
+
if st.get("default"):
|
|
169
|
+
body["next"] = entry[st["default"]]
|
|
170
|
+
else:
|
|
171
|
+
conds.append({"condition": True, "raise": {"error": "States.NoChoiceMatched", "cause": name}})
|
|
172
|
+
return [{sid: body}]
|
|
173
|
+
if kind == "succeed":
|
|
174
|
+
return [{sid: {"return": "${data}"}}]
|
|
175
|
+
if kind == "fail":
|
|
176
|
+
return [{sid: {"raise": {"error": st.get("error", "States.Fail"), "cause": st.get("cause", "")}}}]
|
|
177
|
+
if kind == "parallel":
|
|
178
|
+
if st.get("retry") or st.get("catch"):
|
|
179
|
+
self.warnings.append(f"{self.workflow}.{name}: retry/catch on parallel states is ignored on GCP")
|
|
180
|
+
var = _sid("par", prefix, name)
|
|
181
|
+
branches = []
|
|
182
|
+
for i, branch in enumerate(st["branches"]):
|
|
183
|
+
sub = self._sub(branch, f"{prefix}_{name}_b{i}")
|
|
184
|
+
branches.append(
|
|
185
|
+
{
|
|
186
|
+
f"b{i}": {
|
|
187
|
+
"steps": [
|
|
188
|
+
{
|
|
189
|
+
_sid(sid, f"b{i}", "call"): {
|
|
190
|
+
"call": sub,
|
|
191
|
+
"args": {"input": input_expr},
|
|
192
|
+
"result": f"r{i}",
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
{_sid(sid, f"b{i}", "save"): {"assign": [{f"{var}.b{i}": f"${{r{i}}}"}]}},
|
|
196
|
+
]
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
)
|
|
200
|
+
collect = "${[" + ", ".join(f"{var}.b{i}" for i in range(len(branches))) + "]}"
|
|
201
|
+
return [
|
|
202
|
+
{_sid(prefix, name, "init"): {"assign": [{var: {}}]}},
|
|
203
|
+
{sid: {"parallel": {"shared": [var], "branches": branches}}},
|
|
204
|
+
self._store(prefix, name, st, collect, entry),
|
|
205
|
+
]
|
|
206
|
+
if kind == "map":
|
|
207
|
+
if st.get("retry") or st.get("catch"):
|
|
208
|
+
self.warnings.append(f"{self.workflow}.{name}: retry/catch on map states is ignored on GCP")
|
|
209
|
+
sub = self._sub(st["iterator"], f"{prefix}_{name}_it")
|
|
210
|
+
items, res, out = _sid("items", prefix, name), _sid("res", prefix, name), _sid("out", prefix, name)
|
|
211
|
+
items_src = read_expr(st.get("items_path"), read_expr(st.get("input_path")))
|
|
212
|
+
parallel: Dict[str, Any] = {
|
|
213
|
+
"shared": [res],
|
|
214
|
+
"for": {
|
|
215
|
+
"value": "item",
|
|
216
|
+
"index": "i",
|
|
217
|
+
"in": "${" + items + "}",
|
|
218
|
+
"steps": [
|
|
219
|
+
{_sid(sid, "call"): {"call": sub, "args": {"input": "${item}"}, "result": "r"}},
|
|
220
|
+
{_sid(sid, "save"): {"assign": [{f"{res}[string(i)]": "${r}"}]}},
|
|
221
|
+
],
|
|
222
|
+
},
|
|
223
|
+
}
|
|
224
|
+
if st.get("max_concurrency"):
|
|
225
|
+
parallel["concurrency_limit"] = int(st["max_concurrency"])
|
|
226
|
+
return [
|
|
227
|
+
{_sid(prefix, name, "init"): {"assign": [{items: "${" + items_src + "}"}, {res: {}}, {out: []}]}},
|
|
228
|
+
{sid: {"parallel": parallel}},
|
|
229
|
+
{
|
|
230
|
+
_sid(prefix, name, "collect"): {
|
|
231
|
+
"for": {
|
|
232
|
+
"value": "j",
|
|
233
|
+
"range": "${[0, len(" + items + ") - 1]}",
|
|
234
|
+
"steps": [
|
|
235
|
+
{_sid(sid, "append"): {"assign": [{out: f"${{list.concat({out}, {res}[string(j)])}}"}]}}
|
|
236
|
+
],
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
self._store(prefix, name, st, "${" + out + "}", entry),
|
|
241
|
+
]
|
|
242
|
+
raise ValueError(f"unknown state type {kind}") # pragma: no cover
|
|
243
|
+
|
|
244
|
+
def task(
|
|
245
|
+
self, prefix: str, name: str, st: Dict[str, Any], entry: Dict[str, str], input_expr: str
|
|
246
|
+
) -> List[Dict[str, Any]]:
|
|
247
|
+
sid = _sid(prefix, name)
|
|
248
|
+
result_var = _sid("res", prefix, name)
|
|
249
|
+
timeout = min(int(st.get("timeout_seconds") or _MAX_HTTP_TIMEOUT), _MAX_HTTP_TIMEOUT)
|
|
250
|
+
call = {
|
|
251
|
+
"call": "http.post",
|
|
252
|
+
"args": {
|
|
253
|
+
"url": self.url_of(st["function"]),
|
|
254
|
+
"auth": {"type": "OIDC"},
|
|
255
|
+
"timeout": timeout,
|
|
256
|
+
"body": {"__pskit": "workflow", "data": input_expr},
|
|
257
|
+
},
|
|
258
|
+
"result": result_var,
|
|
259
|
+
}
|
|
260
|
+
steps: List[Dict[str, Any]] = []
|
|
261
|
+
retry = st.get("retry") or []
|
|
262
|
+
catch = st.get("catch") or []
|
|
263
|
+
if len(retry) > 1:
|
|
264
|
+
self.warnings.append(f"{self.workflow}.{name}: only the first retry rule is used on GCP")
|
|
265
|
+
if not retry and not catch:
|
|
266
|
+
steps.append({sid: call})
|
|
267
|
+
else:
|
|
268
|
+
body: Dict[str, Any] = {"try": call}
|
|
269
|
+
if retry:
|
|
270
|
+
rule = retry[0]
|
|
271
|
+
pred = _sid(self.workflow, prefix, name, "retry_pred")
|
|
272
|
+
self.subworkflows[pred] = {
|
|
273
|
+
"params": ["e"],
|
|
274
|
+
"steps": [
|
|
275
|
+
{
|
|
276
|
+
"check": {
|
|
277
|
+
"switch": [{"condition": _errors_condition(rule.get("errors", ["*"])), "return": True}]
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
{"no": {"return": False}},
|
|
281
|
+
],
|
|
282
|
+
}
|
|
283
|
+
body["retry"] = {
|
|
284
|
+
"predicate": "${" + pred + "}",
|
|
285
|
+
"max_retries": int(rule.get("max_attempts", 3)),
|
|
286
|
+
"backoff": {
|
|
287
|
+
"initial_delay": max(1, float(rule.get("interval_seconds", 1))),
|
|
288
|
+
"max_delay": float(rule.get("max_delay_seconds") or 300),
|
|
289
|
+
"multiplier": float(rule.get("backoff_rate", 2.0)),
|
|
290
|
+
},
|
|
291
|
+
}
|
|
292
|
+
if catch:
|
|
293
|
+
info = {
|
|
294
|
+
"error": '${default(map.get(e, ["body", "errorType"]), "Error")}',
|
|
295
|
+
"cause": '${default(map.get(e, ["body", "cause"]), "")}',
|
|
296
|
+
}
|
|
297
|
+
conds: List[Dict[str, Any]] = []
|
|
298
|
+
for i, c in enumerate(catch):
|
|
299
|
+
assigns: List[Dict[str, Any]] = [{"_caught": c["next"]}]
|
|
300
|
+
target = assign_target(c.get("result_path", "$.error"))
|
|
301
|
+
if target is not None:
|
|
302
|
+
assigns.append({target: info})
|
|
303
|
+
conds.append(
|
|
304
|
+
{
|
|
305
|
+
"condition": _errors_condition(c.get("errors", ["*"])),
|
|
306
|
+
"steps": [{_sid(sid, f"catch{i}"): {"assign": assigns}}],
|
|
307
|
+
}
|
|
308
|
+
)
|
|
309
|
+
conds.append({"condition": True, "raise": "${e}"})
|
|
310
|
+
body["except"] = {"as": "e", "steps": [{_sid(sid, "except"): {"switch": conds}}]}
|
|
311
|
+
steps.append({_sid(prefix, name, "pre"): {"assign": [{"_caught": ""}]}})
|
|
312
|
+
steps.append({sid: body})
|
|
313
|
+
if catch:
|
|
314
|
+
route = [
|
|
315
|
+
{"condition": "${_caught == " + _lit(c["next"]) + "}", "next": entry[c["next"]]} for c in catch
|
|
316
|
+
]
|
|
317
|
+
steps.append({_sid(prefix, name, "route"): {"switch": route}})
|
|
318
|
+
steps.append(self._store(prefix, name, st, "${" + result_var + ".body}", entry))
|
|
319
|
+
return steps
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def compile_gcp(definition: Dict[str, Any], url_of: UrlResolver, workflow: str) -> Tuple[Dict[str, Any], List[str]]:
|
|
323
|
+
"""Return ``(workflow_source_dict, warnings)``."""
|
|
324
|
+
comp = _Compiler(url_of, _sid(workflow))
|
|
325
|
+
main = {"params": ["input"], "steps": comp.machine(definition, "s")}
|
|
326
|
+
source: Dict[str, Any] = {"main": main}
|
|
327
|
+
source.update(comp.subworkflows)
|
|
328
|
+
return source, comp.warnings
|