slab-cli 0.9.0__tar.gz → 0.11.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.9.0 → slab_cli-0.11.0}/PKG-INFO +1 -1
- {slab_cli-0.9.0 → slab_cli-0.11.0}/pyproject.toml +1 -1
- {slab_cli-0.9.0 → slab_cli-0.11.0}/src/slab_cli/client.py +9 -2
- {slab_cli-0.9.0 → slab_cli-0.11.0}/src/slab_cli/commands/breaks.py +8 -3
- {slab_cli-0.9.0 → slab_cli-0.11.0}/src/slab_cli/commands/collection.py +13 -0
- {slab_cli-0.9.0 → slab_cli-0.11.0}/src/slab_cli/commands/custom_sets.py +44 -9
- {slab_cli-0.9.0 → slab_cli-0.11.0}/src/slab_cli/display.py +23 -6
- {slab_cli-0.9.0 → slab_cli-0.11.0}/src/slab_cli/prompts.py +24 -2
- {slab_cli-0.9.0 → slab_cli-0.11.0}/README.md +0 -0
- {slab_cli-0.9.0 → slab_cli-0.11.0}/src/slab_cli/__init__.py +0 -0
- {slab_cli-0.9.0 → slab_cli-0.11.0}/src/slab_cli/banner.py +0 -0
- {slab_cli-0.9.0 → slab_cli-0.11.0}/src/slab_cli/browse.py +0 -0
- {slab_cli-0.9.0 → slab_cli-0.11.0}/src/slab_cli/commands/__init__.py +0 -0
- {slab_cli-0.9.0 → slab_cli-0.11.0}/src/slab_cli/commands/catalog.py +0 -0
- {slab_cli-0.9.0 → slab_cli-0.11.0}/src/slab_cli/commands/export.py +0 -0
- {slab_cli-0.9.0 → slab_cli-0.11.0}/src/slab_cli/commands/lots.py +0 -0
- {slab_cli-0.9.0 → slab_cli-0.11.0}/src/slab_cli/commands/pricing.py +0 -0
- {slab_cli-0.9.0 → slab_cli-0.11.0}/src/slab_cli/commands/registry.py +0 -0
- {slab_cli-0.9.0 → slab_cli-0.11.0}/src/slab_cli/commands/setup.py +0 -0
- {slab_cli-0.9.0 → slab_cli-0.11.0}/src/slab_cli/commands/update.py +0 -0
- {slab_cli-0.9.0 → slab_cli-0.11.0}/src/slab_cli/config.py +0 -0
- {slab_cli-0.9.0 → slab_cli-0.11.0}/src/slab_cli/context.py +0 -0
- {slab_cli-0.9.0 → slab_cli-0.11.0}/src/slab_cli/flags.py +0 -0
- {slab_cli-0.9.0 → slab_cli-0.11.0}/src/slab_cli/help.py +0 -0
- {slab_cli-0.9.0 → slab_cli-0.11.0}/src/slab_cli/main.py +0 -0
- {slab_cli-0.9.0 → slab_cli-0.11.0}/src/slab_cli/paging.py +0 -0
- {slab_cli-0.9.0 → slab_cli-0.11.0}/src/slab_cli/picker.py +0 -0
- {slab_cli-0.9.0 → slab_cli-0.11.0}/src/slab_cli/platforms/__init__.py +0 -0
- {slab_cli-0.9.0 → slab_cli-0.11.0}/src/slab_cli/platforms/base.py +0 -0
- {slab_cli-0.9.0 → slab_cli-0.11.0}/src/slab_cli/platforms/posix.py +0 -0
- {slab_cli-0.9.0 → slab_cli-0.11.0}/src/slab_cli/platforms/windows.py +0 -0
- {slab_cli-0.9.0 → slab_cli-0.11.0}/src/slab_cli/sources.py +0 -0
- {slab_cli-0.9.0 → slab_cli-0.11.0}/src/slab_cli/theme.py +0 -0
- {slab_cli-0.9.0 → slab_cli-0.11.0}/src/slab_cli/updates.py +0 -0
|
@@ -336,7 +336,14 @@ class SlabClient:
|
|
|
336
336
|
resp = self._request("POST", "/custom-sets/search", json=body)
|
|
337
337
|
return CustomSetSearchResult.model_validate(resp.json())
|
|
338
338
|
|
|
339
|
-
def get_custom_set(
|
|
340
|
-
|
|
339
|
+
def get_custom_set(
|
|
340
|
+
self, set_uuid: str, collector_uuid: str | None = None,
|
|
341
|
+
limit: int | None = None, offset: int | None = None,
|
|
342
|
+
) -> CustomSetDetail:
|
|
343
|
+
params: dict = {"collector_uuid": collector_uuid} if collector_uuid else {}
|
|
344
|
+
if limit is not None:
|
|
345
|
+
params["limit"] = limit
|
|
346
|
+
if offset is not None:
|
|
347
|
+
params["offset"] = offset
|
|
341
348
|
resp = self._request("GET", f"/custom-sets/{set_uuid}", params=params)
|
|
342
349
|
return CustomSetDetail.model_validate(resp.json())
|
|
@@ -6,7 +6,6 @@ from __future__ import annotations
|
|
|
6
6
|
|
|
7
7
|
from slab_schemas.cards import SetSearchQuery
|
|
8
8
|
from slab_schemas.collection import BreakCreate, BreakOut, BreakSearchQuery, CollectionSearchQuery
|
|
9
|
-
from slab_schemas.enums import BreakType
|
|
10
9
|
|
|
11
10
|
from ..client import SlabClient
|
|
12
11
|
from ..context import ctx
|
|
@@ -59,7 +58,13 @@ def create_break_interactive(client: SlabClient, collector: str) -> BreakOut | N
|
|
|
59
58
|
if set_choice is None:
|
|
60
59
|
return None
|
|
61
60
|
|
|
62
|
-
|
|
61
|
+
# Offer the formats this set actually comes in (its sealed SKUs); a set with no SKUs — or a
|
|
62
|
+
# failed lookup — falls back to the generic picker. Never let the extra lookup kill the flow.
|
|
63
|
+
try:
|
|
64
|
+
formats = [p.format.value for p in client.get_set_sealed(set_choice.uuid)]
|
|
65
|
+
except Exception:
|
|
66
|
+
formats = []
|
|
67
|
+
break_type = select_break_type(formats)
|
|
63
68
|
break_date = ask_date("When did you open it?")
|
|
64
69
|
total_cost = ask_decimal("Total cost:")
|
|
65
70
|
# Offer the collector's previously-used sources for arrow-key reuse (falls back to a plain
|
|
@@ -70,7 +75,7 @@ def create_break_interactive(client: SlabClient, collector: str) -> BreakOut | N
|
|
|
70
75
|
collector,
|
|
71
76
|
BreakCreate(
|
|
72
77
|
set_uuid=set_choice.uuid,
|
|
73
|
-
break_type=
|
|
78
|
+
break_type=break_type,
|
|
74
79
|
break_date=break_date,
|
|
75
80
|
total_cost=total_cost,
|
|
76
81
|
source=source,
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import re
|
|
6
6
|
from dataclasses import dataclass, field
|
|
7
|
+
from datetime import date
|
|
7
8
|
from decimal import Decimal
|
|
8
9
|
|
|
9
10
|
from slab_schemas.cards import CardOut, CardSearchQuery
|
|
@@ -68,6 +69,7 @@ class AddSession:
|
|
|
68
69
|
lot_uuid: str | None = None # the purchase this batch belongs to; its cost splits across cards
|
|
69
70
|
release_set_uuid: str | None = None # the break's product — scopes every card search to that box
|
|
70
71
|
acquired_source: str | None = None # where these came from (purchases/trades/gifts), set once
|
|
72
|
+
acquired_date: date | None = None # when you got them; None for break/lot cards (theirs wins)
|
|
71
73
|
cards_added: int = 0
|
|
72
74
|
|
|
73
75
|
|
|
@@ -119,6 +121,14 @@ def _ensure_acquisition_context(session: AddSession) -> None:
|
|
|
119
121
|
# Trade / gift / other → ask where these came from, once for the batch.
|
|
120
122
|
session.acquired_source = pick_source(session.client, session.collector)
|
|
121
123
|
|
|
124
|
+
# When you got them — asked once, and ONLY for standalone cards: a break/lot card takes its
|
|
125
|
+
# date from the container (which already asked), and storing a second copy of it would just be
|
|
126
|
+
# something else to keep in sync. Enter defaults to today, so the common "adding what I bought
|
|
127
|
+
# today" case is one keystroke — but an old pickup can be dated correctly, which matters
|
|
128
|
+
# because the portfolio chart only counts a card from the day you got it.
|
|
129
|
+
if session.break_uuid is None and session.lot_uuid is None:
|
|
130
|
+
session.acquired_date = ask_date("When did you get it?")
|
|
131
|
+
|
|
122
132
|
|
|
123
133
|
def _per_card_cost(session: AddSession) -> Decimal | None:
|
|
124
134
|
"""The cost that varies per card. Pack pulls derive cost from the break, and a purchase attached
|
|
@@ -164,6 +174,8 @@ def _show_review(card: CardOut, session: AddSession, acq_cost: Decimal | None,
|
|
|
164
174
|
console.print(f" Purchase: {session.lot_uuid[:8]} [dim](cost split across the lot)[/dim]")
|
|
165
175
|
if acq_cost is not None:
|
|
166
176
|
console.print(f" Cost: ${acq_cost:.2f}")
|
|
177
|
+
if session.acquired_date:
|
|
178
|
+
console.print(f" Date: {session.acquired_date}")
|
|
167
179
|
if session.acquired_source:
|
|
168
180
|
console.print(f" Source: {session.acquired_source}")
|
|
169
181
|
if self_grade:
|
|
@@ -197,6 +209,7 @@ def _submit_and_report(session: AddSession, card: CardOut, acq_cost: Decimal | N
|
|
|
197
209
|
lot_uuid=session.lot_uuid,
|
|
198
210
|
acquisition_type=session.acq_type,
|
|
199
211
|
acquisition_cost=acq_cost,
|
|
212
|
+
acquired_date=session.acquired_date,
|
|
200
213
|
acquired_source=session.acquired_source,
|
|
201
214
|
self_grade=self_grade,
|
|
202
215
|
grading_company=grading_company,
|
|
@@ -5,13 +5,16 @@ from __future__ import annotations
|
|
|
5
5
|
from InquirerPy import inquirer
|
|
6
6
|
from InquirerPy.base.control import Choice
|
|
7
7
|
|
|
8
|
+
from types import SimpleNamespace
|
|
9
|
+
|
|
8
10
|
from slab_schemas.custom_sets import (
|
|
9
11
|
CustomSetCardAdd,
|
|
10
12
|
CustomSetCreate,
|
|
13
|
+
CustomSetDetail,
|
|
11
14
|
CustomSetOut,
|
|
12
15
|
CustomSetSearchQuery,
|
|
13
16
|
)
|
|
14
|
-
from slab_schemas.enums import CustomSetType, MatchMode, Visibility
|
|
17
|
+
from slab_schemas.enums import CustomSetType, DynamicMatch, MatchMode, Visibility
|
|
15
18
|
|
|
16
19
|
from ..client import SlabClient
|
|
17
20
|
from ..context import ctx
|
|
@@ -21,6 +24,7 @@ from ..display import (
|
|
|
21
24
|
format_card_summary,
|
|
22
25
|
print_custom_set_detail,
|
|
23
26
|
)
|
|
27
|
+
from ..paging import fetch_all_pages
|
|
24
28
|
from ..picker import search_and_pick_subject
|
|
25
29
|
from ..prompts import (
|
|
26
30
|
ask_int_optional,
|
|
@@ -28,6 +32,7 @@ from ..prompts import (
|
|
|
28
32
|
ask_text_optional,
|
|
29
33
|
confirm,
|
|
30
34
|
select_custom_set_type,
|
|
35
|
+
select_dynamic_match,
|
|
31
36
|
select_match_mode,
|
|
32
37
|
select_printing,
|
|
33
38
|
select_visibility,
|
|
@@ -38,6 +43,25 @@ from ..prompts import (
|
|
|
38
43
|
# Helpers
|
|
39
44
|
# ---------------------------------------------------------------------------
|
|
40
45
|
|
|
46
|
+
def _fetch_full_detail(
|
|
47
|
+
client: SlabClient, set_uuid: str, collector: str | None
|
|
48
|
+
) -> CustomSetDetail:
|
|
49
|
+
"""The set with its WHOLE card list — the detail endpoint pages `cards` (completion stats
|
|
50
|
+
already cover the full set), so flows that show or iterate every entry stitch the pages back
|
|
51
|
+
together here. Capped at 5000 entries as a runaway bound (a `year`-only dynamic filter can
|
|
52
|
+
match a whole catalog); past the cap, `card_count` > len(cards) and the display says so."""
|
|
53
|
+
first: list = []
|
|
54
|
+
|
|
55
|
+
def fetch(limit: int, offset: int):
|
|
56
|
+
d = client.get_custom_set(set_uuid, collector, limit=limit, offset=offset)
|
|
57
|
+
if not first:
|
|
58
|
+
first.append(d)
|
|
59
|
+
return SimpleNamespace(items=d.cards, total=d.card_count)
|
|
60
|
+
|
|
61
|
+
cards, _ = fetch_all_pages(fetch, page=500, cap=5000)
|
|
62
|
+
return first[0].model_copy(update={"cards": cards, "offset": 0})
|
|
63
|
+
|
|
64
|
+
|
|
41
65
|
def _select_custom_set(client: SlabClient, collector: str, message: str = "Select a set:") -> CustomSetOut | None:
|
|
42
66
|
"""Browse the collector's custom sets and pick one."""
|
|
43
67
|
sets = client.collector_custom_sets(collector)
|
|
@@ -98,6 +122,7 @@ def cmd_chase_create(args: list[str]) -> None:
|
|
|
98
122
|
visibility = select_visibility()
|
|
99
123
|
|
|
100
124
|
filter_json = None
|
|
125
|
+
dynamic_match = "any_printing"
|
|
101
126
|
if set_type == "dynamic":
|
|
102
127
|
console.print("\n[dim]A dynamic set is defined by a search filter. Enter the filter criteria.[/dim]")
|
|
103
128
|
console.print("[dim]Cards matching this filter will automatically be in the set.[/dim]\n")
|
|
@@ -106,6 +131,15 @@ def cmd_chase_create(args: list[str]) -> None:
|
|
|
106
131
|
if filter_json is None:
|
|
107
132
|
console.print("[dim]Cancelled.[/dim]")
|
|
108
133
|
return
|
|
134
|
+
# One set-wide granularity. Default follows the filter's intent: a filter that names a
|
|
135
|
+
# finish/parallel dimension reads as a per-printing chase; anything else (team, player,
|
|
136
|
+
# season) reads as "one of each card — base or any parallel counts".
|
|
137
|
+
chasing_printings = any(
|
|
138
|
+
k in filter_json for k in ("finish", "parallel_only", "is_numbered", "is_1of1")
|
|
139
|
+
)
|
|
140
|
+
dynamic_match = select_dynamic_match(
|
|
141
|
+
default="exact" if chasing_printings else "any_printing"
|
|
142
|
+
)
|
|
109
143
|
|
|
110
144
|
cs = ctx.client.create_custom_set(collector, CustomSetCreate(
|
|
111
145
|
name=name,
|
|
@@ -113,6 +147,7 @@ def cmd_chase_create(args: list[str]) -> None:
|
|
|
113
147
|
visibility=Visibility(visibility),
|
|
114
148
|
set_type=CustomSetType(set_type),
|
|
115
149
|
filter_json=filter_json,
|
|
150
|
+
dynamic_match=DynamicMatch(dynamic_match),
|
|
116
151
|
))
|
|
117
152
|
|
|
118
153
|
console.print(f"\n[green]Created![/green] {cs.name} [dim]({cs.uuid[:8]})[/dim]")
|
|
@@ -277,12 +312,12 @@ def cmd_chase_view(args: list[str]) -> None:
|
|
|
277
312
|
client = ctx.client
|
|
278
313
|
|
|
279
314
|
if args:
|
|
280
|
-
detail = client
|
|
315
|
+
detail = _fetch_full_detail(client, args[0], collector)
|
|
281
316
|
else:
|
|
282
317
|
cs = _select_custom_set(client, collector, message="Which set to view?")
|
|
283
318
|
if cs is None:
|
|
284
319
|
return
|
|
285
|
-
detail = client
|
|
320
|
+
detail = _fetch_full_detail(client, cs.uuid, collector)
|
|
286
321
|
|
|
287
322
|
print_custom_set_detail(detail)
|
|
288
323
|
|
|
@@ -311,8 +346,8 @@ def cmd_chase_edit(args: list[str]) -> None:
|
|
|
311
346
|
if cs is None:
|
|
312
347
|
return
|
|
313
348
|
|
|
314
|
-
# Check ownership
|
|
315
|
-
detail = client.get_custom_set(cs.uuid, collector)
|
|
349
|
+
# Check ownership — metadata only, skip the card list
|
|
350
|
+
detail = client.get_custom_set(cs.uuid, collector, limit=1)
|
|
316
351
|
if detail.creator_uuid != collector:
|
|
317
352
|
console.print("[yellow]You can only edit sets you created.[/yellow]")
|
|
318
353
|
return
|
|
@@ -350,7 +385,7 @@ def cmd_chase_edit(args: list[str]) -> None:
|
|
|
350
385
|
|
|
351
386
|
def _chase_detail(s) -> None:
|
|
352
387
|
"""The drill-in for a browsed chase set: its full card list + completion, owner-aware."""
|
|
353
|
-
print_custom_set_detail(ctx.client
|
|
388
|
+
print_custom_set_detail(_fetch_full_detail(ctx.client, s.uuid, ctx.collector))
|
|
354
389
|
|
|
355
390
|
|
|
356
391
|
def cmd_chase_list(args: list[str]) -> None:
|
|
@@ -436,7 +471,7 @@ def cmd_chase_remove(args: list[str]) -> None:
|
|
|
436
471
|
console.print("[yellow]Can only remove cards from curated sets.[/yellow]")
|
|
437
472
|
return
|
|
438
473
|
|
|
439
|
-
detail = client
|
|
474
|
+
detail = _fetch_full_detail(client, cs.uuid, collector)
|
|
440
475
|
if not detail.cards:
|
|
441
476
|
console.print("[dim]This set has no cards.[/dim]")
|
|
442
477
|
return
|
|
@@ -475,8 +510,8 @@ def cmd_chase_delete(args: list[str]) -> None:
|
|
|
475
510
|
if cs is None:
|
|
476
511
|
return
|
|
477
512
|
|
|
478
|
-
# Check ownership
|
|
479
|
-
detail = client.get_custom_set(cs.uuid, collector)
|
|
513
|
+
# Check ownership — metadata only, skip the card list
|
|
514
|
+
detail = client.get_custom_set(cs.uuid, collector, limit=1)
|
|
480
515
|
if detail.creator_uuid != collector:
|
|
481
516
|
console.print("[yellow]You can only delete sets you created.[/yellow]")
|
|
482
517
|
return
|
|
@@ -261,7 +261,7 @@ BREAK_COLUMNS = [
|
|
|
261
261
|
def break_cells(b) -> list[str]:
|
|
262
262
|
"""One break as its BREAK_COLUMNS cells."""
|
|
263
263
|
cpc = money(b.cost_per_card) if b.cost_per_card is not None else "—"
|
|
264
|
-
return [b.set_name or "", b.break_type, money(b.total_cost), str(b.copy_count), cpc,
|
|
264
|
+
return [b.set_name or "", _format_label(b.break_type), money(b.total_cost), str(b.copy_count), cpc,
|
|
265
265
|
str(b.break_date or "—"), b.source or "—", b.uuid[:8]]
|
|
266
266
|
|
|
267
267
|
|
|
@@ -314,7 +314,7 @@ def render_break_pulls(brk, result: CollectionResult) -> None:
|
|
|
314
314
|
console.print()
|
|
315
315
|
t = Text()
|
|
316
316
|
t.append(brk.set_name or "Break", style="slab.value")
|
|
317
|
-
t.append(f" — {brk.break_type} · {brk.break_date or '—'}"
|
|
317
|
+
t.append(f" — {brk.break_type.replace('_', ' ')} · {brk.break_date or '—'}"
|
|
318
318
|
+ (f" · {brk.source}" if brk.source else ""), style="slab.dim")
|
|
319
319
|
console.print(t)
|
|
320
320
|
|
|
@@ -530,10 +530,15 @@ def render_dashboard(d: DashboardStats) -> None:
|
|
|
530
530
|
if d.priced_coverage is not None:
|
|
531
531
|
rows.append(("Coverage", f"{pct_coverage(float(d.priced_coverage) * 100)} [slab.dim]priced[/]"))
|
|
532
532
|
# Portfolio value over time — the CLI stand-in for the portal's chart, from the same payload.
|
|
533
|
+
# Each point values only the copies owned that day, so the net includes cards you ADDED in
|
|
534
|
+
# the window, not just price movement — said plainly so it isn't read as a 90d gain.
|
|
533
535
|
if len(d.portfolio_series) >= 2:
|
|
534
536
|
vals = [p.portfolio_value for p in d.portfolio_series]
|
|
535
537
|
net = float(vals[-1]) - float(vals[0])
|
|
536
|
-
rows.append((
|
|
538
|
+
rows.append((
|
|
539
|
+
"Trend 90d",
|
|
540
|
+
f"{sparkline(vals)} {signed_money(net)} [slab.dim]incl. cards added[/]",
|
|
541
|
+
))
|
|
537
542
|
value = section("Value", kv_grid(rows), foil=True)
|
|
538
543
|
|
|
539
544
|
comp = _composition_panel(
|
|
@@ -555,7 +560,8 @@ def render_dashboard(d: DashboardStats) -> None:
|
|
|
555
560
|
("$/card", {"justify": "right", "style": "slab.dim"}),
|
|
556
561
|
])
|
|
557
562
|
for b in d.break_stats:
|
|
558
|
-
table.add_row(str(b.break_date or "—"), b.set_name or "—",
|
|
563
|
+
table.add_row(str(b.break_date or "—"), b.set_name or "—",
|
|
564
|
+
_format_label(b.break_type) if b.break_type else "—",
|
|
559
565
|
money(b.total_cost), str(b.cards), money(b.cost_per_card))
|
|
560
566
|
breaks = titled("Breaks", table)
|
|
561
567
|
|
|
@@ -804,7 +810,11 @@ def print_custom_set_detail(detail: CustomSetDetail) -> None:
|
|
|
804
810
|
vis = "Public" if detail.visibility == "public" else "Private"
|
|
805
811
|
sub = "subscribed" if detail.is_subscribed else "not subscribed"
|
|
806
812
|
|
|
807
|
-
|
|
813
|
+
kind = detail.set_type
|
|
814
|
+
if detail.dynamic_match:
|
|
815
|
+
grain = "any printing counts" if detail.dynamic_match == "any_printing" else "exact printings"
|
|
816
|
+
kind = f"{detail.set_type} ({grain})"
|
|
817
|
+
head = [f"[slab.dim]{kind} · {vis} · {sub}[/]"]
|
|
808
818
|
if detail.description:
|
|
809
819
|
head.append(detail.description)
|
|
810
820
|
head.append(f"[slab.label]Creator[/] {detail.creator_name or '—'} "
|
|
@@ -847,6 +857,13 @@ def print_custom_set_detail(detail: CustomSetDetail) -> None:
|
|
|
847
857
|
table.add_row(str(entry.position), label, match, owned)
|
|
848
858
|
|
|
849
859
|
console.print(table)
|
|
860
|
+
if detail.card_count > len(detail.cards):
|
|
861
|
+
first = detail.offset + 1
|
|
862
|
+
last = detail.offset + len(detail.cards)
|
|
863
|
+
console.print(
|
|
864
|
+
f" [slab.dim]Showing entries {first}–{last} of {detail.card_count} "
|
|
865
|
+
f"(completion covers the full set).[/]"
|
|
866
|
+
)
|
|
850
867
|
console.print()
|
|
851
868
|
|
|
852
869
|
|
|
@@ -1073,7 +1090,7 @@ def _render_most_valuable_break(mvb: dict | None) -> None:
|
|
|
1073
1090
|
date_str = f" · {mvb['break_date']}" if mvb.get("break_date") else ""
|
|
1074
1091
|
title = Text()
|
|
1075
1092
|
title.append(mvb["set_name"], style="slab.value")
|
|
1076
|
-
title.append(f" {mvb['break_type']}{date_str}", style="slab.dim")
|
|
1093
|
+
title.append(f" {mvb['break_type'].replace('_', ' ')}{date_str}", style="slab.dim")
|
|
1077
1094
|
|
|
1078
1095
|
g = mvb["gain"]
|
|
1079
1096
|
verdict = "paid off" if g >= 0 else "underwater"
|
|
@@ -62,7 +62,7 @@ def _break_label(b: BreakOut) -> str:
|
|
|
62
62
|
when = b.break_date.isoformat() if b.break_date else "—"
|
|
63
63
|
src = f" · {b.source}" if b.source else ""
|
|
64
64
|
cpc = f" @ ${b.cost_per_card:.2f}/card" if b.cost_per_card else ""
|
|
65
|
-
return (f"{when} · {b.set_name} · {b.break_type} · ${b.total_cost:.2f}{src} · "
|
|
65
|
+
return (f"{when} · {b.set_name} · {b.break_type.replace('_', ' ')} · ${b.total_cost:.2f}{src} · "
|
|
66
66
|
f"{b.copy_count} cards{cpc} [{b.uuid[:8]}]")
|
|
67
67
|
|
|
68
68
|
|
|
@@ -388,7 +388,16 @@ def select_acquisition_type() -> str:
|
|
|
388
388
|
return inquirer.select(message="How did you acquire this card?", choices=choices).execute()
|
|
389
389
|
|
|
390
390
|
|
|
391
|
-
def select_break_type() -> str:
|
|
391
|
+
def select_break_type(formats: list[str] | None = None) -> str:
|
|
392
|
+
"""What was opened. When the set's sealed SKUs are known, offer those formats first (the
|
|
393
|
+
things actually purchasable for THIS set) with an Other… escape to the generic list; a set
|
|
394
|
+
with no SKUs goes straight to the generic pack/box/case/other."""
|
|
395
|
+
if formats:
|
|
396
|
+
choices = [Choice(value=f, name=f.replace("_", " ").title()) for f in formats]
|
|
397
|
+
choices.append(Choice(value=None, name="Other…"))
|
|
398
|
+
picked = inquirer.select(message="What did you open?", choices=choices).execute()
|
|
399
|
+
if picked is not None:
|
|
400
|
+
return picked
|
|
392
401
|
choices = [Choice(value=t.value, name=_enum_label(t, _BREAK_TYPE_LABELS)) for t in BreakType]
|
|
393
402
|
return inquirer.select(message="What did you open?", choices=choices).execute()
|
|
394
403
|
|
|
@@ -443,6 +452,19 @@ def select_custom_set_type() -> str:
|
|
|
443
452
|
return inquirer.select(message="Set type:", choices=choices).execute()
|
|
444
453
|
|
|
445
454
|
|
|
455
|
+
def select_dynamic_match(default: str = "any_printing") -> str:
|
|
456
|
+
"""The set-wide granularity of a dynamic set — what counts as completing a card."""
|
|
457
|
+
choices = [
|
|
458
|
+
Choice(value="any_printing",
|
|
459
|
+
name="Any printing — one entry per card; base or any parallel completes it"),
|
|
460
|
+
Choice(value="exact",
|
|
461
|
+
name="Exact — every matching parallel is its own entry to chase"),
|
|
462
|
+
]
|
|
463
|
+
return inquirer.select(
|
|
464
|
+
message="What counts as having a card?", choices=choices, default=default,
|
|
465
|
+
).execute()
|
|
466
|
+
|
|
467
|
+
|
|
446
468
|
def select_cost_category() -> str:
|
|
447
469
|
choices = [
|
|
448
470
|
Choice(value="grading", name="Grading fee"),
|
|
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
|