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,176 @@
|
|
|
1
|
+
"""Google Cloud Functions (2nd gen) adapter.
|
|
2
|
+
|
|
3
|
+
Two entrypoints are exposed through the generated ``main.py``:
|
|
4
|
+
|
|
5
|
+
* :func:`http` - ``@functions_framework.http``; receives a Flask request. A body
|
|
6
|
+
carrying the ``__pskit`` envelope (Cloud Scheduler, Cloud Workflows,
|
|
7
|
+
``invoke()``) is dispatched as a *non-HTTP* event and answered with the raw
|
|
8
|
+
JSON result, or a 500 ``{"errorType", "cause"}`` that the workflow compiler
|
|
9
|
+
understands.
|
|
10
|
+
* :func:`cloud_event` - ``@functions_framework.cloud_event``; Pub/Sub
|
|
11
|
+
``messagePublished`` (queue / topic) and Cloud Storage object events.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import uuid
|
|
19
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
20
|
+
|
|
21
|
+
from ..context import Context
|
|
22
|
+
from ..dispatch import execute
|
|
23
|
+
from ..event import Event, EventType, Headers, HttpRequest, Record, parse_time
|
|
24
|
+
from ..response import Response, dumps
|
|
25
|
+
from .base import action_from, b64_text, function_name, unwrap_envelope
|
|
26
|
+
|
|
27
|
+
PROVIDER = "gcp"
|
|
28
|
+
_JSON = "application/json"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _context(fn: str, request_id: Optional[str] = None, raw: Any = None) -> Context:
|
|
32
|
+
return Context(request_id=request_id or str(uuid.uuid4()), function=fn, provider=PROVIDER, raw=raw)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _request_id(headers: Any) -> Optional[str]:
|
|
36
|
+
for key in ("Function-Execution-Id", "X-Cloud-Trace-Context"):
|
|
37
|
+
value = headers.get(key) if headers is not None else None
|
|
38
|
+
if value:
|
|
39
|
+
return str(value).split("/")[0]
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ------------------------------------------------------------------ HTTP
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def to_http_event(request: Any, fn: str) -> Event:
|
|
47
|
+
headers = dict(request.headers.items()) if hasattr(request.headers, "items") else dict(request.headers or {})
|
|
48
|
+
args = getattr(request, "args", None) or {}
|
|
49
|
+
multi: Dict[str, List[str]] = {}
|
|
50
|
+
if hasattr(args, "getlist"):
|
|
51
|
+
multi = {k: list(args.getlist(k)) for k in args.keys()}
|
|
52
|
+
query = {k: (v[-1] if v else "") for k, v in multi.items()} if multi else dict(args)
|
|
53
|
+
req = HttpRequest(
|
|
54
|
+
method=request.method,
|
|
55
|
+
path=request.path or "/",
|
|
56
|
+
headers=Headers(headers),
|
|
57
|
+
query=query,
|
|
58
|
+
multi_query=multi or {k: [v] for k, v in query.items()},
|
|
59
|
+
body=request.get_data() or b"",
|
|
60
|
+
source_ip=headers.get("X-Forwarded-For", "").split(",")[0].strip() or getattr(request, "remote_addr", None),
|
|
61
|
+
)
|
|
62
|
+
return Event(type=EventType.HTTP, provider=PROVIDER, function=fn, http=req, raw=request)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _render(resp: Response) -> Tuple[bytes, int, List[Tuple[str, str]]]:
|
|
66
|
+
status, headers, body = resp.render()
|
|
67
|
+
items = list(headers.items()) + [("set-cookie", c) for c in resp.cookies]
|
|
68
|
+
return body, status, items
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def http(request: Any) -> Tuple[bytes, int, Any]:
|
|
72
|
+
fn = function_name()
|
|
73
|
+
body = request.get_data() or b""
|
|
74
|
+
req_id = _request_id(request.headers)
|
|
75
|
+
wrapped = unwrap_envelope(body) if body[:1] == b"{" else None
|
|
76
|
+
if wrapped is not None:
|
|
77
|
+
kind, data, meta = wrapped
|
|
78
|
+
event = Event(type=kind, provider=PROVIDER, function=fn, data=data, source=meta.get("source"), raw=request)
|
|
79
|
+
try:
|
|
80
|
+
result = execute(event, _context(fn, req_id, request))
|
|
81
|
+
except Exception as exc: # noqa: BLE001 - reported to the caller (workflow / scheduler)
|
|
82
|
+
payload = {"errorType": type(exc).__name__, "cause": str(exc)}
|
|
83
|
+
return dumps(payload).encode("utf-8"), 500, [("content-type", _JSON)]
|
|
84
|
+
return dumps(result).encode("utf-8"), 200, [("content-type", _JSON)]
|
|
85
|
+
event = to_http_event(request, fn)
|
|
86
|
+
return _render(execute(event, _context(fn, req_id, request)))
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# ------------------------------------------------------------------ CloudEvents
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _ce_attr(ce: Any, key: str, default: Any = None) -> Any:
|
|
93
|
+
try:
|
|
94
|
+
value = ce[key]
|
|
95
|
+
except (KeyError, TypeError, IndexError):
|
|
96
|
+
value = None
|
|
97
|
+
if value is None:
|
|
98
|
+
value = getattr(ce, key, None)
|
|
99
|
+
return default if value is None else value
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _ce_data(ce: Any) -> Any:
|
|
103
|
+
data = getattr(ce, "data", None)
|
|
104
|
+
if data is None and isinstance(ce, dict):
|
|
105
|
+
data = ce.get("data")
|
|
106
|
+
if isinstance(data, (bytes, str)):
|
|
107
|
+
try:
|
|
108
|
+
data = json.loads(data)
|
|
109
|
+
except ValueError:
|
|
110
|
+
pass
|
|
111
|
+
return data
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _storage_wanted(key: str) -> bool:
|
|
115
|
+
prefix = os.environ.get("PSKIT_STORAGE_PREFIX", "")
|
|
116
|
+
suffix = os.environ.get("PSKIT_STORAGE_SUFFIX", "")
|
|
117
|
+
return key.startswith(prefix) and key.endswith(suffix)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def to_cloud_event(ce: Any, fn: str = "") -> Optional[Event]:
|
|
121
|
+
"""Translate a CloudEvent; returns ``None`` when filtered out (storage prefix/suffix)."""
|
|
122
|
+
ce_type = str(_ce_attr(ce, "type", ""))
|
|
123
|
+
source = _ce_attr(ce, "source")
|
|
124
|
+
when = parse_time(_ce_attr(ce, "time"))
|
|
125
|
+
data = _ce_data(ce) or {}
|
|
126
|
+
if ce_type.startswith("google.cloud.pubsub.topic"):
|
|
127
|
+
msg = data.get("message") or {}
|
|
128
|
+
body = b64_text(msg.get("data"))
|
|
129
|
+
wrapped = unwrap_envelope(body) if isinstance(body, str) else None
|
|
130
|
+
if wrapped is not None: # Cloud Scheduler -> Pub/Sub target
|
|
131
|
+
kind, payload, meta = wrapped
|
|
132
|
+
return Event(
|
|
133
|
+
type=kind,
|
|
134
|
+
provider=PROVIDER,
|
|
135
|
+
function=fn,
|
|
136
|
+
data=payload,
|
|
137
|
+
time=when,
|
|
138
|
+
source=meta.get("source", source),
|
|
139
|
+
raw=ce,
|
|
140
|
+
)
|
|
141
|
+
kind = EventType.QUEUE if os.environ.get("PSKIT_TRIGGER_KIND") == "queue" else EventType.TOPIC
|
|
142
|
+
record = Record(
|
|
143
|
+
id=msg.get("messageId") or msg.get("message_id") or _ce_attr(ce, "id"),
|
|
144
|
+
body=body,
|
|
145
|
+
attributes=dict(msg.get("attributes") or {}),
|
|
146
|
+
source=data.get("subscription") or source,
|
|
147
|
+
raw=msg,
|
|
148
|
+
)
|
|
149
|
+
return Event(type=kind, provider=PROVIDER, function=fn, records=[record], time=when, source=source, raw=ce)
|
|
150
|
+
if ce_type.startswith("google.cloud.storage.object"):
|
|
151
|
+
key = data.get("name", "")
|
|
152
|
+
if not _storage_wanted(key):
|
|
153
|
+
return None
|
|
154
|
+
size = data.get("size")
|
|
155
|
+
record = Record(
|
|
156
|
+
id=str(data.get("generation") or _ce_attr(ce, "id") or ""),
|
|
157
|
+
bucket=data.get("bucket"),
|
|
158
|
+
key=key,
|
|
159
|
+
size=int(size) if size not in (None, "") else None,
|
|
160
|
+
action=action_from(ce_type.rsplit(".", 1)[-1]),
|
|
161
|
+
attributes={"content_type": data.get("contentType"), "event_type": ce_type},
|
|
162
|
+
source=source,
|
|
163
|
+
raw=data,
|
|
164
|
+
)
|
|
165
|
+
return Event(
|
|
166
|
+
type=EventType.STORAGE, provider=PROVIDER, function=fn, records=[record], time=when, source=source, raw=ce
|
|
167
|
+
)
|
|
168
|
+
return Event(type=EventType.INVOKE, provider=PROVIDER, function=fn, data=data, time=when, source=source, raw=ce)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def cloud_event(ce: Any) -> Any:
|
|
172
|
+
fn = function_name()
|
|
173
|
+
event = to_cloud_event(ce, fn)
|
|
174
|
+
if event is None:
|
|
175
|
+
return None
|
|
176
|
+
return execute(event, _context(fn, str(_ce_attr(ce, "id", "")) or None, ce))
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Huawei Cloud FunctionGraph adapter.
|
|
2
|
+
|
|
3
|
+
Handles APIG (dedicated gateway) events, timer triggers, OBS triggers, SMN
|
|
4
|
+
topic triggers and direct invocation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import time
|
|
10
|
+
from typing import Any, Dict
|
|
11
|
+
from urllib.parse import unquote_plus
|
|
12
|
+
|
|
13
|
+
from ..context import Context
|
|
14
|
+
from ..dispatch import execute
|
|
15
|
+
from ..event import Event, EventType, Headers, HttpRequest, Record, parse_time
|
|
16
|
+
from ..response import Response
|
|
17
|
+
from .base import action_from, function_name, maybe_json, unwrap_envelope
|
|
18
|
+
|
|
19
|
+
PROVIDER = "huawei"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _call(ctx: Any, method: str, default: Any = None) -> Any:
|
|
23
|
+
fn = getattr(ctx, method, None)
|
|
24
|
+
if callable(fn):
|
|
25
|
+
try:
|
|
26
|
+
return fn()
|
|
27
|
+
except Exception: # noqa: BLE001 - context helpers are best effort
|
|
28
|
+
return default
|
|
29
|
+
return default
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _http(evt: Dict[str, Any]) -> HttpRequest:
|
|
33
|
+
rc = evt.get("requestContext") or {}
|
|
34
|
+
query: Dict[str, str] = {}
|
|
35
|
+
multi = {}
|
|
36
|
+
for key, value in (evt.get("queryStringParameters") or {}).items():
|
|
37
|
+
values = [str(v) for v in value] if isinstance(value, list) else [str(value)]
|
|
38
|
+
multi[key] = values
|
|
39
|
+
query[key] = values[-1] if values else ""
|
|
40
|
+
return HttpRequest(
|
|
41
|
+
method=evt.get("httpMethod", "GET"),
|
|
42
|
+
path=evt.get("path") or "/",
|
|
43
|
+
headers=Headers(evt.get("headers") or {}),
|
|
44
|
+
query=query,
|
|
45
|
+
multi_query=multi,
|
|
46
|
+
path_params={k: str(v) for k, v in (evt.get("pathParameters") or {}).items()},
|
|
47
|
+
body=HttpRequest.decode_body(evt.get("body"), bool(evt.get("isBase64Encoded"))),
|
|
48
|
+
source_ip=rc.get("sourceIp"),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _obs(evt: Dict[str, Any], fn: str) -> Event:
|
|
53
|
+
records = []
|
|
54
|
+
for r in evt.get("Records") or []:
|
|
55
|
+
obs = r.get("obs") or r.get("s3") or {}
|
|
56
|
+
obj = obs.get("object") or {}
|
|
57
|
+
records.append(
|
|
58
|
+
Record(
|
|
59
|
+
id=obj.get("sequencer"),
|
|
60
|
+
bucket=(obs.get("bucket") or {}).get("name"),
|
|
61
|
+
key=unquote_plus(obj.get("key", "")),
|
|
62
|
+
size=obj.get("size"),
|
|
63
|
+
action=action_from(r.get("eventName", "")),
|
|
64
|
+
attributes={"event_name": r.get("eventName"), "etag": obj.get("eTag")},
|
|
65
|
+
source="obs",
|
|
66
|
+
raw=r,
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
first = (evt.get("Records") or [{}])[0]
|
|
70
|
+
return Event(
|
|
71
|
+
type=EventType.STORAGE,
|
|
72
|
+
provider=PROVIDER,
|
|
73
|
+
function=fn,
|
|
74
|
+
records=records,
|
|
75
|
+
source="obs",
|
|
76
|
+
time=parse_time(first.get("eventTime")),
|
|
77
|
+
raw=evt,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _smn(evt: Dict[str, Any], fn: str) -> Event:
|
|
82
|
+
records = []
|
|
83
|
+
for r in evt.get("record") or []:
|
|
84
|
+
smn = r.get("smn") or {}
|
|
85
|
+
records.append(
|
|
86
|
+
Record(
|
|
87
|
+
id=smn.get("message_id"),
|
|
88
|
+
body=smn.get("message"),
|
|
89
|
+
attributes={"subject": smn.get("subject"), **(smn.get("message_attributes") or {})},
|
|
90
|
+
source=smn.get("topic_urn"),
|
|
91
|
+
raw=r,
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
first = ((evt.get("record") or [{}])[0].get("smn")) or {}
|
|
95
|
+
return Event(
|
|
96
|
+
type=EventType.TOPIC,
|
|
97
|
+
provider=PROVIDER,
|
|
98
|
+
function=fn,
|
|
99
|
+
records=records,
|
|
100
|
+
source=first.get("topic_urn"),
|
|
101
|
+
time=parse_time(first.get("timestamp")),
|
|
102
|
+
raw=evt,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def to_event(native: Any, ctx: Any = None) -> Event:
|
|
107
|
+
fn = function_name(_call(ctx, "getFunctionName", "") or "")
|
|
108
|
+
evt = maybe_json(native)
|
|
109
|
+
wrapped = unwrap_envelope(evt)
|
|
110
|
+
if wrapped is not None:
|
|
111
|
+
kind, data, meta = wrapped
|
|
112
|
+
return Event(type=kind, provider=PROVIDER, function=fn, data=data, source=meta.get("source"), raw=evt)
|
|
113
|
+
if isinstance(evt, dict):
|
|
114
|
+
if "httpMethod" in evt and ("requestContext" in evt or "path" in evt):
|
|
115
|
+
return Event(type=EventType.HTTP, provider=PROVIDER, function=fn, http=_http(evt), raw=evt)
|
|
116
|
+
if str(evt.get("trigger_type", "")).upper() == "TIMER":
|
|
117
|
+
user_event = evt.get("user_event")
|
|
118
|
+
inner = unwrap_envelope(user_event) if user_event else None
|
|
119
|
+
return Event(
|
|
120
|
+
type=EventType.SCHEDULE,
|
|
121
|
+
provider=PROVIDER,
|
|
122
|
+
function=fn,
|
|
123
|
+
data=inner[1] if inner else (maybe_json(user_event) if user_event else None),
|
|
124
|
+
time=parse_time(evt.get("time")),
|
|
125
|
+
source=evt.get("trigger_name"),
|
|
126
|
+
raw=evt,
|
|
127
|
+
)
|
|
128
|
+
records = evt.get("Records")
|
|
129
|
+
if (
|
|
130
|
+
isinstance(records, list)
|
|
131
|
+
and records
|
|
132
|
+
and isinstance(records[0], dict)
|
|
133
|
+
and ("obs" in records[0] or "s3" in records[0])
|
|
134
|
+
):
|
|
135
|
+
return _obs(evt, fn)
|
|
136
|
+
smn = evt.get("record")
|
|
137
|
+
if isinstance(smn, list) and smn and isinstance(smn[0], dict) and "smn" in smn[0]:
|
|
138
|
+
return _smn(evt, fn)
|
|
139
|
+
return Event(type=EventType.INVOKE, provider=PROVIDER, function=fn, data=evt, raw=native)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def to_context(ctx: Any, fn: str) -> Context:
|
|
143
|
+
if ctx is None:
|
|
144
|
+
return Context(function=fn, provider=PROVIDER)
|
|
145
|
+
remaining = getattr(ctx, "getRemainingTimeInMilliSeconds", None)
|
|
146
|
+
memory = _call(ctx, "getMemorySize")
|
|
147
|
+
remaining_ms = _call(ctx, "getRemainingTimeInMilliSeconds")
|
|
148
|
+
return Context(
|
|
149
|
+
request_id=str(_call(ctx, "getRequestID", "") or ""),
|
|
150
|
+
function=fn,
|
|
151
|
+
provider=PROVIDER,
|
|
152
|
+
memory_mb=int(memory) if memory else None,
|
|
153
|
+
deadline=(time.time() + float(remaining_ms) / 1000.0) if remaining_ms else None,
|
|
154
|
+
raw=ctx,
|
|
155
|
+
_remaining=remaining if callable(remaining) else None,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def to_native(result: Any, event: Event) -> Any:
|
|
160
|
+
if event.http is not None:
|
|
161
|
+
resp: Response = result
|
|
162
|
+
status, headers, body, is_b64 = resp.render_for_gateway()
|
|
163
|
+
if resp.cookies:
|
|
164
|
+
headers["set-cookie"] = resp.cookies[0] if len(resp.cookies) == 1 else ", ".join(resp.cookies)
|
|
165
|
+
return {"statusCode": status, "isBase64Encoded": is_b64, "headers": headers, "body": body}
|
|
166
|
+
return result
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def handler(native: Any, ctx: Any = None) -> Any:
|
|
170
|
+
"""FunctionGraph entrypoint - the generated ``index.handler`` points here."""
|
|
171
|
+
event = to_event(native, ctx)
|
|
172
|
+
context = to_context(ctx, event.function)
|
|
173
|
+
return to_native(execute(event, context), event)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""IBM Cloud Code Engine functions adapter.
|
|
2
|
+
|
|
3
|
+
Code Engine calls ``main(params)``; HTTP request data arrives in ``__ce_*`` keys
|
|
4
|
+
(``__ow_*`` from the older Cloud Functions / OpenWhisk platform is accepted too)
|
|
5
|
+
and the function returns ``{"statusCode", "headers", "body"}``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
from typing import Any, Dict, Optional
|
|
12
|
+
from urllib.parse import parse_qs
|
|
13
|
+
|
|
14
|
+
from ..context import Context
|
|
15
|
+
from ..event import Event, EventType, Headers, HttpRequest
|
|
16
|
+
from ..response import dumps
|
|
17
|
+
from ._httpfn import decode_body, event_from_request, run
|
|
18
|
+
from .base import function_name, unwrap_envelope
|
|
19
|
+
|
|
20
|
+
PROVIDER = "ibm"
|
|
21
|
+
_PREFIXES = ("__ce_", "__ow_")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _get(params: Dict[str, Any], key: str) -> Any:
|
|
25
|
+
for prefix in _PREFIXES:
|
|
26
|
+
if prefix + key in params:
|
|
27
|
+
return params[prefix + key]
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _query(raw: Any) -> Dict[str, Any]:
|
|
32
|
+
if isinstance(raw, dict):
|
|
33
|
+
return {str(k): v if isinstance(v, list) else [str(v)] for k, v in raw.items()}
|
|
34
|
+
if isinstance(raw, str) and raw:
|
|
35
|
+
return parse_qs(raw.lstrip("?"), keep_blank_values=True)
|
|
36
|
+
return {}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def to_request(params: Dict[str, Any]) -> Optional[HttpRequest]:
|
|
40
|
+
method = _get(params, "method")
|
|
41
|
+
if not method:
|
|
42
|
+
return None
|
|
43
|
+
headers = dict(_get(params, "headers") or {})
|
|
44
|
+
body = _get(params, "body")
|
|
45
|
+
if body is None:
|
|
46
|
+
# Code Engine merges JSON bodies into params; rebuild it from the plain keys.
|
|
47
|
+
plain = {k: v for k, v in params.items() if not k.startswith(_PREFIXES)}
|
|
48
|
+
body = dumps(plain) if plain else None
|
|
49
|
+
multi = _query(_get(params, "query"))
|
|
50
|
+
return HttpRequest(
|
|
51
|
+
method=str(method),
|
|
52
|
+
path=_get(params, "path") or "/",
|
|
53
|
+
headers=Headers(headers),
|
|
54
|
+
query={k: v[-1] for k, v in multi.items() if v},
|
|
55
|
+
multi_query=multi,
|
|
56
|
+
body=decode_body(body, headers),
|
|
57
|
+
source_ip=headers.get("x-forwarded-for", "").split(",")[0].strip() or None,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def main(params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
62
|
+
params = params or {}
|
|
63
|
+
fn = function_name()
|
|
64
|
+
req = to_request(params)
|
|
65
|
+
if req is None:
|
|
66
|
+
# Direct invocation without HTTP metadata: params are the payload.
|
|
67
|
+
wrapped = unwrap_envelope(params)
|
|
68
|
+
if wrapped is not None:
|
|
69
|
+
event = Event(type=wrapped[0], provider=PROVIDER, function=fn, data=wrapped[1], raw=params)
|
|
70
|
+
else:
|
|
71
|
+
event = Event(type=EventType.INVOKE, provider=PROVIDER, function=fn, data=params, raw=params)
|
|
72
|
+
else:
|
|
73
|
+
event = event_from_request(req, PROVIDER, fn, raw=params)
|
|
74
|
+
context = Context(
|
|
75
|
+
request_id=os.environ.get("CE_REQUEST_ID", "") or headers_id(params),
|
|
76
|
+
function=fn,
|
|
77
|
+
provider=PROVIDER,
|
|
78
|
+
raw=params,
|
|
79
|
+
)
|
|
80
|
+
status, headers, body, _is_b64 = run(event, context)
|
|
81
|
+
return {"statusCode": status, "headers": headers, "body": body}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def headers_id(params: Dict[str, Any]) -> str:
|
|
85
|
+
headers = _get(params, "headers") or {}
|
|
86
|
+
for key in ("x-request-id", "X-Request-Id", "x-ce-request-id"):
|
|
87
|
+
if key in headers:
|
|
88
|
+
return str(headers[key])
|
|
89
|
+
import uuid
|
|
90
|
+
|
|
91
|
+
return str(uuid.uuid4())
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Oracle Cloud (OCI) Functions adapter, built on the Fn Project Python FDK.
|
|
2
|
+
|
|
3
|
+
``func.py`` exposes ``handler(ctx, data)``. Invocations come from:
|
|
4
|
+
|
|
5
|
+
* OCI API Gateway - the FDK context carries ``Fn-Http-Method``,
|
|
6
|
+
``Fn-Http-Request-Url`` and ``Fn-Http-H-<name>`` headers;
|
|
7
|
+
* OCI Events (object storage) - a CloudEvents JSON body;
|
|
8
|
+
* direct invoke (``oci fn function invoke``) / the ``__pskit`` envelope.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import base64
|
|
14
|
+
import io
|
|
15
|
+
from typing import Any, Dict, Optional
|
|
16
|
+
from urllib.parse import parse_qs, urlsplit
|
|
17
|
+
|
|
18
|
+
from ..context import Context
|
|
19
|
+
from ..event import Event, EventType, Headers, HttpRequest, Record, parse_time
|
|
20
|
+
from ._httpfn import event_from_request, run
|
|
21
|
+
from .base import action_from, function_name, maybe_json, unwrap_envelope
|
|
22
|
+
|
|
23
|
+
PROVIDER = "oracle"
|
|
24
|
+
_HEADER_PREFIX = "fn-http-h-"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _call(obj: Any, name: str, default: Any = None) -> Any:
|
|
28
|
+
fn = getattr(obj, name, None)
|
|
29
|
+
if callable(fn):
|
|
30
|
+
try:
|
|
31
|
+
return fn()
|
|
32
|
+
except Exception: # noqa: BLE001 - FDK accessors are best effort
|
|
33
|
+
return default
|
|
34
|
+
return default
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _ctx_headers(ctx: Any) -> Dict[str, Any]:
|
|
38
|
+
raw = _call(ctx, "Headers") or {}
|
|
39
|
+
return {str(k).lower(): v for k, v in dict(raw).items()}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _first(value: Any) -> Any:
|
|
43
|
+
if isinstance(value, (list, tuple)):
|
|
44
|
+
return value[0] if value else None
|
|
45
|
+
return value
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _read(data: Any) -> bytes:
|
|
49
|
+
if data is None:
|
|
50
|
+
return b""
|
|
51
|
+
if isinstance(data, (bytes, bytearray)):
|
|
52
|
+
return bytes(data)
|
|
53
|
+
if isinstance(data, str):
|
|
54
|
+
return data.encode("utf-8")
|
|
55
|
+
if hasattr(data, "getvalue"):
|
|
56
|
+
return data.getvalue()
|
|
57
|
+
if hasattr(data, "read"):
|
|
58
|
+
return data.read()
|
|
59
|
+
return str(data).encode("utf-8")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def to_request(ctx: Any, body: bytes) -> Optional[HttpRequest]:
|
|
63
|
+
headers = _ctx_headers(ctx)
|
|
64
|
+
method = _first(headers.get("fn-http-method"))
|
|
65
|
+
if not method:
|
|
66
|
+
http_headers = _call(ctx, "HttpHeaders")
|
|
67
|
+
if http_headers is None:
|
|
68
|
+
return None
|
|
69
|
+
method = _call(ctx, "Method")
|
|
70
|
+
if not method:
|
|
71
|
+
return None
|
|
72
|
+
url = _first(headers.get("fn-http-request-url")) or _call(ctx, "RequestURL") or "/"
|
|
73
|
+
http_headers = _call(ctx, "HttpHeaders")
|
|
74
|
+
if http_headers is None:
|
|
75
|
+
http_headers = {k[len(_HEADER_PREFIX) :]: v for k, v in headers.items() if k.startswith(_HEADER_PREFIX)}
|
|
76
|
+
parts = urlsplit(str(url))
|
|
77
|
+
multi = parse_qs(parts.query, keep_blank_values=True)
|
|
78
|
+
return HttpRequest(
|
|
79
|
+
method=str(method),
|
|
80
|
+
path=parts.path or "/",
|
|
81
|
+
headers=Headers(dict(http_headers)),
|
|
82
|
+
query={k: v[-1] for k, v in multi.items()},
|
|
83
|
+
multi_query=multi,
|
|
84
|
+
body=body,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _cloud_event(payload: Dict[str, Any], fn: str) -> Optional[Event]:
|
|
89
|
+
etype = str(payload.get("eventType") or payload.get("type") or "")
|
|
90
|
+
if "objectstorage" not in etype:
|
|
91
|
+
return None
|
|
92
|
+
data = payload.get("data") or {}
|
|
93
|
+
details = data.get("additionalDetails") or {}
|
|
94
|
+
record = Record(
|
|
95
|
+
id=str(payload.get("eventID") or payload.get("id") or ""),
|
|
96
|
+
bucket=details.get("bucketName"),
|
|
97
|
+
key=data.get("resourceName"),
|
|
98
|
+
action=action_from(etype),
|
|
99
|
+
attributes={"event_type": etype, "namespace": details.get("namespace")},
|
|
100
|
+
source=str(payload.get("source") or "objectstorage"),
|
|
101
|
+
raw=payload,
|
|
102
|
+
)
|
|
103
|
+
return Event(
|
|
104
|
+
type=EventType.STORAGE,
|
|
105
|
+
provider=PROVIDER,
|
|
106
|
+
function=fn,
|
|
107
|
+
records=[record],
|
|
108
|
+
source=record.source,
|
|
109
|
+
time=parse_time(payload.get("eventTime") or payload.get("time")),
|
|
110
|
+
raw=payload,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def to_event(ctx: Any, data: Any, fn: str) -> Event:
|
|
115
|
+
body = _read(data)
|
|
116
|
+
req = to_request(ctx, body)
|
|
117
|
+
if req is not None:
|
|
118
|
+
return event_from_request(req, PROVIDER, fn, raw=ctx)
|
|
119
|
+
payload = maybe_json(body) if body else None
|
|
120
|
+
wrapped = unwrap_envelope(payload)
|
|
121
|
+
if wrapped is not None:
|
|
122
|
+
kind, value, meta = wrapped
|
|
123
|
+
return Event(type=kind, provider=PROVIDER, function=fn, data=value, source=meta.get("source"), raw=payload)
|
|
124
|
+
if isinstance(payload, dict):
|
|
125
|
+
storage = _cloud_event(payload, fn)
|
|
126
|
+
if storage is not None:
|
|
127
|
+
return storage
|
|
128
|
+
return Event(type=EventType.INVOKE, provider=PROVIDER, function=fn, data=payload, raw=payload)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _respond(ctx: Any, status: int, headers: Dict[str, str], body: Any, is_http: bool) -> Any:
|
|
132
|
+
try:
|
|
133
|
+
from fdk import response as fdk_response # type: ignore[import-not-found]
|
|
134
|
+
except ImportError: # local tests without the FDK
|
|
135
|
+
return {"status": status, "headers": headers, "body": body}
|
|
136
|
+
if is_http:
|
|
137
|
+
try:
|
|
138
|
+
from fdk import context as fdk_context # type: ignore[import-not-found]
|
|
139
|
+
|
|
140
|
+
factory = getattr(fdk_context, "GetHTTPGatewayContext", None) or getattr(
|
|
141
|
+
fdk_context, "HTTPGatewayContext", None
|
|
142
|
+
)
|
|
143
|
+
if factory is not None:
|
|
144
|
+
gw = factory(ctx)
|
|
145
|
+
gw.SetStatusCode(status)
|
|
146
|
+
gw.SetResponseHeaders(headers, status)
|
|
147
|
+
except Exception: # noqa: BLE001, S110 - fall back to protocol headers
|
|
148
|
+
pass
|
|
149
|
+
headers = dict(headers)
|
|
150
|
+
headers["Fn-Http-Status"] = str(status)
|
|
151
|
+
return fdk_response.Response(ctx, response_data=body, headers=headers, status_code=200 if is_http else status)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def handler(ctx: Any, data: Any = None) -> Any:
|
|
155
|
+
"""FDK entrypoint used by the generated ``func.py``."""
|
|
156
|
+
fn = function_name()
|
|
157
|
+
event = to_event(ctx, data if data is not None else io.BytesIO(b""), fn)
|
|
158
|
+
context = Context(request_id=str(_call(ctx, "CallID") or ""), function=fn, provider=PROVIDER, raw=ctx)
|
|
159
|
+
status, headers, body, is_b64 = run(event, context)
|
|
160
|
+
payload: Any = base64.b64decode(body) if is_b64 else body
|
|
161
|
+
return _respond(ctx, status, headers, payload, event.http is not None)
|