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.
Files changed (83) hide show
  1. caraer_cli/__init__.py +4 -0
  2. caraer_cli/api/__init__.py +1 -0
  3. caraer_cli/api/apps.py +78 -0
  4. caraer_cli/api/auth.py +53 -0
  5. caraer_cli/api/client.py +179 -0
  6. caraer_cli/api/functions.py +72 -0
  7. caraer_cli/api/installation.py +93 -0
  8. caraer_cli/api/integration_runtime.py +77 -0
  9. caraer_cli/api/projects.py +213 -0
  10. caraer_cli/api/sandboxes.py +29 -0
  11. caraer_cli/api/webhooks.py +59 -0
  12. caraer_cli/app_sync.py +697 -0
  13. caraer_cli/apps_local.py +274 -0
  14. caraer_cli/commands/__init__.py +1 -0
  15. caraer_cli/commands/apps.py +2025 -0
  16. caraer_cli/commands/auth.py +173 -0
  17. caraer_cli/commands/company.py +68 -0
  18. caraer_cli/commands/installation.py +182 -0
  19. caraer_cli/commands/profile.py +239 -0
  20. caraer_cli/commands/publish.py +40 -0
  21. caraer_cli/commands/sandbox.py +92 -0
  22. caraer_cli/commands/skill.py +165 -0
  23. caraer_cli/commands/webhooks.py +68 -0
  24. caraer_cli/completion.py +77 -0
  25. caraer_cli/completion_callbacks.py +162 -0
  26. caraer_cli/context.py +41 -0
  27. caraer_cli/errors.py +84 -0
  28. caraer_cli/formatters/__init__.py +1 -0
  29. caraer_cli/formatters/output.py +381 -0
  30. caraer_cli/formatters/timestamps.py +98 -0
  31. caraer_cli/install_completion.py +38 -0
  32. caraer_cli/local_app.py +135 -0
  33. caraer_cli/main.py +104 -0
  34. caraer_cli/models/__init__.py +1 -0
  35. caraer_cli/models/api.py +52 -0
  36. caraer_cli/project/__init__.py +17 -0
  37. caraer_cli/project/app_bars_sync.py +154 -0
  38. caraer_cli/project/app_manifest_template.py +68 -0
  39. caraer_cli/project/inbound_sync.py +192 -0
  40. caraer_cli/project/json_schemas.py +66 -0
  41. caraer_cli/project/lifecycle_sync.py +119 -0
  42. caraer_cli/project/local_dev.py +825 -0
  43. caraer_cli/project/manifest_edit.py +51 -0
  44. caraer_cli/project/marketplace_assemble.py +165 -0
  45. caraer_cli/project/oauth_providers_sync.py +180 -0
  46. caraer_cli/project/paths.py +102 -0
  47. caraer_cli/project/pricing_sync.py +81 -0
  48. caraer_cli/project/push_plan.py +590 -0
  49. caraer_cli/project/release.py +128 -0
  50. caraer_cli/project/scaffold.py +537 -0
  51. caraer_cli/project/schedules_sync.py +170 -0
  52. caraer_cli/project/schema.py +160 -0
  53. caraer_cli/project/settings_sync.py +77 -0
  54. caraer_cli/project/state.py +51 -0
  55. caraer_cli/project/sync.py +308 -0
  56. caraer_cli/project/typegen.py +278 -0
  57. caraer_cli/project/validate_app.py +1215 -0
  58. caraer_cli/project/webhooks_sync.py +233 -0
  59. caraer_cli/resolve.py +55 -0
  60. caraer_cli/schemas/app.caraer.schema.json +175 -0
  61. caraer_cli/schemas/function.caraer.schema.json +29 -0
  62. caraer_cli/schemas/inbound.caraer.schema.json +49 -0
  63. caraer_cli/schemas/lifecycle.caraer.schema.json +50 -0
  64. caraer_cli/schemas/schedule.caraer.schema.json +39 -0
  65. caraer_cli/schemas/webhook.caraer.schema.json +50 -0
  66. caraer_cli/skills/.gitkeep +2 -0
  67. caraer_cli/skills/caraer-apps/SKILL.md +172 -0
  68. caraer_cli/skills/caraer-apps/reference.md +163 -0
  69. caraer_cli/state/__init__.py +1 -0
  70. caraer_cli/state/config.py +148 -0
  71. caraer_cli/state/session.py +98 -0
  72. caraer_cli/utils.py +40 -0
  73. caraer_cli/wizard/__init__.py +9 -0
  74. caraer_cli/wizard/catalog.py +170 -0
  75. caraer_cli/wizard/marketplace.py +350 -0
  76. caraer_cli/wizard/prompts.py +99 -0
  77. caraer_cli/wizard/public_app.py +519 -0
  78. caraer_cli/wizard/scope_macros.py +214 -0
  79. caraer_cli-0.1.2.dist-info/METADATA +266 -0
  80. caraer_cli-0.1.2.dist-info/RECORD +83 -0
  81. caraer_cli-0.1.2.dist-info/WHEEL +4 -0
  82. caraer_cli-0.1.2.dist-info/entry_points.txt +2 -0
  83. caraer_cli-0.1.2.dist-info/licenses/LICENSE +8 -0
