slab-cli 0.22.0__tar.gz → 0.24.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 (35) hide show
  1. {slab_cli-0.22.0 → slab_cli-0.24.0}/PKG-INFO +2 -2
  2. {slab_cli-0.22.0 → slab_cli-0.24.0}/pyproject.toml +2 -2
  3. {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/client.py +7 -0
  4. {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/commands/pricing.py +12 -0
  5. {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/commands/registry.py +1 -0
  6. {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/display.py +46 -0
  7. {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/prompts.py +17 -1
  8. {slab_cli-0.22.0 → slab_cli-0.24.0}/README.md +0 -0
  9. {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/__init__.py +0 -0
  10. {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/banner.py +0 -0
  11. {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/browse.py +0 -0
  12. {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/commands/__init__.py +0 -0
  13. {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/commands/breaks.py +0 -0
  14. {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/commands/catalog.py +0 -0
  15. {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/commands/collection.py +0 -0
  16. {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/commands/custom_sets.py +0 -0
  17. {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/commands/export.py +0 -0
  18. {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/commands/lots.py +0 -0
  19. {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/commands/setup.py +0 -0
  20. {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/commands/update.py +0 -0
  21. {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/config.py +0 -0
  22. {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/context.py +0 -0
  23. {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/flags.py +0 -0
  24. {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/help.py +0 -0
  25. {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/main.py +0 -0
  26. {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/nav.py +0 -0
  27. {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/paging.py +0 -0
  28. {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/picker.py +0 -0
  29. {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/platforms/__init__.py +0 -0
  30. {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/platforms/base.py +0 -0
  31. {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/platforms/posix.py +0 -0
  32. {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/platforms/windows.py +0 -0
  33. {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/sources.py +0 -0
  34. {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/theme.py +0 -0
  35. {slab_cli-0.22.0 → slab_cli-0.24.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.22.0
3
+ Version: 0.24.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.35.0
6
+ Requires-Dist: slab-schemas>=0.41.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.22.0"
3
+ version = "0.24.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"}]
@@ -12,7 +12,7 @@ dependencies = [
12
12
  # 0.34 reframes the grading desk from verdicts to break-even readings (BreakEvenRegion,
13
13
  # GradingDesk.break_even) — the desk shows data, never tells the user to grade.
14
14
  # 0.35 adds Liquidity/LiquidityLabel, which the market view's Sells column renders.
15
- "slab-schemas>=0.35.0",
15
+ "slab-schemas>=0.41.0", # CardSerials (slab card serials)
16
16
  "httpx>=0.27",
17
17
  "rich>=13.0",
18
18
  "InquirerPy>=0.3",
@@ -17,6 +17,7 @@ from slab_schemas.cards import (
17
17
  SetSearchResult,
18
18
  )
19
19
  from slab_schemas.community import CommunityBoard
20
+ from slab_schemas.serials import CardSerials
20
21
  from slab_schemas.collection import (
21
22
  BreakCreate,
22
23
  BreakOut,
@@ -151,6 +152,12 @@ class SlabClient:
151
152
  )
152
153
  return CollectionGradingDesk.model_validate(resp.json())
153
154
 
155
+ def get_card_serials(self, card_uuid: str) -> CardSerials:
156
+ """Which numbered copies of this printing have surfaced — sales with a stated serial and
157
+ collection copies — grouped by serial (GET /cards/{uuid}/serials)."""
158
+ resp = self._request("GET", f"/cards/{card_uuid}/serials")
159
+ return CardSerials.model_validate(resp.json())
160
+
154
161
  def get_card_price_history(
155
162
  self,
156
163
  card_uuid: str,
@@ -17,6 +17,7 @@ from ..display import (
17
17
  render_portfolio,
18
18
  render_price_history,
19
19
  render_sealed_price_history,
20
+ render_serials,
20
21
  render_set_market,
21
22
  )
22
23
  from ..paging import fetch_all_pages
@@ -71,6 +72,17 @@ def cmd_comps(args: list[str]) -> None:
71
72
  render_comps(ctx.client.get_card_comps(card.uuid))
72
73
 
73
74
 
75
+ def cmd_serials(args: list[str]) -> None:
76
+ """Which numbered copies of a card have surfaced. Same player → set → card flow as `price`,
77
+ then one row per serial seen: how many collections hold it, how many times it has sold, and
78
+ its last sale. Sales and held copies are shown apart — the collector who bought #24 on eBay
79
+ is one transfer seen from both sides. Unnumbered printings say so and stop."""
80
+ card = _pick_card(args)
81
+ if card is None:
82
+ return
83
+ render_serials(ctx.client.get_card_serials(card.uuid))
84
+
85
+
74
86
  def _split_desk_flags(args: list[str]) -> tuple[list[str], float | None, str | None]:
75
87
  """Pull `--fee <amount>` and `--company <name>` out of a grading query. Hand-rolled (the
76
88
  shared flag grammar is search-specific); an unparseable fee errors loudly rather than
@@ -61,6 +61,7 @@ GROUPS: list[Group] = [
61
61
  Command("price", "Market price + 7d/30d trend", pricing.cmd_price, "<query>"),
62
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
+ Command("serials", "Which numbered copies have surfaced (sales + collections)", pricing.cmd_serials, "<query>"),
64
65
  Command("history", "Price history over time", pricing.cmd_history, "<query>"),
65
66
  ]),
66
67
  Group("set", "browse and price sealed products", [
@@ -24,6 +24,7 @@ from slab_schemas.enums import BreakEvenRegion, Grade, LiquidityLabel, SealedFor
24
24
  from slab_schemas.liquidity import Liquidity
25
25
  from slab_schemas.pricing import CardComps, CardMarket, PortfolioSummary, SetTopCards
26
26
  from slab_schemas.sealed import SealedPriceHistory, SealedProductOut
27
+ from slab_schemas.serials import CardSerials
27
28
  from slab_schemas.timeseries import CardPriceHistory
28
29
 
29
30
  from .browse import Page, browse
@@ -1254,6 +1255,51 @@ def render_set_market(s: SetOut, sealed: list[SealedProductOut], top: SetTopCard
1254
1255
  console.print(" [slab.dim]Drill in with `slab card price` / `slab card comps`.[/]\n")
1255
1256
 
1256
1257
 
1258
+ def render_serials(s: CardSerials) -> None:
1259
+ """Which numbered copies of a printing have surfaced — one row per serial, sales and held
1260
+ copies kept apart (the same transfer can be seen from both sides). The meaning text comes
1261
+ from the response's glossary, so the CLI, the site and the API say the same words."""
1262
+ subjects = ", ".join(s.subjects) or "—"
1263
+ console.print()
1264
+ console.print(_card_headline(s.card_number, subjects, s.set_name, _slot(s)))
1265
+
1266
+ if not s.print_run:
1267
+ console.print("\n [slab.dim]This printing isn't serial-numbered, so there are no serials to track.[/]\n")
1268
+ return
1269
+
1270
+ unseen = max(0, s.print_run - s.serials_seen)
1271
+ pct = f" ({s.coverage_pct:.0f}%)" if s.coverage_pct is not None else ""
1272
+ console.print(heading("Serials Seen", f"{s.serials_seen} of /{s.print_run}{pct} · {unseen} never surfaced"))
1273
+ if not s.serials:
1274
+ console.print(" [slab.dim]No numbered copy of this card has surfaced in a sale or a collection yet.[/]\n")
1275
+ return
1276
+
1277
+ table = slab_table([
1278
+ ("Serial", {"justify": "right", "style": "slab.foil"}),
1279
+ ("Held", {"justify": "right"}),
1280
+ ("Sales", {"justify": "right"}),
1281
+ ("Last sale", {"justify": "right", "style": "slab.foil"}),
1282
+ ("Grade", {"style": "slab.dim"}),
1283
+ ("Last seen", {"style": "slab.dim"}),
1284
+ ])
1285
+ for sig in s.serials:
1286
+ last = sig.sales[-1] if sig.sales else None
1287
+ table.add_row(
1288
+ f"{sig.serial_number}/{s.print_run}",
1289
+ str(sig.held) if sig.held else "[slab.dim]—[/]",
1290
+ str(len(sig.sales)) if sig.sales else "[slab.dim]—[/]",
1291
+ money(last.sale_price) if last and last.sale_price is not None else "—",
1292
+ (last.grade_key or "—") if last else "—",
1293
+ sig.last_seen.isoformat() if sig.last_seen else "—",
1294
+ )
1295
+ console.print(table)
1296
+ for key in ("serials.coverage", "serials.held"):
1297
+ info = s.glossary.get(key)
1298
+ if info:
1299
+ console.print(f" [slab.dim]{info.label}: {info.summary}[/]")
1300
+ console.print()
1301
+
1302
+
1257
1303
  def render_comps(c: CardComps) -> None:
1258
1304
  """Display recent comps (raw sales) for a catalog card — the evidence behind its FMV."""
1259
1305
  subjects = ", ".join(c.subjects) or "—"
@@ -184,6 +184,20 @@ def select_printing(cards: list[CardOut], subject: str, show_set: bool = False)
184
184
  ).execute()
185
185
 
186
186
 
187
+ def _released_at(s: SetOut) -> date:
188
+ """When a set hit shelves, for newest-first ordering.
189
+
190
+ `year` is the SEASON (2025 for a 2025-26 product) and a season's products ship across fourteen
191
+ months, so ranking by it ties a whole season together and puts a 2025-26 product that shipped
192
+ in July above a 2024-25 one that shipped in October. Sets whose overview hasn't been ingested
193
+ carry no release date, so those fall back to Jan 1 of the season's second calendar year — the
194
+ middle of that season's window — which keeps them among their own season. Same rule as the
195
+ API's `-release` sort."""
196
+ if s.release_date:
197
+ return s.release_date
198
+ return date(s.year + 1, 1, 1) if s.year else date.min
199
+
200
+
187
201
  def select_set(sets: list[SetOut], initial: str | None = None) -> SetOut | None:
188
202
  """Pick one set from the catalog — fuzzy type-to-filter, pre-seeded with the user's query so
189
203
  the closest names surface immediately (backspace to widen). Auto-selects a lone match;
@@ -192,7 +206,9 @@ def select_set(sets: list[SetOut], initial: str | None = None) -> SetOut | None:
192
206
  return None
193
207
  if len(sets) == 1:
194
208
  return sets[0]
195
- ranked = sorted(sets, key=lambda s: (-(s.year or 0), s.name or "")) # newest first
209
+ # Newest release first, name A-Z within a day — negate the ordinal rather than reversing the
210
+ # whole sort, which would flip the names too.
211
+ ranked = sorted(sets, key=lambda s: (-_released_at(s).toordinal(), s.name or ""))
196
212
  choices = [Choice(value=s, name=_set_label(s)) for s in ranked]
197
213
  return inquirer.fuzzy(
198
214
  message=f"Which set? ({len(sets)} in catalog — type to filter):",
File without changes
File without changes