slab-cli 0.11.0__tar.gz → 0.13.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.11.0 → slab_cli-0.13.0}/PKG-INFO +1 -1
- {slab_cli-0.11.0 → slab_cli-0.13.0}/pyproject.toml +1 -1
- {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/commands/custom_sets.py +91 -38
- {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/display.py +15 -4
- {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/prompts.py +13 -2
- {slab_cli-0.11.0 → slab_cli-0.13.0}/README.md +0 -0
- {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/__init__.py +0 -0
- {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/banner.py +0 -0
- {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/browse.py +0 -0
- {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/client.py +0 -0
- {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/commands/__init__.py +0 -0
- {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/commands/breaks.py +0 -0
- {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/commands/catalog.py +0 -0
- {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/commands/collection.py +0 -0
- {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/commands/export.py +0 -0
- {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/commands/lots.py +0 -0
- {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/commands/pricing.py +0 -0
- {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/commands/registry.py +0 -0
- {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/commands/setup.py +0 -0
- {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/commands/update.py +0 -0
- {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/config.py +0 -0
- {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/context.py +0 -0
- {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/flags.py +0 -0
- {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/help.py +0 -0
- {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/main.py +0 -0
- {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/paging.py +0 -0
- {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/picker.py +0 -0
- {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/platforms/__init__.py +0 -0
- {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/platforms/base.py +0 -0
- {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/platforms/posix.py +0 -0
- {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/platforms/windows.py +0 -0
- {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/sources.py +0 -0
- {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/theme.py +0 -0
- {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/updates.py +0 -0
|
@@ -33,6 +33,7 @@ from ..prompts import (
|
|
|
33
33
|
confirm,
|
|
34
34
|
select_custom_set_type,
|
|
35
35
|
select_dynamic_match,
|
|
36
|
+
select_entry_kind,
|
|
36
37
|
select_match_mode,
|
|
37
38
|
select_printing,
|
|
38
39
|
select_visibility,
|
|
@@ -249,53 +250,101 @@ def cmd_chase_add(args: list[str]) -> None:
|
|
|
249
250
|
_batch_add_cards(client, collector, cs.uuid)
|
|
250
251
|
|
|
251
252
|
|
|
253
|
+
def _build_player_slot(position: int) -> tuple[CustomSetCardAdd, str] | None:
|
|
254
|
+
"""The player-slot path: pick a subject, optionally narrow with qualifiers. Returns the
|
|
255
|
+
payload plus a display label, or None if the user backed out."""
|
|
256
|
+
picked = search_and_pick_subject(None, prompt="Search for the player:")
|
|
257
|
+
if picked is None:
|
|
258
|
+
return None
|
|
259
|
+
cards, subject_name = picked
|
|
260
|
+
subject_uuid = next(
|
|
261
|
+
(s.uuid for c in cards for s in c.subjects if s.name == subject_name and s.uuid), None)
|
|
262
|
+
if subject_uuid is None:
|
|
263
|
+
console.print("[yellow]The API did not serve a subject id — is it up to date?[/yellow]")
|
|
264
|
+
return None
|
|
265
|
+
|
|
266
|
+
console.print(f"\n [bold]{subject_name}[/bold] — any card of this player fills the slot.")
|
|
267
|
+
console.print(" [dim]Optionally narrow what counts. Enter skips each — skip all three for"
|
|
268
|
+
" truly any card.[/dim]")
|
|
269
|
+
year = ask_int_optional("Only a season? (2025 = 2025-26):")
|
|
270
|
+
team = ask_text_optional("Only showing them on a team? (e.g. Hurricanes):")
|
|
271
|
+
attribute = ask_text_optional("Require an attribute? (e.g. Autograph, Rookie):")
|
|
272
|
+
|
|
273
|
+
quals = [q for q in (
|
|
274
|
+
f"{year}-{str(year + 1)[-2:]}" if year else None, team or None, attribute or None) if q]
|
|
275
|
+
label = f"{subject_name} — any card" + (f" · {' · '.join(quals)}" if quals else "")
|
|
276
|
+
payload = CustomSetCardAdd(
|
|
277
|
+
subject_uuid=subject_uuid,
|
|
278
|
+
match_mode=MatchMode.any_card,
|
|
279
|
+
subject_year=year,
|
|
280
|
+
subject_team=team or None,
|
|
281
|
+
subject_attribute=attribute or None,
|
|
282
|
+
position=position,
|
|
283
|
+
)
|
|
284
|
+
return payload, label
|
|
285
|
+
|
|
286
|
+
|
|
252
287
|
def _batch_add_cards(client: SlabClient, collector: str, set_uuid: str) -> None:
|
|
253
|
-
"""Batch loop:
|
|
288
|
+
"""Batch loop: choose the kind of goal (a card, or a player), build the entry, add, repeat.
|
|
254
289
|
|
|
255
|
-
|
|
256
|
-
|
|
290
|
+
For card entries the match mode is decided once and reused (like acquisition type in
|
|
291
|
+
`slab collection add`); serial number likewise. Player slots ask their qualifiers each time —
|
|
292
|
+
they ARE the entry."""
|
|
257
293
|
default_match: str | None = None
|
|
258
294
|
default_serial: int | None = None
|
|
259
295
|
added = 0
|
|
260
296
|
|
|
261
297
|
while True:
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
if
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
298
|
+
kind = select_entry_kind()
|
|
299
|
+
|
|
300
|
+
if kind == "player":
|
|
301
|
+
built = _build_player_slot(added)
|
|
302
|
+
if built is None:
|
|
303
|
+
if added == 0 and not confirm("Try again?", default=True):
|
|
304
|
+
return
|
|
305
|
+
continue
|
|
306
|
+
payload, label = built
|
|
307
|
+
else:
|
|
308
|
+
card_uuid, subject = _search_and_select_card_for_set()
|
|
309
|
+
if card_uuid is None:
|
|
310
|
+
if added > 0 and not confirm("Try again?", default=True):
|
|
311
|
+
break
|
|
312
|
+
elif added == 0:
|
|
313
|
+
return
|
|
314
|
+
continue
|
|
315
|
+
|
|
316
|
+
# Match mode — ask once, reuse across batch
|
|
317
|
+
if default_match is None:
|
|
318
|
+
default_match = select_match_mode()
|
|
319
|
+
if default_match == "exact_serial":
|
|
320
|
+
default_serial = ask_int_optional("Which serial number (applies to all cards in this batch)?")
|
|
321
|
+
if default_serial is None:
|
|
322
|
+
console.print("[yellow]Serial number is required for exact_serial match.[/yellow]")
|
|
323
|
+
default_match = None
|
|
324
|
+
continue
|
|
325
|
+
|
|
326
|
+
serial = default_serial if default_match == "exact_serial" else None
|
|
327
|
+
payload = CustomSetCardAdd(
|
|
285
328
|
card_uuid=card_uuid,
|
|
286
329
|
match_mode=MatchMode(default_match),
|
|
287
330
|
serial_number=serial,
|
|
288
331
|
position=added,
|
|
289
|
-
)
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
332
|
+
)
|
|
333
|
+
label = f"{subject or 'Card'} — " + (
|
|
334
|
+
f"serial #{serial}" if serial is not None else default_match)
|
|
335
|
+
|
|
336
|
+
try:
|
|
337
|
+
client.add_set_card(collector, set_uuid, payload)
|
|
338
|
+
except Exception as exc: # 409 duplicate slot etc. — keep the batch alive
|
|
339
|
+
console.print(f"[yellow]Not added: {exc}[/yellow]")
|
|
340
|
+
if not confirm("Add another entry to this set?", default=True):
|
|
341
|
+
break
|
|
342
|
+
continue
|
|
343
|
+
console.print(f"[green]Added![/green] {label}")
|
|
295
344
|
console.print()
|
|
296
345
|
added += 1
|
|
297
346
|
|
|
298
|
-
if not confirm("Add another
|
|
347
|
+
if not confirm("Add another entry to this set?", default=True):
|
|
299
348
|
break
|
|
300
349
|
|
|
301
350
|
if added > 0:
|
|
@@ -322,7 +371,8 @@ def cmd_chase_view(args: list[str]) -> None:
|
|
|
322
371
|
print_custom_set_detail(detail)
|
|
323
372
|
|
|
324
373
|
# Offer to add missing cards to the collection
|
|
325
|
-
|
|
374
|
+
# Only card-backed entries can pre-fill an add; a player slot has no single card to offer.
|
|
375
|
+
missing = [e for e in detail.cards if not e.owned and e.card]
|
|
326
376
|
if missing and confirm(f"Add any of the {len(missing)} missing cards to your collection?", default=False):
|
|
327
377
|
from .collection import _add_single_card, AddSession
|
|
328
378
|
session = AddSession(client=client, collector=collector)
|
|
@@ -479,10 +529,13 @@ def cmd_chase_remove(args: list[str]) -> None:
|
|
|
479
529
|
choices = []
|
|
480
530
|
for entry in detail.cards:
|
|
481
531
|
card = entry.card
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
532
|
+
if card:
|
|
533
|
+
subjects = ", ".join(s.name for s in card.subjects)
|
|
534
|
+
label = f"{card.card_number} {subjects} — {entry.match_mode}"
|
|
535
|
+
if entry.serial_number is not None:
|
|
536
|
+
label += f" #{entry.serial_number}"
|
|
537
|
+
else:
|
|
538
|
+
label = f"{entry.subject or '—'} — any card"
|
|
486
539
|
owned = " ✓" if entry.owned else ""
|
|
487
540
|
choices.append(Choice(value=entry, name=f"{label}{owned}"))
|
|
488
541
|
|
|
@@ -841,14 +841,25 @@ def print_custom_set_detail(detail: CustomSetDetail) -> None:
|
|
|
841
841
|
|
|
842
842
|
for entry in detail.cards:
|
|
843
843
|
card = entry.card
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
844
|
+
if card:
|
|
845
|
+
subjects = ", ".join(s.name for s in card.subjects)
|
|
846
|
+
slot = _slot(card)
|
|
847
|
+
pr = f" /{card.print_run}" if card.print_run else ""
|
|
848
|
+
label = f"{card.card_number} {subjects} [slab.dim]— {slot}{pr}[/]"
|
|
849
|
+
else:
|
|
850
|
+
# A player slot: the subject is the goal; show its qualifiers inline.
|
|
851
|
+
quals = " · ".join(q for q in (
|
|
852
|
+
f"{entry.subject_year}-{str(entry.subject_year + 1)[-2:]}"
|
|
853
|
+
if entry.subject_year else None,
|
|
854
|
+
entry.subject_team, entry.subject_attribute) if q)
|
|
855
|
+
label = f"{entry.subject or '—'} [slab.dim]— any card"
|
|
856
|
+
label += f" · {quals}[/]" if quals else "[/]"
|
|
848
857
|
|
|
849
858
|
match = entry.match_mode
|
|
850
859
|
if entry.match_mode == "exact_serial" and entry.serial_number is not None:
|
|
851
860
|
match = f"serial #{entry.serial_number}"
|
|
861
|
+
elif entry.match_mode == "any_card":
|
|
862
|
+
match = "player"
|
|
852
863
|
|
|
853
864
|
if entry.owned:
|
|
854
865
|
owned = f"[slab.gain]✓[/] [slab.dim]{entry.owned_printing or ''}[/]"
|
|
@@ -436,6 +436,15 @@ def select_match_mode() -> str:
|
|
|
436
436
|
return inquirer.select(message="Match mode:", choices=choices).execute()
|
|
437
437
|
|
|
438
438
|
|
|
439
|
+
def select_entry_kind() -> str:
|
|
440
|
+
"""The first fork when adding to a chase set: is the goal a card, or a player?"""
|
|
441
|
+
choices = [
|
|
442
|
+
Choice(value="card", name="A specific card — search the catalog, pick the printing"),
|
|
443
|
+
Choice(value="player", name="A player — any card of them counts (optionally narrowed)"),
|
|
444
|
+
]
|
|
445
|
+
return inquirer.select(message="What are you chasing?", choices=choices).execute()
|
|
446
|
+
|
|
447
|
+
|
|
439
448
|
def select_visibility() -> str:
|
|
440
449
|
choices = [
|
|
441
450
|
Choice(value="private", name="Private — only you can see it"),
|
|
@@ -453,15 +462,17 @@ def select_custom_set_type() -> str:
|
|
|
453
462
|
|
|
454
463
|
|
|
455
464
|
def select_dynamic_match(default: str = "any_printing") -> str:
|
|
456
|
-
"""The set-wide granularity of a dynamic set — what counts as completing
|
|
465
|
+
"""The set-wide granularity of a dynamic set — what counts as completing an entry."""
|
|
457
466
|
choices = [
|
|
458
467
|
Choice(value="any_printing",
|
|
459
468
|
name="Any printing — one entry per card; base or any parallel completes it"),
|
|
469
|
+
Choice(value="any_card",
|
|
470
|
+
name="Any card — one entry per PLAYER; any matching card completes it (roster chase)"),
|
|
460
471
|
Choice(value="exact",
|
|
461
472
|
name="Exact — every matching parallel is its own entry to chase"),
|
|
462
473
|
]
|
|
463
474
|
return inquirer.select(
|
|
464
|
-
message="What counts as having
|
|
475
|
+
message="What counts as having an entry?", choices=choices, default=default,
|
|
465
476
|
).execute()
|
|
466
477
|
|
|
467
478
|
|
|
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
|