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,292 @@
1
+ """Huawei Cloud: FunctionGraph + dedicated API Gateway + timer / OBS / SMN triggers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ from typing import Any, Dict, List, Optional
8
+
9
+ from ..config.schema import FunctionConfig, ServiceConfig
10
+ from ..runtime.adapters.base import envelope
11
+ from . import schedule as sched
12
+ from .base import HTTP, SCHEDULE, STORAGE, TOPIC, Issue, Provider, RenderContext
13
+ from .terraform_doc import TerraformDoc, escape, ref, tf_id
14
+
15
+ FUNCTION = "huaweicloud_fgs_function"
16
+ TRIGGER = "huaweicloud_fgs_trigger"
17
+ _AUTH = {"none": "NONE", "iam": "IAM", "key": "APP"}
18
+
19
+
20
+ def quartz_cron(ev: Any) -> str:
21
+ """FunctionGraph cron: ``sec min hour dom month dow`` where dom or dow must be ``?``."""
22
+ minute, hour, dom, month, dow = sched.named_cron5(ev).split()
23
+ if dow == "*":
24
+ dow = "?"
25
+ elif dom == "*":
26
+ dom = "?"
27
+ return f"0 {minute} {hour} {dom} {month} {dow}"
28
+
29
+
30
+ def rate_expression(ev: Any) -> str:
31
+ value, unit = sched.parse_rate(ev.rate or "")
32
+ return f"{value}{unit[0]}"
33
+
34
+
35
+ def _gateway_path(path: str) -> tuple[str, str]:
36
+ segments = [s for s in path.split("/") if s]
37
+ if segments and segments[-1].endswith("+}"):
38
+ return "/" + "/".join(segments[:-1]), "Prefix"
39
+ return "/" + "/".join(segments), "Exact"
40
+
41
+
42
+ class HuaweiProvider(Provider):
43
+ name = "huawei"
44
+ display_name = "Huawei Cloud FunctionGraph"
45
+ tier = "beta"
46
+ terraform_source = "huaweicloud/huaweicloud"
47
+ terraform_version = ">= 1.60.0"
48
+ terraform_local_name = "huaweicloud"
49
+ default_region = "cn-north-4"
50
+ capabilities = frozenset({HTTP, SCHEDULE, STORAGE, TOPIC})
51
+ max_name_length = 60
52
+ name_pattern = r"[^a-z0-9_-]"
53
+ runtimes = {"python3.9": "Python3.9", "python3.10": "Python3.10", "python3.12": "Python3.12"}
54
+ vendor_dependencies = True
55
+ max_timeout = 900
56
+ notes = (
57
+ "HTTP needs a dedicated APIG instance (options.apig_instance_id); timers, OBS and SMN triggers; "
58
+ "workflows run on the embedded engine."
59
+ )
60
+
61
+ # ------------------------------------------------------------------ bundle
62
+ def shim_files(self, config: ServiceConfig) -> Dict[str, str]:
63
+ return {
64
+ "index.py": (
65
+ "# Generated by py-serverless-kit. FunctionGraph handler: index.handler\n"
66
+ "from py_serverless_kit.runtime.adapters.huawei import handler # noqa: F401\n"
67
+ )
68
+ }
69
+
70
+ def provider_block(self, ctx: RenderContext) -> Dict[str, Any]:
71
+ block: Dict[str, Any] = {"region": ctx.region}
72
+ for key in ("profile", "enterprise_project_id"):
73
+ if ctx.opt(key):
74
+ block[key] = ctx.opt(key)
75
+ return block
76
+
77
+ def physical_name(self, service: str, stage: str, logical: str) -> str:
78
+ name = super().physical_name(service, stage, logical)
79
+ return name if re.match(r"[a-z]", name) else f"f{name}"[: self.max_name_length]
80
+
81
+ def extra_validation(self, config: ServiceConfig) -> List[Issue]:
82
+ issues = []
83
+ has_http = any(fn.events_of("http") for fn in config.functions.values())
84
+ if has_http and not config.provider.options.get("apig_instance_id"):
85
+ issues.append(
86
+ Issue(
87
+ "error",
88
+ "HTTP events need a dedicated API Gateway instance: set provider.options.apig_instance_id",
89
+ "provider.options",
90
+ )
91
+ )
92
+ if config.provider.iam.statements:
93
+ issues.append(
94
+ Issue(
95
+ "warning",
96
+ "iam.statements are ignored on Huawei; set iam.role to an IAM agency name",
97
+ "provider.iam",
98
+ )
99
+ )
100
+ for name, fn in config.functions.items():
101
+ for ev in fn.events_of("http"):
102
+ if ev.auth not in _AUTH:
103
+ issues.append(
104
+ Issue(
105
+ "error",
106
+ f"http auth `{ev.auth}` is not supported on Huawei (none, iam, key)",
107
+ f"functions.{name}",
108
+ )
109
+ )
110
+ return issues
111
+
112
+ # ------------------------------------------------------------------ render
113
+ def render_resources(self, doc: TerraformDoc, ctx: RenderContext) -> None:
114
+ cfg = ctx.config
115
+ fn_out: Dict[str, Dict[str, Any]] = {}
116
+ gateway: Optional[Dict[str, str]] = None
117
+ for name, fn in ctx.function_items():
118
+ res = self._function(doc, ctx, name, fn)
119
+ out: Dict[str, Any] = {
120
+ "id": ref(res, "name"),
121
+ "arn": ref(res, "urn"),
122
+ "console": f"https://console.huaweicloud.com/functiongraph/?region={ctx.region}#/serverless/functions",
123
+ }
124
+ http_events = fn.events_of("http")
125
+ if http_events:
126
+ gateway = gateway or self._gateway(doc, ctx)
127
+ self._apis(doc, ctx, gateway, name, res, fn, http_events)
128
+ out["base_url"] = gateway["url"]
129
+ for i, ev in enumerate(fn.events_of("schedule")):
130
+ self._timer(doc, name, res, i, ev)
131
+ for ev in fn.events_of("storage"):
132
+ self._obs(doc, ctx, name, res, ev)
133
+ for ev in fn.events_of("topic"):
134
+ self._smn(doc, ctx, name, res, ev)
135
+ fn_out[name] = out
136
+
137
+ wf_out = {n: {"id": ref(FUNCTION, tf_id(f"{n}-orchestrator"), "name")} for n in sorted(cfg.workflows)}
138
+ doc.output(
139
+ "pskit",
140
+ {"functions": fn_out, "workflows": wf_out, "endpoint": gateway["url"] if gateway else None},
141
+ description="py-serverless-kit deployment summary",
142
+ )
143
+
144
+ # ------------------------------------------------------------------ pieces
145
+ def _function(self, doc: TerraformDoc, ctx: RenderContext, name: str, fn: FunctionConfig) -> str:
146
+ cfg = ctx.config
147
+ body: Dict[str, Any] = {
148
+ "name": ctx.physical_name(name),
149
+ "app": ctx.opt("app", "default"),
150
+ "description": fn.description[:512],
151
+ "handler": "index.handler",
152
+ "runtime": self.runtime_id(cfg.runtime_of(fn)),
153
+ "memory_size": cfg.memory_of(fn),
154
+ "timeout": cfg.timeout_of(fn),
155
+ "code_type": "zip",
156
+ "code_filename": "artifact.zip",
157
+ "func_code": '${filebase64("${path.module}/artifact.zip")}',
158
+ "user_data": escape(json.dumps(ctx.environment(name, fn), sort_keys=True)),
159
+ "tags": ctx.tags(fn),
160
+ }
161
+ agency = fn.iam.role or cfg.provider.iam.role
162
+ if agency:
163
+ body["agency"] = agency
164
+ if fn.concurrency:
165
+ body["concurrency_num"] = fn.concurrency
166
+ body.update(fn.options) # escape hatch: vpc_id, network_id, depend_list, ...
167
+ return doc.resource(FUNCTION, name, body)
168
+
169
+ def _gateway(self, doc: TerraformDoc, ctx: RenderContext) -> Dict[str, str]:
170
+ instance = ctx.opt("apig_instance_id")
171
+ group = doc.resource(
172
+ "huaweicloud_apig_group",
173
+ "http",
174
+ {
175
+ "instance_id": instance,
176
+ "name": re.sub(r"[^A-Za-z0-9_]", "_", ctx.prefix)[:64],
177
+ "description": f"py-serverless-kit {ctx.service} ({ctx.stage})",
178
+ "domain_access_enabled": True,
179
+ },
180
+ )
181
+ env = doc.resource(
182
+ "huaweicloud_apig_environment",
183
+ "stage",
184
+ {"instance_id": instance, "name": ("pskit_" + re.sub(r"[^A-Za-z0-9_]", "_", ctx.stage))[:64]},
185
+ )
186
+ groups = doc.data("huaweicloud_apig_groups", "http", {"instance_id": instance, "group_id": ref(group, "id")})
187
+ return {
188
+ "instance": instance,
189
+ "group": group,
190
+ "env": env,
191
+ "url": "https://" + ref(groups, "groups[0].sl_domain"),
192
+ }
193
+
194
+ def _apis(
195
+ self,
196
+ doc: TerraformDoc,
197
+ ctx: RenderContext,
198
+ gw: Dict[str, str],
199
+ name: str,
200
+ fn_res: str,
201
+ fn: FunctionConfig,
202
+ events: List[Any],
203
+ ) -> None:
204
+ for i, ev in enumerate(events):
205
+ path, matching = _gateway_path(ev.path)
206
+ body: Dict[str, Any] = {
207
+ "instance_id": gw["instance"],
208
+ "group_id": ref(gw["group"], "id"),
209
+ "name": re.sub(r"[^A-Za-z0-9_]", "_", f"{name}_{i}")[:64],
210
+ "type": "Public",
211
+ "request_protocol": "BOTH",
212
+ "request_method": ev.method,
213
+ "request_path": path,
214
+ "matching": matching,
215
+ "security_authentication": _AUTH.get(ev.auth, "NONE"),
216
+ "cors": ev.cors_config is not None,
217
+ "description": f"{ev.method} {ev.path} -> {name}",
218
+ "func_graph": {
219
+ "function_urn": ref(fn_res, "urn"),
220
+ "invocation_type": "sync",
221
+ "network_type": "NON-VPC",
222
+ "timeout": min(ctx.config.timeout_of(fn) * 1000, 600000),
223
+ },
224
+ }
225
+ params = re.findall(r"\{([A-Za-z_]\w*)\}", path)
226
+ if params:
227
+ body["request_params"] = [
228
+ {"name": p, "location": "PATH", "type": "STRING", "required": True, "passthrough": True}
229
+ for p in params
230
+ ]
231
+ api = doc.resource("huaweicloud_apig_api", f"{name}-{i}", body)
232
+ doc.resource(
233
+ "huaweicloud_apig_api_publishment",
234
+ f"{name}-{i}",
235
+ {"instance_id": gw["instance"], "api_id": ref(api, "id"), "env_id": ref(gw["env"], "id")},
236
+ )
237
+
238
+ def _timer(self, doc: TerraformDoc, name: str, fn_res: str, i: int, ev: Any) -> None:
239
+ timer: Dict[str, Any] = {
240
+ "name": re.sub(r"[^A-Za-z0-9_-]", "-", ev.name or f"timer-{i}")[:64],
241
+ "schedule_type": "Rate" if ev.rate else "Cron",
242
+ "schedule": rate_expression(ev) if ev.rate else quartz_cron(ev),
243
+ "additional_information": escape(json.dumps(envelope("schedule", ev.input, source="huawei.timer"))),
244
+ }
245
+ doc.resource(
246
+ TRIGGER,
247
+ f"{name}-timer-{i}",
248
+ {
249
+ "function_urn": ref(fn_res, "urn"),
250
+ "type": "TIMER",
251
+ "status": "ACTIVE" if ev.enabled else "DISABLED",
252
+ "timer": timer,
253
+ },
254
+ )
255
+
256
+ def _obs(self, doc: TerraformDoc, ctx: RenderContext, name: str, fn_res: str, ev: Any) -> None:
257
+ label = f"b-{ev.bucket}"
258
+ body: Dict[str, Any] = {
259
+ "function_urn": ref(fn_res, "urn"),
260
+ "type": "OBS",
261
+ "status": "ACTIVE",
262
+ "obs": {
263
+ "bucket_name": ev.bucket,
264
+ "event_notification_name": re.sub(r"[^A-Za-z0-9_-]", "-", f"pskit-{name}")[:64],
265
+ "events": [{"created": "ObjectCreated", "deleted": "ObjectRemoved"}[e] for e in ev.events],
266
+ },
267
+ }
268
+ if ev.prefix:
269
+ body["obs"]["prefix"] = ev.prefix
270
+ if ev.suffix:
271
+ body["obs"]["suffix"] = ev.suffix
272
+ if not ev.existing:
273
+ if not doc.has_resource("huaweicloud_obs_bucket", label):
274
+ doc.resource(
275
+ "huaweicloud_obs_bucket", label, {"bucket": ev.bucket, "acl": "private", "tags": ctx.tags()}
276
+ )
277
+ body["depends_on"] = [f"huaweicloud_obs_bucket.{tf_id(label)}"]
278
+ doc.resource(TRIGGER, f"{name}-obs-{ev.bucket}", body)
279
+
280
+ def _smn(self, doc: TerraformDoc, ctx: RenderContext, name: str, fn_res: str, ev: Any) -> None:
281
+ label = f"t-{ev.name}"
282
+ if ev.existing:
283
+ topic_urn = ref(doc.data("huaweicloud_smn_topics", label, {"name": ev.name}), "topics[0].topic_urn")
284
+ else:
285
+ if not doc.has_resource("huaweicloud_smn_topic", label):
286
+ doc.resource("huaweicloud_smn_topic", label, {"name": ev.name, "tags": ctx.tags()})
287
+ topic_urn = ref("huaweicloud_smn_topic", tf_id(label), "topic_urn")
288
+ doc.resource(
289
+ TRIGGER,
290
+ f"{name}-smn-{ev.name}",
291
+ {"function_urn": ref(fn_res, "urn"), "type": "SMN", "status": "ACTIVE", "smn": {"topic_urn": topic_urn}},
292
+ )
@@ -0,0 +1,132 @@
1
+ """IBM Cloud: Code Engine functions (HTTP endpoints)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Dict, List, Optional, Tuple
6
+
7
+ from ..config.schema import ServiceConfig
8
+ from .base import HTTP, Issue, Provider, RenderContext
9
+ from .terraform_doc import TerraformDoc, escape, ref, tf_id
10
+
11
+ FUNCTION = "ibm_code_engine_function"
12
+ # Code Engine function CPU / memory combinations (vCPU, memory).
13
+ _SIZES: Tuple[Tuple[str, str, int], ...] = (
14
+ ("0.125", "0.5G", 512),
15
+ ("0.25", "1G", 1024),
16
+ ("0.5", "2G", 2048),
17
+ ("1", "4G", 4096),
18
+ )
19
+
20
+
21
+ def size_for(memory_mb: int) -> Tuple[str, str]:
22
+ for cpu, mem, mb in _SIZES:
23
+ if memory_mb <= mb:
24
+ return cpu, mem
25
+ return _SIZES[-1][0], _SIZES[-1][1]
26
+
27
+
28
+ class IbmProvider(Provider):
29
+ name = "ibm"
30
+ display_name = "IBM Cloud Code Engine"
31
+ tier = "beta"
32
+ terraform_source = "IBM-Cloud/ibm"
33
+ terraform_version = ">= 1.70"
34
+ default_region = "us-south"
35
+ capabilities = frozenset({HTTP})
36
+ max_name_length = 63
37
+ runtimes = {"python3.11": "python-3.11"}
38
+ vendor_dependencies = True
39
+ max_timeout = 120
40
+ notes = (
41
+ "Every Code Engine function gets a public HTTPS endpoint; routes are matched by the pskit runtime. "
42
+ "No schedule/queue/storage/topic triggers (the Terraform provider has no function event "
43
+ "subscriptions). Code is uploaded inline as a base64 data URL, so keep bundles small or set "
44
+ "options.code_reference to a code bundle image. Auth: set IC_API_KEY."
45
+ )
46
+
47
+ # ------------------------------------------------------------------ bundle
48
+ def shim_files(self, config: ServiceConfig) -> Dict[str, str]:
49
+ return {
50
+ "__main__.py": (
51
+ "# Generated by py-serverless-kit. Code Engine entrypoint: main(params)\n"
52
+ "from py_serverless_kit.runtime.adapters.ibm import main # noqa: F401\n"
53
+ )
54
+ }
55
+
56
+ def provider_block(self, ctx: RenderContext) -> Dict[str, Any]:
57
+ block: Dict[str, Any] = {"region": ctx.region}
58
+ if ctx.opt("resource_group"):
59
+ block["resource_group"] = ctx.opt("resource_group")
60
+ return block
61
+
62
+ def extra_validation(self, config: ServiceConfig) -> List[Issue]:
63
+ issues: List[Issue] = []
64
+ for name, fn in config.functions.items():
65
+ for ev in fn.events_of("http"):
66
+ if ev.auth != "none":
67
+ issues.append(
68
+ Issue(
69
+ "error",
70
+ f"auth `{ev.auth}` is not supported on Code Engine functions; "
71
+ "validate tokens in the handler",
72
+ f"functions.{name}",
73
+ )
74
+ )
75
+ if config.memory_of(fn) > _SIZES[-1][2]:
76
+ issues.append(
77
+ Issue("error", "Code Engine functions support at most 4096 MB", f"functions.{name}.memory")
78
+ )
79
+ return issues
80
+
81
+ # ------------------------------------------------------------------ render
82
+ def _project(self, doc: TerraformDoc, ctx: RenderContext) -> str:
83
+ if ctx.opt("project_id"):
84
+ return str(ctx.opt("project_id"))
85
+ body: Dict[str, Any] = {"name": ctx.prefix}
86
+ if ctx.opt("resource_group_id"):
87
+ body["resource_group_id"] = ctx.opt("resource_group_id")
88
+ return ref(doc.resource("ibm_code_engine_project", "project", body), "project_id")
89
+
90
+ def render_resources(self, doc: TerraformDoc, ctx: RenderContext) -> None:
91
+ cfg = ctx.config
92
+ project = self._project(doc, ctx)
93
+ code_ref = ctx.opt("code_reference") or 'data:text/plain;base64,${filebase64("${path.module}/artifact.zip")}'
94
+ fn_out: Dict[str, Dict[str, Any]] = {}
95
+ endpoint: Optional[str] = None
96
+ for name, fn in ctx.function_items():
97
+ cpu, mem = size_for(cfg.memory_of(fn))
98
+ env = [
99
+ {"type": "literal", "name": k, "value": escape(v)} for k, v in sorted(ctx.environment(name, fn).items())
100
+ ]
101
+ body: Dict[str, Any] = {
102
+ "project_id": project,
103
+ "name": ctx.physical_name(name),
104
+ "runtime": self.runtime_id(cfg.runtime_of(fn)),
105
+ "code_reference": code_ref,
106
+ "code_binary": not ctx.opt("code_reference"),
107
+ "code_main": "main",
108
+ "scale_cpu_limit": cpu,
109
+ "scale_memory_limit": mem,
110
+ "scale_max_execution_time": cfg.timeout_of(fn),
111
+ "managed_domain_mappings": fn.options.get("visibility", ctx.opt("visibility", "local_public")),
112
+ "run_env_variables": env,
113
+ }
114
+ if fn.concurrency:
115
+ body["scale_concurrency"] = fn.concurrency
116
+ body.update({k: v for k, v in fn.options.items() if k != "visibility"})
117
+ res = doc.resource(FUNCTION, name, body)
118
+ out: Dict[str, Any] = {
119
+ "id": ref(res, "function_id"),
120
+ "url": ref(res, "endpoint"),
121
+ "console": f"https://cloud.ibm.com/codeengine/project/{ctx.region}/{project}/functions",
122
+ }
123
+ if fn.events_of("http"):
124
+ out["base_url"] = ref(res, "endpoint")
125
+ endpoint = endpoint or ref(res, "endpoint")
126
+ fn_out[name] = out
127
+ wf_out = {n: {"id": ref(FUNCTION, tf_id(f"{n}-orchestrator"), "endpoint")} for n in cfg.workflows}
128
+ doc.output(
129
+ "pskit",
130
+ {"functions": fn_out, "workflows": wf_out, "endpoint": endpoint},
131
+ description="py-serverless-kit deployment summary",
132
+ )