@openlucaskaka/kagent 0.1.7
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.
- package/README.md +353 -0
- package/npm/bin/kagent-serve.js +6 -0
- package/npm/bin/kagent.js +6 -0
- package/npm/lib/App.js +524 -0
- package/npm/lib/app-state.js +224 -0
- package/npm/lib/approval-choice.js +25 -0
- package/npm/lib/commands.js +59 -0
- package/npm/lib/editor.js +188 -0
- package/npm/lib/ink-runner.js +41 -0
- package/npm/lib/kagent-home.js +39 -0
- package/npm/lib/launcher.js +221 -0
- package/npm/lib/protocol.js +33 -0
- package/npm/lib/provider-setup.js +139 -0
- package/npm/lib/python-runner.js +892 -0
- package/npm/lib/runtime-client.js +390 -0
- package/npm/lib/terminal-input.js +127 -0
- package/npm/lib/terminal-text.js +19 -0
- package/npm/lib/terminal-width.js +14 -0
- package/npm/lib/transcript.js +227 -0
- package/npm/lib/ui-components.js +247 -0
- package/npm/lib/update-manager.js +334 -0
- package/package.json +39 -0
- package/pyproject.toml +55 -0
- package/src/kagent/__init__.py +90 -0
- package/src/kagent/cli/__init__.py +5 -0
- package/src/kagent/cli/__main__.py +6 -0
- package/src/kagent/cli/commands.py +112 -0
- package/src/kagent/cli/conversation.py +127 -0
- package/src/kagent/cli/interactive.py +841 -0
- package/src/kagent/cli/main.py +685 -0
- package/src/kagent/cli/memory.py +460 -0
- package/src/kagent/cli/pending_approval.py +169 -0
- package/src/kagent/cli/provider.py +27 -0
- package/src/kagent/cli/session_commands.py +401 -0
- package/src/kagent/cli/stdio_runtime.py +784 -0
- package/src/kagent/cli/trace.py +67 -0
- package/src/kagent/cli/ui.py +931 -0
- package/src/kagent/core/__init__.py +0 -0
- package/src/kagent/core/agent.py +296 -0
- package/src/kagent/core/faults.py +11 -0
- package/src/kagent/core/invariants.py +47 -0
- package/src/kagent/core/normalization.py +81 -0
- package/src/kagent/core/planning.py +48 -0
- package/src/kagent/core/state.py +73 -0
- package/src/kagent/core/summary.py +64 -0
- package/src/kagent/core/tools.py +196 -0
- package/src/kagent/core/trace.py +57 -0
- package/src/kagent/eval/__init__.py +11 -0
- package/src/kagent/eval/cases.py +229 -0
- package/src/kagent/eval/evaluator.py +216 -0
- package/src/kagent/integrations/__init__.py +3 -0
- package/src/kagent/integrations/audit.py +95 -0
- package/src/kagent/integrations/backends.py +131 -0
- package/src/kagent/integrations/memory.py +301 -0
- package/src/kagent/ops/__init__.py +0 -0
- package/src/kagent/ops/batch.py +156 -0
- package/src/kagent/ops/doctor.py +255 -0
- package/src/kagent/ops/metrics.py +214 -0
- package/src/kagent/ops/release_evidence.py +877 -0
- package/src/kagent/ops/release_manifest.py +142 -0
- package/src/kagent/ops/trace_replay.py +285 -0
- package/src/kagent/providers/__init__.py +7 -0
- package/src/kagent/providers/embeddings.py +187 -0
- package/src/kagent/providers/llm.py +770 -0
- package/src/kagent/py.typed +1 -0
- package/src/kagent/runtime/__init__.py +28 -0
- package/src/kagent/runtime/action_graph.py +543 -0
- package/src/kagent/runtime/agent.py +2089 -0
- package/src/kagent/runtime/approval.py +64 -0
- package/src/kagent/runtime/cancellation.py +64 -0
- package/src/kagent/runtime/checkpoint_state.py +146 -0
- package/src/kagent/runtime/checkpoint_storage.py +270 -0
- package/src/kagent/runtime/context.py +65 -0
- package/src/kagent/runtime/file_transaction.py +195 -0
- package/src/kagent/runtime/hooks.py +74 -0
- package/src/kagent/runtime/metadata.py +116 -0
- package/src/kagent/runtime/patch_checkpoints.py +385 -0
- package/src/kagent/runtime/policy.py +50 -0
- package/src/kagent/runtime/presentation.py +205 -0
- package/src/kagent/runtime/redaction.py +130 -0
- package/src/kagent/runtime/sandbox.py +331 -0
- package/src/kagent/runtime/skills.py +66 -0
- package/src/kagent/runtime/steering.py +56 -0
- package/src/kagent/runtime/steps.py +255 -0
- package/src/kagent/runtime/task_state.py +40 -0
- package/src/kagent/runtime/tools.py +3532 -0
- package/src/kagent/runtime/types.py +240 -0
- package/src/kagent/runtime/workspace.py +597 -0
- package/src/kagent/service/__init__.py +26 -0
- package/src/kagent/service/__main__.py +6 -0
- package/src/kagent/service/active_runs.py +178 -0
- package/src/kagent/service/cli.py +737 -0
- package/src/kagent/service/contract.py +2571 -0
- package/src/kagent/service/errors.py +71 -0
- package/src/kagent/service/idempotency.py +584 -0
- package/src/kagent/service/router.py +884 -0
- package/src/kagent/service/run.py +150 -0
- package/src/kagent/service/runtime.py +1915 -0
- package/src/kagent/service/runtime_approval.py +20 -0
- package/src/kagent/service/runtime_cancel.py +192 -0
- package/src/kagent/service/runtime_lifecycle.py +229 -0
- package/src/kagent/service/runtime_metadata.py +21 -0
- package/src/kagent/service/runtime_policy.py +115 -0
- package/src/kagent/service/runtime_recovery.py +573 -0
- package/src/kagent/service/runtime_resume.py +382 -0
- package/src/kagent/service/runtime_resume_claim.py +116 -0
- package/src/kagent/service/runtime_run.py +350 -0
- package/src/kagent/service/runtime_status.py +2007 -0
- package/src/kagent/service/safety.py +139 -0
- package/src/kagent/service/server.py +114 -0
- package/src/kagent/service/status.py +233 -0
- package/src/kagent/service/trace_store.py +322 -0
- package/src/kagent/service/transport.py +24 -0
- package/src/kagent/utils/__init__.py +0 -0
- package/src/kagent/utils/config_validation.py +21 -0
- package/src/kagent/utils/json_output.py +23 -0
- package/src/kagent/utils/paths.py +473 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import fcntl
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import tempfile
|
|
8
|
+
import time
|
|
9
|
+
from contextlib import contextmanager
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Dict, Iterator
|
|
12
|
+
|
|
13
|
+
from kagent.runtime import RUNTIME_TRACE_TYPE
|
|
14
|
+
from kagent.service.safety import (
|
|
15
|
+
safe_trace_file_stem,
|
|
16
|
+
)
|
|
17
|
+
from kagent.utils.json_output import format_and_write_json, json_ready
|
|
18
|
+
|
|
19
|
+
DEFAULT_RUNTIME_RETENTION_STATUSES = ("cancelled", "done", "failed", "resumed")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def trace_path_for_run_id(run_id: Any, trace_dir: str) -> Path:
|
|
23
|
+
return Path(trace_dir) / f"{safe_trace_file_stem(run_id)}.json"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@contextmanager
|
|
27
|
+
def runtime_trace_lock(run_id: Any, trace_dir: str) -> Iterator[None]:
|
|
28
|
+
"""Serialize cross-process read/modify/write operations for one runtime trace."""
|
|
29
|
+
|
|
30
|
+
output_dir = Path(trace_dir)
|
|
31
|
+
_ensure_owner_only_trace_dir(output_dir)
|
|
32
|
+
lock_path = output_dir / f".{safe_trace_file_stem(run_id)}.runtime.lock"
|
|
33
|
+
flags = os.O_CREAT | os.O_RDWR
|
|
34
|
+
if hasattr(os, "O_NOFOLLOW"):
|
|
35
|
+
flags |= os.O_NOFOLLOW
|
|
36
|
+
lock_fd = os.open(lock_path, flags, 0o600)
|
|
37
|
+
try:
|
|
38
|
+
os.fchmod(lock_fd, 0o600)
|
|
39
|
+
fcntl.flock(lock_fd, fcntl.LOCK_EX)
|
|
40
|
+
yield
|
|
41
|
+
finally:
|
|
42
|
+
try:
|
|
43
|
+
fcntl.flock(lock_fd, fcntl.LOCK_UN)
|
|
44
|
+
finally:
|
|
45
|
+
os.close(lock_fd)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def persist_trace(trace: Dict[str, Any], trace_dir: str) -> str:
|
|
49
|
+
output_dir = Path(trace_dir)
|
|
50
|
+
_ensure_owner_only_trace_dir(output_dir)
|
|
51
|
+
output_path = trace_path_for_run_id(trace.get("run_id"), trace_dir)
|
|
52
|
+
temporary_path = _write_owner_only_temporary_trace(
|
|
53
|
+
output_dir,
|
|
54
|
+
output_path.name,
|
|
55
|
+
json.dumps(json_ready(trace), sort_keys=True) + "\n",
|
|
56
|
+
)
|
|
57
|
+
try:
|
|
58
|
+
temporary_path.replace(output_path)
|
|
59
|
+
finally:
|
|
60
|
+
temporary_path.unlink(missing_ok=True)
|
|
61
|
+
return str(output_path)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def load_trace_by_run_id(run_id: Any, trace_dir: str) -> Dict[str, Any] | None:
|
|
65
|
+
trace_path = trace_path_for_run_id(run_id, trace_dir)
|
|
66
|
+
if trace_path.is_symlink():
|
|
67
|
+
raise OSError("trace file must not be a symlink")
|
|
68
|
+
try:
|
|
69
|
+
payload = json.loads(trace_path.read_text(encoding="utf-8"))
|
|
70
|
+
except FileNotFoundError:
|
|
71
|
+
return None
|
|
72
|
+
if not isinstance(payload, dict):
|
|
73
|
+
raise ValueError("trace payload must be a JSON object")
|
|
74
|
+
return payload
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _ensure_owner_only_trace_dir(output_dir: Path) -> None:
|
|
78
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
79
|
+
output_dir.chmod(0o700)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _write_owner_only_temporary_trace(output_dir: Path, output_name: str, data: str) -> Path:
|
|
83
|
+
fd, temporary_name = tempfile.mkstemp(
|
|
84
|
+
prefix=f".{output_name}.",
|
|
85
|
+
suffix=".tmp",
|
|
86
|
+
dir=output_dir,
|
|
87
|
+
text=True,
|
|
88
|
+
)
|
|
89
|
+
temporary_path = Path(temporary_name)
|
|
90
|
+
try:
|
|
91
|
+
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
|
92
|
+
fd = -1
|
|
93
|
+
handle.write(data)
|
|
94
|
+
handle.flush()
|
|
95
|
+
os.fsync(handle.fileno())
|
|
96
|
+
except Exception:
|
|
97
|
+
if fd != -1:
|
|
98
|
+
os.close(fd)
|
|
99
|
+
temporary_path.unlink(missing_ok=True)
|
|
100
|
+
raise
|
|
101
|
+
return temporary_path
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def prune_traces(
|
|
105
|
+
trace_dir: str | Path,
|
|
106
|
+
*,
|
|
107
|
+
max_age_seconds: float,
|
|
108
|
+
now: float | None = None,
|
|
109
|
+
dry_run: bool = True,
|
|
110
|
+
) -> Dict[str, Any]:
|
|
111
|
+
if max_age_seconds < 0:
|
|
112
|
+
raise ValueError("max_age_seconds must be non-negative")
|
|
113
|
+
output_dir = Path(trace_dir)
|
|
114
|
+
current_time = time.time() if now is None else now
|
|
115
|
+
cutoff = current_time - max_age_seconds
|
|
116
|
+
scanned = 0
|
|
117
|
+
matched = 0
|
|
118
|
+
deleted = 0
|
|
119
|
+
errors = []
|
|
120
|
+
|
|
121
|
+
for path in sorted(output_dir.glob("*.json")):
|
|
122
|
+
if path.is_symlink():
|
|
123
|
+
continue
|
|
124
|
+
if not path.is_file():
|
|
125
|
+
continue
|
|
126
|
+
scanned += 1
|
|
127
|
+
if path.stat().st_mtime > cutoff:
|
|
128
|
+
continue
|
|
129
|
+
matched += 1
|
|
130
|
+
if dry_run:
|
|
131
|
+
continue
|
|
132
|
+
try:
|
|
133
|
+
path.unlink()
|
|
134
|
+
deleted += 1
|
|
135
|
+
except OSError as exc:
|
|
136
|
+
errors.append({"path": str(path), "error": str(exc)})
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
"trace_dir": str(output_dir),
|
|
140
|
+
"max_age_seconds": int(max_age_seconds),
|
|
141
|
+
"dry_run": dry_run,
|
|
142
|
+
"scanned": scanned,
|
|
143
|
+
"matched": matched,
|
|
144
|
+
"deleted": deleted,
|
|
145
|
+
"kept": scanned - matched,
|
|
146
|
+
"errors": errors,
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def prune_runtime_traces(
|
|
151
|
+
trace_dir: str | Path,
|
|
152
|
+
*,
|
|
153
|
+
max_age_seconds: float,
|
|
154
|
+
statuses: tuple[str, ...] = DEFAULT_RUNTIME_RETENTION_STATUSES,
|
|
155
|
+
now: float | None = None,
|
|
156
|
+
dry_run: bool = True,
|
|
157
|
+
) -> Dict[str, Any]:
|
|
158
|
+
if max_age_seconds < 0:
|
|
159
|
+
raise ValueError("max_age_seconds must be non-negative")
|
|
160
|
+
normalized_statuses = tuple(sorted({status for status in statuses if status}))
|
|
161
|
+
if not normalized_statuses:
|
|
162
|
+
raise ValueError("statuses must contain at least one runtime status")
|
|
163
|
+
output_dir = Path(trace_dir)
|
|
164
|
+
current_time = time.time() if now is None else now
|
|
165
|
+
cutoff = current_time - max_age_seconds
|
|
166
|
+
scanned = 0
|
|
167
|
+
runtime_scanned = 0
|
|
168
|
+
matched = 0
|
|
169
|
+
deleted = 0
|
|
170
|
+
protected_pending = 0
|
|
171
|
+
skipped_non_runtime = 0
|
|
172
|
+
skipped_fresh = 0
|
|
173
|
+
skipped_status = 0
|
|
174
|
+
unreadable = 0
|
|
175
|
+
matched_by_status: Dict[str, str] = {}
|
|
176
|
+
errors = []
|
|
177
|
+
|
|
178
|
+
for path in sorted(output_dir.glob("*.json")):
|
|
179
|
+
if path.is_symlink():
|
|
180
|
+
continue
|
|
181
|
+
if not path.is_file():
|
|
182
|
+
continue
|
|
183
|
+
scanned += 1
|
|
184
|
+
try:
|
|
185
|
+
trace = json.loads(path.read_text(encoding="utf-8"))
|
|
186
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
187
|
+
unreadable += 1
|
|
188
|
+
errors.append({"path": str(path), "error": str(exc)})
|
|
189
|
+
continue
|
|
190
|
+
if not isinstance(trace, dict):
|
|
191
|
+
unreadable += 1
|
|
192
|
+
errors.append({"path": str(path), "error": "trace payload must be a JSON object"})
|
|
193
|
+
continue
|
|
194
|
+
if trace.get("trace_type") != RUNTIME_TRACE_TYPE:
|
|
195
|
+
skipped_non_runtime += 1
|
|
196
|
+
continue
|
|
197
|
+
runtime_scanned += 1
|
|
198
|
+
status = str(trace.get("status", ""))
|
|
199
|
+
if status == "requires_approval" and status not in normalized_statuses:
|
|
200
|
+
protected_pending += 1
|
|
201
|
+
skipped_status += 1
|
|
202
|
+
continue
|
|
203
|
+
if status not in normalized_statuses:
|
|
204
|
+
skipped_status += 1
|
|
205
|
+
continue
|
|
206
|
+
if path.stat().st_mtime > cutoff:
|
|
207
|
+
skipped_fresh += 1
|
|
208
|
+
continue
|
|
209
|
+
matched += 1
|
|
210
|
+
matched_by_status[status] = str(int(matched_by_status.get(status, "0")) + 1)
|
|
211
|
+
if dry_run:
|
|
212
|
+
continue
|
|
213
|
+
try:
|
|
214
|
+
path.unlink()
|
|
215
|
+
deleted += 1
|
|
216
|
+
except OSError as exc:
|
|
217
|
+
errors.append({"path": str(path), "error": str(exc)})
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
"trace_dir": str(output_dir),
|
|
221
|
+
"max_age_seconds": int(max_age_seconds),
|
|
222
|
+
"dry_run": dry_run,
|
|
223
|
+
"statuses": list(normalized_statuses),
|
|
224
|
+
"scanned": scanned,
|
|
225
|
+
"runtime_scanned": runtime_scanned,
|
|
226
|
+
"matched": matched,
|
|
227
|
+
"deleted": deleted,
|
|
228
|
+
"kept": scanned - matched,
|
|
229
|
+
"protected_pending": protected_pending,
|
|
230
|
+
"skipped_non_runtime": skipped_non_runtime,
|
|
231
|
+
"skipped_fresh": skipped_fresh,
|
|
232
|
+
"skipped_status": skipped_status,
|
|
233
|
+
"unreadable": unreadable,
|
|
234
|
+
"matched_by_status": matched_by_status,
|
|
235
|
+
"errors": errors,
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def main() -> None:
|
|
240
|
+
parser = argparse.ArgumentParser(
|
|
241
|
+
description="Prune persisted kagent trace JSON files."
|
|
242
|
+
)
|
|
243
|
+
parser.add_argument("trace_dir", help="Trace directory to scan.")
|
|
244
|
+
parser.add_argument(
|
|
245
|
+
"--max-age-days",
|
|
246
|
+
type=float,
|
|
247
|
+
required=True,
|
|
248
|
+
help="Match trace JSON files older than this many days.",
|
|
249
|
+
)
|
|
250
|
+
parser.add_argument(
|
|
251
|
+
"--delete",
|
|
252
|
+
action="store_true",
|
|
253
|
+
help="Delete matched traces. Without this flag the command is a dry run.",
|
|
254
|
+
)
|
|
255
|
+
parser.add_argument(
|
|
256
|
+
"--runtime-only",
|
|
257
|
+
action="store_true",
|
|
258
|
+
help=(
|
|
259
|
+
"Prune only Codex-style runtime traces. By default this matches old "
|
|
260
|
+
"done, failed, and cancelled runs while protecting requires_approval."
|
|
261
|
+
),
|
|
262
|
+
)
|
|
263
|
+
parser.add_argument(
|
|
264
|
+
"--statuses",
|
|
265
|
+
default=",".join(DEFAULT_RUNTIME_RETENTION_STATUSES),
|
|
266
|
+
help=(
|
|
267
|
+
"Comma-separated runtime statuses matched with --runtime-only. "
|
|
268
|
+
"Defaults to cancelled,done,failed,resumed."
|
|
269
|
+
),
|
|
270
|
+
)
|
|
271
|
+
parser.add_argument(
|
|
272
|
+
"--output",
|
|
273
|
+
default="",
|
|
274
|
+
metavar="PATH",
|
|
275
|
+
help="Write the JSON summary to PATH as well as stdout.",
|
|
276
|
+
)
|
|
277
|
+
parser.add_argument(
|
|
278
|
+
"--fail-on-errors",
|
|
279
|
+
action="store_true",
|
|
280
|
+
help=(
|
|
281
|
+
"Exit with status 1 after writing the summary when unreadable traces "
|
|
282
|
+
"or delete errors are reported."
|
|
283
|
+
),
|
|
284
|
+
)
|
|
285
|
+
args = parser.parse_args()
|
|
286
|
+
if args.max_age_days < 0:
|
|
287
|
+
parser.error("--max-age-days must be non-negative")
|
|
288
|
+
if args.runtime_only:
|
|
289
|
+
statuses = tuple(status.strip() for status in args.statuses.split(",") if status.strip())
|
|
290
|
+
summary = prune_runtime_traces(
|
|
291
|
+
args.trace_dir,
|
|
292
|
+
max_age_seconds=args.max_age_days * 24 * 60 * 60,
|
|
293
|
+
statuses=statuses,
|
|
294
|
+
dry_run=not args.delete,
|
|
295
|
+
)
|
|
296
|
+
else:
|
|
297
|
+
summary = prune_traces(
|
|
298
|
+
args.trace_dir,
|
|
299
|
+
max_age_seconds=args.max_age_days * 24 * 60 * 60,
|
|
300
|
+
dry_run=not args.delete,
|
|
301
|
+
)
|
|
302
|
+
print(format_and_write_json(summary, args.output))
|
|
303
|
+
if args.fail_on_errors and _trace_prune_has_errors(summary):
|
|
304
|
+
raise SystemExit(1)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _trace_prune_has_errors(summary: Dict[str, Any]) -> bool:
|
|
308
|
+
errors = summary.get("errors")
|
|
309
|
+
if isinstance(errors, list) and errors:
|
|
310
|
+
return True
|
|
311
|
+
return _summary_int(summary.get("unreadable")) > 0
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _summary_int(value: Any) -> int:
|
|
315
|
+
try:
|
|
316
|
+
return int(str(value))
|
|
317
|
+
except (TypeError, ValueError):
|
|
318
|
+
return 0
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
if __name__ == "__main__":
|
|
322
|
+
main()
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any, Tuple
|
|
5
|
+
|
|
6
|
+
JSON_CONTENT_TYPE = "application/json"
|
|
7
|
+
PROMETHEUS_TEXT_CONTENT_TYPE = "text/plain; version=0.0.4; charset=utf-8"
|
|
8
|
+
NOSNIFF_HEADER_VALUE = "nosniff"
|
|
9
|
+
CACHE_CONTROL_HEADER_VALUE = "no-store"
|
|
10
|
+
REFERRER_POLICY_HEADER_VALUE = "no-referrer"
|
|
11
|
+
CONTENT_SECURITY_POLICY_HEADER_VALUE = "default-src 'none'; frame-ancestors 'none'; base-uri 'none'"
|
|
12
|
+
X_FRAME_OPTIONS_HEADER_VALUE = "DENY"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def response_body(payload: Any) -> Tuple[bytes, str]:
|
|
16
|
+
if isinstance(payload, str):
|
|
17
|
+
return payload.encode("utf-8"), PROMETHEUS_TEXT_CONTENT_TYPE
|
|
18
|
+
return json.dumps(payload, sort_keys=True).encode("utf-8"), JSON_CONTENT_TYPE
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def error_code_from_payload(payload: Any) -> str:
|
|
22
|
+
if isinstance(payload, dict):
|
|
23
|
+
return str(payload.get("error_code", ""))
|
|
24
|
+
return ""
|
|
File without changes
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def optional_json_int(payload: Dict[str, Any], key: str, default: int) -> int:
|
|
7
|
+
value = payload.get(key)
|
|
8
|
+
if value in {None, ""}:
|
|
9
|
+
return default
|
|
10
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
11
|
+
raise ValueError(f"{key} must be an integer")
|
|
12
|
+
return value
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def optional_json_bool(payload: Dict[str, Any], key: str, default: bool) -> bool:
|
|
16
|
+
if key not in payload:
|
|
17
|
+
return default
|
|
18
|
+
value = payload[key]
|
|
19
|
+
if not isinstance(value, bool):
|
|
20
|
+
raise ValueError(f"{key} must be a boolean")
|
|
21
|
+
return value
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def format_and_write_json(payload: Any, output_path: str) -> str:
|
|
10
|
+
json_payload = json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True)
|
|
11
|
+
if output_path:
|
|
12
|
+
Path(output_path).write_text(json_payload + "\n", encoding="utf-8")
|
|
13
|
+
return json_payload
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def json_ready(value: Any) -> Any:
|
|
17
|
+
if isinstance(value, Enum):
|
|
18
|
+
return value.value
|
|
19
|
+
if isinstance(value, dict):
|
|
20
|
+
return {key: json_ready(item) for key, item in value.items()}
|
|
21
|
+
if isinstance(value, list):
|
|
22
|
+
return [json_ready(item) for item in value]
|
|
23
|
+
return value
|