trajrl 0.2.0__tar.gz

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.
trajrl-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,206 @@
1
+ Metadata-Version: 2.4
2
+ Name: trajrl
3
+ Version: 0.2.0
4
+ Summary: CLI for TrajectoryRL, Bittensor subnet 11 — agent-friendly access to live validator, miner, and evaluation data.
5
+ License-Expression: MIT
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: typer>=0.9
9
+ Requires-Dist: httpx>=0.25
10
+ Requires-Dist: rich>=13.0
11
+
12
+ # trajrl
13
+
14
+ CLI for the [TrajectoryRL subnet](https://trajrl.com) (Bittensor SN11). Query live validator, miner, and evaluation data from the terminal.
15
+
16
+ Designed for AI agents (Claude Code, Cursor) and humans alike — outputs JSON when piped, Rich tables when interactive.
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ pip install trajrl
22
+ ```
23
+
24
+ ## Commands
25
+
26
+ ```
27
+ trajrl status # Network health overview
28
+ trajrl validators # List all validators
29
+ trajrl scores <validator_hotkey> # Per-miner scores from a validator
30
+ trajrl scores --uid <uid> # Query by validator UID instead
31
+ trajrl miner <hotkey> # Miner detail + diagnostics
32
+ trajrl miner --uid <uid> # Query by miner UID instead
33
+ trajrl pack <hotkey> <pack_hash> # Pack evaluation detail
34
+ trajrl submissions [--failed] # Recent pack submissions
35
+ trajrl eval-history <validator> # List eval cycle IDs for a validator
36
+ trajrl eval-history <v> --from <date> # Filter by date range
37
+ trajrl cycle-log <validator> # Download and display a cycle log
38
+ trajrl cycle-log <v> --format summary # Show parsed summary tables
39
+ trajrl logs [--type cycle|miner] # Eval log archives
40
+ trajrl --version # Show CLI version
41
+ ```
42
+
43
+ ### Global Options
44
+
45
+ Every command accepts:
46
+
47
+ | Option | Description |
48
+ |--------|-------------|
49
+ | `--json` / `-j` | Force JSON output (auto-enabled when stdout is piped) |
50
+ | `--base-url URL` | Override API base (default: `https://trajrl.com`, env: `TRAJRL_BASE_URL`) |
51
+ | `--version` / `-v` | Show CLI version and exit |
52
+
53
+ ### New in v0.2.0
54
+
55
+ - **UID support**: Query validators and miners by UID instead of hotkey
56
+ ```bash
57
+ trajrl miner --uid 65 # Instead of full hotkey
58
+ trajrl scores --uid 221 # Query validator by UID
59
+ ```
60
+
61
+ - **Date filtering**: Filter eval history by date range
62
+ ```bash
63
+ trajrl eval-history 5Cd6h... --from 2026-03-25 --to 2026-03-26
64
+ ```
65
+
66
+ - **Cycle log summary**: Parse cycle logs into structured tables
67
+ ```bash
68
+ trajrl cycle-log 5Cd6h... --format summary
69
+ ```
70
+ Shows: eval metrics, winner info, top qualified miners in tables instead of raw text
71
+
72
+ - **Version command**: Check your CLI version
73
+ ```bash
74
+ trajrl --version
75
+ ```
76
+
77
+ ## Usage Examples
78
+
79
+ ### Quick network check
80
+
81
+ ```bash
82
+ trajrl status
83
+ ```
84
+ ```
85
+ ╭──────────────────── Network Status ────────────────────╮
86
+ │ Validators: 7 total, 7 active (seen <1h) │
87
+ │ LLM Models: zhipu/glm-5 (3), chutes/GLM-5-TEE (3) │
88
+ │ Latest Eval: 7h ago │
89
+ │ Submissions: 65 passed, 35 failed (last batch) │
90
+ ╰────────────────────────────────────────────────────────╯
91
+ ```
92
+
93
+ ### List validators
94
+
95
+ ```bash
96
+ trajrl validators
97
+ ```
98
+ ```
99
+ Hotkey UID Version LLM Model Last Eval Last Seen
100
+ 5Cd6h…sn11 29 0.2.7 chutes/zai-org/GLM-5… 7h ago 2m ago
101
+ 5EcgNd…797f 221 0.2.7 zhipu/glm-5 10h ago 6m ago
102
+ ...
103
+ ```
104
+
105
+ ### Inspect a miner
106
+
107
+ By hotkey:
108
+ ```bash
109
+ trajrl miner 5HMgR6LnNqUAtaKRwa6bLF4Vy4KBf7TaxCLehyff9mWPhSHt
110
+ ```
111
+
112
+ Or by UID (v0.2.0+):
113
+ ```bash
114
+ trajrl miner --uid 65
115
+ ```
116
+
117
+ Shows rank, qualification status, cost, scenario breakdown, per-validator reports, recent submissions, and ban records.
118
+
119
+ ### View failed submissions
120
+
121
+ ```bash
122
+ trajrl submissions --failed
123
+ ```
124
+
125
+ ### Investigate a validator's eval cycle
126
+
127
+ First, list recent eval cycles for a validator:
128
+
129
+ ```bash
130
+ trajrl eval-history 5Cd6h...
131
+ ```
132
+ ```
133
+ Eval IDs (5) — 5Cd6h…sn11
134
+ Eval ID Validator Block Logs Created
135
+ 20260325_060012 5Cd6h…sn11 421890 12 3h ago
136
+ 20260324_060008 5Cd6h…sn11 421530 11 1d ago
137
+ 20260323_060015 5Cd6h…sn11 421170 13 2d ago
138
+ ...
139
+ ```
140
+
141
+ Then fetch the full cycle log for a specific eval:
142
+
143
+ ```bash
144
+ trajrl cycle-log 5Cd6h... --eval-id 20260325_060012
145
+ ```
146
+
147
+ Or just grab the latest one:
148
+
149
+ ```bash
150
+ trajrl cycle-log 5Cd6h...
151
+ ```
152
+
153
+ The cycle log contains the complete eval cycle output: metagraph sync, miner enumeration, per-miner eval timing, WEIGHT RESULTS, and set_weights status.
154
+
155
+ > **Note:** Eval IDs are defined by each validator independently (typically a timestamp like `20260325_060012`). They are **not** globally unique — the same eval ID from different validators refers to different evaluation cycles. Always pair an eval ID with a specific validator hotkey.
156
+
157
+ ### Filter eval logs
158
+
159
+ ```bash
160
+ trajrl logs --type cycle --limit 5
161
+ trajrl logs --validator 5Cd6h... --type miner
162
+ trajrl logs --eval-id 20260324_000340
163
+ ```
164
+
165
+ ### JSON output for agents
166
+
167
+ Pipe to any tool — JSON is automatic:
168
+
169
+ ```bash
170
+ trajrl validators | jq '.validators[].hotkey'
171
+ trajrl scores 5Cd6h... --json | python3 -c "
172
+ import sys, json
173
+ d = json.load(sys.stdin)
174
+ for e in d['entries'][:5]:
175
+ print(f\"{e['minerHotkey'][:12]} qual={e['qualified']} cost={e['costUsd']}\")
176
+ "
177
+ ```
178
+
179
+ Force JSON in an interactive terminal:
180
+
181
+ ```bash
182
+ trajrl miner 5HMgR6... --json
183
+ ```
184
+
185
+ ## API Reference
186
+
187
+ All data comes from the [TrajectoryRL Public API](https://trajrl.com) — read-only, no authentication required.
188
+
189
+ | Endpoint | CLI Command |
190
+ |----------|-------------|
191
+ | `GET /api/validators` | `trajrl validators` |
192
+ | `GET /api/scores/by-validator?validator=` | `trajrl scores <hotkey>` |
193
+ | `GET /api/miners/:hotkey` | `trajrl miner <hotkey>` |
194
+ | `GET /api/miners/:hotkey/packs/:hash` | `trajrl pack <hotkey> <hash>` |
195
+ | `GET /api/submissions` | `trajrl submissions` |
196
+ | `GET /api/eval-logs` | `trajrl logs` |
197
+ | `GET /api/eval-logs?log_type=cycle` | `trajrl eval-history <hotkey>` |
198
+ | `GET /api/eval-logs` + GCS download | `trajrl cycle-log <hotkey>` |
199
+
200
+ ## Development
201
+
202
+ ```bash
203
+ git clone <repo> && cd trajrl
204
+ pip install -e .
205
+ trajrl --help
206
+ ```
trajrl-0.2.0/README.md ADDED
@@ -0,0 +1,195 @@
1
+ # trajrl
2
+
3
+ CLI for the [TrajectoryRL subnet](https://trajrl.com) (Bittensor SN11). Query live validator, miner, and evaluation data from the terminal.
4
+
5
+ Designed for AI agents (Claude Code, Cursor) and humans alike — outputs JSON when piped, Rich tables when interactive.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install trajrl
11
+ ```
12
+
13
+ ## Commands
14
+
15
+ ```
16
+ trajrl status # Network health overview
17
+ trajrl validators # List all validators
18
+ trajrl scores <validator_hotkey> # Per-miner scores from a validator
19
+ trajrl scores --uid <uid> # Query by validator UID instead
20
+ trajrl miner <hotkey> # Miner detail + diagnostics
21
+ trajrl miner --uid <uid> # Query by miner UID instead
22
+ trajrl pack <hotkey> <pack_hash> # Pack evaluation detail
23
+ trajrl submissions [--failed] # Recent pack submissions
24
+ trajrl eval-history <validator> # List eval cycle IDs for a validator
25
+ trajrl eval-history <v> --from <date> # Filter by date range
26
+ trajrl cycle-log <validator> # Download and display a cycle log
27
+ trajrl cycle-log <v> --format summary # Show parsed summary tables
28
+ trajrl logs [--type cycle|miner] # Eval log archives
29
+ trajrl --version # Show CLI version
30
+ ```
31
+
32
+ ### Global Options
33
+
34
+ Every command accepts:
35
+
36
+ | Option | Description |
37
+ |--------|-------------|
38
+ | `--json` / `-j` | Force JSON output (auto-enabled when stdout is piped) |
39
+ | `--base-url URL` | Override API base (default: `https://trajrl.com`, env: `TRAJRL_BASE_URL`) |
40
+ | `--version` / `-v` | Show CLI version and exit |
41
+
42
+ ### New in v0.2.0
43
+
44
+ - **UID support**: Query validators and miners by UID instead of hotkey
45
+ ```bash
46
+ trajrl miner --uid 65 # Instead of full hotkey
47
+ trajrl scores --uid 221 # Query validator by UID
48
+ ```
49
+
50
+ - **Date filtering**: Filter eval history by date range
51
+ ```bash
52
+ trajrl eval-history 5Cd6h... --from 2026-03-25 --to 2026-03-26
53
+ ```
54
+
55
+ - **Cycle log summary**: Parse cycle logs into structured tables
56
+ ```bash
57
+ trajrl cycle-log 5Cd6h... --format summary
58
+ ```
59
+ Shows: eval metrics, winner info, top qualified miners in tables instead of raw text
60
+
61
+ - **Version command**: Check your CLI version
62
+ ```bash
63
+ trajrl --version
64
+ ```
65
+
66
+ ## Usage Examples
67
+
68
+ ### Quick network check
69
+
70
+ ```bash
71
+ trajrl status
72
+ ```
73
+ ```
74
+ ╭──────────────────── Network Status ────────────────────╮
75
+ │ Validators: 7 total, 7 active (seen <1h) │
76
+ │ LLM Models: zhipu/glm-5 (3), chutes/GLM-5-TEE (3) │
77
+ │ Latest Eval: 7h ago │
78
+ │ Submissions: 65 passed, 35 failed (last batch) │
79
+ ╰────────────────────────────────────────────────────────╯
80
+ ```
81
+
82
+ ### List validators
83
+
84
+ ```bash
85
+ trajrl validators
86
+ ```
87
+ ```
88
+ Hotkey UID Version LLM Model Last Eval Last Seen
89
+ 5Cd6h…sn11 29 0.2.7 chutes/zai-org/GLM-5… 7h ago 2m ago
90
+ 5EcgNd…797f 221 0.2.7 zhipu/glm-5 10h ago 6m ago
91
+ ...
92
+ ```
93
+
94
+ ### Inspect a miner
95
+
96
+ By hotkey:
97
+ ```bash
98
+ trajrl miner 5HMgR6LnNqUAtaKRwa6bLF4Vy4KBf7TaxCLehyff9mWPhSHt
99
+ ```
100
+
101
+ Or by UID (v0.2.0+):
102
+ ```bash
103
+ trajrl miner --uid 65
104
+ ```
105
+
106
+ Shows rank, qualification status, cost, scenario breakdown, per-validator reports, recent submissions, and ban records.
107
+
108
+ ### View failed submissions
109
+
110
+ ```bash
111
+ trajrl submissions --failed
112
+ ```
113
+
114
+ ### Investigate a validator's eval cycle
115
+
116
+ First, list recent eval cycles for a validator:
117
+
118
+ ```bash
119
+ trajrl eval-history 5Cd6h...
120
+ ```
121
+ ```
122
+ Eval IDs (5) — 5Cd6h…sn11
123
+ Eval ID Validator Block Logs Created
124
+ 20260325_060012 5Cd6h…sn11 421890 12 3h ago
125
+ 20260324_060008 5Cd6h…sn11 421530 11 1d ago
126
+ 20260323_060015 5Cd6h…sn11 421170 13 2d ago
127
+ ...
128
+ ```
129
+
130
+ Then fetch the full cycle log for a specific eval:
131
+
132
+ ```bash
133
+ trajrl cycle-log 5Cd6h... --eval-id 20260325_060012
134
+ ```
135
+
136
+ Or just grab the latest one:
137
+
138
+ ```bash
139
+ trajrl cycle-log 5Cd6h...
140
+ ```
141
+
142
+ The cycle log contains the complete eval cycle output: metagraph sync, miner enumeration, per-miner eval timing, WEIGHT RESULTS, and set_weights status.
143
+
144
+ > **Note:** Eval IDs are defined by each validator independently (typically a timestamp like `20260325_060012`). They are **not** globally unique — the same eval ID from different validators refers to different evaluation cycles. Always pair an eval ID with a specific validator hotkey.
145
+
146
+ ### Filter eval logs
147
+
148
+ ```bash
149
+ trajrl logs --type cycle --limit 5
150
+ trajrl logs --validator 5Cd6h... --type miner
151
+ trajrl logs --eval-id 20260324_000340
152
+ ```
153
+
154
+ ### JSON output for agents
155
+
156
+ Pipe to any tool — JSON is automatic:
157
+
158
+ ```bash
159
+ trajrl validators | jq '.validators[].hotkey'
160
+ trajrl scores 5Cd6h... --json | python3 -c "
161
+ import sys, json
162
+ d = json.load(sys.stdin)
163
+ for e in d['entries'][:5]:
164
+ print(f\"{e['minerHotkey'][:12]} qual={e['qualified']} cost={e['costUsd']}\")
165
+ "
166
+ ```
167
+
168
+ Force JSON in an interactive terminal:
169
+
170
+ ```bash
171
+ trajrl miner 5HMgR6... --json
172
+ ```
173
+
174
+ ## API Reference
175
+
176
+ All data comes from the [TrajectoryRL Public API](https://trajrl.com) — read-only, no authentication required.
177
+
178
+ | Endpoint | CLI Command |
179
+ |----------|-------------|
180
+ | `GET /api/validators` | `trajrl validators` |
181
+ | `GET /api/scores/by-validator?validator=` | `trajrl scores <hotkey>` |
182
+ | `GET /api/miners/:hotkey` | `trajrl miner <hotkey>` |
183
+ | `GET /api/miners/:hotkey/packs/:hash` | `trajrl pack <hotkey> <hash>` |
184
+ | `GET /api/submissions` | `trajrl submissions` |
185
+ | `GET /api/eval-logs` | `trajrl logs` |
186
+ | `GET /api/eval-logs?log_type=cycle` | `trajrl eval-history <hotkey>` |
187
+ | `GET /api/eval-logs` + GCS download | `trajrl cycle-log <hotkey>` |
188
+
189
+ ## Development
190
+
191
+ ```bash
192
+ git clone <repo> && cd trajrl
193
+ pip install -e .
194
+ trajrl --help
195
+ ```
@@ -0,0 +1,19 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "trajrl"
7
+ version = "0.2.0"
8
+ description = "CLI for TrajectoryRL, Bittensor subnet 11 — agent-friendly access to live validator, miner, and evaluation data."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ dependencies = [
13
+ "typer>=0.9",
14
+ "httpx>=0.25",
15
+ "rich>=13.0",
16
+ ]
17
+
18
+ [project.scripts]
19
+ trajrl = "trajrl.cli:app"
trajrl-0.2.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,185 @@
1
+ """Typed HTTP client for the TrajectoryRL public API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import gzip
6
+ import io
7
+ import tarfile
8
+ from dataclasses import dataclass, field
9
+ from typing import Any
10
+
11
+ import httpx
12
+
13
+ DEFAULT_BASE_URL = "https://trajrl.com"
14
+ _TIMEOUT = 30.0
15
+
16
+
17
+ def extract_cycle_log(archive_bytes: bytes) -> str | None:
18
+ """Extract validator.log text from a cycle log archive (tar.gz or gzip)."""
19
+ try:
20
+ buf = io.BytesIO(archive_bytes)
21
+ with tarfile.open(fileobj=buf, mode="r:gz") as tar:
22
+ for member in tar.getmembers():
23
+ if member.name.endswith("validator.log"):
24
+ f = tar.extractfile(member)
25
+ if f:
26
+ return f.read().decode("utf-8", errors="replace")
27
+ except Exception:
28
+ pass
29
+ try:
30
+ return gzip.decompress(archive_bytes).decode("utf-8", errors="replace")
31
+ except Exception:
32
+ pass
33
+ return archive_bytes.decode("utf-8", errors="replace")
34
+
35
+
36
+ @dataclass
37
+ class TrajRLClient:
38
+ base_url: str = DEFAULT_BASE_URL
39
+ _client: httpx.Client = field(init=False, repr=False)
40
+
41
+ def __post_init__(self) -> None:
42
+ self._client = httpx.Client(
43
+ base_url=self.base_url.rstrip("/"),
44
+ timeout=_TIMEOUT,
45
+ headers={"Accept": "application/json"},
46
+ )
47
+
48
+ # -- endpoints ---------------------------------------------------------
49
+
50
+ def validators(self) -> dict[str, Any]:
51
+ """GET /api/validators"""
52
+ return self._get("/api/validators")
53
+
54
+ def scores_by_validator(self, validator: str | None = None, uid: int | None = None) -> dict[str, Any]:
55
+ """GET /api/scores/by-validator?validator=<hotkey> or resolve UID to hotkey."""
56
+ if uid is not None:
57
+ # Resolve UID to hotkey via validators endpoint
58
+ validators_data = self.validators()
59
+ for vali in validators_data.get("validators", []):
60
+ if vali.get("uid") == uid:
61
+ validator = vali.get("hotkey")
62
+ if validator:
63
+ return self._get("/api/scores/by-validator", params={"validator": validator})
64
+ raise ValueError(f"Could not find validator with UID {uid}")
65
+ if validator is None:
66
+ raise ValueError("Either validator hotkey or uid must be provided")
67
+ return self._get("/api/scores/by-validator", params={"validator": validator})
68
+
69
+ def miner(self, hotkey: str | None = None, uid: int | None = None) -> dict[str, Any]:
70
+ """GET /api/miners/:hotkey or resolve UID to hotkey first."""
71
+ if uid is not None:
72
+ # Resolve UID to hotkey via validators endpoint
73
+ validators_data = self.validators()
74
+ # Search through validator scores to find miner by UID
75
+ # This is a fallback - ideally the API would support UID lookup
76
+ for vali in validators_data.get("validators", []):
77
+ vali_key = vali.get("hotkey")
78
+ if vali_key:
79
+ try:
80
+ scores = self.scores_by_validator(vali_key)
81
+ for entry in scores.get("entries", []):
82
+ if entry.get("uid") == uid:
83
+ hotkey = entry.get("minerHotkey")
84
+ if hotkey:
85
+ return self._get(f"/api/miners/{hotkey}")
86
+ except Exception:
87
+ continue
88
+ raise ValueError(f"Could not resolve UID {uid} to a miner hotkey")
89
+ if hotkey is None:
90
+ raise ValueError("Either hotkey or uid must be provided")
91
+ return self._get(f"/api/miners/{hotkey}")
92
+
93
+ def pack(self, hotkey: str, pack_hash: str) -> dict[str, Any]:
94
+ """GET /api/miners/:hotkey/packs/:packHash"""
95
+ return self._get(f"/api/miners/{hotkey}/packs/{pack_hash}")
96
+
97
+ def submissions(self, limit: int | None = None) -> dict[str, Any]:
98
+ """GET /api/submissions"""
99
+ return self._get("/api/submissions", params=_compact({"limit": limit}))
100
+
101
+ def eval_logs(
102
+ self,
103
+ *,
104
+ validator: str | None = None,
105
+ miner: str | None = None,
106
+ log_type: str | None = None,
107
+ eval_id: str | None = None,
108
+ pack_hash: str | None = None,
109
+ from_date: str | None = None,
110
+ to_date: str | None = None,
111
+ limit: int | None = None,
112
+ offset: int | None = None,
113
+ ) -> dict[str, Any]:
114
+ """GET /api/eval-logs
115
+
116
+ Args:
117
+ from_date: ISO 8601 date/datetime string (e.g. "2026-03-25" or "2026-03-25T00:00:00Z")
118
+ to_date: ISO 8601 date/datetime string (e.g. "2026-03-26" or "2026-03-26T23:59:59Z")
119
+ """
120
+ params = _compact({
121
+ "validator": validator,
122
+ "miner": miner,
123
+ "type": log_type,
124
+ "eval_id": eval_id,
125
+ "pack_hash": pack_hash,
126
+ "from": from_date,
127
+ "to": to_date,
128
+ "limit": limit,
129
+ "offset": offset,
130
+ })
131
+ return self._get("/api/eval-logs", params=params)
132
+
133
+ def cycle_log(
134
+ self,
135
+ validator: str,
136
+ *,
137
+ eval_id: str | None = None,
138
+ ) -> dict[str, Any]:
139
+ """Fetch the latest cycle log text for a validator.
140
+
141
+ Uses the eval-logs endpoint to find the cycle log entry, then
142
+ downloads and extracts the validator.log from the archive.
143
+
144
+ Returns ``{"log_entry": <metadata dict>, "text": <log text>}``.
145
+ Raises ``ValueError`` when no usable cycle log is found.
146
+ """
147
+ params: dict[str, Any] = {"validator": validator, "log_type": "cycle"}
148
+ if eval_id is not None:
149
+ params["eval_id"] = eval_id
150
+ else:
151
+ params["limit"] = 5
152
+
153
+ data = self.eval_logs(**params)
154
+ logs = data.get("logs", [])
155
+ if not logs:
156
+ raise ValueError("No cycle logs found for this validator")
157
+
158
+ log_entry = logs[0]
159
+ gcs_url = log_entry.get("gcsUrl")
160
+ if not gcs_url:
161
+ raise ValueError("Cycle log has no download URL")
162
+
163
+ try:
164
+ resp = httpx.get(gcs_url, timeout=30, follow_redirects=True)
165
+ resp.raise_for_status()
166
+ except Exception as exc:
167
+ raise ValueError(f"Failed to download cycle log: {exc}") from exc
168
+
169
+ text = extract_cycle_log(resp.content)
170
+ if not text:
171
+ raise ValueError("Failed to extract validator.log from archive")
172
+
173
+ return {"log_entry": log_entry, "text": text}
174
+
175
+ # -- internal ----------------------------------------------------------
176
+
177
+ def _get(self, path: str, params: dict | None = None) -> dict[str, Any]:
178
+ resp = self._client.get(path, params=params)
179
+ resp.raise_for_status()
180
+ return resp.json()
181
+
182
+
183
+ def _compact(d: dict) -> dict:
184
+ """Remove None values from a dict."""
185
+ return {k: v for k, v in d.items() if v is not None}