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.
- {slab_cli-0.22.0 → slab_cli-0.24.0}/PKG-INFO +2 -2
- {slab_cli-0.22.0 → slab_cli-0.24.0}/pyproject.toml +2 -2
- {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/client.py +7 -0
- {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/commands/pricing.py +12 -0
- {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/commands/registry.py +1 -0
- {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/display.py +46 -0
- {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/prompts.py +17 -1
- {slab_cli-0.22.0 → slab_cli-0.24.0}/README.md +0 -0
- {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/__init__.py +0 -0
- {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/banner.py +0 -0
- {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/browse.py +0 -0
- {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/commands/__init__.py +0 -0
- {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/commands/breaks.py +0 -0
- {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/commands/catalog.py +0 -0
- {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/commands/collection.py +0 -0
- {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/commands/custom_sets.py +0 -0
- {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/commands/export.py +0 -0
- {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/commands/lots.py +0 -0
- {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/commands/setup.py +0 -0
- {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/commands/update.py +0 -0
- {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/config.py +0 -0
- {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/context.py +0 -0
- {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/flags.py +0 -0
- {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/help.py +0 -0
- {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/main.py +0 -0
- {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/nav.py +0 -0
- {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/paging.py +0 -0
- {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/picker.py +0 -0
- {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/platforms/__init__.py +0 -0
- {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/platforms/base.py +0 -0
- {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/platforms/posix.py +0 -0
- {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/platforms/windows.py +0 -0
- {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/sources.py +0 -0
- {slab_cli-0.22.0 → slab_cli-0.24.0}/src/slab_cli/theme.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|