slab-cli 0.12.0__tar.gz → 0.14.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.12.0 → slab_cli-0.14.0}/PKG-INFO +1 -1
  2. {slab_cli-0.12.0 → slab_cli-0.14.0}/pyproject.toml +1 -1
  3. {slab_cli-0.12.0 → slab_cli-0.14.0}/src/slab_cli/client.py +12 -0
  4. {slab_cli-0.12.0 → slab_cli-0.14.0}/src/slab_cli/commands/custom_sets.py +123 -24
  5. {slab_cli-0.12.0 → slab_cli-0.14.0}/src/slab_cli/commands/registry.py +4 -3
  6. {slab_cli-0.12.0 → slab_cli-0.14.0}/README.md +0 -0
  7. {slab_cli-0.12.0 → slab_cli-0.14.0}/src/slab_cli/__init__.py +0 -0
  8. {slab_cli-0.12.0 → slab_cli-0.14.0}/src/slab_cli/banner.py +0 -0
  9. {slab_cli-0.12.0 → slab_cli-0.14.0}/src/slab_cli/browse.py +0 -0
  10. {slab_cli-0.12.0 → slab_cli-0.14.0}/src/slab_cli/commands/__init__.py +0 -0
  11. {slab_cli-0.12.0 → slab_cli-0.14.0}/src/slab_cli/commands/breaks.py +0 -0
  12. {slab_cli-0.12.0 → slab_cli-0.14.0}/src/slab_cli/commands/catalog.py +0 -0
  13. {slab_cli-0.12.0 → slab_cli-0.14.0}/src/slab_cli/commands/collection.py +0 -0
  14. {slab_cli-0.12.0 → slab_cli-0.14.0}/src/slab_cli/commands/export.py +0 -0
  15. {slab_cli-0.12.0 → slab_cli-0.14.0}/src/slab_cli/commands/lots.py +0 -0
  16. {slab_cli-0.12.0 → slab_cli-0.14.0}/src/slab_cli/commands/pricing.py +0 -0
  17. {slab_cli-0.12.0 → slab_cli-0.14.0}/src/slab_cli/commands/setup.py +0 -0
  18. {slab_cli-0.12.0 → slab_cli-0.14.0}/src/slab_cli/commands/update.py +0 -0
  19. {slab_cli-0.12.0 → slab_cli-0.14.0}/src/slab_cli/config.py +0 -0
  20. {slab_cli-0.12.0 → slab_cli-0.14.0}/src/slab_cli/context.py +0 -0
  21. {slab_cli-0.12.0 → slab_cli-0.14.0}/src/slab_cli/display.py +0 -0
  22. {slab_cli-0.12.0 → slab_cli-0.14.0}/src/slab_cli/flags.py +0 -0
  23. {slab_cli-0.12.0 → slab_cli-0.14.0}/src/slab_cli/help.py +0 -0
  24. {slab_cli-0.12.0 → slab_cli-0.14.0}/src/slab_cli/main.py +0 -0
  25. {slab_cli-0.12.0 → slab_cli-0.14.0}/src/slab_cli/paging.py +0 -0
  26. {slab_cli-0.12.0 → slab_cli-0.14.0}/src/slab_cli/picker.py +0 -0
  27. {slab_cli-0.12.0 → slab_cli-0.14.0}/src/slab_cli/platforms/__init__.py +0 -0
  28. {slab_cli-0.12.0 → slab_cli-0.14.0}/src/slab_cli/platforms/base.py +0 -0
  29. {slab_cli-0.12.0 → slab_cli-0.14.0}/src/slab_cli/platforms/posix.py +0 -0
  30. {slab_cli-0.12.0 → slab_cli-0.14.0}/src/slab_cli/platforms/windows.py +0 -0
  31. {slab_cli-0.12.0 → slab_cli-0.14.0}/src/slab_cli/prompts.py +0 -0
  32. {slab_cli-0.12.0 → slab_cli-0.14.0}/src/slab_cli/sources.py +0 -0
  33. {slab_cli-0.12.0 → slab_cli-0.14.0}/src/slab_cli/theme.py +0 -0
  34. {slab_cli-0.12.0 → slab_cli-0.14.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.12.0
3
+ Version: 0.14.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.12.0"
3
+ version = "0.14.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"}]
@@ -40,6 +40,7 @@ from slab_schemas.collection import (
40
40
  from slab_schemas.custom_sets import (
41
41
  CustomSetCardAdd,
42
42
  CustomSetCardOut,
43
+ CustomSetCardUpdate,
43
44
  CustomSetCreate,
44
45
  CustomSetDetail,
45
46
  CustomSetOut,
@@ -316,6 +317,17 @@ class SlabClient:
316
317
  )
317
318
  return CustomSetCardOut.model_validate(resp.json())
318
319
 
320
+ def update_set_card(
321
+ self, collector_uuid: str, set_uuid: str, entry_uuid: str, payload: CustomSetCardUpdate
322
+ ) -> CustomSetCardOut:
323
+ # Sparse by fields-set (not exclude_none): explicitly passing None CLEARS a qualifier.
324
+ resp = self._request(
325
+ "PATCH",
326
+ f"/collectors/{collector_uuid}/custom-sets/{set_uuid}/cards/{entry_uuid}",
327
+ json=payload.model_dump(mode="json", exclude_unset=True),
328
+ )
329
+ return CustomSetCardOut.model_validate(resp.json())
330
+
319
331
  def delete_set_card(self, collector_uuid: str, set_uuid: str, entry_uuid: str) -> None:
320
332
  self._request("DELETE", f"/collectors/{collector_uuid}/custom-sets/{set_uuid}/cards/{entry_uuid}")
321
333
 
@@ -236,7 +236,8 @@ def _build_dynamic_filter() -> dict | None:
236
236
  # ---------------------------------------------------------------------------
237
237
 
238
238
  def cmd_chase_add(args: list[str]) -> None:
239
- """Add cards to a curated custom set — batch mode like `slab collection add`."""
239
+ """Add entries to a curated custom set — specific cards or player slots, batch mode like
240
+ `slab collection add`."""
240
241
  collector = ctx.collector
241
242
  client = ctx.client
242
243
 
@@ -388,7 +389,7 @@ def cmd_chase_view(args: list[str]) -> None:
388
389
 
389
390
 
390
391
  def cmd_chase_edit(args: list[str]) -> None:
391
- """Edit a custom set's name, description, or visibility."""
392
+ """Edit a custom set's name, description, visibility, or (dynamic) match granularity."""
392
393
  collector = ctx.collector
393
394
  client = ctx.client
394
395
 
@@ -404,7 +405,10 @@ def cmd_chase_edit(args: list[str]) -> None:
404
405
 
405
406
  console.print(f"\n Current name: {cs.name}")
406
407
  console.print(f" Current description: {cs.description or '—'}")
407
- console.print(f" Current visibility: {cs.visibility}\n")
408
+ console.print(f" Current visibility: {cs.visibility}")
409
+ if cs.set_type == "dynamic":
410
+ console.print(f" Current match granularity: {detail.dynamic_match}")
411
+ console.print()
408
412
 
409
413
  from slab_schemas.custom_sets import CustomSetUpdate
410
414
 
@@ -413,11 +417,20 @@ def cmd_chase_edit(args: list[str]) -> None:
413
417
  vis = None
414
418
  if confirm("Change visibility?", default=False):
415
419
  vis = select_visibility()
420
+ new_match = None
421
+ if cs.set_type == "dynamic" and confirm(
422
+ "Change what counts as having an entry? (per printing / per card / per player)",
423
+ default=False,
424
+ ):
425
+ new_match = select_dynamic_match(default=detail.dynamic_match or "any_printing")
426
+ if new_match == detail.dynamic_match:
427
+ new_match = None
416
428
 
417
429
  update = CustomSetUpdate(
418
430
  name=name,
419
431
  description=description,
420
432
  visibility=Visibility(vis) if vis else None,
433
+ dynamic_match=DynamicMatch(new_match) if new_match else None,
421
434
  )
422
435
 
423
436
  # Only send if something changed
@@ -427,6 +440,11 @@ def cmd_chase_edit(args: list[str]) -> None:
427
440
 
428
441
  updated = client.update_custom_set(collector, cs.uuid, update)
429
442
  console.print(f"[green]Updated![/green] {updated.name}")
443
+ if new_match:
444
+ console.print(
445
+ f" [dim]Membership redefined — the set now tracks one entry per "
446
+ f"{'player' if new_match == 'any_card' else 'card slot' if new_match == 'any_printing' else 'printing'}.[/dim]"
447
+ )
430
448
 
431
449
 
432
450
  # ---------------------------------------------------------------------------
@@ -521,33 +539,114 @@ def cmd_chase_remove(args: list[str]) -> None:
521
539
  console.print("[yellow]Can only remove cards from curated sets.[/yellow]")
522
540
  return
523
541
 
524
- detail = _fetch_full_detail(client, cs.uuid, collector)
525
- if not detail.cards:
526
- console.print("[dim]This set has no cards.[/dim]")
527
- return
542
+ entry = _pick_entry(client, collector, cs.uuid, "Remove which entry?")
543
+ if entry and confirm(f"Remove this entry? ({entry.uuid[:8]})", default=False):
544
+ client.delete_set_card(collector, cs.uuid, entry.uuid)
545
+ console.print("[green]Removed.[/green]")
528
546
 
529
- choices = []
530
- for entry in detail.cards:
531
- card = entry.card
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"
539
- owned = " ✓" if entry.owned else ""
540
- choices.append(Choice(value=entry, name=f"{label}{owned}"))
541
547
 
542
- entry = inquirer.select(
543
- message=f"Remove which card? ({len(detail.cards)} in set):",
548
+ def _entry_label(entry) -> str:
549
+ """One line describing an entry — the card and its mode, or the player slot + qualifiers."""
550
+ card = entry.card
551
+ if card:
552
+ subjects = ", ".join(s.name for s in card.subjects)
553
+ label = f"{card.card_number} {subjects} — {entry.match_mode}"
554
+ if entry.serial_number is not None:
555
+ label += f" #{entry.serial_number}"
556
+ return label
557
+ quals = " · ".join(str(q) for q in (
558
+ f"{entry.subject_year}-{str(entry.subject_year + 1)[-2:]}" if entry.subject_year else None,
559
+ entry.subject_team, entry.subject_attribute) if q)
560
+ return f"{entry.subject or '—'} — any card" + (f" · {quals}" if quals else "")
561
+
562
+
563
+ def _pick_entry(client: SlabClient, collector: str, set_uuid: str, message: str):
564
+ """Fetch the full entry list and pick one (None if empty or backed out)."""
565
+ detail = _fetch_full_detail(client, set_uuid, collector)
566
+ if not detail.cards:
567
+ console.print("[dim]This set has no entries.[/dim]")
568
+ return None
569
+ choices = [
570
+ Choice(value=e, name=f"{_entry_label(e)}{' ✓' if e.owned else ''}")
571
+ for e in detail.cards
572
+ ]
573
+ return inquirer.select(
574
+ message=f"{message} ({len(detail.cards)} in set):",
544
575
  choices=choices,
545
576
  max_height="70%",
546
577
  ).execute()
547
578
 
548
- if entry and confirm(f"Remove this card entry? ({entry.uuid[:8]})", default=False):
549
- client.delete_set_card(collector, cs.uuid, entry.uuid)
550
- console.print("[green]Removed.[/green]")
579
+
580
+ # ---------------------------------------------------------------------------
581
+ # slab chase tune — retune one entry (match mode / player-slot qualifiers)
582
+ # ---------------------------------------------------------------------------
583
+
584
+ def cmd_chase_tune(args: list[str]) -> None:
585
+ """Retune one entry of a curated set: a card entry's match mode, or a player slot's
586
+ season/team/attribute qualifiers. An entry's KIND (card vs player) is fixed — switch by
587
+ removing and re-adding."""
588
+ collector = ctx.collector
589
+ client = ctx.client
590
+
591
+ cs = _select_custom_set(client, collector, message="Which set to tune?")
592
+ if cs is None:
593
+ return
594
+ if cs.set_type != "curated":
595
+ console.print("[yellow]Dynamic sets have one set-wide granularity — use `slab chase edit`.[/yellow]")
596
+ return
597
+ detail = client.get_custom_set(cs.uuid, collector, limit=1)
598
+ if detail.creator_uuid != collector:
599
+ console.print("[yellow]You can only tune sets you created.[/yellow]")
600
+ return
601
+
602
+ entry = _pick_entry(client, collector, cs.uuid, "Tune which entry?")
603
+ if entry is None:
604
+ return
605
+
606
+ from slab_schemas.custom_sets import CustomSetCardUpdate
607
+
608
+ changes: dict = {}
609
+ if entry.card:
610
+ console.print(f"\n {_entry_label(entry)}")
611
+ new_mode = select_match_mode()
612
+ if new_mode != entry.match_mode:
613
+ changes["match_mode"] = MatchMode(new_mode)
614
+ if new_mode == "exact_serial":
615
+ serial = ask_int_optional("Which serial number?")
616
+ if serial is None:
617
+ console.print("[yellow]Serial number is required for exact_serial.[/yellow]")
618
+ return
619
+ if serial != entry.serial_number:
620
+ changes["serial_number"] = serial
621
+ else:
622
+ console.print(f"\n {_entry_label(entry)}")
623
+ console.print(" [dim]Retune the qualifiers — Enter keeps the current value, '-' clears it.[/dim]")
624
+ for field, label, cast in (
625
+ ("subject_year", f"Season (current: {entry.subject_year or 'any'}):", int),
626
+ ("subject_team", f"Team (current: {entry.subject_team or 'any'}):", str),
627
+ ("subject_attribute", f"Attribute (current: {entry.subject_attribute or 'any'}):", str),
628
+ ):
629
+ raw = ask_text_optional(label)
630
+ if not raw:
631
+ continue
632
+ if raw.strip() == "-":
633
+ if getattr(entry, field) is not None:
634
+ changes[field] = None
635
+ continue
636
+ try:
637
+ value = cast(raw.strip())
638
+ except ValueError:
639
+ console.print(f"[yellow] Skipped — not a valid value for {field}.[/yellow]")
640
+ continue
641
+ if value != getattr(entry, field):
642
+ changes[field] = value
643
+
644
+ if not changes:
645
+ console.print("[dim]No changes.[/dim]")
646
+ return
647
+
648
+ updated = client.update_set_card(collector, cs.uuid, entry.uuid, CustomSetCardUpdate(**changes))
649
+ console.print(f"[green]Tuned![/green] {_entry_label(updated)}")
551
650
 
552
651
 
553
652
  # ---------------------------------------------------------------------------
@@ -94,13 +94,14 @@ GROUPS: list[Group] = [
94
94
  ]),
95
95
  Group("chase", "custom sets & set-completion", [
96
96
  Command("create", "Create a custom set (curated or dynamic)", custom_sets.cmd_chase_create),
97
- Command("add", "Add cards to a curated set", custom_sets.cmd_chase_add),
97
+ Command("add", "Add cards or player slots to a curated set", custom_sets.cmd_chase_add),
98
98
  Command("view", "View a set with completion + add missing", custom_sets.cmd_chase_view, "[uuid]"),
99
- Command("edit", "Edit name, description, or visibility", custom_sets.cmd_chase_edit),
99
+ Command("edit", "Edit name, description, visibility, or granularity", custom_sets.cmd_chase_edit),
100
+ Command("tune", "Retune one entry — match mode or player-slot qualifiers", custom_sets.cmd_chase_tune),
100
101
  Command("list", "Your sets (created + subscribed)", custom_sets.cmd_chase_list),
101
102
  Command("find", "Discover public sets by name", custom_sets.cmd_chase_find, "[query]"),
102
103
  Command("subscribe", "Subscribe to / unsubscribe from a public set", custom_sets.cmd_chase_subscribe, "[query]"),
103
- Command("remove", "Remove a card from a set", custom_sets.cmd_chase_remove),
104
+ Command("remove", "Remove an entry from a set", custom_sets.cmd_chase_remove),
104
105
  Command("delete", "Delete a set you created", custom_sets.cmd_chase_delete),
105
106
  ]),
106
107
  Group("collector", "your identity", [
File without changes