slab-cli 0.18.0__tar.gz → 0.19.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.
- {slab_cli-0.18.0 → slab_cli-0.19.0}/PKG-INFO +2 -2
- {slab_cli-0.18.0 → slab_cli-0.19.0}/pyproject.toml +4 -2
- {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/commands/pricing.py +5 -5
- {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/commands/registry.py +2 -2
- {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/display.py +46 -39
- {slab_cli-0.18.0 → slab_cli-0.19.0}/README.md +0 -0
- {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/__init__.py +0 -0
- {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/banner.py +0 -0
- {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/browse.py +0 -0
- {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/client.py +0 -0
- {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/commands/__init__.py +0 -0
- {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/commands/breaks.py +0 -0
- {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/commands/catalog.py +0 -0
- {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/commands/collection.py +0 -0
- {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/commands/custom_sets.py +0 -0
- {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/commands/export.py +0 -0
- {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/commands/lots.py +0 -0
- {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/commands/setup.py +0 -0
- {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/commands/update.py +0 -0
- {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/config.py +0 -0
- {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/context.py +0 -0
- {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/flags.py +0 -0
- {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/help.py +0 -0
- {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/main.py +0 -0
- {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/paging.py +0 -0
- {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/picker.py +0 -0
- {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/platforms/__init__.py +0 -0
- {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/platforms/base.py +0 -0
- {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/platforms/posix.py +0 -0
- {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/platforms/windows.py +0 -0
- {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/prompts.py +0 -0
- {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/sources.py +0 -0
- {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/theme.py +0 -0
- {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/updates.py +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: slab-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.19.0
|
|
4
4
|
Summary: CLI for the slab trading-card API — catalog, collect, and track your cards from the terminal.
|
|
5
5
|
Author: dev_jeb
|
|
6
|
-
Requires-Dist: slab-schemas>=0.
|
|
6
|
+
Requires-Dist: slab-schemas>=0.34.0
|
|
7
7
|
Requires-Dist: httpx>=0.27
|
|
8
8
|
Requires-Dist: rich>=13.0
|
|
9
9
|
Requires-Dist: inquirerpy>=0.3
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "slab-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.19.0"
|
|
4
4
|
description = "CLI for the slab trading-card API — catalog, collect, and track your cards from the terminal."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [{name = "dev_jeb"}]
|
|
@@ -9,7 +9,9 @@ dependencies = [
|
|
|
9
9
|
# 0.31: CardSearchQuery.collapse_parallels — `card search` sends it on every query.
|
|
10
10
|
# 0.32 adds CompOut.grade_unconfirmed (the comps table's grade-sanity "?" marker) and the
|
|
11
11
|
# grading-desk models `slab card grade` / `slab collection grade` parse responses into.
|
|
12
|
-
|
|
12
|
+
# 0.34 reframes the grading desk from verdicts to break-even readings (BreakEvenRegion,
|
|
13
|
+
# GradingDesk.break_even) — the desk shows data, never tells the user to grade.
|
|
14
|
+
"slab-schemas>=0.34.0",
|
|
13
15
|
"httpx>=0.27",
|
|
14
16
|
"rich>=13.0",
|
|
15
17
|
"InquirerPy>=0.3",
|
|
@@ -105,9 +105,9 @@ def _split_desk_flags(args: list[str]) -> tuple[list[str], float | None, str | N
|
|
|
105
105
|
|
|
106
106
|
|
|
107
107
|
def cmd_grade(args: list[str]) -> None:
|
|
108
|
-
"""
|
|
109
|
-
what each grade nets after the fee
|
|
110
|
-
|
|
108
|
+
"""The grading math for a card. Same player → set → card flow as `price`, then the grading
|
|
109
|
+
desk: what each grade nets after the fee and where break-even sits — the data to decide with;
|
|
110
|
+
the decision is yours. `--fee 19` prices at your rate; `--company SGC` switches grader."""
|
|
111
111
|
args, fee, company = _split_desk_flags(args)
|
|
112
112
|
card = _pick_card(args)
|
|
113
113
|
if card is None:
|
|
@@ -116,8 +116,8 @@ def cmd_grade(args: list[str]) -> None:
|
|
|
116
116
|
|
|
117
117
|
|
|
118
118
|
def cmd_grade_sweep(args: list[str]) -> None:
|
|
119
|
-
"""
|
|
120
|
-
|
|
119
|
+
"""The grading math across your raw cards, biggest upside first — where the per-card
|
|
120
|
+
break-even bars become batch odds. `--fee 19` prices at your rate."""
|
|
121
121
|
args, fee, company = _split_desk_flags(args)
|
|
122
122
|
render_collection_grading_desk(
|
|
123
123
|
ctx.client.get_collection_grading_desk(ctx.collector, fee=fee, company=company)
|
|
@@ -59,7 +59,7 @@ GROUPS: list[Group] = [
|
|
|
59
59
|
Command("search", "Search the catalog (player, set, team…)", catalog.cmd_search, "[query] [flags]"),
|
|
60
60
|
Command("parallels", "Every parallel (the rainbow) of a card", catalog.cmd_rainbow, "<query>"),
|
|
61
61
|
Command("price", "Market price + 7d/30d trend", pricing.cmd_price, "<query>"),
|
|
62
|
-
Command("grade", "
|
|
62
|
+
Command("grade", "The grading math: payoffs per grade + break-even", pricing.cmd_grade, "<query> [--fee N]"),
|
|
63
63
|
Command("comps", "Recent raw sales behind the price", pricing.cmd_comps, "<query>"),
|
|
64
64
|
Command("history", "Price history over time", pricing.cmd_history, "<query>"),
|
|
65
65
|
]),
|
|
@@ -80,7 +80,7 @@ GROUPS: list[Group] = [
|
|
|
80
80
|
Command("export", "Export a set to a printable checklist", export.cmd_export),
|
|
81
81
|
Command("dashboard", "Overview: counts, value, composition", collection.cmd_dashboard),
|
|
82
82
|
Command("portfolio", "Portfolio: cost basis vs FMV + top movers", pricing.cmd_portfolio),
|
|
83
|
-
Command("grade", "
|
|
83
|
+
Command("grade", "Grading math for every raw copy, best upside first", pricing.cmd_grade_sweep, "[--fee N]"),
|
|
84
84
|
]),
|
|
85
85
|
Group("break", "case / box breaks", [
|
|
86
86
|
Command("list", "Your breaks — pick one to see value vs cost + top pulls", breaks.cmd_break_list),
|
|
@@ -20,7 +20,7 @@ from slab_schemas.community import (
|
|
|
20
20
|
from slab_schemas.custom_sets import CustomSetDetail, CustomSetOut, CustomSetSearchResult
|
|
21
21
|
from slab_schemas.dashboard import CatalogStats, DashboardStats, HighlightCard, LabeledCount
|
|
22
22
|
from slab_schemas.grading_desk import CollectionGradingDesk, GradingDesk
|
|
23
|
-
from slab_schemas.enums import
|
|
23
|
+
from slab_schemas.enums import BreakEvenRegion, Grade, SealedFormat
|
|
24
24
|
from slab_schemas.pricing import CardComps, CardMarket, PortfolioSummary, SetTopCards
|
|
25
25
|
from slab_schemas.sealed import SealedPriceHistory, SealedProductOut
|
|
26
26
|
from slab_schemas.timeseries import CardPriceHistory
|
|
@@ -1142,36 +1142,41 @@ def render_comps(c: CardComps) -> None:
|
|
|
1142
1142
|
console.print()
|
|
1143
1143
|
|
|
1144
1144
|
|
|
1145
|
-
def
|
|
1146
|
-
"""
|
|
1147
|
-
|
|
1148
|
-
`grading.
|
|
1145
|
+
def _desk_reading_line(d: GradingDesk) -> str:
|
|
1146
|
+
"""Where the math lands, as one plain DATA sentence — never an instruction. slab's principle:
|
|
1147
|
+
grading is subjective and the user is holding the card, so the desk states what the prices
|
|
1148
|
+
show and leaves the send/keep decision with them. Wording mirrors the `grading.break_even`
|
|
1149
|
+
glossary entry."""
|
|
1149
1150
|
def payoff_of(grade_key: str | None):
|
|
1150
1151
|
return next((l.payoff for l in d.lanes if l.grade_key == grade_key), None)
|
|
1151
1152
|
|
|
1152
1153
|
gem = next((l for l in d.lanes if l.grade_key.startswith(f"{d.grading_company}-")), None)
|
|
1153
|
-
if d.
|
|
1154
|
+
if d.break_even == BreakEvenRegion.not_enough_data:
|
|
1154
1155
|
return ("[slab.dim]No graded market to read yet — nobody has sold graded copies of this "
|
|
1155
1156
|
"card recently. Not a no; an honest shrug.[/]")
|
|
1156
|
-
if d.
|
|
1157
|
-
top = f"even a {gem.grade_key}
|
|
1158
|
-
return f"[slab.loss]
|
|
1157
|
+
if d.break_even == BreakEvenRegion.pays_at_no_grade:
|
|
1158
|
+
top = f"even a {gem.grade_key} returns {signed_money(gem.payoff)}" if gem and gem.payoff is not None else "no grade covers the fee"
|
|
1159
|
+
return (f"[slab.loss]No grade covers the fee[/] — at today's prices {top} "
|
|
1160
|
+
f"after the {money(d.fee)} fee.")
|
|
1159
1161
|
miss_pay = payoff_of(d.miss_grade)
|
|
1160
|
-
if d.
|
|
1162
|
+
if d.break_even == BreakEvenRegion.pays_at_any_grade:
|
|
1161
1163
|
miss_txt = (f"even a {d.miss_grade} nets {signed_money(miss_pay)}"
|
|
1162
|
-
if miss_pay is not None else "even a realistic miss
|
|
1163
|
-
return f"[slab.gain]
|
|
1164
|
+
if miss_pay is not None else "even a realistic miss nets more than staying raw")
|
|
1165
|
+
return (f"[slab.gain]Every grade pays[/] — at today's prices {miss_txt} "
|
|
1166
|
+
f"after the {money(d.fee)} fee.")
|
|
1164
1167
|
pct = f"{d.ten_confidence_needed:.0%}"
|
|
1165
1168
|
gem_txt = f"a {gem.grade_key} nets {signed_money(gem.payoff)}" if gem else "a gem pays"
|
|
1166
1169
|
miss_txt = (f"a {d.miss_grade} comes back {signed_money(miss_pay)}"
|
|
1167
1170
|
if miss_pay is not None else f"a miss holds roughly raw value")
|
|
1168
|
-
return (f"[slab.foil]
|
|
1169
|
-
f"{
|
|
1171
|
+
return (f"[slab.foil]Depends on the grade[/] — {gem_txt}, {miss_txt}. Break-even sits at "
|
|
1172
|
+
f"{pct}: below that chance of a gem, grading loses; above it, grading wins. "
|
|
1173
|
+
f"You can see the card; that read is yours.")
|
|
1170
1174
|
|
|
1171
1175
|
|
|
1172
1176
|
def render_grading_desk(d: GradingDesk) -> None:
|
|
1173
|
-
"""One card's grading
|
|
1174
|
-
|
|
1177
|
+
"""One card's grading math: raw baseline, payoff per graded lane, and where break-even lands.
|
|
1178
|
+
Data to decide with, never a recommendation. Every sentence traces to a `grading.*` glossary
|
|
1179
|
+
entry."""
|
|
1175
1180
|
subjects = ", ".join(d.subjects) or "—"
|
|
1176
1181
|
slot = f"{d.subset or '—'} · {d.finish}" if d.finish else (d.subset or "base")
|
|
1177
1182
|
console.print()
|
|
@@ -1207,43 +1212,43 @@ def render_grading_desk(d: GradingDesk) -> None:
|
|
|
1207
1212
|
"not a market.[/]")
|
|
1208
1213
|
|
|
1209
1214
|
console.print()
|
|
1210
|
-
console.print(f" {
|
|
1211
|
-
if d.
|
|
1215
|
+
console.print(f" {_desk_reading_line(d)}")
|
|
1216
|
+
if d.break_even in (BreakEvenRegion.pays_at_any_grade, BreakEvenRegion.depends_on_grade) \
|
|
1212
1217
|
and d.ten_confidence_needed_bad_day is not None and d.bad_day_grade:
|
|
1213
1218
|
console.print(
|
|
1214
1219
|
f" [slab.dim]Cautious read: if misses come back {d.bad_day_grade} instead, "
|
|
1215
|
-
f"
|
|
1220
|
+
f"break-even moves to {d.ten_confidence_needed_bad_day:.0%}.[/]"
|
|
1216
1221
|
)
|
|
1217
1222
|
if d.thin_data:
|
|
1218
|
-
console.print(" [slab.warn]Thin data[/][slab.dim] — part of this
|
|
1223
|
+
console.print(" [slab.warn]Thin data[/][slab.dim] — part of this reading rests on "
|
|
1219
1224
|
"1-2 sales. Treat it as a hint, not a market reading.[/]")
|
|
1220
1225
|
console.print()
|
|
1221
1226
|
|
|
1222
1227
|
|
|
1223
1228
|
def render_collection_grading_desk(sweep: CollectionGradingDesk) -> None:
|
|
1224
|
-
"""The batch view: raw copies
|
|
1225
|
-
odds — graded as a group, wins at these bars pay for the losses (the rip-EV
|
|
1229
|
+
"""The batch view: raw copies with their grading math, biggest upside first. Here the per-card
|
|
1230
|
+
bars become real odds — graded as a group, wins at these bars pay for the losses (the rip-EV
|
|
1231
|
+
logic). Every cell is data; which cards to send, if any, stays the user's call."""
|
|
1226
1232
|
console.print()
|
|
1227
1233
|
covered = f"{sweep.deskable} of your {sweep.total_raw_copies} raw copies have a graded market to read"
|
|
1228
1234
|
console.print(heading("Grading Desk", covered))
|
|
1229
1235
|
if not sweep.entries:
|
|
1230
1236
|
console.print("\n [slab.dim]None of your raw cards have graded sales to compare "
|
|
1231
|
-
"against yet — no
|
|
1237
|
+
"against yet — no market to read. That's the honest answer, not an "
|
|
1232
1238
|
"error.[/]\n")
|
|
1233
1239
|
return
|
|
1234
1240
|
|
|
1235
1241
|
table = slab_table([
|
|
1236
|
-
("Verdict", {}),
|
|
1237
1242
|
("Card", {"no_wrap": False}),
|
|
1238
1243
|
("Raw", {"justify": "right", "style": "slab.dim"}),
|
|
1239
1244
|
("Gem pays", {"justify": "right"}),
|
|
1240
|
-
("
|
|
1245
|
+
("Miss pays", {"justify": "right"}),
|
|
1246
|
+
("Break-even", {"justify": "right", "style": "slab.foil"}),
|
|
1241
1247
|
("Qty", {"justify": "right", "style": "slab.dim"}),
|
|
1242
1248
|
])
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
GradingVerdict.easy_no: "[slab.loss]keep raw[/]",
|
|
1249
|
+
BREAK_EVEN_TXT = {
|
|
1250
|
+
BreakEvenRegion.pays_at_any_grade: "[slab.gain]any grade[/]",
|
|
1251
|
+
BreakEvenRegion.pays_at_no_grade: "[slab.loss]no grade[/]",
|
|
1247
1252
|
}
|
|
1248
1253
|
for e in sweep.entries:
|
|
1249
1254
|
d = e.desk
|
|
@@ -1252,23 +1257,25 @@ def render_collection_grading_desk(sweep: CollectionGradingDesk) -> None:
|
|
|
1252
1257
|
gem = next((l for l in d.lanes
|
|
1253
1258
|
if l.grade_key.startswith(f"{d.grading_company}-") and l.payoff is not None),
|
|
1254
1259
|
None)
|
|
1255
|
-
|
|
1256
|
-
if d.
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
+
miss = next((l for l in d.lanes if l.grade_key == d.miss_grade), None)
|
|
1261
|
+
if d.break_even == BreakEvenRegion.depends_on_grade and d.ten_confidence_needed is not None:
|
|
1262
|
+
be = f"{d.ten_confidence_needed:.0%} gem"
|
|
1263
|
+
else:
|
|
1264
|
+
be = BREAK_EVEN_TXT.get(d.break_even, d.break_even.value)
|
|
1260
1265
|
thin = " [slab.warn]!" if d.thin_data else ""
|
|
1261
1266
|
table.add_row(
|
|
1262
|
-
|
|
1267
|
+
card,
|
|
1263
1268
|
money(d.raw.fair_market_value) if d.raw else "—",
|
|
1264
1269
|
signed_money(gem.payoff) if gem else "[slab.dim]—[/]",
|
|
1265
|
-
|
|
1270
|
+
signed_money(miss.payoff) if miss and miss.payoff is not None else "[slab.dim]—[/]",
|
|
1271
|
+
be + thin, str(e.quantity),
|
|
1266
1272
|
)
|
|
1267
1273
|
console.print(table)
|
|
1268
1274
|
console.print(f" [slab.dim]{sweep.fee_note}[/]")
|
|
1269
|
-
console.print(" [slab.dim]
|
|
1270
|
-
"
|
|
1271
|
-
"
|
|
1275
|
+
console.print(" [slab.dim]Break-even = the chance of a gem at which grading and staying raw "
|
|
1276
|
+
"pay the same — below it grading loses, above it grading wins; 'any grade' = "
|
|
1277
|
+
"even a miss pays, 'no grade' = even a gem doesn't. You can see each card's "
|
|
1278
|
+
"corners; slab can't — the call is yours. ! = rests on very few sales.[/]")
|
|
1272
1279
|
console.print()
|
|
1273
1280
|
|
|
1274
1281
|
|
|
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
|
|
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
|