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.
Files changed (34) hide show
  1. {slab_cli-0.18.0 → slab_cli-0.19.0}/PKG-INFO +2 -2
  2. {slab_cli-0.18.0 → slab_cli-0.19.0}/pyproject.toml +4 -2
  3. {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/commands/pricing.py +5 -5
  4. {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/commands/registry.py +2 -2
  5. {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/display.py +46 -39
  6. {slab_cli-0.18.0 → slab_cli-0.19.0}/README.md +0 -0
  7. {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/__init__.py +0 -0
  8. {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/banner.py +0 -0
  9. {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/browse.py +0 -0
  10. {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/client.py +0 -0
  11. {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/commands/__init__.py +0 -0
  12. {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/commands/breaks.py +0 -0
  13. {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/commands/catalog.py +0 -0
  14. {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/commands/collection.py +0 -0
  15. {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/commands/custom_sets.py +0 -0
  16. {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/commands/export.py +0 -0
  17. {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/commands/lots.py +0 -0
  18. {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/commands/setup.py +0 -0
  19. {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/commands/update.py +0 -0
  20. {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/config.py +0 -0
  21. {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/context.py +0 -0
  22. {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/flags.py +0 -0
  23. {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/help.py +0 -0
  24. {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/main.py +0 -0
  25. {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/paging.py +0 -0
  26. {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/picker.py +0 -0
  27. {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/platforms/__init__.py +0 -0
  28. {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/platforms/base.py +0 -0
  29. {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/platforms/posix.py +0 -0
  30. {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/platforms/windows.py +0 -0
  31. {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/prompts.py +0 -0
  32. {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/sources.py +0 -0
  33. {slab_cli-0.18.0 → slab_cli-0.19.0}/src/slab_cli/theme.py +0 -0
  34. {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.18.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.32.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.18.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
- "slab-schemas>=0.32.0",
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
- """Should I grade this card? Same player → set → card flow as `price`, then the grading desk:
109
- what each grade nets after the fee, and how sure you'd need to be it gems before grading
110
- beats keeping it raw. `--fee 19` prices at your rate; `--company SGC` switches grader."""
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
- """Which of your raw cards are worth grading? Ranks every ungraded copy best-case first —
120
- where the per-card bars become batch odds. `--fee 19` prices at your rate."""
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", "Should I grade it? Payoffs, the bar, a verdict", pricing.cmd_grade, "<query> [--fee N]"),
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", "Which of your raw cards are worth grading", pricing.cmd_grade_sweep, "[--fee N]"),
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 Grade, GradingVerdict, SealedFormat
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 _desk_verdict_line(d: GradingDesk) -> str:
1146
- """The verdict as one plain sentence — the confidence-BAR framing (you grade a card once, so
1147
- the number is how good YOUR copy has to look, never "how often"). Wording mirrors the
1148
- `grading.verdict` glossary entry."""
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.verdict == GradingVerdict.not_enough_data:
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.verdict == GradingVerdict.easy_no:
1157
- top = f"even a {gem.grade_key} only returns {signed_money(gem.payoff)}" if gem and gem.payoff is not None else "no grade covers the fee"
1158
- return f"[slab.loss]Keep it raw[/] — {top} after the {money(d.fee)} fee."
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.verdict == GradingVerdict.easy_yes:
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 beats staying raw")
1163
- return f"[slab.gain]Send it[/] — {miss_txt} after the {money(d.fee)} fee."
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]Your call[/] — grade it only if you're more than {pct} sure it's a gem: "
1169
- f"{gem_txt}, {miss_txt}.")
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 decision: raw baseline, payoff per graded lane, and the verdict in the
1174
- bar framing. Every sentence traces to a `grading.*` glossary entry."""
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" {_desk_verdict_line(d)}")
1211
- if d.verdict in (GradingVerdict.easy_yes, GradingVerdict.judgment_call) \
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"the bar is {d.ten_confidence_needed_bad_day:.0%}.[/]"
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 verdict rests on "
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 ranked best-case first. Here the per-card bars become real
1225
- odds — graded as a group, wins at these bars pay for the losses (the rip-EV logic)."""
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 verdicts to give. That's the honest answer, not an "
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
- ("Bar", {"justify": "right", "style": "slab.foil"}),
1245
+ ("Miss pays", {"justify": "right"}),
1246
+ ("Break-even", {"justify": "right", "style": "slab.foil"}),
1241
1247
  ("Qty", {"justify": "right", "style": "slab.dim"}),
1242
1248
  ])
1243
- VERDICT_TXT = {
1244
- GradingVerdict.easy_yes: "[slab.gain]send it[/]",
1245
- GradingVerdict.judgment_call: "[slab.foil]your call[/]",
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
- bar = "—"
1256
- if d.verdict == GradingVerdict.judgment_call and d.ten_confidence_needed is not None:
1257
- bar = f"{d.ten_confidence_needed:.0%}"
1258
- elif d.verdict == GradingVerdict.easy_yes:
1259
- bar = "any"
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
- VERDICT_TXT.get(d.verdict, d.verdict.value) + thin, card,
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
- bar, str(e.quantity),
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]Bar = how sure you'd need to be the card gems before grading "
1270
- "beats keeping it raw. You can see its corners; slab can't. "
1271
- "! = verdict rests on very few sales.[/]")
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