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.
Files changed (71) hide show
  1. py_serverless_kit/__init__.py +47 -0
  2. py_serverless_kit/__main__.py +3 -0
  3. py_serverless_kit/_version.py +2 -0
  4. py_serverless_kit/cli/__init__.py +0 -0
  5. py_serverless_kit/cli/main.py +491 -0
  6. py_serverless_kit/cli/templates.py +212 -0
  7. py_serverless_kit/config/__init__.py +12 -0
  8. py_serverless_kit/config/loader.py +316 -0
  9. py_serverless_kit/config/schema.py +457 -0
  10. py_serverless_kit/core/__init__.py +0 -0
  11. py_serverless_kit/core/deployer.py +180 -0
  12. py_serverless_kit/core/describe.py +112 -0
  13. py_serverless_kit/core/project.py +147 -0
  14. py_serverless_kit/dashboard/__init__.py +3 -0
  15. py_serverless_kit/dashboard/generator.py +65 -0
  16. py_serverless_kit/dashboard/template.html +248 -0
  17. py_serverless_kit/local/__init__.py +3 -0
  18. py_serverless_kit/local/runner.py +234 -0
  19. py_serverless_kit/local/server.py +200 -0
  20. py_serverless_kit/packaging/__init__.py +3 -0
  21. py_serverless_kit/packaging/builder.py +252 -0
  22. py_serverless_kit/providers/__init__.py +15 -0
  23. py_serverless_kit/providers/alibaba.py +268 -0
  24. py_serverless_kit/providers/aws.py +508 -0
  25. py_serverless_kit/providers/azure.py +417 -0
  26. py_serverless_kit/providers/base.py +310 -0
  27. py_serverless_kit/providers/cloudflare.py +264 -0
  28. py_serverless_kit/providers/digitalocean.py +211 -0
  29. py_serverless_kit/providers/gcp.py +480 -0
  30. py_serverless_kit/providers/huawei.py +292 -0
  31. py_serverless_kit/providers/ibm.py +132 -0
  32. py_serverless_kit/providers/oracle.py +304 -0
  33. py_serverless_kit/providers/registry.py +60 -0
  34. py_serverless_kit/providers/schedule.py +153 -0
  35. py_serverless_kit/providers/tencent.py +275 -0
  36. py_serverless_kit/providers/terraform_doc.py +125 -0
  37. py_serverless_kit/runtime/__init__.py +6 -0
  38. py_serverless_kit/runtime/adapters/__init__.py +18 -0
  39. py_serverless_kit/runtime/adapters/_httpfn.py +91 -0
  40. py_serverless_kit/runtime/adapters/alibaba.py +147 -0
  41. py_serverless_kit/runtime/adapters/aws.py +201 -0
  42. py_serverless_kit/runtime/adapters/azure.py +185 -0
  43. py_serverless_kit/runtime/adapters/base.py +86 -0
  44. py_serverless_kit/runtime/adapters/cloudflare.py +126 -0
  45. py_serverless_kit/runtime/adapters/digitalocean.py +74 -0
  46. py_serverless_kit/runtime/adapters/gcp.py +176 -0
  47. py_serverless_kit/runtime/adapters/huawei.py +173 -0
  48. py_serverless_kit/runtime/adapters/ibm.py +91 -0
  49. py_serverless_kit/runtime/adapters/oracle.py +161 -0
  50. py_serverless_kit/runtime/adapters/tencent.py +151 -0
  51. py_serverless_kit/runtime/batch.py +47 -0
  52. py_serverless_kit/runtime/context.py +45 -0
  53. py_serverless_kit/runtime/dispatch.py +277 -0
  54. py_serverless_kit/runtime/errors.py +83 -0
  55. py_serverless_kit/runtime/event.py +267 -0
  56. py_serverless_kit/runtime/invoke.py +119 -0
  57. py_serverless_kit/runtime/log.py +69 -0
  58. py_serverless_kit/runtime/manifest.py +121 -0
  59. py_serverless_kit/runtime/response.py +177 -0
  60. py_serverless_kit/runtime/routing.py +74 -0
  61. py_serverless_kit/runtime/workflow.py +312 -0
  62. py_serverless_kit/terraform/__init__.py +3 -0
  63. py_serverless_kit/terraform/runner.py +104 -0
  64. py_serverless_kit/workflows/__init__.py +35 -0
  65. py_serverless_kit/workflows/asl.py +170 -0
  66. py_serverless_kit/workflows/gcp.py +328 -0
  67. py_serverless_kit-0.1.0.dist-info/METADATA +300 -0
  68. py_serverless_kit-0.1.0.dist-info/RECORD +71 -0
  69. py_serverless_kit-0.1.0.dist-info/WHEEL +4 -0
  70. py_serverless_kit-0.1.0.dist-info/entry_points.txt +3 -0
  71. 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