caraer-cli 0.1.2__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.
- caraer_cli/__init__.py +4 -0
- caraer_cli/api/__init__.py +1 -0
- caraer_cli/api/apps.py +78 -0
- caraer_cli/api/auth.py +53 -0
- caraer_cli/api/client.py +179 -0
- caraer_cli/api/functions.py +72 -0
- caraer_cli/api/installation.py +93 -0
- caraer_cli/api/integration_runtime.py +77 -0
- caraer_cli/api/projects.py +213 -0
- caraer_cli/api/sandboxes.py +29 -0
- caraer_cli/api/webhooks.py +59 -0
- caraer_cli/app_sync.py +697 -0
- caraer_cli/apps_local.py +274 -0
- caraer_cli/commands/__init__.py +1 -0
- caraer_cli/commands/apps.py +2025 -0
- caraer_cli/commands/auth.py +173 -0
- caraer_cli/commands/company.py +68 -0
- caraer_cli/commands/installation.py +182 -0
- caraer_cli/commands/profile.py +239 -0
- caraer_cli/commands/publish.py +40 -0
- caraer_cli/commands/sandbox.py +92 -0
- caraer_cli/commands/skill.py +165 -0
- caraer_cli/commands/webhooks.py +68 -0
- caraer_cli/completion.py +77 -0
- caraer_cli/completion_callbacks.py +162 -0
- caraer_cli/context.py +41 -0
- caraer_cli/errors.py +84 -0
- caraer_cli/formatters/__init__.py +1 -0
- caraer_cli/formatters/output.py +381 -0
- caraer_cli/formatters/timestamps.py +98 -0
- caraer_cli/install_completion.py +38 -0
- caraer_cli/local_app.py +135 -0
- caraer_cli/main.py +104 -0
- caraer_cli/models/__init__.py +1 -0
- caraer_cli/models/api.py +52 -0
- caraer_cli/project/__init__.py +17 -0
- caraer_cli/project/app_bars_sync.py +154 -0
- caraer_cli/project/app_manifest_template.py +68 -0
- caraer_cli/project/inbound_sync.py +192 -0
- caraer_cli/project/json_schemas.py +66 -0
- caraer_cli/project/lifecycle_sync.py +119 -0
- caraer_cli/project/local_dev.py +825 -0
- caraer_cli/project/manifest_edit.py +51 -0
- caraer_cli/project/marketplace_assemble.py +165 -0
- caraer_cli/project/oauth_providers_sync.py +180 -0
- caraer_cli/project/paths.py +102 -0
- caraer_cli/project/pricing_sync.py +81 -0
- caraer_cli/project/push_plan.py +590 -0
- caraer_cli/project/release.py +128 -0
- caraer_cli/project/scaffold.py +537 -0
- caraer_cli/project/schedules_sync.py +170 -0
- caraer_cli/project/schema.py +160 -0
- caraer_cli/project/settings_sync.py +77 -0
- caraer_cli/project/state.py +51 -0
- caraer_cli/project/sync.py +308 -0
- caraer_cli/project/typegen.py +278 -0
- caraer_cli/project/validate_app.py +1215 -0
- caraer_cli/project/webhooks_sync.py +233 -0
- caraer_cli/resolve.py +55 -0
- caraer_cli/schemas/app.caraer.schema.json +175 -0
- caraer_cli/schemas/function.caraer.schema.json +29 -0
- caraer_cli/schemas/inbound.caraer.schema.json +49 -0
- caraer_cli/schemas/lifecycle.caraer.schema.json +50 -0
- caraer_cli/schemas/schedule.caraer.schema.json +39 -0
- caraer_cli/schemas/webhook.caraer.schema.json +50 -0
- caraer_cli/skills/.gitkeep +2 -0
- caraer_cli/skills/caraer-apps/SKILL.md +172 -0
- caraer_cli/skills/caraer-apps/reference.md +163 -0
- caraer_cli/state/__init__.py +1 -0
- caraer_cli/state/config.py +148 -0
- caraer_cli/state/session.py +98 -0
- caraer_cli/utils.py +40 -0
- caraer_cli/wizard/__init__.py +9 -0
- caraer_cli/wizard/catalog.py +170 -0
- caraer_cli/wizard/marketplace.py +350 -0
- caraer_cli/wizard/prompts.py +99 -0
- caraer_cli/wizard/public_app.py +519 -0
- caraer_cli/wizard/scope_macros.py +214 -0
- caraer_cli-0.1.2.dist-info/METADATA +266 -0
- caraer_cli-0.1.2.dist-info/RECORD +83 -0
- caraer_cli-0.1.2.dist-info/WHEEL +4 -0
- caraer_cli-0.1.2.dist-info/entry_points.txt +2 -0
- caraer_cli-0.1.2.dist-info/licenses/LICENSE +8 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""Sync inbound routes between src/app/inbound/*.json and the remote API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
import secrets
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from caraer_cli.api import integration_runtime as api
|
|
12
|
+
from caraer_cli.api.client import CaraerApiClient
|
|
13
|
+
from caraer_cli.project.paths import inbound_dir
|
|
14
|
+
from caraer_cli.project.schema import ProjectConfig
|
|
15
|
+
from caraer_cli.project.state import load_state, save_state
|
|
16
|
+
|
|
17
|
+
LOCAL_INBOUND_KEYS = (
|
|
18
|
+
"uuid",
|
|
19
|
+
"name",
|
|
20
|
+
"label",
|
|
21
|
+
"authMode",
|
|
22
|
+
"enqueue",
|
|
23
|
+
"sharedSecret",
|
|
24
|
+
"serverlessFunction",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _slug(value: str) -> str:
|
|
29
|
+
text = value.strip().lower()
|
|
30
|
+
text = re.sub(r"[^a-z0-9]+", "-", text)
|
|
31
|
+
return text.strip("-") or "inbound"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def inbound_filename(item: dict[str, Any]) -> str:
|
|
35
|
+
name = str(item.get("name") or "inbound")
|
|
36
|
+
uuid = str(item.get("uuid") or "")[:8]
|
|
37
|
+
parts = [_slug(name)]
|
|
38
|
+
if uuid:
|
|
39
|
+
parts.append(uuid)
|
|
40
|
+
return "-".join(parts) + ".json"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def discover_local_inbound(root: Path, config: ProjectConfig) -> list[tuple[Path, dict[str, Any]]]:
|
|
44
|
+
base = inbound_dir(root, config.srcDir)
|
|
45
|
+
if not base.is_dir():
|
|
46
|
+
return []
|
|
47
|
+
found: list[tuple[Path, dict[str, Any]]] = []
|
|
48
|
+
for path in sorted(base.glob("*.json")):
|
|
49
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
50
|
+
if isinstance(data, dict):
|
|
51
|
+
found.append((path, data))
|
|
52
|
+
return found
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _sanitize_local(item: dict[str, Any]) -> dict[str, Any]:
|
|
56
|
+
payload: dict[str, Any] = {}
|
|
57
|
+
for key in LOCAL_INBOUND_KEYS:
|
|
58
|
+
if key in item and item[key] is not None:
|
|
59
|
+
payload[key] = item[key]
|
|
60
|
+
# Never persist sharedSecret after push in pulled files — keep local-only until push.
|
|
61
|
+
sf = payload.get("serverlessFunction")
|
|
62
|
+
if isinstance(sf, dict):
|
|
63
|
+
cleaned: dict[str, Any] = {}
|
|
64
|
+
if sf.get("name"):
|
|
65
|
+
cleaned["name"] = sf["name"]
|
|
66
|
+
elif sf.get("uuid"):
|
|
67
|
+
cleaned["uuid"] = sf["uuid"]
|
|
68
|
+
if cleaned:
|
|
69
|
+
payload["serverlessFunction"] = cleaned
|
|
70
|
+
else:
|
|
71
|
+
payload.pop("serverlessFunction", None)
|
|
72
|
+
return payload
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _sanitize_remote(item: dict[str, Any]) -> dict[str, Any]:
|
|
76
|
+
payload = _sanitize_local(item)
|
|
77
|
+
payload.pop("sharedSecret", None)
|
|
78
|
+
if item.get("hasSharedSecret"):
|
|
79
|
+
payload["hasSharedSecret"] = True
|
|
80
|
+
return payload
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _resolve_sf(payload: dict[str, Any], *, fn_by_name: dict[str, str]) -> dict[str, Any]:
|
|
84
|
+
out = dict(payload)
|
|
85
|
+
out.pop("uuid", None)
|
|
86
|
+
out.pop("hasSharedSecret", None)
|
|
87
|
+
sf = out.get("serverlessFunction")
|
|
88
|
+
if not isinstance(sf, dict):
|
|
89
|
+
return out
|
|
90
|
+
if sf.get("uuid"):
|
|
91
|
+
out["serverlessFunction"] = {"uuid": sf["uuid"]}
|
|
92
|
+
return out
|
|
93
|
+
name = sf.get("name")
|
|
94
|
+
if name and name in fn_by_name:
|
|
95
|
+
out["serverlessFunction"] = {"uuid": fn_by_name[name]}
|
|
96
|
+
return out
|
|
97
|
+
if name:
|
|
98
|
+
raise ValueError(
|
|
99
|
+
f"Inbound route references function '{name}' but no UUID is known. Push functions first."
|
|
100
|
+
)
|
|
101
|
+
return out
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def pull_inbound(client: CaraerApiClient, root: Path, config: ProjectConfig) -> int:
|
|
105
|
+
if not config.appUuid:
|
|
106
|
+
raise ValueError("App is not linked.")
|
|
107
|
+
response = api.list_inbound_routes(client, config.appUuid)
|
|
108
|
+
items = response.get("data") or []
|
|
109
|
+
if not isinstance(items, list):
|
|
110
|
+
items = []
|
|
111
|
+
base = inbound_dir(root, config.srcDir)
|
|
112
|
+
base.mkdir(parents=True, exist_ok=True)
|
|
113
|
+
for existing in base.glob("*.json"):
|
|
114
|
+
existing.unlink()
|
|
115
|
+
state = load_state(root)
|
|
116
|
+
inbound_map: dict[str, str] = {}
|
|
117
|
+
for item in items:
|
|
118
|
+
if not isinstance(item, dict):
|
|
119
|
+
continue
|
|
120
|
+
sanitized = _sanitize_remote(item)
|
|
121
|
+
path = base / inbound_filename(sanitized)
|
|
122
|
+
path.write_text(json.dumps(sanitized, indent=2) + "\n", encoding="utf-8")
|
|
123
|
+
if sanitized.get("uuid") and sanitized.get("name"):
|
|
124
|
+
inbound_map[str(sanitized["name"])] = str(sanitized["uuid"])
|
|
125
|
+
state["inbound"] = inbound_map
|
|
126
|
+
save_state(root, state)
|
|
127
|
+
return len(inbound_map)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def push_inbound(
|
|
131
|
+
client: CaraerApiClient,
|
|
132
|
+
root: Path,
|
|
133
|
+
config: ProjectConfig,
|
|
134
|
+
*,
|
|
135
|
+
delete_missing: bool = False,
|
|
136
|
+
) -> dict[str, Any]:
|
|
137
|
+
if not config.appUuid:
|
|
138
|
+
raise ValueError("App is not linked.")
|
|
139
|
+
state = load_state(root)
|
|
140
|
+
fn_state = state.get("functions") or {}
|
|
141
|
+
fn_by_name = {
|
|
142
|
+
name: str(meta["uuid"])
|
|
143
|
+
for name, meta in fn_state.items()
|
|
144
|
+
if isinstance(meta, dict) and meta.get("uuid")
|
|
145
|
+
}
|
|
146
|
+
remote = api.list_inbound_routes(client, config.appUuid).get("data") or []
|
|
147
|
+
remote_by_uuid = {
|
|
148
|
+
str(i["uuid"]): i for i in remote if isinstance(i, dict) and i.get("uuid")
|
|
149
|
+
}
|
|
150
|
+
remote_by_name = {
|
|
151
|
+
str(i["name"]): i for i in remote if isinstance(i, dict) and i.get("name")
|
|
152
|
+
}
|
|
153
|
+
local = discover_local_inbound(root, config)
|
|
154
|
+
pushed: dict[str, str] = {}
|
|
155
|
+
kept_uuids: set[str] = set()
|
|
156
|
+
for path, raw in local:
|
|
157
|
+
sanitized = _sanitize_local(raw)
|
|
158
|
+
payload = _resolve_sf(sanitized, fn_by_name=fn_by_name)
|
|
159
|
+
name = str(sanitized.get("name") or "")
|
|
160
|
+
uuid = str(sanitized.get("uuid") or "")
|
|
161
|
+
existing = remote_by_uuid.get(uuid) or remote_by_name.get(name)
|
|
162
|
+
auth_mode = str(payload.get("authMode") or "SHARED_SECRET").upper()
|
|
163
|
+
if (
|
|
164
|
+
not existing
|
|
165
|
+
and auth_mode == "SHARED_SECRET"
|
|
166
|
+
and not str(payload.get("sharedSecret") or "").strip()
|
|
167
|
+
):
|
|
168
|
+
generated = secrets.token_urlsafe(24)
|
|
169
|
+
payload["sharedSecret"] = generated
|
|
170
|
+
# Keep locally so Pub/Sub can be configured; not written back after create.
|
|
171
|
+
sanitized["sharedSecret"] = generated
|
|
172
|
+
path.write_text(json.dumps(sanitized, indent=2) + "\n", encoding="utf-8")
|
|
173
|
+
if existing and existing.get("uuid"):
|
|
174
|
+
resp = api.update_inbound_route(client, config.appUuid, str(existing["uuid"]), payload)
|
|
175
|
+
data = resp.get("data") or existing
|
|
176
|
+
else:
|
|
177
|
+
resp = api.create_inbound_route(client, config.appUuid, payload)
|
|
178
|
+
data = resp.get("data") or {}
|
|
179
|
+
if isinstance(data, dict) and data.get("uuid"):
|
|
180
|
+
kept_uuids.add(str(data["uuid"]))
|
|
181
|
+
if data.get("name"):
|
|
182
|
+
pushed[str(data["name"])] = str(data["uuid"])
|
|
183
|
+
write_back = _sanitize_remote({**sanitized, **data})
|
|
184
|
+
write_back.pop("sharedSecret", None)
|
|
185
|
+
path.write_text(json.dumps(write_back, indent=2) + "\n", encoding="utf-8")
|
|
186
|
+
if delete_missing:
|
|
187
|
+
for uuid in remote_by_uuid:
|
|
188
|
+
if uuid not in kept_uuids:
|
|
189
|
+
api.delete_inbound_route(client, config.appUuid, uuid)
|
|
190
|
+
state["inbound"] = pushed
|
|
191
|
+
save_state(root, state)
|
|
192
|
+
return {"pushed": len(pushed), "deletedMissing": delete_missing}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""JSON Schema URLs and helpers for Caraer app config files."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
SCHEMA_REPO_BASE = (
|
|
10
|
+
"https://raw.githubusercontent.com/caraer/caraer-cli/main/schemas"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
APP_MANIFEST_SCHEMA_URL = f"{SCHEMA_REPO_BASE}/app.caraer.schema.json"
|
|
14
|
+
FUNCTION_SCHEMA_URL = f"{SCHEMA_REPO_BASE}/function.caraer.schema.json"
|
|
15
|
+
WEBHOOK_SCHEMA_URL = f"{SCHEMA_REPO_BASE}/webhook.caraer.schema.json"
|
|
16
|
+
SCHEDULE_SCHEMA_URL = f"{SCHEMA_REPO_BASE}/schedule.caraer.schema.json"
|
|
17
|
+
INBOUND_SCHEMA_URL = f"{SCHEMA_REPO_BASE}/inbound.caraer.schema.json"
|
|
18
|
+
LIFECYCLE_SCHEMA_URL = f"{SCHEMA_REPO_BASE}/lifecycle.caraer.schema.json"
|
|
19
|
+
|
|
20
|
+
YAML_LANGUAGE_SERVER_COMMENT = (
|
|
21
|
+
f"# yaml-language-server: $schema={APP_MANIFEST_SCHEMA_URL}"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
SCHEMA_FILENAMES = {
|
|
25
|
+
"app": "app.caraer.schema.json",
|
|
26
|
+
"function": "function.caraer.schema.json",
|
|
27
|
+
"webhook": "webhook.caraer.schema.json",
|
|
28
|
+
"schedule": "schedule.caraer.schema.json",
|
|
29
|
+
"inbound": "inbound.caraer.schema.json",
|
|
30
|
+
"lifecycle": "lifecycle.caraer.schema.json",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def schemas_dir() -> Path | None:
|
|
35
|
+
"""Locate the local ``schemas/`` directory when available."""
|
|
36
|
+
here = Path(__file__).resolve()
|
|
37
|
+
candidates = [
|
|
38
|
+
# Packaged: caraer_cli/schemas (via hatch force-include)
|
|
39
|
+
here.parents[1] / "schemas",
|
|
40
|
+
# Dev checkout: <repo>/schemas
|
|
41
|
+
here.parents[2] / "schemas",
|
|
42
|
+
]
|
|
43
|
+
for path in candidates:
|
|
44
|
+
if path.is_dir() and (path / "function.caraer.schema.json").is_file():
|
|
45
|
+
return path
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def with_json_schema(payload: dict[str, Any], schema_url: str) -> dict[str, Any]:
|
|
50
|
+
"""Return a copy of payload with ``$schema`` first."""
|
|
51
|
+
out: dict[str, Any] = {"$schema": schema_url}
|
|
52
|
+
for key, value in payload.items():
|
|
53
|
+
if key == "$schema":
|
|
54
|
+
continue
|
|
55
|
+
out[key] = value
|
|
56
|
+
return out
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def dump_json_with_schema(
|
|
60
|
+
path: Path, payload: dict[str, Any], schema_url: str
|
|
61
|
+
) -> None:
|
|
62
|
+
"""Write JSON with a leading ``$schema`` field."""
|
|
63
|
+
path.write_text(
|
|
64
|
+
json.dumps(with_json_schema(payload, schema_url), indent=2) + "\n",
|
|
65
|
+
encoding="utf-8",
|
|
66
|
+
)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Discover / write lifecycle webhook configs under src/app/lifecycle/."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from caraer_cli.project.paths import lifecycle_dir
|
|
10
|
+
from caraer_cli.project.schema import ProjectConfig
|
|
11
|
+
|
|
12
|
+
# filename stem → (manifest key, expected topic)
|
|
13
|
+
LIFECYCLE_HOOKS: dict[str, tuple[str, str]] = {
|
|
14
|
+
"install": ("installWebhook", "app.installed"),
|
|
15
|
+
"uninstall": ("uninstallWebhook", "app.uninstalled"),
|
|
16
|
+
"rotate": ("rotateWebhook", "app.rotated"),
|
|
17
|
+
"update": ("updateWebhook", "app.updated"),
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
MANIFEST_KEY_TO_FILE = {
|
|
21
|
+
manifest_key: stem for stem, (manifest_key, _) in LIFECYCLE_HOOKS.items()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
LOCAL_LIFECYCLE_KEYS = (
|
|
25
|
+
"topic",
|
|
26
|
+
"deliveryMode",
|
|
27
|
+
"url",
|
|
28
|
+
"description",
|
|
29
|
+
"enabled",
|
|
30
|
+
"webhookFormat",
|
|
31
|
+
"secret",
|
|
32
|
+
"serverlessFunction",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def sanitize_lifecycle(item: dict[str, Any]) -> dict[str, Any]:
|
|
37
|
+
payload: dict[str, Any] = {}
|
|
38
|
+
for key in LOCAL_LIFECYCLE_KEYS:
|
|
39
|
+
if key in item and item[key] is not None:
|
|
40
|
+
payload[key] = item[key]
|
|
41
|
+
sf = payload.get("serverlessFunction")
|
|
42
|
+
if isinstance(sf, dict):
|
|
43
|
+
cleaned: dict[str, Any] = {}
|
|
44
|
+
if sf.get("name"):
|
|
45
|
+
cleaned["name"] = sf["name"]
|
|
46
|
+
elif sf.get("uuid"):
|
|
47
|
+
cleaned["uuid"] = sf["uuid"]
|
|
48
|
+
if cleaned:
|
|
49
|
+
payload["serverlessFunction"] = cleaned
|
|
50
|
+
else:
|
|
51
|
+
payload.pop("serverlessFunction", None)
|
|
52
|
+
return payload
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def discover_local_lifecycle(
|
|
56
|
+
root: Path, config: ProjectConfig
|
|
57
|
+
) -> dict[str, dict[str, Any]]:
|
|
58
|
+
"""Return manifest_key → sanitized webhook dict for present lifecycle files."""
|
|
59
|
+
base = lifecycle_dir(root, config.srcDir)
|
|
60
|
+
if not base.is_dir():
|
|
61
|
+
return {}
|
|
62
|
+
found: dict[str, dict[str, Any]] = {}
|
|
63
|
+
for stem, (manifest_key, _topic) in LIFECYCLE_HOOKS.items():
|
|
64
|
+
path = base / f"{stem}.json"
|
|
65
|
+
if not path.is_file():
|
|
66
|
+
continue
|
|
67
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
68
|
+
if isinstance(data, dict):
|
|
69
|
+
found[manifest_key] = sanitize_lifecycle(data)
|
|
70
|
+
return found
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def write_lifecycle_files(
|
|
74
|
+
root: Path, config: ProjectConfig, hooks: dict[str, dict[str, Any] | None]
|
|
75
|
+
) -> int:
|
|
76
|
+
"""Write lifecycle/*.json from manifest hook objects. Clears missing files."""
|
|
77
|
+
base = lifecycle_dir(root, config.srcDir)
|
|
78
|
+
base.mkdir(parents=True, exist_ok=True)
|
|
79
|
+
count = 0
|
|
80
|
+
for stem, (manifest_key, expected_topic) in LIFECYCLE_HOOKS.items():
|
|
81
|
+
path = base / f"{stem}.json"
|
|
82
|
+
raw = hooks.get(manifest_key)
|
|
83
|
+
if not isinstance(raw, dict) or not raw:
|
|
84
|
+
if path.exists():
|
|
85
|
+
path.unlink()
|
|
86
|
+
continue
|
|
87
|
+
sanitized = sanitize_lifecycle(raw)
|
|
88
|
+
if not sanitized.get("topic"):
|
|
89
|
+
sanitized["topic"] = expected_topic
|
|
90
|
+
path.write_text(json.dumps(sanitized, indent=2) + "\n", encoding="utf-8")
|
|
91
|
+
count += 1
|
|
92
|
+
return count
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def resolve_lifecycle_functions(
|
|
96
|
+
webhook: dict[str, Any],
|
|
97
|
+
*,
|
|
98
|
+
fn_by_name: dict[str, str],
|
|
99
|
+
strict: bool = True,
|
|
100
|
+
) -> dict[str, Any] | None:
|
|
101
|
+
out = dict(webhook)
|
|
102
|
+
sf = out.get("serverlessFunction")
|
|
103
|
+
if not isinstance(sf, dict):
|
|
104
|
+
return out
|
|
105
|
+
if sf.get("uuid"):
|
|
106
|
+
out["serverlessFunction"] = {"uuid": sf["uuid"]}
|
|
107
|
+
return out
|
|
108
|
+
name = sf.get("name")
|
|
109
|
+
if name and name in fn_by_name:
|
|
110
|
+
out["serverlessFunction"] = {"uuid": fn_by_name[name]}
|
|
111
|
+
return out
|
|
112
|
+
if name:
|
|
113
|
+
if strict:
|
|
114
|
+
raise ValueError(
|
|
115
|
+
f"Lifecycle webhook references function '{name}' but no UUID is known. "
|
|
116
|
+
"Push functions first."
|
|
117
|
+
)
|
|
118
|
+
return None
|
|
119
|
+
return out
|