agent-discogs 0.1.0__tar.gz → 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.
- {agent_discogs-0.1.0 → agent_discogs-0.2.0}/PKG-INFO +54 -4
- {agent_discogs-0.1.0 → agent_discogs-0.2.0}/README.md +52 -2
- {agent_discogs-0.1.0 → agent_discogs-0.2.0}/pyproject.toml +3 -2
- {agent_discogs-0.1.0 → agent_discogs-0.2.0}/pyproject.toml.orig +3 -2
- {agent_discogs-0.1.0 → agent_discogs-0.2.0}/src/agent_discogs/__init__.py +31 -2
- {agent_discogs-0.1.0 → agent_discogs-0.2.0}/src/agent_discogs/client.py +9 -1
- {agent_discogs-0.1.0 → agent_discogs-0.2.0}/src/agent_discogs/errors.py +27 -1
- {agent_discogs-0.1.0 → agent_discogs-0.2.0}/src/agent_discogs/pagination.py +6 -0
- {agent_discogs-0.1.0 → agent_discogs-0.2.0}/src/agent_discogs/skills/core/SKILL.md +33 -2
- {agent_discogs-0.1.0 → agent_discogs-0.2.0}/src/agent_discogs/skills/core/references/commands.md +7 -0
- agent_discogs-0.2.0/src/agent_discogs/trace.py +223 -0
- {agent_discogs-0.1.0 → agent_discogs-0.2.0}/LICENSE.txt +0 -0
- {agent_discogs-0.1.0 → agent_discogs-0.2.0}/src/agent_discogs/__main__.py +0 -0
- {agent_discogs-0.1.0 → agent_discogs-0.2.0}/src/agent_discogs/commands/__init__.py +0 -0
- {agent_discogs-0.1.0 → agent_discogs-0.2.0}/src/agent_discogs/commands/cache.py +0 -0
- {agent_discogs-0.1.0 → agent_discogs-0.2.0}/src/agent_discogs/commands/get.py +0 -0
- {agent_discogs-0.1.0 → agent_discogs-0.2.0}/src/agent_discogs/commands/search.py +0 -0
- {agent_discogs-0.1.0 → agent_discogs-0.2.0}/src/agent_discogs/commands/skills.py +0 -0
- {agent_discogs-0.1.0 → agent_discogs-0.2.0}/src/agent_discogs/commands/status.py +0 -0
- {agent_discogs-0.1.0 → agent_discogs-0.2.0}/src/agent_discogs/formatting.py +0 -0
- {agent_discogs-0.1.0 → agent_discogs-0.2.0}/src/agent_discogs/json_output.py +0 -0
- {agent_discogs-0.1.0 → agent_discogs-0.2.0}/src/agent_discogs/projections.py +0 -0
- {agent_discogs-0.1.0 → agent_discogs-0.2.0}/src/agent_discogs/py.typed +0 -0
- {agent_discogs-0.1.0 → agent_discogs-0.2.0}/src/agent_discogs/refs.py +0 -0
- {agent_discogs-0.1.0 → agent_discogs-0.2.0}/src/agent_discogs/skills/core/references/discogs-domain.md +0 -0
- {agent_discogs-0.1.0 → agent_discogs-0.2.0}/src/agent_discogs/skills/core/references/pressings-guide.md +0 -0
- {agent_discogs-0.1.0 → agent_discogs-0.2.0}/src/agent_discogs/skills/core/references/search-patterns.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agent-discogs
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Token-efficient Discogs CLI for AI agents
|
|
5
5
|
Keywords: ai-agent,cli,discogs,discogs-api,music-database
|
|
6
6
|
Author: Jean-Marc Fontaine
|
|
@@ -15,7 +15,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.14
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.15
|
|
17
17
|
Requires-Dist: click>=8.3.1
|
|
18
|
-
Requires-Dist: discogs-sdk>=0.
|
|
18
|
+
Requires-Dist: discogs-sdk>=0.5.0
|
|
19
19
|
Requires-Dist: pydantic>=2.12.5
|
|
20
20
|
Requires-Python: >=3.10
|
|
21
21
|
Project-URL: Homepage, https://github.com/jmfontaine/agent-discogs
|
|
@@ -173,7 +173,7 @@ Errors under `--json` are a JSON document on stdout with exit code 1:
|
|
|
173
173
|
{"error":{"code":"not_found","message":"Master @m... not found.","hint":"Try: agent-discogs search \"<title>\"","status":404}}
|
|
174
174
|
```
|
|
175
175
|
|
|
176
|
-
Codes: `not_found`, `seller_settings_required`, `auth_required`, `forbidden`, `rate_limited` (with `retry_after` seconds when provided), `api_error`, `connection_error`, `invalid_argument`, `unexpected`.
|
|
176
|
+
Codes: `not_found`, `seller_settings_required`, `auth_required`, `forbidden`, `rate_limited` (with `retry_after` seconds when provided, and `limit`/`remaining` for the current minute when Discogs reports them), `cache_miss` (only under `--cached`), `api_error`, `connection_error`, `invalid_argument`, `unexpected`.
|
|
177
177
|
|
|
178
178
|
## Ref system
|
|
179
179
|
|
|
@@ -194,6 +194,55 @@ Smart resolution: `get versions @r352665` auto-resolves the release to its maste
|
|
|
194
194
|
|
|
195
195
|
HTTP responses are cached automatically with a 1-hour TTL. The cache is stored at `~/.cache/agent-discogs/` (or `$XDG_CACHE_HOME/agent-discogs/`). Clear it with `agent-discogs cache clear`.
|
|
196
196
|
|
|
197
|
+
Two global flags control it for one command. They precede the subcommand and are mutually exclusive (exit code 2 otherwise):
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
agent-discogs --cached price @r352665 # serve only from the cache; never touches the API
|
|
201
|
+
agent-discogs --fresh get release @r352665 # bypass the cache; go to the API
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
`--cached` fails with a `cache_miss` error (exit code 1) on any request the cache cannot serve, before any network I/O. With several refs, hits print normally and each miss reports inline, exactly like other per-ref failures; under `--json` the miss item carries `error.code == "cache_miss"`. The check is exact: it uses the real cache key at the real moment, so a command that issues several requests fails at the first one that is not cached. There is no separate "is it cached?" query because the answer depends on the whole request sequence a command issues.
|
|
205
|
+
|
|
206
|
+
`--fresh` fetches every request from the API and does **not** store the response, so a later run without `--fresh` can still be served the older cached entry until its 1-hour TTL expires.
|
|
207
|
+
|
|
208
|
+
## Budget
|
|
209
|
+
|
|
210
|
+
Discogs throttles by source IP: 60 requests per minute with a token, 25 without, as a moving 60-second window. After any command that talks to the API, stderr ends with one budget line:
|
|
211
|
+
|
|
212
|
+
```
|
|
213
|
+
api: 3 requests · 43/60 left this minute
|
|
214
|
+
api: 0 requests · cached
|
|
215
|
+
api: 4 requests · 6/60 left this minute ⚠ pause ~60s before uncached calls
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
The count is what this command spent; `43/60` is what Discogs reported after the last request. `⚠` appears when 10 or fewer requests remain: pause about a minute, or keep to refs that are already cached (`--cached` guarantees zero spend). Without a token the warning suggests setting `DISCOGS_TOKEN` instead. The line is on stderr and never inside `--json` output; commands that make no request (`status`, `skills`, `cache clear`) print nothing.
|
|
219
|
+
|
|
220
|
+
What commands cost:
|
|
221
|
+
|
|
222
|
+
| Command | Requests |
|
|
223
|
+
|---|---|
|
|
224
|
+
| `get <noun>`, `tracks`, `price` | 1 per ref (`price`: up to 3; `versions @r...`: 2) |
|
|
225
|
+
| `search`, `get releases`, `get versions` (server-side pages) | 1 |
|
|
226
|
+
| Client-side scans: `search --release-type official\|unofficial`, `get releases --role`, `get releases @l... --year` | up to 5 |
|
|
227
|
+
| Anything repeated within 1 hour | 0 (cached) |
|
|
228
|
+
|
|
229
|
+
The CLI reports; it never sleeps or throttles on your behalf. When Discogs answers 429, the error says how much of the minute is left (`Rate limited: 0/60 requests left this minute.`), and under `--json` the envelope carries `retry_after`, `limit`, and `remaining`.
|
|
230
|
+
|
|
231
|
+
## Debugging
|
|
232
|
+
|
|
233
|
+
`--debug` (or `AGENT_DISCOGS_DEBUG=1`) prints a trace panel to stderr after the budget line: one row per request with status and timing (`cache` for cache hits, `×N attempts` when the SDK retried), scan statistics for client-side filters, cache size, and output size with a rough token estimate. The SDK's own log lines (`[sdk] HTTP request ...`, `[sdk] Retrying ... waiting 30s`) stream to stderr in real time, which is the only way to see a rate-limit retry while it is happening. stdout is byte-identical with and without the flag.
|
|
234
|
+
|
|
235
|
+
```
|
|
236
|
+
api: 2 requests · 58/60 left this minute
|
|
237
|
+
── debug ───────────────────────────────────────────────
|
|
238
|
+
total 1310ms
|
|
239
|
+
GET /artists/3857 200 338ms
|
|
240
|
+
GET /artists/3857/releases?page=1&per_page=15 200 947ms
|
|
241
|
+
scan 1 API calls, 15 rows fetched, 5 kept, not capped
|
|
242
|
+
cache ~/.cache/agent-discogs 540.0 KB ttl 1h
|
|
243
|
+
output 494 chars, 9 lines, ≈123 tokens
|
|
244
|
+
```
|
|
245
|
+
|
|
197
246
|
## Error handling
|
|
198
247
|
|
|
199
248
|
The CLI maps API errors to recovery-oriented messages with suggested next steps:
|
|
@@ -201,7 +250,8 @@ The CLI maps API errors to recovery-oriented messages with suggested next steps:
|
|
|
201
250
|
```
|
|
202
251
|
✗ Release 999999999 not found. Try: agent-discogs search "<title>"
|
|
203
252
|
✗ Authentication failed. Check your DISCOGS_TOKEN.
|
|
204
|
-
✗ Rate
|
|
253
|
+
✗ Rate limited: 0/60 requests left this minute. Retry in 30s.
|
|
254
|
+
✗ Not cached: GET /marketplace/price_suggestions/352665 (only under --cached)
|
|
205
255
|
✗ Connection error. Check your network and retry.
|
|
206
256
|
```
|
|
207
257
|
|
|
@@ -148,7 +148,7 @@ Errors under `--json` are a JSON document on stdout with exit code 1:
|
|
|
148
148
|
{"error":{"code":"not_found","message":"Master @m... not found.","hint":"Try: agent-discogs search \"<title>\"","status":404}}
|
|
149
149
|
```
|
|
150
150
|
|
|
151
|
-
Codes: `not_found`, `seller_settings_required`, `auth_required`, `forbidden`, `rate_limited` (with `retry_after` seconds when provided), `api_error`, `connection_error`, `invalid_argument`, `unexpected`.
|
|
151
|
+
Codes: `not_found`, `seller_settings_required`, `auth_required`, `forbidden`, `rate_limited` (with `retry_after` seconds when provided, and `limit`/`remaining` for the current minute when Discogs reports them), `cache_miss` (only under `--cached`), `api_error`, `connection_error`, `invalid_argument`, `unexpected`.
|
|
152
152
|
|
|
153
153
|
## Ref system
|
|
154
154
|
|
|
@@ -169,6 +169,55 @@ Smart resolution: `get versions @r352665` auto-resolves the release to its maste
|
|
|
169
169
|
|
|
170
170
|
HTTP responses are cached automatically with a 1-hour TTL. The cache is stored at `~/.cache/agent-discogs/` (or `$XDG_CACHE_HOME/agent-discogs/`). Clear it with `agent-discogs cache clear`.
|
|
171
171
|
|
|
172
|
+
Two global flags control it for one command. They precede the subcommand and are mutually exclusive (exit code 2 otherwise):
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
agent-discogs --cached price @r352665 # serve only from the cache; never touches the API
|
|
176
|
+
agent-discogs --fresh get release @r352665 # bypass the cache; go to the API
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
`--cached` fails with a `cache_miss` error (exit code 1) on any request the cache cannot serve, before any network I/O. With several refs, hits print normally and each miss reports inline, exactly like other per-ref failures; under `--json` the miss item carries `error.code == "cache_miss"`. The check is exact: it uses the real cache key at the real moment, so a command that issues several requests fails at the first one that is not cached. There is no separate "is it cached?" query because the answer depends on the whole request sequence a command issues.
|
|
180
|
+
|
|
181
|
+
`--fresh` fetches every request from the API and does **not** store the response, so a later run without `--fresh` can still be served the older cached entry until its 1-hour TTL expires.
|
|
182
|
+
|
|
183
|
+
## Budget
|
|
184
|
+
|
|
185
|
+
Discogs throttles by source IP: 60 requests per minute with a token, 25 without, as a moving 60-second window. After any command that talks to the API, stderr ends with one budget line:
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
api: 3 requests · 43/60 left this minute
|
|
189
|
+
api: 0 requests · cached
|
|
190
|
+
api: 4 requests · 6/60 left this minute ⚠ pause ~60s before uncached calls
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
The count is what this command spent; `43/60` is what Discogs reported after the last request. `⚠` appears when 10 or fewer requests remain: pause about a minute, or keep to refs that are already cached (`--cached` guarantees zero spend). Without a token the warning suggests setting `DISCOGS_TOKEN` instead. The line is on stderr and never inside `--json` output; commands that make no request (`status`, `skills`, `cache clear`) print nothing.
|
|
194
|
+
|
|
195
|
+
What commands cost:
|
|
196
|
+
|
|
197
|
+
| Command | Requests |
|
|
198
|
+
|---|---|
|
|
199
|
+
| `get <noun>`, `tracks`, `price` | 1 per ref (`price`: up to 3; `versions @r...`: 2) |
|
|
200
|
+
| `search`, `get releases`, `get versions` (server-side pages) | 1 |
|
|
201
|
+
| Client-side scans: `search --release-type official\|unofficial`, `get releases --role`, `get releases @l... --year` | up to 5 |
|
|
202
|
+
| Anything repeated within 1 hour | 0 (cached) |
|
|
203
|
+
|
|
204
|
+
The CLI reports; it never sleeps or throttles on your behalf. When Discogs answers 429, the error says how much of the minute is left (`Rate limited: 0/60 requests left this minute.`), and under `--json` the envelope carries `retry_after`, `limit`, and `remaining`.
|
|
205
|
+
|
|
206
|
+
## Debugging
|
|
207
|
+
|
|
208
|
+
`--debug` (or `AGENT_DISCOGS_DEBUG=1`) prints a trace panel to stderr after the budget line: one row per request with status and timing (`cache` for cache hits, `×N attempts` when the SDK retried), scan statistics for client-side filters, cache size, and output size with a rough token estimate. The SDK's own log lines (`[sdk] HTTP request ...`, `[sdk] Retrying ... waiting 30s`) stream to stderr in real time, which is the only way to see a rate-limit retry while it is happening. stdout is byte-identical with and without the flag.
|
|
209
|
+
|
|
210
|
+
```
|
|
211
|
+
api: 2 requests · 58/60 left this minute
|
|
212
|
+
── debug ───────────────────────────────────────────────
|
|
213
|
+
total 1310ms
|
|
214
|
+
GET /artists/3857 200 338ms
|
|
215
|
+
GET /artists/3857/releases?page=1&per_page=15 200 947ms
|
|
216
|
+
scan 1 API calls, 15 rows fetched, 5 kept, not capped
|
|
217
|
+
cache ~/.cache/agent-discogs 540.0 KB ttl 1h
|
|
218
|
+
output 494 chars, 9 lines, ≈123 tokens
|
|
219
|
+
```
|
|
220
|
+
|
|
172
221
|
## Error handling
|
|
173
222
|
|
|
174
223
|
The CLI maps API errors to recovery-oriented messages with suggested next steps:
|
|
@@ -176,7 +225,8 @@ The CLI maps API errors to recovery-oriented messages with suggested next steps:
|
|
|
176
225
|
```
|
|
177
226
|
✗ Release 999999999 not found. Try: agent-discogs search "<title>"
|
|
178
227
|
✗ Authentication failed. Check your DISCOGS_TOKEN.
|
|
179
|
-
✗ Rate
|
|
228
|
+
✗ Rate limited: 0/60 requests left this minute. Retry in 30s.
|
|
229
|
+
✗ Not cached: GET /marketplace/price_suggestions/352665 (only under --cached)
|
|
180
230
|
✗ Connection error. Check your network and retry.
|
|
181
231
|
```
|
|
182
232
|
|
|
@@ -4,7 +4,7 @@ requires = ["uv-build>=0.12.13,<0.13"]
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "agent-discogs"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "Token-efficient Discogs CLI for AI agents"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
keywords = [
|
|
@@ -28,7 +28,7 @@ classifiers = [
|
|
|
28
28
|
]
|
|
29
29
|
dependencies = [
|
|
30
30
|
"click>=8.3.1",
|
|
31
|
-
"discogs-sdk>=0.
|
|
31
|
+
"discogs-sdk>=0.5.0",
|
|
32
32
|
"pydantic>=2.12.5",
|
|
33
33
|
]
|
|
34
34
|
|
|
@@ -142,6 +142,7 @@ ignore-names = [
|
|
|
142
142
|
"_patch_get",
|
|
143
143
|
"_patch_search",
|
|
144
144
|
"_reset_singleton",
|
|
145
|
+
"_scoped_client",
|
|
145
146
|
"format_help",
|
|
146
147
|
"get_skill",
|
|
147
148
|
"skill_path",
|
|
@@ -4,7 +4,7 @@ requires = [ "uv-build>=0.12.13,<0.13" ]
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "agent-discogs"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "Token-efficient Discogs CLI for AI agents"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
keywords = [ "ai-agent", "cli", "discogs", "discogs-api", "music-database" ]
|
|
@@ -21,7 +21,7 @@ classifiers = [
|
|
|
21
21
|
"Programming Language :: Python :: 3.14",
|
|
22
22
|
"Programming Language :: Python :: 3.15",
|
|
23
23
|
]
|
|
24
|
-
dependencies = [ "click>=8.3.1", "discogs-sdk>=0.
|
|
24
|
+
dependencies = [ "click>=8.3.1", "discogs-sdk>=0.5.0", "pydantic>=2.12.5" ]
|
|
25
25
|
urls.Homepage = "https://github.com/jmfontaine/agent-discogs"
|
|
26
26
|
urls.Issues = "https://github.com/jmfontaine/agent-discogs/issues"
|
|
27
27
|
urls.Repository = "https://github.com/jmfontaine/agent-discogs"
|
|
@@ -125,6 +125,7 @@ ignore-names = [
|
|
|
125
125
|
"_patch_get",
|
|
126
126
|
"_patch_search",
|
|
127
127
|
"_reset_singleton",
|
|
128
|
+
"_scoped_client",
|
|
128
129
|
"format_help",
|
|
129
130
|
"get_skill", # click subcommands are registered by decorator, invisible to deadcode
|
|
130
131
|
"skill_path",
|
|
@@ -7,6 +7,7 @@ from typing import Any
|
|
|
7
7
|
|
|
8
8
|
import click
|
|
9
9
|
|
|
10
|
+
from agent_discogs import client, trace
|
|
10
11
|
from agent_discogs.commands.cache import cache
|
|
11
12
|
from agent_discogs.commands.get import get, price, tracks
|
|
12
13
|
from agent_discogs.commands.search import search
|
|
@@ -44,6 +45,11 @@ Refs:
|
|
|
44
45
|
@m3719 (master), @l647 (label). Use refs with get commands.
|
|
45
46
|
|
|
46
47
|
Options:
|
|
48
|
+
--cached Serve only from the cache; a miss fails (cache_miss), spends
|
|
49
|
+
nothing. Precedes the command: agent-discogs --cached price @r...
|
|
50
|
+
--fresh Bypass the cache for this command (response is not stored)
|
|
51
|
+
--debug Request rows, timings, retries, scan stats, cache and output
|
|
52
|
+
size on stderr, plus the SDK's own log lines
|
|
47
53
|
--json JSON output: a compact projection of the text view (search, get)
|
|
48
54
|
--full With --json: the raw SDK record instead of the projection
|
|
49
55
|
--limit N Results per page (search, get)
|
|
@@ -63,7 +69,14 @@ Options:
|
|
|
63
69
|
--help Show this message and exit
|
|
64
70
|
|
|
65
71
|
Environment:
|
|
66
|
-
DISCOGS_TOKEN
|
|
72
|
+
DISCOGS_TOKEN Personal access token (60 req/min, required for price data)
|
|
73
|
+
AGENT_DISCOGS_DEBUG Same as --debug
|
|
74
|
+
|
|
75
|
+
Budget:
|
|
76
|
+
After any command that talks to the API, stderr ends with a budget line, e.g.
|
|
77
|
+
'api: 3 requests · 43/60 left this minute'. ⚠ means 10 or fewer requests
|
|
78
|
+
remain in the moving 60s window: pause ~60s or stick to cached refs. Cache
|
|
79
|
+
hits cost nothing ('api: 0 requests · cached'). Never inside --json output.
|
|
67
80
|
|
|
68
81
|
Price data also needs seller settings filled out on the Discogs account the
|
|
69
82
|
token belongs to (discogs.com/settings/seller). Without them the API answers
|
|
@@ -130,9 +143,25 @@ class AliasGroup(click.Group):
|
|
|
130
143
|
|
|
131
144
|
@click.group(cls=AliasGroup, invoke_without_command=True)
|
|
132
145
|
@click.version_option(package_name="agent-discogs", prog_name="agent-discogs")
|
|
146
|
+
@click.option(
|
|
147
|
+
"--debug",
|
|
148
|
+
is_flag=True,
|
|
149
|
+
envvar="AGENT_DISCOGS_DEBUG",
|
|
150
|
+
help="Print request trace and timings to stderr.",
|
|
151
|
+
)
|
|
152
|
+
@click.option("--cached", is_flag=True, help="Serve only from cache; fail on a miss.")
|
|
153
|
+
@click.option("--fresh", is_flag=True, help="Bypass the cache for this command.")
|
|
133
154
|
@click.pass_context
|
|
134
|
-
def cli(ctx: click.Context) -> None:
|
|
155
|
+
def cli(ctx: click.Context, debug: bool, cached: bool, fresh: bool) -> None:
|
|
135
156
|
"""Token-efficient Discogs CLI for AI agents."""
|
|
157
|
+
if cached and fresh:
|
|
158
|
+
msg = "--cached and --fresh are mutually exclusive."
|
|
159
|
+
raise click.UsageError(msg)
|
|
160
|
+
trace.begin(ctx, debug=debug)
|
|
161
|
+
if cached:
|
|
162
|
+
ctx.with_resource(client.get_client().cache_only())
|
|
163
|
+
elif fresh:
|
|
164
|
+
ctx.with_resource(client.get_client().no_cache())
|
|
136
165
|
if ctx.invoked_subcommand is None:
|
|
137
166
|
click.echo(ctx.get_help())
|
|
138
167
|
|
|
@@ -7,6 +7,8 @@ from pathlib import Path
|
|
|
7
7
|
|
|
8
8
|
from discogs_sdk import Discogs
|
|
9
9
|
|
|
10
|
+
from agent_discogs import trace
|
|
11
|
+
|
|
10
12
|
_client: Discogs | None = None
|
|
11
13
|
|
|
12
14
|
CACHE_DIR = (
|
|
@@ -21,7 +23,13 @@ def get_client() -> Discogs:
|
|
|
21
23
|
return _client
|
|
22
24
|
|
|
23
25
|
token = os.environ.get("DISCOGS_TOKEN")
|
|
24
|
-
_client = Discogs(
|
|
26
|
+
_client = Discogs(
|
|
27
|
+
token=token,
|
|
28
|
+
cache=True,
|
|
29
|
+
cache_dir=CACHE_DIR,
|
|
30
|
+
cache_ttl=trace.CACHE_TTL,
|
|
31
|
+
on_request=trace.record,
|
|
32
|
+
)
|
|
25
33
|
return _client
|
|
26
34
|
|
|
27
35
|
|
|
@@ -6,6 +6,9 @@ import json
|
|
|
6
6
|
import sys
|
|
7
7
|
from dataclasses import asdict, dataclass
|
|
8
8
|
from typing import Any, NoReturn
|
|
9
|
+
from urllib.parse import urlsplit
|
|
10
|
+
|
|
11
|
+
from agent_discogs import trace
|
|
9
12
|
|
|
10
13
|
|
|
11
14
|
@dataclass(frozen=True)
|
|
@@ -18,6 +21,8 @@ class ErrorInfo:
|
|
|
18
21
|
hint: str | None = None
|
|
19
22
|
retry_after: int | None = None
|
|
20
23
|
status: int | None = None
|
|
24
|
+
limit: int | None = None
|
|
25
|
+
remaining: int | None = None
|
|
21
26
|
|
|
22
27
|
|
|
23
28
|
def _api_message(exc: Exception) -> str:
|
|
@@ -40,6 +45,7 @@ def classify(exc: Exception, context: str | None = None) -> ErrorInfo:
|
|
|
40
45
|
failed, e.g. "Release @r847868", and is used in not-found messages."""
|
|
41
46
|
from discogs_sdk import (
|
|
42
47
|
AuthenticationError,
|
|
48
|
+
CacheMissError,
|
|
43
49
|
DiscogsAPIError,
|
|
44
50
|
DiscogsConnectionError,
|
|
45
51
|
ForbiddenError,
|
|
@@ -91,12 +97,20 @@ def classify(exc: Exception, context: str | None = None) -> ErrorInfo:
|
|
|
91
97
|
wait = (
|
|
92
98
|
f"Retry in {retry_after}s." if retry_after else "Wait a moment and retry."
|
|
93
99
|
)
|
|
100
|
+
rl = exc.ratelimit
|
|
101
|
+
message = (
|
|
102
|
+
"Rate limit exceeded."
|
|
103
|
+
if rl is None
|
|
104
|
+
else f"Rate limited: {rl.remaining}/{rl.limit} requests left this minute."
|
|
105
|
+
)
|
|
94
106
|
return ErrorInfo(
|
|
95
107
|
"rate_limited",
|
|
96
|
-
|
|
108
|
+
message,
|
|
97
109
|
f"{wait} 60 req/min with DISCOGS_TOKEN, 25 without.",
|
|
98
110
|
retry_after=retry_after,
|
|
99
111
|
status=429,
|
|
112
|
+
limit=None if rl is None else rl.limit,
|
|
113
|
+
remaining=None if rl is None else rl.remaining,
|
|
100
114
|
)
|
|
101
115
|
|
|
102
116
|
if isinstance(exc, DiscogsAPIError):
|
|
@@ -109,6 +123,18 @@ def classify(exc: Exception, context: str | None = None) -> ErrorInfo:
|
|
|
109
123
|
"connection_error", "Connection error.", "Check your network and retry."
|
|
110
124
|
)
|
|
111
125
|
|
|
126
|
+
if isinstance(exc, CacheMissError):
|
|
127
|
+
# The SDK emits no request event for a miss, so the footer learns
|
|
128
|
+
# about it here: every miss an agent sees passes through classify().
|
|
129
|
+
trace.note_cache_miss()
|
|
130
|
+
split = urlsplit(exc.url)
|
|
131
|
+
where = split.path + (f"?{split.query}" if split.query else "")
|
|
132
|
+
return ErrorInfo(
|
|
133
|
+
"cache_miss",
|
|
134
|
+
f"Not cached: {exc.method} {where}",
|
|
135
|
+
"Rerun without --cached to fetch it from the API.",
|
|
136
|
+
)
|
|
137
|
+
|
|
112
138
|
if isinstance(exc, ValueError):
|
|
113
139
|
return ErrorInfo("invalid_argument", str(exc))
|
|
114
140
|
|
|
@@ -10,6 +10,8 @@ from typing import Any
|
|
|
10
10
|
from discogs_sdk import Discogs
|
|
11
11
|
from pydantic import BaseModel
|
|
12
12
|
|
|
13
|
+
from agent_discogs import trace
|
|
14
|
+
|
|
13
15
|
MAX_API_CALLS = 5
|
|
14
16
|
DEFAULT_LIMIT = 5
|
|
15
17
|
|
|
@@ -152,11 +154,14 @@ def fetch_filtered_page(
|
|
|
152
154
|
collected: list[Any] = []
|
|
153
155
|
next_cursor: str | None = None
|
|
154
156
|
capped = False
|
|
157
|
+
api_calls = rows = 0
|
|
155
158
|
result = PageResult(items=[], page=page, total_items=0, total_pages=1)
|
|
156
159
|
|
|
157
160
|
for _ in range(MAX_API_CALLS):
|
|
158
161
|
fetch_params = {**params, "page": api_page, "per_page": api_per_page}
|
|
159
162
|
result = fetch_page(client, path, fetch_params, model_cls, items_key)
|
|
163
|
+
api_calls += 1
|
|
164
|
+
rows += len(result.items)
|
|
160
165
|
|
|
161
166
|
for idx in range(offset, len(result.items)):
|
|
162
167
|
item = result.items[idx]
|
|
@@ -179,6 +184,7 @@ def fetch_filtered_page(
|
|
|
179
184
|
capped = True
|
|
180
185
|
next_cursor = f"{page + 1}:{api_page}.0"
|
|
181
186
|
|
|
187
|
+
trace.note_scan(trace.ScanNote(api_calls, rows, len(collected), capped))
|
|
182
188
|
return PageResult(
|
|
183
189
|
items=collected,
|
|
184
190
|
page=page,
|
|
@@ -94,7 +94,7 @@ agent-discogs get release @r847868 --json -v # adds notes, credits, and
|
|
|
94
94
|
agent-discogs get release @r847868 --json --full # raw Discogs record (images, URLs, ~20x larger); only when the projection lacks a field you need
|
|
95
95
|
```
|
|
96
96
|
|
|
97
|
-
With `--json`, errors are a JSON document on stdout and exit code 1: `{"error":{"code":"not_found","message":"Master @m... not found.","hint":"Try: agent-discogs search \"<title>\"","status":404}}`. Branch on `error.code`: `not_found`, `seller_settings_required`, `auth_required`, `forbidden`, `rate_limited` (with `retry_after` seconds when Discogs sends it), `api_error`, `connection_error`, `invalid_argument`, `unexpected`.
|
|
97
|
+
With `--json`, errors are a JSON document on stdout and exit code 1: `{"error":{"code":"not_found","message":"Master @m... not found.","hint":"Try: agent-discogs search \"<title>\"","status":404}}`. Branch on `error.code`: `not_found`, `seller_settings_required`, `auth_required`, `forbidden`, `rate_limited` (with `retry_after` seconds when Discogs sends it, plus `limit`/`remaining` for the current minute), `cache_miss` (only under `--cached`), `api_error`, `connection_error`, `invalid_argument`, `unexpected`.
|
|
98
98
|
|
|
99
99
|
## Anti-Patterns
|
|
100
100
|
|
|
@@ -104,6 +104,7 @@ With `--json`, errors are a JSON document on stdout and exit code 1: `{"error":{
|
|
|
104
104
|
- **Don't compute page numbers.** Paste the `Next page:` / `Continue scan:` command printed under a list. Filtered lists (the default search, `releases --role`, `releases @l... --year`) continue with an `--after` cursor; `--page` is rejected there and the error says what to use.
|
|
105
105
|
- **Don't guess IDs.** Always search first to find the right entity.
|
|
106
106
|
- **Don't use `get versions` on a release ID.** Release rows already show `→ @m...`; use that master ref (smart resolution costs an extra API call).
|
|
107
|
+
- **Don't use `--fresh` by default.** Catalogue data an hour old is fine; reserve `--fresh` for `price` when you need current market numbers.
|
|
107
108
|
|
|
108
109
|
## Error Recovery
|
|
109
110
|
|
|
@@ -111,7 +112,8 @@ With `--json`, errors are a JSON document on stdout and exit code 1: `{"error":{
|
|
|
111
112
|
- **Auth required** — price suggestions and search require `DISCOGS_TOKEN`. Run `agent-discogs status` to check.
|
|
112
113
|
- **"Price data requires seller settings"** — not a bad ref. The release exists, but Discogs only serves price suggestions to accounts with seller settings filled out. Nothing to retry: use `get release @r...` for the `num_for_sale`/`lowest_price` summary instead.
|
|
113
114
|
- **Invalid ref** — refs are Discogs IDs and never expire; there is no session. Check the prefix matches the noun (`@r` release, `@m` master, `@a` artist, `@l` label). A bare `@123` is invalid: either add the type letter or pass the raw number `123`.
|
|
114
|
-
- **Rate limited** —
|
|
115
|
+
- **Rate limited** — the error says what is left (`Rate limited: 0/60 requests left this minute.`, `remaining`/`limit` in JSON). Pause ~60s, or continue with `--cached` on refs you already fetched. Authenticated requests get 60/min; unauthenticated get 25/min.
|
|
116
|
+
- **Unexpected cost or a stall?** Re-run once with `--debug` to see each request, retries and cache hits on stderr. Do not leave it on.
|
|
115
117
|
- **"Continue scan:" instead of "Next page:"** — the filter was sparse and the scan stopped at 5 API calls before filling the page. The next window may also be short or empty; keep pasting the printed command until it disappears (no footer = nothing left to scan). Counts shown as `≤N` are the unfiltered upper bound.
|
|
116
118
|
|
|
117
119
|
## Refs
|
|
@@ -130,6 +132,35 @@ Refs encode entity type and Discogs ID: `@a3857` (artist), `@r847868` (release),
|
|
|
130
132
|
|
|
131
133
|
Prefer the most specific command: `tracks @r...` over `get release @r...` when you only need the tracklist, and `price @r...` over `get release @r...` when you only need pricing. This reduces output tokens and avoids unnecessary data.
|
|
132
134
|
|
|
135
|
+
## Budget
|
|
136
|
+
|
|
137
|
+
Discogs allows 60 requests per minute with a token (25 without), as a moving 60s window shared by every process on this machine. After any command that talks to the API, stderr ends with one line:
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
api: 3 requests · 43/60 left this minute
|
|
141
|
+
api: 0 requests · cached
|
|
142
|
+
api: 4 requests · 6/60 left this minute ⚠ pause ~60s before uncached calls
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The count is what this command spent; `43/60` is what Discogs reported after its last request. It is never part of `--json` output. Commands that make no request print nothing.
|
|
146
|
+
|
|
147
|
+
| Command | Requests |
|
|
148
|
+
|---|---|
|
|
149
|
+
| `get <noun>`, `tracks` | 1 per ref |
|
|
150
|
+
| `price` | 3 per ref; `get versions @r...` 2 (resolve, then list) |
|
|
151
|
+
| `search`, `get releases`, `get versions` (server-side pages) | 1 |
|
|
152
|
+
| Client-side scans: `search` with `--release-type official\|unofficial` (the default), `get releases --role`, `get releases @l... --year` | up to 5 |
|
|
153
|
+
| Anything repeated within 1 hour | 0 (cached) |
|
|
154
|
+
|
|
155
|
+
At `⚠` (10 or fewer left): pause ~60s before uncached calls, or spend nothing with `--cached`. Unauthenticated runs get 25/min; setting `DISCOGS_TOKEN` is the fix there.
|
|
156
|
+
|
|
157
|
+
**Try everything with `--cached` first, then spend only on the misses.** `agent-discogs --cached get release @r847868 @r...` serves what the cache has and reports each miss inline as `✗ Not cached: GET ...` (`error.code == "cache_miss"` under `--json`) without touching the API. It matters when several agents share one machine (one cache, one IP budget), and after a `⚠`. The flags precede the command:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
agent-discogs --cached price @r847868 # zero spend; misses fail with cache_miss
|
|
161
|
+
agent-discogs --fresh price @r847868 # bypass the cache; the response is not stored
|
|
162
|
+
```
|
|
163
|
+
|
|
133
164
|
## Reference Docs
|
|
134
165
|
|
|
135
166
|
| Document | Content |
|
{agent_discogs-0.1.0 → agent_discogs-0.2.0}/src/agent_discogs/skills/core/references/commands.md
RENAMED
|
@@ -2,11 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## Global Flags
|
|
4
4
|
|
|
5
|
+
Global flags precede the command (`agent-discogs --cached price @r847868`).
|
|
6
|
+
|
|
5
7
|
| Flag | Description |
|
|
6
8
|
|------|-------------|
|
|
9
|
+
| `--cached` | Serve only from the response cache. A request the cache cannot serve fails with error code `cache_miss` (exit 1, per ref when several are given) before any network I/O. Never spends a request. |
|
|
10
|
+
| `--fresh` | Bypass the cache for this command. Every request goes to the API; the response is not stored, so a later run without `--fresh` may still see the older cached entry until its 1h TTL expires. Mutually exclusive with `--cached` (exit 2). |
|
|
11
|
+
| `--debug` | Trace panel on stderr after the budget line: one row per request (path, status, timing or `cache`, `×N attempts` on retry), scan statistics, cache size, output size with a token estimate, plus the SDK's own log lines in real time. `AGENT_DISCOGS_DEBUG=1` does the same. stdout is unchanged. |
|
|
7
12
|
| `--help` | Show help |
|
|
8
13
|
| `--version` | Show version |
|
|
9
14
|
|
|
15
|
+
After any command that talks to the API, stderr ends with a budget line such as `api: 3 requests · 43/60 left this minute` (`⚠` when 10 or fewer remain). See the Budget section of the core skill.
|
|
16
|
+
|
|
10
17
|
|
|
11
18
|
## search
|
|
12
19
|
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""Per-invocation request trace: the budget footer and the --debug panel.
|
|
2
|
+
|
|
3
|
+
One process-wide collector, reset by `begin()` at the top of every invocation
|
|
4
|
+
and drained by `finish()` when the click context closes. `record()` is the
|
|
5
|
+
SDK's `on_request` hook; `note_scan()` is called by client-side scans.
|
|
6
|
+
Everything printed here goes to stderr so stdout stays a pure data channel.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
import sys
|
|
13
|
+
import time
|
|
14
|
+
from collections.abc import Sequence
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import TYPE_CHECKING, Any
|
|
18
|
+
from urllib.parse import urlsplit
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
import click
|
|
22
|
+
from discogs_sdk import RequestEvent
|
|
23
|
+
|
|
24
|
+
# Remaining requests at which the footer warns. One client-side scan costs up
|
|
25
|
+
# to MAX_API_CALLS (5) requests; two back-to-back scans can spend 10.
|
|
26
|
+
LOW_BUDGET = 10
|
|
27
|
+
# Passed to Discogs(cache_ttl=...) and shown by --debug.
|
|
28
|
+
CACHE_TTL = 3600.0
|
|
29
|
+
|
|
30
|
+
_SDK_LOGGER = "discogs_sdk"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class ScanNote:
|
|
35
|
+
"""What one `fetch_filtered_page` call spent and kept."""
|
|
36
|
+
|
|
37
|
+
api_calls: int
|
|
38
|
+
rows: int
|
|
39
|
+
kept: int
|
|
40
|
+
capped: bool
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class _CountingWriter:
|
|
44
|
+
"""Proxy around a text stream that counts what was written to it."""
|
|
45
|
+
|
|
46
|
+
def __init__(self, target: Any) -> None:
|
|
47
|
+
self.target: Any = target
|
|
48
|
+
self.chars: int = 0
|
|
49
|
+
self.lines: int = 0
|
|
50
|
+
|
|
51
|
+
def write(self, s: str) -> int:
|
|
52
|
+
n = self.target.write(s)
|
|
53
|
+
self.chars += len(s)
|
|
54
|
+
self.lines += s.count("\n")
|
|
55
|
+
return n
|
|
56
|
+
|
|
57
|
+
def flush(self) -> None:
|
|
58
|
+
self.target.flush()
|
|
59
|
+
|
|
60
|
+
def __getattr__(self, name: str) -> Any:
|
|
61
|
+
return getattr(self.target, name)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class Trace:
|
|
66
|
+
events: list[RequestEvent] = field(default_factory=list)
|
|
67
|
+
scans: list[ScanNote] = field(default_factory=list)
|
|
68
|
+
started: float = 0.0
|
|
69
|
+
debug: bool = False
|
|
70
|
+
cache_misses: int = 0
|
|
71
|
+
stdout: _CountingWriter | None = None
|
|
72
|
+
log_handler: logging.Handler | None = None
|
|
73
|
+
log_level: int = logging.NOTSET
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
_trace = Trace()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def begin(ctx: click.Context, *, debug: bool) -> None:
|
|
80
|
+
"""Reset the trace for this invocation and arrange for `finish()` to run
|
|
81
|
+
when `ctx` closes, i.e. after the subcommand, success or failure."""
|
|
82
|
+
global _trace # noqa: PLW0603 # one invocation per process; reset for tests
|
|
83
|
+
_trace = Trace(started=time.perf_counter(), debug=debug)
|
|
84
|
+
if debug:
|
|
85
|
+
_trace.stdout = _CountingWriter(sys.stdout)
|
|
86
|
+
sys.stdout = _trace.stdout # type: ignore[assignment]
|
|
87
|
+
handler = logging.StreamHandler(sys.stderr)
|
|
88
|
+
handler.setLevel(logging.DEBUG)
|
|
89
|
+
handler.setFormatter(logging.Formatter("[sdk] %(message)s"))
|
|
90
|
+
logger = logging.getLogger(_SDK_LOGGER)
|
|
91
|
+
_trace.log_level = logger.level
|
|
92
|
+
logger.setLevel(logging.DEBUG)
|
|
93
|
+
logger.addHandler(handler)
|
|
94
|
+
_trace.log_handler = handler
|
|
95
|
+
ctx.call_on_close(finish)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def record(event: RequestEvent) -> None:
|
|
99
|
+
"""SDK `on_request` hook: one event per request, cache hit or network."""
|
|
100
|
+
_trace.events.append(event)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def note_scan(note: ScanNote) -> None:
|
|
104
|
+
"""Record the cost of one client-side filtered scan."""
|
|
105
|
+
_trace.scans.append(note)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def note_cache_miss() -> None:
|
|
109
|
+
"""Record a `--cached` lookup the cache could not serve. The SDK emits no
|
|
110
|
+
event for it, so this is the only trace such a request leaves."""
|
|
111
|
+
_trace.cache_misses += 1
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def render_footer(
|
|
115
|
+
events: Sequence[RequestEvent], *, authenticated: bool, cache_misses: int = 0
|
|
116
|
+
) -> str | None:
|
|
117
|
+
"""The budget line, or None when nothing was requested.
|
|
118
|
+
|
|
119
|
+
Counts network requests only; the budget comes from the last network
|
|
120
|
+
response that reported rate-limit headers, so cache hits never move it.
|
|
121
|
+
A `--cached` miss counts as a request that cost nothing.
|
|
122
|
+
"""
|
|
123
|
+
if not events and not cache_misses:
|
|
124
|
+
return None
|
|
125
|
+
network = [e for e in events if e.source == "network"]
|
|
126
|
+
n = len(network)
|
|
127
|
+
if n == 0:
|
|
128
|
+
return "api: 0 requests · cached"
|
|
129
|
+
line = f"api: {n} request" + ("" if n == 1 else "s")
|
|
130
|
+
rl = next((e.ratelimit for e in reversed(network) if e.ratelimit), None)
|
|
131
|
+
if rl is None:
|
|
132
|
+
return line
|
|
133
|
+
line += f" · {rl.remaining}/{rl.limit} left this minute"
|
|
134
|
+
if rl.remaining <= LOW_BUDGET:
|
|
135
|
+
line += (
|
|
136
|
+
" ⚠ pause ~60s before uncached calls"
|
|
137
|
+
if authenticated
|
|
138
|
+
else " ⚠ set DISCOGS_TOKEN for 60/min, or pause ~60s"
|
|
139
|
+
)
|
|
140
|
+
return line
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _request_path(url: str) -> str:
|
|
144
|
+
split = urlsplit(url)
|
|
145
|
+
return split.path + (f"?{split.query}" if split.query else "")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
_KB = 1024
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _cache_size(cache_dir: Path) -> str:
|
|
152
|
+
# SQLite may leave -wal/-shm beside the database file.
|
|
153
|
+
size = sum(p.stat().st_size for p in cache_dir.glob("cache.db*"))
|
|
154
|
+
if size < _KB:
|
|
155
|
+
return f"{size} B"
|
|
156
|
+
if size < _KB * _KB:
|
|
157
|
+
return f"{size / _KB:.1f} KB"
|
|
158
|
+
return f"{size / (_KB * _KB):.1f} MB"
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _tilde(path: Path) -> str:
|
|
162
|
+
home = Path.home()
|
|
163
|
+
try:
|
|
164
|
+
return f"~/{path.relative_to(home)}"
|
|
165
|
+
except ValueError:
|
|
166
|
+
return str(path)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def render_panel(trace: Trace, *, cache_dir: Path, elapsed_ms: float) -> str:
|
|
170
|
+
"""The `--debug` panel: one row per request, then scan, cache and output
|
|
171
|
+
statistics. Takes plain values; never touches the client or the network."""
|
|
172
|
+
lines = ["── debug ".ljust(56, "─"), f"total {elapsed_ms:.0f}ms"]
|
|
173
|
+
paths = [_request_path(e.url) for e in trace.events]
|
|
174
|
+
width = max((len(p) for p in paths), default=0)
|
|
175
|
+
for event, path in zip(trace.events, paths, strict=True):
|
|
176
|
+
if event.source == "cache":
|
|
177
|
+
cost = "cache"
|
|
178
|
+
else:
|
|
179
|
+
cost = f"{event.elapsed_ms:.0f}ms"
|
|
180
|
+
if event.attempts > 1:
|
|
181
|
+
cost += f" \u00d7{event.attempts} attempts" # multiplication sign
|
|
182
|
+
lines.append(f"{event.method} {path:<{width}} {event.status_code} {cost}")
|
|
183
|
+
for scan in trace.scans:
|
|
184
|
+
capped = (
|
|
185
|
+
f"capped at {scan.api_calls} API calls" if scan.capped else "not capped"
|
|
186
|
+
)
|
|
187
|
+
lines.append(
|
|
188
|
+
f"scan {scan.api_calls} API calls, {scan.rows} rows fetched, "
|
|
189
|
+
f"{scan.kept} kept, {capped}"
|
|
190
|
+
)
|
|
191
|
+
lines.append(
|
|
192
|
+
f"cache {_tilde(cache_dir)} {_cache_size(cache_dir)} "
|
|
193
|
+
f"ttl {CACHE_TTL / 3600:g}h"
|
|
194
|
+
)
|
|
195
|
+
if trace.stdout is not None:
|
|
196
|
+
out = trace.stdout
|
|
197
|
+
lines.append(
|
|
198
|
+
f"output {out.chars} chars, {out.lines} lines, ≈{out.chars // 4} tokens"
|
|
199
|
+
)
|
|
200
|
+
return "\n".join(lines)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def finish() -> None:
|
|
204
|
+
"""Print the footer (and the panel under --debug) to stderr and undo
|
|
205
|
+
whatever `begin()` installed."""
|
|
206
|
+
from agent_discogs.client import CACHE_DIR, has_token
|
|
207
|
+
|
|
208
|
+
if _trace.stdout is not None:
|
|
209
|
+
sys.stdout = _trace.stdout.target
|
|
210
|
+
footer = render_footer(
|
|
211
|
+
_trace.events, authenticated=has_token(), cache_misses=_trace.cache_misses
|
|
212
|
+
)
|
|
213
|
+
if footer:
|
|
214
|
+
print(footer, file=sys.stderr)
|
|
215
|
+
if _trace.debug:
|
|
216
|
+
elapsed_ms = (time.perf_counter() - _trace.started) * 1000
|
|
217
|
+
panel = render_panel(_trace, cache_dir=CACHE_DIR, elapsed_ms=elapsed_ms)
|
|
218
|
+
print(panel, file=sys.stderr)
|
|
219
|
+
if _trace.log_handler is not None:
|
|
220
|
+
logger = logging.getLogger(_SDK_LOGGER)
|
|
221
|
+
logger.removeHandler(_trace.log_handler)
|
|
222
|
+
logger.setLevel(_trace.log_level)
|
|
223
|
+
_trace.log_handler = None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|