@@ -0,0 +1,308 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from caraer_cli.api import functions as functions_api
9
+ from caraer_cli.api.client import CaraerApiClient
10
+ from caraer_cli.project.paths import functions_dir
11
+ from caraer_cli.project.schema import (
12
+ FunctionManifest,
13
+ ProjectConfig,
14
+ load_function_manifest,
15
+ save_function_manifest,
16
+ )
17
+ from caraer_cli.project.state import load_state, save_state
18
+
19
+
20
+ def _hash_text(value: str) -> str:
21
+ return hashlib.sha256(value.encode("utf-8")).hexdigest()
22
+
23
+
24
+ def discover_local_functions(
25
+ root: Path, config: ProjectConfig
26
+ ) -> list[tuple[FunctionManifest, Path, str, dict[str, str]]]:
27
+ base = functions_dir(root, config.srcDir)
28
+ if not base.is_dir():
29
+ return []
30
+ found: list[tuple[FunctionManifest, Path, str, dict[str, str]]] = []
31
+ for child in sorted(base.iterdir()):
32
+ if not child.is_dir():
33
+ continue
34
+ manifest_path = child / "function.caraer.json"
35
+ if not manifest_path.is_file():
36
+ continue
37
+ manifest = load_function_manifest(manifest_path)
38
+ entry_path = child / manifest.resolved_entry()
39
+ if not entry_path.is_file():
40
+ raise FileNotFoundError(f"Missing entry file for function '{manifest.name}': {entry_path}")
41
+ code = entry_path.read_text(encoding="utf-8")
42
+ source_files = _collect_source_files(child, entry_path)
43
+ found.append((manifest, entry_path, code, source_files))
44
+ return found
45
+
46
+
47
+ def _collect_source_files(folder: Path, entry_path: Path) -> dict[str, str]:
48
+ """All non-manifest files in the function folder except the entry file."""
49
+ files: dict[str, str] = {}
50
+ entry_resolved = entry_path.resolve()
51
+ for path in sorted(folder.rglob("*")):
52
+ if not path.is_file():
53
+ continue
54
+ if path.name == "function.caraer.json":
55
+ continue
56
+ if path.resolve() == entry_resolved:
57
+ continue
58
+ relative = path.relative_to(folder).as_posix()
59
+ if ".." in relative.split("/"):
60
+ continue
61
+ files[relative] = path.read_text(encoding="utf-8")
62
+ return files
63
+
64
+
65
+ def list_local_function_names(root: Path, config: ProjectConfig) -> list[str]:
66
+ return [manifest.name for manifest, _, _, _ in discover_local_functions(root, config)]
67
+
68
+
69
+ def resolve_local_function_name(
70
+ root: Path,
71
+ config: ProjectConfig,
72
+ function: str | None = None,
73
+ *,
74
+ interactive: bool = True,
75
+ ) -> str:
76
+ """Resolve a function name from an explicit arg or the local functions folder."""
77
+ names = list_local_function_names(root, config)
78
+ if function and function.strip():
79
+ name = function.strip()
80
+ if name not in names and names:
81
+ raise ValueError(
82
+ f"Unknown function '{name}'. Available: {', '.join(names)}"
83
+ )
84
+ return name
85
+ if not names:
86
+ raise ValueError(
87
+ "No local functions found under src/app/functions/. "
88
+ "Pass --function <name> or add a function folder."
89
+ )
90
+ if len(names) == 1:
91
+ return names[0]
92
+ if interactive:
93
+ try:
94
+ from caraer_cli.wizard.prompts import WizardCancelled, ask_select
95
+
96
+ return ask_select("Select a function", names)
97
+ except WizardCancelled:
98
+ raise ValueError("No function selected.") from None
99
+ except Exception:
100
+ pass
101
+ raise ValueError(
102
+ "Multiple functions found; pass --function <name>. "
103
+ f"Available: {', '.join(names)}"
104
+ )
105
+
106
+
107
+ def upload_functions(
108
+ client: CaraerApiClient,
109
+ root: Path,
110
+ config: ProjectConfig,
111
+ *,
112
+ delete_missing: bool = False,
113
+ ) -> dict[str, Any]:
114
+ if not config.appUuid:
115
+ raise ValueError("App has no remote UUID. Run 'caraer apps select' or 'caraer apps push'.")
116
+
117
+ remote = functions_api.list_functions(client, config.appUuid, page=1, limit=200)
118
+ remote_items = remote.get("data") or []
119
+ by_name = {str(item.get("name") or ""): item for item in remote_items if isinstance(item, dict)}
120
+ by_uuid = {str(item.get("uuid") or ""): item for item in remote_items if isinstance(item, dict)}
121
+
122
+ state = load_state(root)
123
+ fn_state: dict[str, Any] = state.setdefault("functions", {})
124
+ results: list[dict[str, Any]] = []
125
+ local_names: set[str] = set()
126
+
127
+ for manifest, _entry_path, code, source_files in discover_local_functions(root, config):
128
+ local_names.add(manifest.name)
129
+ extras_blob = json.dumps(source_files, sort_keys=True) if source_files else ""
130
+ content_hash = _hash_text(
131
+ f"{manifest.runtime}\n{manifest.description}\n{code}\n{extras_blob}"
132
+ )
133
+ existing_state = fn_state.get(manifest.name) or {}
134
+ existing_uuid = existing_state.get("uuid")
135
+ remote_item = by_uuid.get(existing_uuid) if existing_uuid else by_name.get(manifest.name)
136
+
137
+ payload: dict[str, Any] = {
138
+ "name": manifest.name,
139
+ "label": manifest.name,
140
+ "runtime": manifest.runtime,
141
+ "code": code,
142
+ "description": manifest.description or manifest.name,
143
+ "sourceFiles": source_files,
144
+ }
145
+
146
+ remote_code = remote_item.get("code") if remote_item else None
147
+ remote_files = remote_item.get("sourceFiles") if remote_item else None
148
+ if remote_item and remote_item.get("uuid"):
149
+ uuid = str(remote_item["uuid"])
150
+ if (
151
+ existing_state.get("hash") == content_hash
152
+ and remote_code == code
153
+ and (remote_files or {}) == source_files
154
+ ):
155
+ results.append({"name": manifest.name, "uuid": uuid, "action": "unchanged"})
156
+ else:
157
+ response = functions_api.update_function(client, config.appUuid, uuid, payload)
158
+ data = response.get("data") or {}
159
+ results.append({"name": manifest.name, "uuid": data.get("uuid", uuid), "action": "updated"})
160
+ fn_state[manifest.name] = {"uuid": data.get("uuid", uuid), "hash": content_hash}
161
+ else:
162
+ response = functions_api.create_function(client, config.appUuid, payload)
163
+ data = response.get("data") or {}
164
+ uuid = data.get("uuid")
165
+ results.append({"name": manifest.name, "uuid": uuid, "action": "created"})
166
+ if uuid:
167
+ fn_state[manifest.name] = {"uuid": uuid, "hash": content_hash}
168
+
169
+ deleted: list[dict[str, Any]] = []
170
+ if delete_missing:
171
+ for name, meta in list(fn_state.items()):
172
+ if name in local_names:
173
+ continue
174
+ uuid = meta.get("uuid")
175
+ if uuid:
176
+ functions_api.delete_function(client, config.appUuid, str(uuid))
177
+ deleted.append({"name": name, "uuid": uuid, "action": "deleted"})
178
+ fn_state.pop(name, None)
179
+
180
+ save_state(root, state)
181
+ return {"functions": results, "deleted": deleted}
182
+
183
+
184
+ def pull_functions(client: CaraerApiClient, root: Path, config: ProjectConfig) -> dict[str, Any]:
185
+ if not config.appUuid:
186
+ raise ValueError("App has no remote UUID. Run 'caraer apps select' or 'caraer apps push'.")
187
+
188
+ remote = functions_api.list_functions(client, config.appUuid, page=1, limit=200)
189
+ remote_items = remote.get("data") or []
190
+ base = functions_dir(root, config.srcDir)
191
+ base.mkdir(parents=True, exist_ok=True)
192
+ state = load_state(root)
193
+ fn_state: dict[str, Any] = state.setdefault("functions", {})
194
+ pulled: list[dict[str, Any]] = []
195
+
196
+ for item in remote_items:
197
+ if not isinstance(item, dict):
198
+ continue
199
+ name = str(item.get("name") or item.get("uuid") or "function")
200
+ runtime = str(item.get("runtime") or "nodejs22")
201
+ description = str(item.get("description") or "")
202
+ code = str(item.get("code") or "")
203
+ source_files = item.get("sourceFiles") if isinstance(item.get("sourceFiles"), dict) else {}
204
+ uuid = str(item.get("uuid") or "")
205
+ folder = base / name
206
+ folder.mkdir(parents=True, exist_ok=True)
207
+ manifest = FunctionManifest(name=name, runtime=runtime, description=description)
208
+ save_function_manifest(folder / "function.caraer.json", manifest)
209
+ entry = folder / manifest.resolved_entry()
210
+ entry.write_text(code, encoding="utf-8")
211
+ for relative, content in source_files.items():
212
+ if not isinstance(relative, str) or not relative.strip() or ".." in relative.split("/"):
213
+ continue
214
+ target = folder / relative
215
+ target.parent.mkdir(parents=True, exist_ok=True)
216
+ target.write_text(str(content or ""), encoding="utf-8")
217
+ extras_blob = json.dumps(source_files, sort_keys=True) if source_files else ""
218
+ fn_state[name] = {
219
+ "uuid": uuid,
220
+ "hash": _hash_text(f"{runtime}\n{description}\n{code}\n{extras_blob}"),
221
+ }
222
+ pulled.append({"name": name, "uuid": uuid, "path": str(folder)})
223
+
224
+ save_state(root, state)
225
+ return {"functions": pulled}
226
+
227
+
228
+ def status_summary(client: CaraerApiClient, root: Path, config: ProjectConfig) -> dict[str, Any]:
229
+ local = discover_local_functions(root, config)
230
+ local_names = {m.name for m, _, _, _ in local}
231
+ remote_items: list[dict[str, Any]] = []
232
+ remote_app: dict[str, Any] = {}
233
+ if config.appUuid:
234
+ remote = functions_api.list_functions(client, config.appUuid, page=1, limit=200)
235
+ remote_items = [i for i in (remote.get("data") or []) if isinstance(i, dict)]
236
+ try:
237
+ from caraer_cli.api import apps as apps_api
238
+
239
+ app_response = apps_api.get_public_app(client, config.appUuid)
240
+ if isinstance(app_response.get("data"), dict):
241
+ remote_app = app_response["data"]
242
+ except Exception: # noqa: BLE001
243
+ remote_app = {}
244
+ remote_names = {str(i.get("name") or "") for i in remote_items}
245
+ state = load_state(root)
246
+ summary: dict[str, Any] = {
247
+ "appUuid": config.appUuid,
248
+ "projectUuid": state.get("projectUuid"),
249
+ "platformVersion": config.platformVersion,
250
+ "appPlatformVersion": remote_app.get("platformVersion") or config.api_platform_version(),
251
+ "localFunctions": sorted(local_names),
252
+ "remoteFunctions": sorted(n for n in remote_names if n),
253
+ "onlyLocal": sorted(local_names - remote_names),
254
+ "onlyRemote": sorted(remote_names - local_names),
255
+ "tracked": state.get("functions") or {},
256
+ "lastBuildUuid": state.get("lastBuildUuid"),
257
+ "lastDeployUuid": state.get("lastDeployUuid"),
258
+ }
259
+ if config.is_app_platform_v2() or remote_app.get("platformVersion") == 2:
260
+ summary["runtime"] = remote_app.get("runtime") or config.runtime
261
+ summary["runtimeStatus"] = remote_app.get("runtimeStatus")
262
+ summary["runtimeBaseUrl"] = remote_app.get("runtimeBaseUrl")
263
+ summary["runtimeRevision"] = remote_app.get("runtimeRevision")
264
+ summary["runtimeError"] = remote_app.get("runtimeError")
265
+ return summary
266
+
267
+
268
+ def scaffold_function(
269
+ root: Path,
270
+ config: ProjectConfig,
271
+ name: str,
272
+ runtime: str = "nodejs22",
273
+ *,
274
+ description: str | None = None,
275
+ force: bool = False,
276
+ ) -> Path:
277
+ """Create ``src/app/functions/<name>/`` with manifest + entry source."""
278
+ folder = functions_dir(root, config.srcDir) / name
279
+ manifest_path = folder / "function.caraer.json"
280
+ if folder.exists() and any(folder.iterdir()) and not force:
281
+ raise FileExistsError(
282
+ f"Function folder already exists: {folder}. Use --force to overwrite scaffold files."
283
+ )
284
+ folder.mkdir(parents=True, exist_ok=True)
285
+ manifest = FunctionManifest(
286
+ name=name,
287
+ runtime=runtime,
288
+ description=description if description is not None else name,
289
+ )
290
+ save_function_manifest(manifest_path, manifest)
291
+ entry = folder / manifest.resolved_entry()
292
+ if force or not entry.exists():
293
+ if runtime.startswith("python"):
294
+ entry.write_text(
295
+ 'def handler(request):\n'
296
+ ' """Caraer serverless entrypoint."""\n'
297
+ ' return {"statusCode": 200, "body": {"ok": True}}\n',
298
+ encoding="utf-8",
299
+ )
300
+ else:
301
+ entry.write_text(
302
+ "/**\n * Caraer serverless entrypoint.\n */\n"
303
+ "exports.handler = async (req, res) => {\n"
304
+ " res.status(200).json({ ok: true });\n"
305
+ "};\n",
306
+ encoding="utf-8",
307
+ )
308
+ return folder
@@ -0,0 +1,278 @@
1
+ """Generate typed helpers for Caraer serverless function payloads."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any, Literal
7
+
8
+ from caraer_cli.project.schema import ProjectConfig
9
+
10
+ RuntimeKind = Literal["nodejs", "python"]
11
+
12
+ TYPESCRIPT_FILENAME = "caraer.d.ts"
13
+ PYTHON_FILENAME = "caraer_types.py"
14
+
15
+
16
+ def resolve_runtime_kind(runtime: str | None) -> RuntimeKind:
17
+ text = (runtime or "nodejs22").strip().lower()
18
+ if text.startswith("python"):
19
+ return "python"
20
+ return "nodejs"
21
+
22
+
23
+ def types_dir(root: Path, config: ProjectConfig) -> Path:
24
+ return root / config.srcDir / "types"
25
+
26
+
27
+ def typegen_output_path(root: Path, config: ProjectConfig) -> Path:
28
+ kind = resolve_runtime_kind(config.runtime)
29
+ base = types_dir(root, config)
30
+ if kind == "python":
31
+ return base / PYTHON_FILENAME
32
+ return base / TYPESCRIPT_FILENAME
33
+
34
+
35
+ def generate_types(
36
+ root: Path,
37
+ config: ProjectConfig,
38
+ *,
39
+ force: bool = False,
40
+ ) -> dict[str, Any]:
41
+ """Write typed helpers under ``src/types/`` for the app runtime."""
42
+ kind = resolve_runtime_kind(config.runtime)
43
+ out = typegen_output_path(root, config)
44
+ if out.exists() and not force:
45
+ raise FileExistsError(
46
+ f"Typegen output already exists: {out}. Use --force to overwrite."
47
+ )
48
+ out.parent.mkdir(parents=True, exist_ok=True)
49
+ if kind == "python":
50
+ out.write_text(_python_types(), encoding="utf-8")
51
+ else:
52
+ out.write_text(_typescript_types(), encoding="utf-8")
53
+ return {
54
+ "path": out,
55
+ "runtime": kind,
56
+ "relative": str(out.relative_to(root)),
57
+ }
58
+
59
+
60
+ def _typescript_types() -> str:
61
+ return '''\
62
+ /**
63
+ * Generated by `caraer apps typegen`. Do not edit by hand.
64
+ *
65
+ * Common serverless / installation payload shapes for Caraer apps.
66
+ */
67
+
68
+ /** Installation settings field as delivered in lifecycle / webhook bodies. */
69
+ export interface SettingFieldValue {
70
+ name: string;
71
+ label?: string;
72
+ type?: string;
73
+ value?: unknown;
74
+ defaultValue?: unknown;
75
+ hasValue?: boolean;
76
+ }
77
+
78
+ /** Approximate lifecycle hook body (install / update / uninstall / rotate). */
79
+ export interface LifecyclePayload {
80
+ event?: "Installed" | "Updated" | "Uninstalled" | "Rotated" | string;
81
+ timestamp?: number;
82
+ appUuid?: string;
83
+ appName?: string;
84
+ appLabel?: string;
85
+ privateApp?: boolean;
86
+ authMethod?: "OAUTH2" | "API_KEY" | string;
87
+ userUuid?: string;
88
+ companyUuid?: string;
89
+ installationToken?: string;
90
+ oauthConnected?: boolean;
91
+ settingsSchema?: SettingFieldValue[];
92
+ scopes?: string[];
93
+ settingsChanged?: boolean;
94
+ scopesChanged?: boolean;
95
+ filtersChanged?: boolean;
96
+ [key: string]: unknown;
97
+ }
98
+
99
+ /** Record / domain webhook body (USER_FRIENDLY shape varies by topic). */
100
+ export interface WebhookPayload {
101
+ topic?: string;
102
+ event?: string;
103
+ timestamp?: number;
104
+ appUuid?: string;
105
+ companyUuid?: string;
106
+ installationToken?: string;
107
+ record?: Record<string, unknown>;
108
+ objectName?: string;
109
+ settingsSchema?: SettingFieldValue[];
110
+ [key: string]: unknown;
111
+ }
112
+
113
+ /** Cron / schedule invocation body. */
114
+ export interface SchedulePayload {
115
+ scheduleName?: string;
116
+ name?: string;
117
+ appUuid?: string;
118
+ companyUuid?: string;
119
+ installationToken?: string;
120
+ scheduledAt?: string | number;
121
+ settingsSchema?: SettingFieldValue[];
122
+ [key: string]: unknown;
123
+ }
124
+
125
+ /** Inbound HTTP route body (provider-specific; wrapped by the platform). */
126
+ export interface InboundPayload {
127
+ appUuid?: string;
128
+ companyUuid?: string;
129
+ installationToken?: string;
130
+ routeName?: string;
131
+ body?: unknown;
132
+ headers?: Record<string, string>;
133
+ [key: string]: unknown;
134
+ }
135
+
136
+ /** Async installation job payload (function receives this as req.body). */
137
+ export interface JobPayload {
138
+ jobId?: string;
139
+ functionName?: string;
140
+ appUuid?: string;
141
+ companyUuid?: string;
142
+ installationToken?: string;
143
+ [key: string]: unknown;
144
+ }
145
+
146
+ /** Installation state document (arbitrary JSON map). */
147
+ export type InstallationState = Record<string, unknown>;
148
+
149
+ /** Installation secrets map (values are strings when readable). */
150
+ export type InstallationSecrets = Record<string, string>;
151
+
152
+ /** POST /installation/jobs request body. */
153
+ export interface EnqueueJobRequest {
154
+ functionName: string;
155
+ payload?: Record<string, unknown>;
156
+ delaySeconds?: number;
157
+ }
158
+
159
+ /** Installation job status document. */
160
+ export interface InstallationJob {
161
+ jobId: string;
162
+ status: "QUEUED" | "RUNNING" | "SUCCEEDED" | "FAILED" | string;
163
+ functionName?: string;
164
+ payload?: Record<string, unknown>;
165
+ result?: unknown;
166
+ error?: string;
167
+ createdAt?: string | number;
168
+ updatedAt?: string | number;
169
+ [key: string]: unknown;
170
+ }
171
+
172
+ /** Wrapper returned by many Caraer API endpoints. */
173
+ export interface ApiSuccess<T = unknown> {
174
+ message?: string;
175
+ data?: T;
176
+ }
177
+ '''
178
+
179
+
180
+ def _python_types() -> str:
181
+ return '''\
182
+ """Generated by `caraer apps typegen`. Do not edit by hand.
183
+
184
+ Common serverless / installation payload shapes for Caraer apps.
185
+ """
186
+
187
+ from __future__ import annotations
188
+
189
+ from typing import Any, NotRequired, TypedDict
190
+
191
+
192
+ class SettingFieldValue(TypedDict, total=False):
193
+ name: str
194
+ label: str
195
+ type: str
196
+ value: Any
197
+ defaultValue: Any
198
+ hasValue: bool
199
+
200
+
201
+ class LifecyclePayload(TypedDict, total=False):
202
+ event: str
203
+ timestamp: int | float
204
+ appUuid: str
205
+ appName: str
206
+ appLabel: str
207
+ privateApp: bool
208
+ authMethod: str
209
+ userUuid: str
210
+ companyUuid: str
211
+ installationToken: str
212
+ oauthConnected: bool
213
+ settingsSchema: list[SettingFieldValue]
214
+ scopes: list[str]
215
+ settingsChanged: bool
216
+ scopesChanged: bool
217
+ filtersChanged: bool
218
+
219
+
220
+ class WebhookPayload(TypedDict, total=False):
221
+ topic: str
222
+ event: str
223
+ timestamp: int | float
224
+ appUuid: str
225
+ companyUuid: str
226
+ installationToken: str
227
+ record: dict[str, Any]
228
+ objectName: str
229
+ settingsSchema: list[SettingFieldValue]
230
+
231
+
232
+ class SchedulePayload(TypedDict, total=False):
233
+ scheduleName: str
234
+ name: str
235
+ appUuid: str
236
+ companyUuid: str
237
+ installationToken: str
238
+ scheduledAt: str | int | float
239
+ settingsSchema: list[SettingFieldValue]
240
+
241
+
242
+ class InboundPayload(TypedDict, total=False):
243
+ appUuid: str
244
+ companyUuid: str
245
+ installationToken: str
246
+ routeName: str
247
+ body: Any
248
+ headers: dict[str, str]
249
+
250
+
251
+ class JobPayload(TypedDict, total=False):
252
+ jobId: str
253
+ functionName: str
254
+ appUuid: str
255
+ companyUuid: str
256
+ installationToken: str
257
+
258
+
259
+ InstallationState = dict[str, Any]
260
+ InstallationSecrets = dict[str, str]
261
+
262
+
263
+ class EnqueueJobRequest(TypedDict):
264
+ functionName: str
265
+ payload: NotRequired[dict[str, Any]]
266
+ delaySeconds: NotRequired[float]
267
+
268
+
269
+ class InstallationJob(TypedDict, total=False):
270
+ jobId: str
271
+ status: str
272
+ functionName: str
273
+ payload: dict[str, Any]
274
+ result: Any
275
+ error: str
276
+ createdAt: str | int | float
277
+ updatedAt: str | int | float
278
+ '''