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.
Files changed (34) hide show
  1. {slab_cli-0.11.0 → slab_cli-0.13.0}/PKG-INFO +1 -1
  2. {slab_cli-0.11.0 → slab_cli-0.13.0}/pyproject.toml +1 -1
  3. {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/commands/custom_sets.py +91 -38
  4. {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/display.py +15 -4
  5. {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/prompts.py +13 -2
  6. {slab_cli-0.11.0 → slab_cli-0.13.0}/README.md +0 -0
  7. {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/__init__.py +0 -0
  8. {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/banner.py +0 -0
  9. {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/browse.py +0 -0
  10. {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/client.py +0 -0
  11. {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/commands/__init__.py +0 -0
  12. {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/commands/breaks.py +0 -0
  13. {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/commands/catalog.py +0 -0
  14. {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/commands/collection.py +0 -0
  15. {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/commands/export.py +0 -0
  16. {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/commands/lots.py +0 -0
  17. {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/commands/pricing.py +0 -0
  18. {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/commands/registry.py +0 -0
  19. {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/commands/setup.py +0 -0
  20. {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/commands/update.py +0 -0
  21. {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/config.py +0 -0
  22. {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/context.py +0 -0
  23. {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/flags.py +0 -0
  24. {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/help.py +0 -0
  25. {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/main.py +0 -0
  26. {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/paging.py +0 -0
  27. {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/picker.py +0 -0
  28. {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/platforms/__init__.py +0 -0
  29. {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/platforms/base.py +0 -0
  30. {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/platforms/posix.py +0 -0
  31. {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/platforms/windows.py +0 -0
  32. {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/sources.py +0 -0
  33. {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/theme.py +0 -0
  34. {slab_cli-0.11.0 → slab_cli-0.13.0}/src/slab_cli/updates.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: slab-cli
3
- Version: 0.11.0
3
+ Version: 0.13.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
6
  Requires-Dist: slab-schemas>=0.1.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "slab-cli"
3
- version = "0.11.0"
3
+ version = "0.13.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"}]
@@ -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: search → pick player → pick printing → choose match mode → add.
288
+ """Batch loop: choose the kind of goal (a card, or a player), build the entry, add, repeat.
254
289
 
255
- Match mode is decided once and reused (like acquisition type in `slab collection add`).
256
- Serial number is only asked once if exact_serial — reused for the batch."""
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
- card_uuid, subject = _search_and_select_card_for_set()
263
- if card_uuid is None:
264
- if added > 0 and not confirm("Try again?", default=True):
265
- break
266
- elif added == 0:
267
- return
268
- continue
269
-
270
- # Match mode — ask once, reuse across batch
271
- if default_match is None:
272
- default_match = select_match_mode()
273
- if default_match == "exact_serial":
274
- default_serial = ask_int_optional("Which serial number (applies to all cards in this batch)?")
275
- if default_serial is None:
276
- console.print("[yellow]Serial number is required for exact_serial match.[/yellow]")
277
- default_match = None
278
- continue
279
-
280
- serial = default_serial if default_match == "exact_serial" else None
281
-
282
- entry = client.add_set_card(
283
- collector, set_uuid,
284
- CustomSetCardAdd(
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
- match_label = entry.match_mode
292
- if entry.serial_number is not None:
293
- match_label = f"serial #{entry.serial_number}"
294
- console.print(f"[green]Added![/green] {subject or 'Card'} — {match_label}")
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 card to this set?", default=True):
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
- missing = [e for e in detail.cards if not e.owned]
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
- subjects = ", ".join(s.name for s in card.subjects) if card else "—"
483
- label = f"{card.card_number} {subjects} — {entry.match_mode}"
484
- if entry.serial_number is not None:
485
- label += f" #{entry.serial_number}"
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
- subjects = ", ".join(s.name for s in card.subjects) if card else "—"
845
- slot = _slot(card) if card else "—"
846
- pr = f" /{card.print_run}" if card and card.print_run else ""
847
- label = f"{card.card_number} {subjects} [slab.dim]— {slot}{pr}[/]" if card else "—"
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 a card."""
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 a card?", choices=choices, default=default,
475
+ message="What counts as having an entry?", choices=choices, default=default,
465
476
  ).execute()
466
477
 
467
478
 
File without changes