hidevs-eval 0.1.1__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.
@@ -0,0 +1,6 @@
1
+ Metadata-Version: 2.4
2
+ Name: hidevs-eval
3
+ Version: 0.1.1
4
+ Summary: HiDevs quest eval client. Runs the local agent and uploads replies for scoring.
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: pyyaml>=6
@@ -0,0 +1,8 @@
1
+ hidevs_quest/__init__.py,sha256=rnObPjuBcEStqSO0S6gsdS_ot8ITOQjVj_-P1LUUYpg,22
2
+ hidevs_quest/cli.py,sha256=Qdx9MWcf8A4qo6EGUAHX5cjcXOHUhvbJGZ985m11zt4,12112
3
+ hidevs_quest/runner.py,sha256=gtX1EMSa-ZwdRRVtF8OU3mSBja61HB7bbH_lL92kmrw,8452
4
+ hidevs_eval-0.1.1.dist-info/METADATA,sha256=hcrbQxr7qgK2K5srqheLzGLKx8ldqk7NmGGdjoWgm5w,193
5
+ hidevs_eval-0.1.1.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
6
+ hidevs_eval-0.1.1.dist-info/entry_points.txt,sha256=M-eo_dbdAwOxU2WBYFGalT0seo03e7oW7oLIFkFlYz4,54
7
+ hidevs_eval-0.1.1.dist-info/top_level.txt,sha256=jkb_W8hpSCXXCEbwvLOs3mjHry2QlDV6a78mA2VFalc,13
8
+ hidevs_eval-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ hidevs-eval = hidevs_quest.cli:main
@@ -0,0 +1 @@
1
+ hidevs_quest
@@ -0,0 +1 @@
1
+ __version__ = "0.1.1"
hidevs_quest/cli.py ADDED
@@ -0,0 +1,332 @@
1
+ """hidevs-eval login, then hidevs-eval eval. The server sends the cases and stores the score."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import sys
8
+ import urllib.error
9
+ from pathlib import Path
10
+
11
+ import yaml
12
+
13
+ from hidevs_quest import __version__
14
+ from hidevs_quest.runner import collect_sources, get_json, post_json, preflight, run_cases
15
+
16
+ SESSION = Path.home() / ".hidevs_quest" / "session.json"
17
+
18
+
19
+ def load_session() -> dict:
20
+ if not SESSION.exists():
21
+ return {}
22
+ return json.loads(SESSION.read_text())
23
+
24
+
25
+ def save_session(session: dict) -> None:
26
+ SESSION.parent.mkdir(parents=True, exist_ok=True)
27
+ SESSION.write_text(json.dumps(session, indent=2))
28
+
29
+
30
+ def server_url(args: argparse.Namespace) -> str:
31
+ return (args.server or load_session().get("server") or "https://cli.server.hidevs.xyz").rstrip("/")
32
+
33
+
34
+ def api(url: str, payload: dict, token: str = "") -> dict:
35
+ try:
36
+ return post_json(url, payload, token)
37
+ except urllib.error.HTTPError as exc:
38
+ detail = exc.read().decode()
39
+ try:
40
+ detail = json.loads(detail).get("detail", detail)
41
+ except json.JSONDecodeError:
42
+ pass
43
+ raise SystemExit(f"{exc.code} {detail}") from exc
44
+
45
+
46
+ def version_key(value: str) -> tuple[int, ...]:
47
+ parts = []
48
+ for part in value.split("."):
49
+ digits = "".join(character for character in part if character.isdigit())
50
+ parts.append(int(digits or "0"))
51
+ return tuple(parts)
52
+
53
+
54
+ def client_notice(base: str) -> dict:
55
+ try:
56
+ settings = get_json(f"{base}/v1/settings")
57
+ except Exception as exc:
58
+ raise SystemExit(f"could not reach {base}: {exc}") from exc
59
+ if version_key(__version__) < version_key(settings.get("min_version") or "0.0.0"):
60
+ extra = f" {settings['message']}" if settings.get("message") else ""
61
+ raise SystemExit(
62
+ f"this install is {__version__}. The server requires {settings['min_version']}.{extra}"
63
+ )
64
+ if version_key(settings.get("latest_version") or "0.0.0") > version_key(__version__):
65
+ print(f"a newer package is published: {settings['latest_version']}. You have {__version__}.")
66
+ if settings.get("message"):
67
+ print(settings["message"])
68
+ published = (settings.get("server_url") or "").rstrip("/")
69
+ if published and published != base:
70
+ print(f"the published server link is {published}")
71
+ print(f"switch with: hidevs-eval server set {published}")
72
+ return settings
73
+
74
+
75
+ def login(args: argparse.Namespace) -> dict:
76
+ previous = load_session()
77
+ code = args.code or input("Platform code: ").strip()
78
+ if not code:
79
+ raise SystemExit("a platform code is required")
80
+ base = server_url(args)
81
+ client_notice(base)
82
+ body = api(f"{base}/v1/auth", {"code": code})
83
+ if previous.get("user_id") and previous.get("user_id") != body["user_id"]:
84
+ print(f"replaced login for {previous['user_id']}")
85
+ session = {
86
+ "server": base,
87
+ "token": body["token"],
88
+ "user_id": body["user_id"],
89
+ "quest_id": body.get("quest_id") or "",
90
+ "ps_id": body["ps_id"],
91
+ }
92
+ save_session(session)
93
+ print(f"validated for user {body['user_id']} quest {session['quest_id']} problem {body['ps_id']}")
94
+ return session
95
+
96
+
97
+ def logout(_: argparse.Namespace) -> None:
98
+ if SESSION.exists():
99
+ SESSION.unlink()
100
+ print("logged out")
101
+
102
+
103
+ def whoami(args: argparse.Namespace) -> None:
104
+ session = load_session()
105
+ print(f"package {__version__}")
106
+ base = server_url(args)
107
+ print(f"server {base}")
108
+ if not session.get("token"):
109
+ print("not logged in")
110
+ return
111
+ print(f"user {session.get('user_id')} quest {session.get('quest_id')} problem {session.get('ps_id')}")
112
+ client_notice(base)
113
+
114
+
115
+ def show_server(args: argparse.Namespace) -> None:
116
+ print(server_url(args))
117
+
118
+
119
+ def set_server(args: argparse.Namespace) -> None:
120
+ url = args.url.rstrip("/")
121
+ session = load_session()
122
+ session["server"] = url
123
+ session.pop("token", None)
124
+ session.pop("user_id", None)
125
+ session.pop("quest_id", None)
126
+ session.pop("ps_id", None)
127
+ save_session(session)
128
+ print(f"server set to {url}")
129
+ print("log in again with the platform code")
130
+
131
+
132
+ HELP = """
133
+ hidevs-eval login ask for the platform code and save it
134
+ hidevs-eval login --code CODE save a different code, replacing the previous login
135
+ hidevs-eval logout forget the saved login
136
+ hidevs-eval whoami show the package version, server, and user
137
+ hidevs-eval preflight import entry and tools from agent_config.yaml
138
+ hidevs-eval eval run level 1, level 2, and level 3
139
+ hidevs-eval eval --level 1 --config agent_config.yaml
140
+ hidevs-eval server show print the server this laptop uses
141
+ hidevs-eval server set URL point this laptop at a different server
142
+ hidevs-eval version print the installed package version
143
+ hidevs-eval help show this list
144
+ """.strip()
145
+
146
+
147
+ def show_help(_: argparse.Namespace) -> None:
148
+ print(HELP)
149
+
150
+
151
+ def show_version(_: argparse.Namespace) -> None:
152
+ print(__version__)
153
+
154
+
155
+ ARENA_URL = "https://app.hidevs.xyz/ai-quest/challenges/ps-lyzr-01/arena"
156
+ LEVEL_HEADINGS = {
157
+ 1: "LEVEL 1: DETERMINISTIC PROPERTY ASSERTIONS",
158
+ 2: "LEVEL 2: INPUT AND TOOL QUALITY",
159
+ 3: "LEVEL 3: LLM JUDGE",
160
+ }
161
+
162
+
163
+ def check_score(check: dict) -> float:
164
+ evidence = check.get("evidence")
165
+ raw = evidence.get("score") if isinstance(evidence, dict) else None
166
+ if isinstance(raw, (int, float)):
167
+ return max(0.0, min(1.0, float(raw)))
168
+ return 1.0 if check.get("status") == "PASS" else 0.0
169
+
170
+
171
+ def print_result(results: list[dict]) -> None:
172
+ line = "=" * 80
173
+ first = results[0] if results else {}
174
+ name = first.get("problem_name") or first.get("ps_id") or "Quest evaluation"
175
+ summary_text = first.get("problem_summary") or ""
176
+ print(line)
177
+ print("BENCHMARK: HiDevs AI Quest Evaluation")
178
+ print(f"PROBLEM STATEMENT: {name}")
179
+ if summary_text:
180
+ print(summary_text)
181
+ print(line)
182
+ rows = []
183
+ passed_all = 0
184
+ total_all = 0
185
+ for result in results:
186
+ print()
187
+ level = result.get("level")
188
+ print(f"[{LEVEL_HEADINGS.get(level, f'LEVEL {level}')}]")
189
+ scores = []
190
+ passed = 0
191
+ for check in result.get("checks") or []:
192
+ score = check_score(check)
193
+ scores.append(score)
194
+ status = check.get("status") or "FAIL"
195
+ if status == "PASS":
196
+ passed += 1
197
+ mark = "PASS" if status == "PASS" else status
198
+ evidence = check.get("evidence") if isinstance(check.get("evidence"), dict) else {}
199
+ reason = (check.get("reason") or "").split("\n")[0]
200
+ if level == 3:
201
+ title = f"{evidence.get('metric', '')} ({evidence.get('case', '')})".strip()
202
+ print(f" [{mark}] {check.get('id', ''):<24} | Score: {score:.2f} | {title}")
203
+ if reason:
204
+ print(f" * Reason: {reason}")
205
+ else:
206
+ print(f" [{mark}] {check.get('id', ''):<24} | Score: {score:.2f} | {reason}")
207
+ total = len(scores)
208
+ level_score = (sum(scores) / total * 100.0) if total else 0.0
209
+ rows.append((LEVEL_HEADINGS.get(level, f"Level {level}"), passed, total, level_score))
210
+ passed_all += passed
211
+ total_all += total
212
+ composite = (sum(row[3] for row in rows) / len(rows)) if rows else 0.0
213
+ print()
214
+ print(line)
215
+ print("FINAL BENCHMARK SCORECARD")
216
+ print(line)
217
+ for heading, passed, total, level_score in rows:
218
+ print(f"{heading:<42}: Passed {passed}/{total:<2} | Level Score: {level_score:.1f}%")
219
+ print("-" * 80)
220
+ print(f"TOTAL SYSTEM AGENT COMPOSITE SCORE : {composite:.1f}% ({passed_all}/{total_all} Passed)")
221
+ print(line)
222
+ print()
223
+ print(f"For the detailed report, open {ARENA_URL}")
224
+
225
+
226
+ def run_preflight(config_path: Path) -> dict:
227
+ if not config_path.exists():
228
+ raise SystemExit(f"missing {config_path}")
229
+ config = yaml.safe_load(config_path.read_text()) or {}
230
+ root = config_path.parent
231
+ errors = preflight(config, root)
232
+ if errors:
233
+ print("preflight failed (not a scored check)")
234
+ for item in errors:
235
+ print(f" - {item}")
236
+ raise SystemExit(1)
237
+ entry = config.get("entry") or {}
238
+ if entry.get("type") == "http":
239
+ label = f"http {entry.get('app')}"
240
+ elif "class" in entry:
241
+ label = f"python class {entry.get('class')}.{entry.get('method')}"
242
+ else:
243
+ label = f"python callable {entry.get('callable')}"
244
+ print(f"preflight ok: {label}")
245
+ tools = config.get("tools") or []
246
+ if tools:
247
+ print(f"preflight ok: {len(tools)} tool function(s) importable")
248
+ return config
249
+
250
+
251
+ def preflight_command(args: argparse.Namespace) -> None:
252
+ run_preflight(Path(args.config).resolve())
253
+
254
+
255
+ def evaluate(args: argparse.Namespace) -> None:
256
+ session = load_session()
257
+ if not session.get("token") or server_url(args) != session.get("server"):
258
+ session = login(args)
259
+ else:
260
+ client_notice(session["server"])
261
+ config_path = Path(args.config).resolve()
262
+ config = run_preflight(config_path)
263
+ root = config_path.parent
264
+ levels = [args.level] if args.level else [1, 2, 3]
265
+ reports = []
266
+ sources = collect_sources(root)
267
+ for level in levels:
268
+ started = api(
269
+ f"{session['server']}/v1/levels/{level}/start",
270
+ {"config": config, "sources": sources},
271
+ session["token"],
272
+ )
273
+ if started.get("result"):
274
+ reports.append(started["result"])
275
+ continue
276
+ results = run_cases(config, root, started["cases"])
277
+ reports.append(api(
278
+ f"{session['server']}/v1/runs/{started['run_id']}",
279
+ {"results": results},
280
+ session["token"],
281
+ ))
282
+ print_result(reports)
283
+
284
+
285
+ def main() -> None:
286
+ parser = argparse.ArgumentParser(prog="hidevs-eval", description=HELP, formatter_class=argparse.RawDescriptionHelpFormatter)
287
+ parser.add_argument("--server", default="", help="eval server for this command")
288
+ sub = parser.add_subparsers(dest="command", required=True)
289
+
290
+ help_parser = sub.add_parser("help", help="list the commands")
291
+ help_parser.set_defaults(func=show_help)
292
+
293
+ version_parser = sub.add_parser("version", help="print the installed package version")
294
+ version_parser.set_defaults(func=show_version)
295
+
296
+ login_parser = sub.add_parser("login", help="save a platform code, replacing any previous login")
297
+ login_parser.add_argument("--code", default="")
298
+ login_parser.set_defaults(func=login)
299
+
300
+ logout_parser = sub.add_parser("logout", help="forget the saved login")
301
+ logout_parser.set_defaults(func=logout)
302
+
303
+ whoami_parser = sub.add_parser("whoami", help="show version, server, and user")
304
+ whoami_parser.set_defaults(func=whoami)
305
+
306
+ server_parser = sub.add_parser("server", help="show or change the server link")
307
+ server_sub = server_parser.add_subparsers(dest="server_command", required=True)
308
+ server_show = server_sub.add_parser("show", help="print the server this laptop uses")
309
+ server_show.set_defaults(func=show_server)
310
+ server_set = server_sub.add_parser("set", help="point this laptop at a different server")
311
+ server_set.add_argument("url")
312
+ server_set.set_defaults(func=set_server)
313
+
314
+ preflight_parser = sub.add_parser("preflight", help="import entry and tools before scoring")
315
+ preflight_parser.add_argument("--config", default="agent_config.yaml")
316
+ preflight_parser.set_defaults(func=preflight_command)
317
+
318
+ eval_parser = sub.add_parser("eval", help="pull cases, run the local agent, upload replies")
319
+ eval_parser.add_argument("--config", default="agent_config.yaml")
320
+ eval_parser.add_argument("--level", type=int, default=None)
321
+ eval_parser.add_argument("--code", default="")
322
+ eval_parser.set_defaults(func=evaluate)
323
+
324
+ args = parser.parse_args()
325
+ try:
326
+ args.func(args)
327
+ except KeyboardInterrupt:
328
+ sys.exit(130)
329
+
330
+
331
+ if __name__ == "__main__":
332
+ main()
hidevs_quest/runner.py ADDED
@@ -0,0 +1,243 @@
1
+ """Call the local agent. The server never sees this process."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib
6
+ import json
7
+ import sys
8
+ import traceback
9
+ import urllib.request
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+
14
+ SKIP_DIRS = {".venv", "venv", ".git", "__pycache__", "node_modules", ".hidevs"}
15
+ MAX_FILE_BYTES = 200_000
16
+ MAX_TOTAL_BYTES = 1_500_000
17
+
18
+
19
+ def as_dict(value: Any) -> dict:
20
+ if hasattr(value, "model_dump"):
21
+ return value.model_dump()
22
+ if isinstance(value, dict):
23
+ return value
24
+ return {"_raw": str(value)}
25
+
26
+
27
+ def collect_sources(root: Path) -> dict[str, str]:
28
+ sources: dict[str, str] = {}
29
+ total = 0
30
+ for path in root.rglob("*.py"):
31
+ if any(part in SKIP_DIRS for part in path.parts):
32
+ continue
33
+ if path.stat().st_size > MAX_FILE_BYTES:
34
+ continue
35
+ text = path.read_text(errors="ignore")
36
+ total += len(text)
37
+ if total > MAX_TOTAL_BYTES:
38
+ break
39
+ sources[path.relative_to(root).as_posix()] = text
40
+ return sources
41
+
42
+
43
+ def build_kwargs(config: dict, case: dict) -> dict:
44
+ entry = config["entry"]
45
+ kwargs = dict(entry.get("constants") or {})
46
+ for source, target in (entry.get("arguments") or {}).items():
47
+ if source in case["input"]:
48
+ kwargs[target] = case["input"][source]
49
+ return kwargs
50
+
51
+
52
+ def _load_symbol(root: Path, spec: str):
53
+ if ":" not in spec:
54
+ raise ValueError(f"expected module:name, got {spec!r}")
55
+ module_name, attr = spec.split(":", 1)
56
+ if str(root) not in sys.path:
57
+ sys.path.insert(0, str(root))
58
+ module = importlib.import_module(module_name)
59
+ if not hasattr(module, attr):
60
+ raise AttributeError(f"{module_name} has no attribute {attr}")
61
+ return getattr(module, attr)
62
+
63
+
64
+ def preflight(config: dict, root: Path) -> list[str]:
65
+ """Import the entry and tools from agent_config.yaml. Not a scored check."""
66
+ errors: list[str] = []
67
+ root = root.resolve()
68
+ entry = config.get("entry") or {}
69
+ try:
70
+ if entry.get("type") == "http":
71
+ app = entry.get("app") or ""
72
+ _load_symbol(root, app)
73
+ if not entry.get("method") or not entry.get("endpoint"):
74
+ errors.append("entry.method and entry.endpoint are required for http")
75
+ elif "class" in entry:
76
+ cls = _load_symbol(root, entry["class"])
77
+ method_name = entry.get("method") or ""
78
+ if not method_name:
79
+ errors.append("entry.method is required for a python class")
80
+ elif not callable(getattr(cls, method_name, None)):
81
+ errors.append(f"class {entry['class']} has no callable method {method_name}")
82
+ elif "callable" in entry:
83
+ fn = _load_symbol(root, entry["callable"])
84
+ if not callable(fn):
85
+ errors.append(f"entry.callable {entry['callable']} is not callable")
86
+ else:
87
+ errors.append("entry must declare http app, python class, or python callable")
88
+ except Exception as exc:
89
+ errors.append(f"entry could not be imported: {exc}")
90
+
91
+ for tool in config.get("tools") or []:
92
+ spec = tool.get("function") or ""
93
+ capability = tool.get("capability") or spec
94
+ if not spec:
95
+ errors.append(f"tool {capability} is missing function")
96
+ continue
97
+ try:
98
+ fn = _load_symbol(root, spec)
99
+ if not callable(fn):
100
+ errors.append(f"tool {capability} function {spec} is not callable")
101
+ except Exception as exc:
102
+ errors.append(f"tool {capability} could not be imported ({spec}): {exc}")
103
+
104
+ req = root / "requirements.txt"
105
+ if req.is_file() and errors:
106
+ errors.append(
107
+ f"install agent libraries in this same Python, then retry: "
108
+ f"pip install -r {req}"
109
+ )
110
+ elif errors:
111
+ errors.append(
112
+ "install agent libraries in the same Python that runs hidevs-eval, then retry"
113
+ )
114
+ return errors
115
+
116
+
117
+ def invoke(config: dict, root: Path, case: dict) -> Any:
118
+ entry = config["entry"]
119
+ kwargs = build_kwargs(config, case)
120
+ if entry.get("type") == "http":
121
+ application = _load_symbol(root, entry["app"])
122
+ from fastapi.testclient import TestClient
123
+
124
+ client = TestClient(application)
125
+ response = client.request(entry["method"], entry["endpoint"], json=kwargs)
126
+ response.raise_for_status()
127
+ return response.json()
128
+ if "class" in entry:
129
+ cls = _load_symbol(root, entry["class"])
130
+ method = getattr(cls(), entry["method"])
131
+ return method(**kwargs)
132
+ fn = _load_symbol(root, entry["callable"])
133
+ return fn(**kwargs)
134
+
135
+
136
+ class Scripted:
137
+ def __init__(self, original, mode: str):
138
+ self.original = original
139
+ self.mode = mode
140
+ self.calls = []
141
+
142
+ def __call__(self, *args, **kwargs):
143
+ self.calls.append(True)
144
+ if self.mode == "raise":
145
+ raise RuntimeError("injected tool failure")
146
+ if self.mode == "timeout":
147
+ raise TimeoutError("injected tool timeout")
148
+ return self.original(*args, **kwargs)
149
+
150
+
151
+ def modules_under(root: Path):
152
+ root = root.resolve()
153
+ for module in list(sys.modules.values()):
154
+ path = getattr(module, "__file__", None)
155
+ if not path:
156
+ continue
157
+ try:
158
+ if Path(path).resolve().is_relative_to(root):
159
+ yield module
160
+ except (OSError, ValueError):
161
+ continue
162
+
163
+
164
+ def patch_tools(root: Path, config: dict, mode: str):
165
+ wrappers = {}
166
+ restores = []
167
+ mode = mode if mode in {"raise", "timeout"} else "ok"
168
+ sys.path.insert(0, str(root))
169
+ for tool in config.get("tools") or []:
170
+ capability = tool.get("capability")
171
+ spec = tool.get("function") or ""
172
+ if not capability or ":" not in spec:
173
+ continue
174
+ module_name, fn_name = spec.split(":", 1)
175
+ module = __import__(module_name, fromlist=[fn_name])
176
+ original = getattr(module, fn_name)
177
+ wrapper = Scripted(original, mode)
178
+ wrappers[capability] = wrapper
179
+ setattr(module, fn_name, wrapper)
180
+ restores.append((module, fn_name, original))
181
+ for loaded in modules_under(root):
182
+ for attr, value in list(vars(loaded).items()):
183
+ if value is original:
184
+ setattr(loaded, attr, wrapper)
185
+ restores.append((loaded, attr, original))
186
+ if isinstance(value, dict):
187
+ for key, item in list(value.items()):
188
+ if item is original:
189
+ value[key] = wrapper
190
+ restores.append((value, key, original))
191
+ return wrappers, restores
192
+
193
+
194
+ def restore(restores) -> None:
195
+ for owner, key, original in reversed(restores):
196
+ if isinstance(owner, dict):
197
+ owner[key] = original
198
+ else:
199
+ setattr(owner, key, original)
200
+
201
+
202
+ def run_cases(config: dict, root: Path, cases: list[dict]) -> list[dict]:
203
+ results = []
204
+ for case in cases:
205
+ mode = case.get("tool_mode") or "ok"
206
+ wrappers, restores = patch_tools(root, config, mode)
207
+ try:
208
+ try:
209
+ payload = as_dict(invoke(config, root, case))
210
+ error = ""
211
+ except Exception:
212
+ payload = {}
213
+ error = traceback.format_exc()
214
+ finally:
215
+ restore(restores)
216
+ counts = {name: len(wrapper.calls) for name, wrapper in wrappers.items()}
217
+ results.append({
218
+ "id": case["id"],
219
+ "payload": payload,
220
+ "error": error[-1500:] if error else "",
221
+ "counts": counts,
222
+ })
223
+ return results
224
+
225
+
226
+ CLIENT_HEADER = "X-Hidevs-Client"
227
+ CLIENT_VALUE = "hidevs-eval"
228
+
229
+
230
+ def get_json(url: str) -> dict:
231
+ request = urllib.request.Request(url, headers={CLIENT_HEADER: CLIENT_VALUE})
232
+ with urllib.request.urlopen(request) as response:
233
+ return json.loads(response.read().decode())
234
+
235
+
236
+ def post_json(url: str, payload: dict, token: str = "") -> dict:
237
+ data = json.dumps(payload).encode()
238
+ headers = {"Content-Type": "application/json", CLIENT_HEADER: CLIENT_VALUE}
239
+ if token:
240
+ headers["Authorization"] = f"Bearer {token}"
241
+ request = urllib.request.Request(url, data=data, headers=headers)
242
+ with urllib.request.urlopen(request) as response:
243
+ return json.loads(response.read().decode())