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,15 @@
1
+ from .base import Artifact, Issue, Provider, ProviderError, RenderContext
2
+ from .registry import all_providers, available, get_provider
3
+ from .terraform_doc import TerraformDoc
4
+
5
+ __all__ = [
6
+ "Artifact",
7
+ "Issue",
8
+ "Provider",
9
+ "ProviderError",
10
+ "RenderContext",
11
+ "TerraformDoc",
12
+ "all_providers",
13
+ "available",
14
+ "get_provider",
15
+ ]
@@ -0,0 +1,268 @@
1
+ """Alibaba Cloud: Function Compute 3.0 + HTTP / timer / OSS / MNS-topic triggers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any, Dict, List, Optional
7
+
8
+ from ..config.schema import FunctionConfig, ServiceConfig
9
+ from ..runtime.adapters.base import envelope
10
+ from . import schedule as sched
11
+ from .base import HTTP, SCHEDULE, STORAGE, TOPIC, Issue, Provider, RenderContext
12
+ from .terraform_doc import TerraformDoc, escape, ref, tf_id
13
+
14
+ FUNCTION = "alicloud_fcv3_function"
15
+ TRIGGER = "alicloud_fcv3_trigger"
16
+ ALL_METHODS = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]
17
+ _TRUST = json.dumps(
18
+ {
19
+ "Version": "1",
20
+ "Statement": [
21
+ {
22
+ "Action": "sts:AssumeRole",
23
+ "Effect": "Allow",
24
+ "Principal": {"Service": ["oss.aliyuncs.com", "mns.aliyuncs.com"]},
25
+ }
26
+ ],
27
+ }
28
+ )
29
+
30
+
31
+ class AlibabaProvider(Provider):
32
+ name = "alibaba"
33
+ display_name = "Alibaba Cloud Function Compute"
34
+ tier = "beta"
35
+ terraform_source = "aliyun/alicloud"
36
+ terraform_version = ">= 1.230.0"
37
+ terraform_local_name = "alicloud"
38
+ default_region = "cn-hangzhou"
39
+ capabilities = frozenset({HTTP, SCHEDULE, STORAGE, TOPIC})
40
+ max_name_length = 64
41
+ name_pattern = r"[^a-z0-9_-]"
42
+ runtimes = {"python3.9": "python3.9", "python3.10": "python3.10", "python3.12": "python3.12"}
43
+ vendor_dependencies = True
44
+ max_timeout = 3600 # FC allows up to 86400s, but the config schema caps timeout at 3600
45
+ notes = (
46
+ "FC 3.0 with one HTTP trigger per function (fcapp.run URL); schedules via timer triggers; "
47
+ "OSS and MNS-topic triggers; workflows run on the embedded engine."
48
+ )
49
+
50
+ # ------------------------------------------------------------------ bundle
51
+ def shim_files(self, config: ServiceConfig) -> Dict[str, str]:
52
+ return {
53
+ "index.py": (
54
+ "# Generated by py-serverless-kit. Function Compute handler: index.handler\n"
55
+ "from py_serverless_kit.runtime.adapters.alibaba import handler # noqa: F401\n"
56
+ )
57
+ }
58
+
59
+ def provider_block(self, ctx: RenderContext) -> Dict[str, Any]:
60
+ block: Dict[str, Any] = {"region": ctx.region}
61
+ if ctx.opt("profile"):
62
+ block["profile"] = ctx.opt("profile")
63
+ return block
64
+
65
+ def extra_validation(self, config: ServiceConfig) -> List[Issue]:
66
+ issues = []
67
+ if config.provider.iam.statements:
68
+ issues.append(
69
+ Issue(
70
+ "warning",
71
+ "iam.statements are not translated to RAM policies on Alibaba; set iam.role to an existing "
72
+ "RAM role ARN with the permissions your functions need",
73
+ "provider.iam",
74
+ )
75
+ )
76
+ for name, fn in config.functions.items():
77
+ if fn.iam.statements:
78
+ issues.append(
79
+ Issue("warning", "iam.statements are ignored on Alibaba; use iam.role", f"functions.{name}.iam")
80
+ )
81
+ for ev in fn.events_of("http"):
82
+ if ev.auth not in ("none",):
83
+ issues.append(
84
+ Issue(
85
+ "error",
86
+ f"http auth `{ev.auth}` is not supported on Alibaba (use none)",
87
+ f"functions.{name}",
88
+ )
89
+ )
90
+ return issues
91
+
92
+ # ------------------------------------------------------------------ render
93
+ def render_resources(self, doc: TerraformDoc, ctx: RenderContext) -> None:
94
+ cfg = ctx.config
95
+ needs_role = any(fn.events_of("storage") or fn.events_of("topic") for fn in cfg.functions.values())
96
+ role_arn = self._trigger_role(doc, ctx) if needs_role else None
97
+ account = doc.data("alicloud_account", "current", {}) if needs_role else None
98
+
99
+ fn_out: Dict[str, Dict[str, Any]] = {}
100
+ endpoint: Optional[str] = None
101
+ for name, fn in ctx.function_items():
102
+ res = self._function(doc, ctx, name, fn)
103
+ out: Dict[str, Any] = {
104
+ "id": ref(res, "function_name"),
105
+ "arn": ref(res, "function_arn"),
106
+ "console": f"https://fcnext.console.aliyun.com/{ctx.region}/functions/{ctx.physical_name(name)}",
107
+ }
108
+ http_events = fn.events_of("http")
109
+ if http_events:
110
+ trig = self._http(doc, name, res, http_events)
111
+ url = ref(trig, "http_trigger[0].url_internet")
112
+ out["url"] = url
113
+ out["base_url"] = url
114
+ endpoint = endpoint or url
115
+ for i, ev in enumerate(fn.events_of("schedule")):
116
+ self._timer(doc, name, res, i, ev)
117
+ for ev in fn.events_of("storage"):
118
+ self._oss(doc, ctx, name, res, ev, role_arn, account)
119
+ for ev in fn.events_of("topic"):
120
+ self._mns_topic(doc, ctx, name, res, ev, role_arn, account)
121
+ fn_out[name] = out
122
+
123
+ wf_out = {n: {"id": ref(FUNCTION, tf_id(f"{n}-orchestrator"), "function_name")} for n in sorted(cfg.workflows)}
124
+ doc.output(
125
+ "pskit",
126
+ {"functions": fn_out, "workflows": wf_out, "endpoint": endpoint},
127
+ description="py-serverless-kit deployment summary",
128
+ )
129
+
130
+ # ------------------------------------------------------------------ pieces
131
+ def _trigger_role(self, doc: TerraformDoc, ctx: RenderContext) -> str:
132
+ role = doc.resource(
133
+ "alicloud_ram_role",
134
+ "triggers",
135
+ {
136
+ "role_name": ctx.physical_name("triggers")[:64],
137
+ "assume_role_policy_document": _TRUST,
138
+ "description": f"py-serverless-kit trigger role for {ctx.prefix}",
139
+ "force": True,
140
+ },
141
+ )
142
+ doc.resource(
143
+ "alicloud_ram_role_policy_attachment",
144
+ "triggers-invoke",
145
+ {"role_name": ref(role, "role_name"), "policy_name": "AliyunFCInvocationAccess", "policy_type": "System"},
146
+ )
147
+ return ref(role, "arn")
148
+
149
+ def _function(self, doc: TerraformDoc, ctx: RenderContext, name: str, fn: FunctionConfig) -> str:
150
+ cfg = ctx.config
151
+ body: Dict[str, Any] = {
152
+ "function_name": ctx.physical_name(name),
153
+ "description": fn.description[:256],
154
+ "handler": "index.handler",
155
+ "runtime": self.runtime_id(cfg.runtime_of(fn)),
156
+ "memory_size": cfg.memory_of(fn),
157
+ "timeout": cfg.timeout_of(fn),
158
+ "internet_access": True,
159
+ "environment_variables": {k: escape(v) for k, v in ctx.environment(name, fn).items()},
160
+ "code": {"zip_file": '${filebase64("${path.module}/artifact.zip")}'},
161
+ "tags": ctx.tags(fn),
162
+ }
163
+ role = fn.iam.role or cfg.provider.iam.role
164
+ if role:
165
+ body["role"] = role
166
+ if fn.concurrency:
167
+ body["instance_concurrency"] = fn.concurrency
168
+ body.update(fn.options) # escape hatch: vpc_config, layers, log_config, ...
169
+ return doc.resource(FUNCTION, name, body)
170
+
171
+ def _http(self, doc: TerraformDoc, name: str, fn_res: str, events: List[Any]) -> str:
172
+ methods: List[str] = []
173
+ for ev in events:
174
+ for m in ALL_METHODS if ev.method == "ANY" else [ev.method]:
175
+ if m not in methods:
176
+ methods.append(m)
177
+ if any(ev.cors_config is not None for ev in events) and "OPTIONS" not in methods:
178
+ methods.append("OPTIONS")
179
+ config = {"authType": "anonymous", "methods": methods, "disableURLInternet": False}
180
+ return doc.resource(
181
+ TRIGGER,
182
+ f"{name}-http",
183
+ {
184
+ "function_name": ref(fn_res, "function_name"),
185
+ "trigger_name": "http",
186
+ "trigger_type": "http",
187
+ "qualifier": "LATEST",
188
+ "trigger_config": json.dumps(config),
189
+ },
190
+ )
191
+
192
+ def _timer(self, doc: TerraformDoc, name: str, fn_res: str, i: int, ev: Any) -> None:
193
+ expr = sched.every_expression(ev) if ev.rate else "0 " + sched.named_cron5(ev)
194
+ if ev.cron and ev.timezone and ev.timezone != "UTC":
195
+ expr = f"CRON_TZ={ev.timezone} {expr}"
196
+ config = {
197
+ "cronExpression": expr,
198
+ "enable": bool(ev.enabled),
199
+ "payload": json.dumps(envelope("schedule", ev.input, source="alibaba.timer")),
200
+ }
201
+ doc.resource(
202
+ TRIGGER,
203
+ f"{name}-timer-{i}",
204
+ {
205
+ "function_name": ref(fn_res, "function_name"),
206
+ "trigger_name": (ev.name or f"timer-{i}")[:128],
207
+ "trigger_type": "timer",
208
+ "qualifier": "LATEST",
209
+ "trigger_config": escape(json.dumps(config)),
210
+ },
211
+ )
212
+
213
+ def _oss(
214
+ self,
215
+ doc: TerraformDoc,
216
+ ctx: RenderContext,
217
+ name: str,
218
+ fn_res: str,
219
+ ev: Any,
220
+ role: Optional[str],
221
+ account: Optional[str],
222
+ ) -> None:
223
+ label = f"b-{ev.bucket}"
224
+ if not ev.existing and not doc.has_resource("alicloud_oss_bucket", label):
225
+ doc.resource("alicloud_oss_bucket", label, {"bucket": ev.bucket, "tags": ctx.tags()})
226
+ events = [{"created": "oss:ObjectCreated:*", "deleted": "oss:ObjectRemoved:*"}[e] for e in ev.events]
227
+ config = {"events": events, "filter": {"key": {"prefix": ev.prefix or "", "suffix": ev.suffix or ""}}}
228
+ body: Dict[str, Any] = {
229
+ "function_name": ref(fn_res, "function_name"),
230
+ "trigger_name": f"oss-{tf_id(ev.bucket)}"[:128],
231
+ "trigger_type": "oss",
232
+ "qualifier": "LATEST",
233
+ "invocation_role": role,
234
+ "source_arn": f"acs:oss:{ctx.region}:{ref(str(account), 'id')}:{ev.bucket}",
235
+ "trigger_config": json.dumps(config),
236
+ }
237
+ if not ev.existing:
238
+ body["depends_on"] = [f"alicloud_oss_bucket.{tf_id(label)}"]
239
+ doc.resource(TRIGGER, f"{name}-oss-{ev.bucket}", body)
240
+
241
+ def _mns_topic(
242
+ self,
243
+ doc: TerraformDoc,
244
+ ctx: RenderContext,
245
+ name: str,
246
+ fn_res: str,
247
+ ev: Any,
248
+ role: Optional[str],
249
+ account: Optional[str],
250
+ ) -> None:
251
+ label = f"t-{ev.name}"
252
+ if not ev.existing and not doc.has_resource("alicloud_message_service_topic", label):
253
+ doc.resource("alicloud_message_service_topic", label, {"topic_name": ev.name, "tags": ctx.tags()})
254
+ config: Dict[str, Any] = {"notifyContentFormat": "JSON", "notifyStrategy": "BACKOFF_RETRY"}
255
+ if ev.filter and isinstance(ev.filter.get("tag"), str):
256
+ config["filterTag"] = ev.filter["tag"]
257
+ body: Dict[str, Any] = {
258
+ "function_name": ref(fn_res, "function_name"),
259
+ "trigger_name": f"mns-{tf_id(ev.name)}"[:128],
260
+ "trigger_type": "mns_topic",
261
+ "qualifier": "LATEST",
262
+ "invocation_role": role,
263
+ "source_arn": f"acs:mns:{ctx.region}:{ref(str(account), 'id')}:/topics/{ev.name}",
264
+ "trigger_config": json.dumps(config),
265
+ }
266
+ if not ev.existing:
267
+ body["depends_on"] = [f"alicloud_message_service_topic.{tf_id(label)}"]
268
+ doc.resource(TRIGGER, f"{name}-mns-{ev.name}", body)