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,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,3 @@
1
+ from .generator import collect_records, local_record, render_html, write_dashboard
2
+
3
+ __all__ = ["collect_records", "local_record", "render_html", "write_dashboard"]
@@ -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>
@@ -0,0 +1,3 @@
1
+ from .runner import LocalRunner, build_event, http_event, parse_data
2
+
3
+ __all__ = ["LocalRunner", "build_event", "http_event", "parse_data"]