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,112 @@
|
|
|
1
|
+
"""Builds the deployment record that powers ``pskit info`` and the dashboard."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
from .._version import __version__
|
|
9
|
+
from ..config.schema import EventSpec, FunctionConfig
|
|
10
|
+
from .project import Project
|
|
11
|
+
|
|
12
|
+
RECORD_FILE = "deployment.json"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def event_summary(ev: EventSpec) -> Dict[str, Any]:
|
|
16
|
+
c = ev.config
|
|
17
|
+
if ev.type == "http":
|
|
18
|
+
text = f"{c.method} {c.path}"
|
|
19
|
+
extra = {"method": c.method, "path": c.path, "auth": c.auth, "cors": bool(c.cors)}
|
|
20
|
+
elif ev.type == "schedule":
|
|
21
|
+
text = f"rate({c.rate})" if c.rate else f"cron({c.cron})"
|
|
22
|
+
extra = {"rate": c.rate, "cron": c.cron, "enabled": c.enabled}
|
|
23
|
+
elif ev.type == "queue":
|
|
24
|
+
text = f"queue {c.name} (batch {c.batch_size})"
|
|
25
|
+
extra = {"name": c.name, "batch_size": c.batch_size, "existing": c.existing}
|
|
26
|
+
elif ev.type == "storage":
|
|
27
|
+
filt = "".join(f" {k}={v}" for k, v in (("prefix", c.prefix), ("suffix", c.suffix)) if v)
|
|
28
|
+
text = f"bucket {c.bucket} [{', '.join(c.events)}]{filt}"
|
|
29
|
+
extra = {"bucket": c.bucket, "events": list(c.events), "prefix": c.prefix, "suffix": c.suffix}
|
|
30
|
+
else:
|
|
31
|
+
text = f"topic {c.name}"
|
|
32
|
+
extra = {"name": c.name, "existing": c.existing}
|
|
33
|
+
return {"type": ev.type, "summary": text, **extra}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def function_record(project: Project, name: str, fn: FunctionConfig, out: Dict[str, Any]) -> Dict[str, Any]:
|
|
37
|
+
cfg = project.config
|
|
38
|
+
events = [event_summary(e) for e in fn.events]
|
|
39
|
+
url = out.get("url")
|
|
40
|
+
urls = []
|
|
41
|
+
for e in events:
|
|
42
|
+
if e["type"] == "http" and out.get("base_url"):
|
|
43
|
+
urls.append({"method": e["method"], "url": out["base_url"].rstrip("/") + e["path"]})
|
|
44
|
+
return {
|
|
45
|
+
"name": name,
|
|
46
|
+
"resource_name": project.provider.physical_name(cfg.service, cfg.provider.stage, name),
|
|
47
|
+
"handler": fn.handler,
|
|
48
|
+
"description": fn.description,
|
|
49
|
+
"kind": fn.kind,
|
|
50
|
+
"generated": fn.handler.startswith("py_serverless_kit."),
|
|
51
|
+
"runtime": cfg.runtime_of(fn),
|
|
52
|
+
"memory": cfg.memory_of(fn),
|
|
53
|
+
"timeout": cfg.timeout_of(fn),
|
|
54
|
+
"architecture": cfg.architecture_of(fn),
|
|
55
|
+
"events": events,
|
|
56
|
+
"environment_keys": sorted(set(cfg.provider.environment) | set(fn.environment)),
|
|
57
|
+
"id": out.get("id"),
|
|
58
|
+
"arn": out.get("arn"),
|
|
59
|
+
"url": url,
|
|
60
|
+
"urls": urls,
|
|
61
|
+
"console": out.get("console"),
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def build_record(
|
|
66
|
+
project: Project,
|
|
67
|
+
outputs: Optional[Dict[str, Any]] = None,
|
|
68
|
+
artifact: Optional[Dict[str, Any]] = None,
|
|
69
|
+
status: str = "deployed",
|
|
70
|
+
) -> Dict[str, Any]:
|
|
71
|
+
outputs = outputs or {}
|
|
72
|
+
pskit_out = outputs.get("pskit") or {}
|
|
73
|
+
fn_out: Dict[str, Any] = pskit_out.get("functions") or {}
|
|
74
|
+
wf_out: Dict[str, Any] = pskit_out.get("workflows") or {}
|
|
75
|
+
cfg = project.config
|
|
76
|
+
functions: List[Dict[str, Any]] = []
|
|
77
|
+
for name, fn in sorted(cfg.functions.items()):
|
|
78
|
+
functions.append(function_record(project, name, fn, dict(fn_out.get(name) or {})))
|
|
79
|
+
workflows = []
|
|
80
|
+
for name, wf in sorted(cfg.workflows.items()):
|
|
81
|
+
out = dict(wf_out.get(name) or {})
|
|
82
|
+
workflows.append(
|
|
83
|
+
{
|
|
84
|
+
"name": name,
|
|
85
|
+
"description": wf.description,
|
|
86
|
+
"engine": project.engines.get(name, "native"),
|
|
87
|
+
"start_at": wf.start_at,
|
|
88
|
+
"states": {k: v.type for k, v in wf.states.items()},
|
|
89
|
+
"functions": sorted(set(wf.task_functions())),
|
|
90
|
+
"id": out.get("id"),
|
|
91
|
+
"url": out.get("url"),
|
|
92
|
+
"console": out.get("console"),
|
|
93
|
+
}
|
|
94
|
+
)
|
|
95
|
+
return {
|
|
96
|
+
"schema": 1,
|
|
97
|
+
"status": status,
|
|
98
|
+
"service": cfg.service,
|
|
99
|
+
"stage": cfg.provider.stage,
|
|
100
|
+
"provider": cfg.provider.name,
|
|
101
|
+
"provider_display": project.provider.display_name,
|
|
102
|
+
"provider_tier": project.provider.tier,
|
|
103
|
+
"region": project.region,
|
|
104
|
+
"endpoint": pskit_out.get("endpoint"),
|
|
105
|
+
"deployed_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
|
106
|
+
"pskit_version": __version__,
|
|
107
|
+
"config_file": str(project.loaded.path.name),
|
|
108
|
+
"artifact": artifact or {},
|
|
109
|
+
"functions": functions,
|
|
110
|
+
"workflows": workflows,
|
|
111
|
+
"outputs": {k: v for k, v in outputs.items() if k != "pskit"},
|
|
112
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""A loaded project: config + provider + working directory, ready to build/deploy."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
from ..config.loader import LoadedConfig, load_config
|
|
10
|
+
from ..config.schema import EventSpec, FunctionConfig, HttpEvent, ServiceConfig
|
|
11
|
+
from ..providers.base import HTTP, ORCHESTRATOR_HANDLER, Issue, Provider
|
|
12
|
+
from ..providers.registry import get_provider
|
|
13
|
+
from ..workflows import definition_dict
|
|
14
|
+
|
|
15
|
+
WORK_DIR = ".pskit"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def orchestrator_name(workflow: str) -> str:
|
|
19
|
+
return f"{workflow}-orchestrator"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class Project:
|
|
24
|
+
loaded: LoadedConfig
|
|
25
|
+
provider: Provider
|
|
26
|
+
config: ServiceConfig
|
|
27
|
+
engines: Dict[str, str] = field(default_factory=dict)
|
|
28
|
+
|
|
29
|
+
# ---------------------------------------------------------------- loading
|
|
30
|
+
@classmethod
|
|
31
|
+
def load(
|
|
32
|
+
cls,
|
|
33
|
+
config_path: Optional[str] = None,
|
|
34
|
+
*,
|
|
35
|
+
provider: Optional[str] = None,
|
|
36
|
+
stage: Optional[str] = None,
|
|
37
|
+
region: Optional[str] = None,
|
|
38
|
+
cwd: Optional[Path] = None,
|
|
39
|
+
) -> Project:
|
|
40
|
+
loaded = load_config(config_path, provider=provider, stage=stage, region=region, cwd=cwd)
|
|
41
|
+
return cls.from_loaded(loaded)
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def from_loaded(cls, loaded: LoadedConfig) -> Project:
|
|
45
|
+
prov = get_provider(loaded.config.provider.name)
|
|
46
|
+
config = loaded.config
|
|
47
|
+
if "runtime" not in (loaded.raw.get("provider") or {}):
|
|
48
|
+
# Not pinned by the user: pick what this cloud supports.
|
|
49
|
+
provider_cfg = config.provider.model_copy(update={"runtime": prov.default_runtime})
|
|
50
|
+
config = config.model_copy(update={"provider": provider_cfg})
|
|
51
|
+
config, engines = expand(config, prov)
|
|
52
|
+
return cls(loaded=loaded, provider=prov, config=config, engines=engines)
|
|
53
|
+
|
|
54
|
+
# ---------------------------------------------------------------- paths
|
|
55
|
+
@property
|
|
56
|
+
def root(self) -> Path:
|
|
57
|
+
return self.loaded.root
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def work_dir(self) -> Path:
|
|
61
|
+
return self.root / WORK_DIR / f"{self.config.provider.name}-{self.config.provider.stage}"
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def region(self) -> str:
|
|
65
|
+
return self.provider.region(self.config)
|
|
66
|
+
|
|
67
|
+
# ---------------------------------------------------------------- checks
|
|
68
|
+
def validate(self) -> List[Issue]:
|
|
69
|
+
issues = self.provider.validate(self.config)
|
|
70
|
+
source = (self.root / self.config.package.source).resolve()
|
|
71
|
+
if not source.is_dir():
|
|
72
|
+
issues.append(Issue("error", f"package.source {source} is not a directory", "package.source"))
|
|
73
|
+
for name, fn in self.config.functions.items():
|
|
74
|
+
if fn.handler == ORCHESTRATOR_HANDLER:
|
|
75
|
+
continue
|
|
76
|
+
module = fn.handler.replace(":", ".").rsplit(".", 1)[0]
|
|
77
|
+
rel = Path(*module.split("."))
|
|
78
|
+
if not ((source / rel).with_suffix(".py").is_file() or (source / rel / "__init__.py").is_file()):
|
|
79
|
+
issues.append(
|
|
80
|
+
Issue("warning", f"handler module {module!r} not found under {source}", f"functions.{name}.handler")
|
|
81
|
+
)
|
|
82
|
+
return issues
|
|
83
|
+
|
|
84
|
+
# ---------------------------------------------------------------- bundle manifest
|
|
85
|
+
def bundle_manifest(self) -> Dict[str, Any]:
|
|
86
|
+
functions: Dict[str, Any] = {}
|
|
87
|
+
for name, fn in sorted(self.config.functions.items()):
|
|
88
|
+
routes = []
|
|
89
|
+
for ev in fn.events_of("http"):
|
|
90
|
+
cors = ev.cors_config
|
|
91
|
+
routes.append(
|
|
92
|
+
{
|
|
93
|
+
"method": ev.method,
|
|
94
|
+
"path": ev.path,
|
|
95
|
+
"cors": cors.model_dump() if cors is not None else None,
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
functions[name] = {
|
|
99
|
+
"handler": fn.handler,
|
|
100
|
+
"routes": routes,
|
|
101
|
+
"timeout": self.config.timeout_of(fn),
|
|
102
|
+
"memory": self.config.memory_of(fn),
|
|
103
|
+
"resource_name": self.provider.physical_name(self.config.service, self.config.provider.stage, name),
|
|
104
|
+
}
|
|
105
|
+
workflows = {
|
|
106
|
+
name: {"orchestrator": orchestrator_name(name), "definition": definition_dict(wf)}
|
|
107
|
+
for name, wf in sorted(self.config.workflows.items())
|
|
108
|
+
if self.engines.get(name) == "embedded"
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
"service": self.config.service,
|
|
112
|
+
"stage": self.config.provider.stage,
|
|
113
|
+
"provider": self.config.provider.name,
|
|
114
|
+
"region": self.region,
|
|
115
|
+
"functions": functions,
|
|
116
|
+
"workflows": workflows,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def expand(config: ServiceConfig, provider: Provider) -> tuple[ServiceConfig, Dict[str, str]]:
|
|
121
|
+
"""Add generated functions (embedded workflow orchestrators) to the config."""
|
|
122
|
+
engines: Dict[str, str] = {}
|
|
123
|
+
functions = dict(config.functions)
|
|
124
|
+
for name, wf in config.workflows.items():
|
|
125
|
+
try:
|
|
126
|
+
engine = provider.workflow_engine(wf)
|
|
127
|
+
except Exception: # noqa: BLE001 - reported properly by Provider.validate
|
|
128
|
+
engine = "embedded"
|
|
129
|
+
engines[name] = engine
|
|
130
|
+
if engine != "embedded":
|
|
131
|
+
continue
|
|
132
|
+
orch = orchestrator_name(name)
|
|
133
|
+
if orch in functions:
|
|
134
|
+
continue
|
|
135
|
+
events: List[EventSpec] = []
|
|
136
|
+
if HTTP in provider.capabilities and wf.options.get("http", True):
|
|
137
|
+
events.append(EventSpec(type="http", config=HttpEvent(method="POST", path=f"/workflows/{name}")))
|
|
138
|
+
functions[orch] = FunctionConfig(
|
|
139
|
+
handler=ORCHESTRATOR_HANDLER,
|
|
140
|
+
description=f"Embedded orchestrator for workflow {name}",
|
|
141
|
+
timeout=min(wf.timeout or provider.max_timeout, provider.max_timeout),
|
|
142
|
+
events=events,
|
|
143
|
+
tags={"pskit-workflow": name},
|
|
144
|
+
)
|
|
145
|
+
if functions == config.functions:
|
|
146
|
+
return config, engines
|
|
147
|
+
return config.model_copy(update={"functions": functions}), engines
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Generate the deployment dashboard: one self-contained ``index.html``.
|
|
2
|
+
|
|
3
|
+
The page lists every deployment found under ``.pskit/*/deployment.json`` (all
|
|
4
|
+
providers and stages of the project) with functions, triggers, endpoints,
|
|
5
|
+
resource identifiers and workflows. It has no external dependencies, so it can
|
|
6
|
+
be opened from disk, served by ``pskit dashboard --serve``, or uploaded to any
|
|
7
|
+
static host / bucket.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import html
|
|
13
|
+
import json
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any, Dict, List, Optional
|
|
17
|
+
|
|
18
|
+
from ..core.describe import RECORD_FILE, build_record
|
|
19
|
+
from ..core.project import WORK_DIR, Project
|
|
20
|
+
|
|
21
|
+
TEMPLATE = Path(__file__).with_name("template.html")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def collect_records(root: Path) -> List[Dict[str, Any]]:
|
|
25
|
+
records: List[Dict[str, Any]] = []
|
|
26
|
+
base = root / WORK_DIR
|
|
27
|
+
if not base.is_dir():
|
|
28
|
+
return records
|
|
29
|
+
for path in sorted(base.glob(f"*/{RECORD_FILE}")):
|
|
30
|
+
try:
|
|
31
|
+
records.append(json.loads(path.read_text("utf-8")))
|
|
32
|
+
except (OSError, ValueError):
|
|
33
|
+
continue
|
|
34
|
+
records.sort(key=lambda r: (r.get("service", ""), r.get("provider", ""), r.get("stage", "")))
|
|
35
|
+
return records
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def local_record(project: Project) -> Dict[str, Any]:
|
|
39
|
+
"""A record built from config only (nothing deployed yet / local dev)."""
|
|
40
|
+
return build_record(project, outputs=None, status="local")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def render_html(records: List[Dict[str, Any]], title: str = "Serverless dashboard") -> str:
|
|
44
|
+
payload = json.dumps(
|
|
45
|
+
{"generated_at": datetime.now(timezone.utc).isoformat(timespec="seconds"), "deployments": records},
|
|
46
|
+
default=str,
|
|
47
|
+
)
|
|
48
|
+
# Make the JSON safe inside a <script> element.
|
|
49
|
+
payload = payload.replace("</", "<\\/").replace("<!--", "<\\!--")
|
|
50
|
+
template = TEMPLATE.read_text("utf-8")
|
|
51
|
+
return template.replace("{{TITLE}}", html.escape(title)).replace("{{DATA}}", payload)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def write_dashboard(
|
|
55
|
+
root: Path,
|
|
56
|
+
records: Optional[List[Dict[str, Any]]] = None,
|
|
57
|
+
output: Optional[Path] = None,
|
|
58
|
+
title: Optional[str] = None,
|
|
59
|
+
) -> Path:
|
|
60
|
+
records = collect_records(root) if records is None else records
|
|
61
|
+
out = output or (root / WORK_DIR / "dashboard" / "index.html")
|
|
62
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
63
|
+
services = sorted({r.get("service", "") for r in records if r.get("service")})
|
|
64
|
+
out.write_text(render_html(records, title or (" / ".join(services) or "Serverless") + " dashboard"), "utf-8")
|
|
65
|
+
return out
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>{{TITLE}}</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--bg: #f6f7f9; --panel: #ffffff; --text: #1d2330; --muted: #5d6677; --line: #e3e6ec;
|
|
10
|
+
--accent: #3b5bdb; --accent-soft: #e7ecff; --ok: #2b8a3e; --ok-soft: #e6f4ea;
|
|
11
|
+
--warn: #b35c00; --warn-soft: #fff1e0; --off: #868e96; --off-soft: #f1f3f5;
|
|
12
|
+
--mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
13
|
+
}
|
|
14
|
+
@media (prefers-color-scheme: dark) {
|
|
15
|
+
:root:not([data-theme="light"]) {
|
|
16
|
+
--bg: #12151c; --panel: #1a1f29; --text: #e6e9ef; --muted: #98a2b3; --line: #2a3140;
|
|
17
|
+
--accent: #8ea6ff; --accent-soft: #232c4a; --ok: #69db7c; --ok-soft: #1d3324;
|
|
18
|
+
--warn: #ffb057; --warn-soft: #3a2a17; --off: #adb5bd; --off-soft: #262b35;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
:root[data-theme="dark"] {
|
|
22
|
+
--bg: #12151c; --panel: #1a1f29; --text: #e6e9ef; --muted: #98a2b3; --line: #2a3140;
|
|
23
|
+
--accent: #8ea6ff; --accent-soft: #232c4a; --ok: #69db7c; --ok-soft: #1d3324;
|
|
24
|
+
--warn: #ffb057; --warn-soft: #3a2a17; --off: #adb5bd; --off-soft: #262b35;
|
|
25
|
+
}
|
|
26
|
+
* { box-sizing: border-box; }
|
|
27
|
+
body { margin: 0; background: var(--bg); color: var(--text);
|
|
28
|
+
font: 14px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; }
|
|
29
|
+
header { padding: 20px 16px 8px; max-width: 1280px; margin: 0 auto; }
|
|
30
|
+
h1 { margin: 0; font-size: 22px; }
|
|
31
|
+
.sub { color: var(--muted); font-size: 13px; }
|
|
32
|
+
main { max-width: 1280px; margin: 0 auto; padding: 8px 16px 48px; }
|
|
33
|
+
.toolbar { display: flex; gap: 8px; flex-wrap: wrap; margin: 12px 0 16px; }
|
|
34
|
+
.toolbar input, .toolbar select, .toolbar button {
|
|
35
|
+
font: inherit; color: var(--text); background: var(--panel); border: 1px solid var(--line);
|
|
36
|
+
border-radius: 8px; padding: 7px 10px; }
|
|
37
|
+
.toolbar input { flex: 1 1 240px; min-width: 0; }
|
|
38
|
+
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; margin-bottom: 18px; }
|
|
39
|
+
.card { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 12px 14px; }
|
|
40
|
+
.card b { display: block; font-size: 22px; }
|
|
41
|
+
.card span { color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: .04em; }
|
|
42
|
+
section.dep { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; margin-bottom: 18px; overflow: hidden; }
|
|
43
|
+
.dep-head { padding: 14px 16px; border-bottom: 1px solid var(--line); display: flex; flex-wrap: wrap; gap: 8px 16px; align-items: center; }
|
|
44
|
+
.dep-head h2 { margin: 0; font-size: 17px; }
|
|
45
|
+
.meta { color: var(--muted); font-size: 12px; display: flex; flex-wrap: wrap; gap: 4px 14px; }
|
|
46
|
+
.meta code { font-family: var(--mono); }
|
|
47
|
+
.badge { display: inline-block; font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 999px;
|
|
48
|
+
background: var(--accent-soft); color: var(--accent); white-space: nowrap; }
|
|
49
|
+
.badge.ok { background: var(--ok-soft); color: var(--ok); }
|
|
50
|
+
.badge.warn { background: var(--warn-soft); color: var(--warn); }
|
|
51
|
+
.badge.off { background: var(--off-soft); color: var(--off); }
|
|
52
|
+
.table-wrap { overflow-x: auto; }
|
|
53
|
+
table { width: 100%; border-collapse: collapse; }
|
|
54
|
+
th, td { text-align: left; padding: 9px 12px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
|
55
|
+
th { font-size: 11px; text-transform: uppercase; letter-spacing: .05em; color: var(--muted); font-weight: 600; }
|
|
56
|
+
tr:last-child td { border-bottom: none; }
|
|
57
|
+
td.name b { display: block; }
|
|
58
|
+
td.rt .mono { white-space: nowrap; }
|
|
59
|
+
td.name small, .mono { font-family: var(--mono); font-size: 12px; color: var(--muted); overflow-wrap: anywhere; }
|
|
60
|
+
.chip { display: inline-block; font-family: var(--mono); font-size: 12px; padding: 1px 7px; margin: 0 4px 4px 0;
|
|
61
|
+
border: 1px solid var(--line); border-radius: 6px; white-space: nowrap; }
|
|
62
|
+
.chip.http { border-color: var(--accent); color: var(--accent); }
|
|
63
|
+
a { color: var(--accent); text-decoration: none; word-break: break-all; }
|
|
64
|
+
a:hover { text-decoration: underline; }
|
|
65
|
+
button.copy { font: inherit; font-size: 11px; color: var(--muted); background: none; border: 1px solid var(--line);
|
|
66
|
+
border-radius: 6px; padding: 0 6px; margin-left: 4px; cursor: pointer; }
|
|
67
|
+
details summary { cursor: pointer; color: var(--muted); font-size: 12px; }
|
|
68
|
+
.sect-title { padding: 10px 16px 0; font-size: 12px; text-transform: uppercase; letter-spacing: .05em; color: var(--muted); font-weight: 600; }
|
|
69
|
+
.empty { padding: 40px 16px; text-align: center; color: var(--muted); }
|
|
70
|
+
.flow { font-family: var(--mono); font-size: 12px; }
|
|
71
|
+
@media (max-width: 720px) {
|
|
72
|
+
thead { display: none; }
|
|
73
|
+
tr { display: block; border-bottom: 1px solid var(--line); padding: 6px 0; }
|
|
74
|
+
td { display: block; border: none; padding: 4px 16px; }
|
|
75
|
+
td[data-label]::before { content: attr(data-label) ": "; color: var(--muted); font-size: 11px; text-transform: uppercase; }
|
|
76
|
+
}
|
|
77
|
+
</style>
|
|
78
|
+
</head>
|
|
79
|
+
<body>
|
|
80
|
+
<header>
|
|
81
|
+
<h1 id="title">{{TITLE}}</h1>
|
|
82
|
+
<div class="sub" id="generated"></div>
|
|
83
|
+
</header>
|
|
84
|
+
<main>
|
|
85
|
+
<div class="cards" id="cards"></div>
|
|
86
|
+
<div class="toolbar">
|
|
87
|
+
<input id="q" type="search" placeholder="Filter by function, trigger, URL, handler…" aria-label="Filter">
|
|
88
|
+
<select id="provider" aria-label="Provider"><option value="">All providers</option></select>
|
|
89
|
+
<select id="stage" aria-label="Stage"><option value="">All stages</option></select>
|
|
90
|
+
<button id="theme" type="button" title="Toggle theme">◐ Theme</button>
|
|
91
|
+
</div>
|
|
92
|
+
<div id="deployments"></div>
|
|
93
|
+
</main>
|
|
94
|
+
<script type="application/json" id="data">{{DATA}}</script>
|
|
95
|
+
<script>
|
|
96
|
+
(function () {
|
|
97
|
+
"use strict";
|
|
98
|
+
var DATA = JSON.parse(document.getElementById("data").textContent);
|
|
99
|
+
var deps = DATA.deployments || [];
|
|
100
|
+
|
|
101
|
+
function h(tag, attrs) {
|
|
102
|
+
var el = document.createElement(tag);
|
|
103
|
+
if (attrs) Object.keys(attrs).forEach(function (k) {
|
|
104
|
+
if (k === "text") el.textContent = attrs[k];
|
|
105
|
+
else if (k === "class") el.className = attrs[k];
|
|
106
|
+
else if (attrs[k] !== null && attrs[k] !== undefined) el.setAttribute(k, attrs[k]);
|
|
107
|
+
});
|
|
108
|
+
for (var i = 2; i < arguments.length; i++) {
|
|
109
|
+
var c = arguments[i];
|
|
110
|
+
if (c === null || c === undefined || c === false) continue;
|
|
111
|
+
if (Array.isArray(c)) c.forEach(function (x) { if (x) el.appendChild(typeof x === "string" ? document.createTextNode(x) : x); });
|
|
112
|
+
else el.appendChild(typeof c === "string" ? document.createTextNode(c) : c);
|
|
113
|
+
}
|
|
114
|
+
return el;
|
|
115
|
+
}
|
|
116
|
+
function safeUrl(u) { return /^https?:\/\//i.test(u || "") ? u : null; }
|
|
117
|
+
function link(u, label) { var s = safeUrl(u); return s ? h("a", { href: s, target: "_blank", rel: "noopener", text: label || u }) : h("span", { class: "mono", text: u || "—" }); }
|
|
118
|
+
function copyBtn(value) {
|
|
119
|
+
var b = h("button", { class: "copy", type: "button", title: "Copy", text: "copy" });
|
|
120
|
+
b.addEventListener("click", function () {
|
|
121
|
+
if (navigator.clipboard) navigator.clipboard.writeText(value).then(function () { b.textContent = "copied"; setTimeout(function () { b.textContent = "copy"; }, 1200); });
|
|
122
|
+
});
|
|
123
|
+
return b;
|
|
124
|
+
}
|
|
125
|
+
function statusBadge(s) {
|
|
126
|
+
var cls = s === "deployed" ? "ok" : s === "removed" ? "off" : "warn";
|
|
127
|
+
return h("span", { class: "badge " + cls, text: s || "unknown" });
|
|
128
|
+
}
|
|
129
|
+
function kindBadge(k) { return h("span", { class: "badge" + (k === "invoke-only" ? " off" : ""), text: k }); }
|
|
130
|
+
|
|
131
|
+
var gen = document.getElementById("generated");
|
|
132
|
+
gen.textContent = "Generated " + new Date(DATA.generated_at).toLocaleString() + " · py-serverless-kit";
|
|
133
|
+
|
|
134
|
+
// filters
|
|
135
|
+
var providers = {}, stages = {};
|
|
136
|
+
deps.forEach(function (d) { providers[d.provider] = d.provider_display || d.provider; stages[d.stage] = 1; });
|
|
137
|
+
var pSel = document.getElementById("provider"), sSel = document.getElementById("stage");
|
|
138
|
+
Object.keys(providers).sort().forEach(function (p) { pSel.appendChild(h("option", { value: p, text: providers[p] })); });
|
|
139
|
+
Object.keys(stages).sort().forEach(function (s) { sSel.appendChild(h("option", { value: s, text: s })); });
|
|
140
|
+
|
|
141
|
+
function cards(list) {
|
|
142
|
+
var fns = 0, http = 0, wfs = 0, prov = {};
|
|
143
|
+
list.forEach(function (d) {
|
|
144
|
+
prov[d.provider] = 1; wfs += (d.workflows || []).length;
|
|
145
|
+
(d.functions || []).forEach(function (f) { fns++; if ((f.events || []).some(function (e) { return e.type === "http"; })) http++; });
|
|
146
|
+
});
|
|
147
|
+
var el = document.getElementById("cards"); el.textContent = "";
|
|
148
|
+
[["Deployments", list.length], ["Functions", fns], ["HTTP functions", http], ["Workflows", wfs], ["Providers", Object.keys(prov).length]]
|
|
149
|
+
.forEach(function (c) { el.appendChild(h("div", { class: "card" }, h("b", { text: String(c[1]) }), h("span", { text: c[0] }))); });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function fnMatches(f, q) {
|
|
153
|
+
if (!q) return true;
|
|
154
|
+
var hay = [f.name, f.resource_name, f.handler, f.kind, f.url, f.id, f.arn, f.description]
|
|
155
|
+
.concat((f.events || []).map(function (e) { return e.summary; }))
|
|
156
|
+
.concat((f.urls || []).map(function (u) { return u.url; })).join(" ").toLowerCase();
|
|
157
|
+
return hay.indexOf(q) !== -1;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function functionRow(f) {
|
|
161
|
+
var endpoints = [];
|
|
162
|
+
(f.urls || []).forEach(function (u) { endpoints.push(h("div", null, h("span", { class: "chip http", text: u.method }), link(u.url))); });
|
|
163
|
+
if (!endpoints.length && f.url) endpoints.push(h("div", null, link(f.url)));
|
|
164
|
+
var ident = f.arn || f.id;
|
|
165
|
+
return h("tr", null,
|
|
166
|
+
h("td", { class: "name", "data-label": "Function" },
|
|
167
|
+
h("b", { text: f.name }), h("small", { text: f.resource_name || "" }),
|
|
168
|
+
f.generated ? h("span", { class: "badge off", text: "generated" }) : null),
|
|
169
|
+
h("td", { "data-label": "Type" }, kindBadge(f.kind)),
|
|
170
|
+
h("td", { "data-label": "Triggers" }, (f.events || []).length
|
|
171
|
+
? (f.events || []).map(function (e) { return h("span", { class: "chip" + (e.type === "http" ? " http" : ""), title: e.type, text: e.summary }); })
|
|
172
|
+
: h("span", { class: "mono", text: "direct invoke" })),
|
|
173
|
+
h("td", { "data-label": "Runtime", class: "rt" }, h("div", { class: "mono", text: f.runtime + " · " + f.architecture }),
|
|
174
|
+
h("div", { class: "mono", text: f.memory + " MB · " + f.timeout + " s" })),
|
|
175
|
+
h("td", { "data-label": "Endpoint" }, endpoints.length ? endpoints : h("span", { class: "mono", text: "—" })),
|
|
176
|
+
h("td", { "data-label": "Resource" },
|
|
177
|
+
ident ? h("div", null, h("span", { class: "mono", text: ident }), copyBtn(ident)) : h("span", { class: "mono", text: "not deployed" }),
|
|
178
|
+
f.console ? h("div", null, link(f.console, "Open in console ↗")) : null,
|
|
179
|
+
h("details", null, h("summary", { text: "details" }),
|
|
180
|
+
h("div", { class: "mono", text: "handler: " + f.handler }),
|
|
181
|
+
f.description ? h("div", { class: "mono", text: f.description }) : null,
|
|
182
|
+
(f.environment_keys || []).length ? h("div", { class: "mono", text: "env: " + f.environment_keys.join(", ") }) : null))
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function workflowTable(d) {
|
|
187
|
+
if (!(d.workflows || []).length) return null;
|
|
188
|
+
var rows = d.workflows.map(function (w) {
|
|
189
|
+
var states = Object.keys(w.states || {}).map(function (s) { return s + (w.states[s] ? " (" + w.states[s] + ")" : ""); }).join(" → ");
|
|
190
|
+
return h("tr", null,
|
|
191
|
+
h("td", { class: "name", "data-label": "Workflow" }, h("b", { text: w.name }), h("small", { text: w.description || "" })),
|
|
192
|
+
h("td", { "data-label": "Engine" }, h("span", { class: "badge" + (w.engine === "native" ? " ok" : " warn"), text: w.engine })),
|
|
193
|
+
h("td", { "data-label": "States", class: "flow" }, states),
|
|
194
|
+
h("td", { "data-label": "Functions" }, (w.functions || []).map(function (f) { return h("span", { class: "chip", text: f }); })),
|
|
195
|
+
h("td", { "data-label": "Resource" }, w.id ? h("div", null, h("span", { class: "mono", text: w.id }), copyBtn(w.id)) : h("span", { class: "mono", text: "—" }),
|
|
196
|
+
w.console ? h("div", null, link(w.console, "Open in console ↗")) : null));
|
|
197
|
+
});
|
|
198
|
+
return [h("div", { class: "sect-title", text: "Workflows" }),
|
|
199
|
+
h("div", { class: "table-wrap" }, h("table", null,
|
|
200
|
+
h("thead", null, h("tr", null, ["Workflow", "Engine", "States", "Functions", "Resource"].map(function (t) { return h("th", { text: t }); }))),
|
|
201
|
+
h("tbody", null, rows)))];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function render() {
|
|
205
|
+
var q = document.getElementById("q").value.trim().toLowerCase();
|
|
206
|
+
var p = pSel.value, s = sSel.value;
|
|
207
|
+
var list = deps.filter(function (d) { return (!p || d.provider === p) && (!s || d.stage === s); });
|
|
208
|
+
cards(list);
|
|
209
|
+
var root = document.getElementById("deployments"); root.textContent = "";
|
|
210
|
+
if (!list.length) {
|
|
211
|
+
root.appendChild(h("div", { class: "empty" }, "No deployments found. Run ", h("code", { text: "pskit deploy" }), " and then ", h("code", { text: "pskit dashboard" }), "."));
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
list.forEach(function (d) {
|
|
215
|
+
var fns = (d.functions || []).filter(function (f) { return fnMatches(f, q); });
|
|
216
|
+
if (q && !fns.length) return;
|
|
217
|
+
var art = d.artifact || {};
|
|
218
|
+
root.appendChild(h("section", { class: "dep" },
|
|
219
|
+
h("div", { class: "dep-head" },
|
|
220
|
+
h("h2", { text: d.service + " · " + d.stage }),
|
|
221
|
+
h("span", { class: "badge", text: d.provider_display || d.provider }),
|
|
222
|
+
d.provider_tier && d.provider_tier !== "stable" ? h("span", { class: "badge warn", text: d.provider_tier }) : null,
|
|
223
|
+
statusBadge(d.status),
|
|
224
|
+
h("div", { class: "meta" },
|
|
225
|
+
h("span", null, "region ", h("code", { text: d.region || "—" })),
|
|
226
|
+
h("span", null, "updated ", h("code", { text: d.deployed_at ? new Date(d.deployed_at).toLocaleString() : "—" })),
|
|
227
|
+
art.sha256 ? h("span", null, "artifact ", h("code", { text: art.sha256.slice(0, 12) })) : null,
|
|
228
|
+
d.endpoint ? h("span", null, "endpoint ", link(d.endpoint)) : null,
|
|
229
|
+
h("span", null, "pskit ", h("code", { text: d.pskit_version || "?" })))),
|
|
230
|
+
h("div", { class: "sect-title", text: "Functions (" + fns.length + ")" }),
|
|
231
|
+
h("div", { class: "table-wrap" }, h("table", null,
|
|
232
|
+
h("thead", null, h("tr", null, ["Function", "Type", "Triggers", "Runtime", "Endpoint", "Resource"].map(function (t) { return h("th", { text: t }); }))),
|
|
233
|
+
h("tbody", null, fns.map(functionRow)))),
|
|
234
|
+
workflowTable(d)));
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
["q", "provider", "stage"].forEach(function (id) { document.getElementById(id).addEventListener("input", render); });
|
|
239
|
+
document.getElementById("theme").addEventListener("click", function () {
|
|
240
|
+
var root = document.documentElement;
|
|
241
|
+
var dark = root.getAttribute("data-theme") ? root.getAttribute("data-theme") === "dark" : window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
242
|
+
root.setAttribute("data-theme", dark ? "light" : "dark");
|
|
243
|
+
});
|
|
244
|
+
render();
|
|
245
|
+
})();
|
|
246
|
+
</script>
|
|
247
|
+
</body>
|
|
248
|
+
</html>
|