slab-cli 0.1.0__py3-none-any.whl

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/theme.py ADDED
@@ -0,0 +1,184 @@
1
+ """SLAB terminal theme — the CLI's shared visual language.
2
+
3
+ Mirrors the portal's retro card-collector look (`portal/src/app/globals.css`): gold **foil** as the
4
+ hero accent over slate surfaces, green gains / red losses, uppercase pixel-style headings, rounded
5
+ "cards". Every command should render through the `console` and factories here so the whole CLI reads
6
+ as one system — restyle in this one file and it propagates everywhere.
7
+
8
+ Building block cheat-sheet:
9
+ - `console` — the themed Console (import this instead of making your own).
10
+ - `heading(t, note=)` — a left-aligned gold uppercase section header (print it above a card).
11
+ - `slab_table(cols)` — a card-styled table; pass `foil=True` for the gold "hero card" border.
12
+ - `slab_panel(body)` — a card-styled panel (foil border by default).
13
+ - `kv_grid(rows)` — an aligned label/value block (replaces hand-padded strings).
14
+ - `money / signed_money / pct_coverage` — palette-colored value formatters.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from rich import box
20
+ from rich.console import Console, Group, RenderableType
21
+ from rich.panel import Panel
22
+ from rich.table import Table
23
+ from rich.text import Text
24
+ from rich.theme import Theme
25
+
26
+ # --- Palette (hex mirrors portal/src/app/globals.css :theme tokens) --------------------------------
27
+ FOIL = "#f0b429" # gold — the hero accent (foil)
28
+ FOIL_DIM = "#b8860b"
29
+ TEXT = "#e2e8f0"
30
+ TEXT_DIM = "#7a8baa"
31
+ TEXT_MUTED = "#4a5a7a"
32
+ ACCENT = "#60a5fa" # blue
33
+ SUCCESS = "#34d399" # gains
34
+ DANGER = "#ef4444" # losses
35
+ BORDER = "#2a3a5c" # slate card border
36
+
37
+ # Semantic style names — reference as `[slab.foil]…[/]` in markup, or via the helpers below.
38
+ SLAB_THEME = Theme({
39
+ "slab.foil": f"bold {FOIL}",
40
+ "slab.foil_dim": FOIL_DIM,
41
+ "slab.text": TEXT,
42
+ "slab.value": f"bold {TEXT}",
43
+ "slab.label": TEXT_DIM,
44
+ "slab.dim": TEXT_DIM,
45
+ "slab.muted": TEXT_MUTED,
46
+ "slab.accent": ACCENT,
47
+ "slab.gain": f"bold {SUCCESS}",
48
+ "slab.loss": f"bold {DANGER}",
49
+ "slab.heading": f"bold {FOIL}",
50
+ "slab.border": BORDER,
51
+ })
52
+
53
+ console = Console(theme=SLAB_THEME)
54
+
55
+ # The card silhouette — rounded + thin, mirroring the portal's rounded-border cards.
56
+ CARD_BOX = box.ROUNDED
57
+
58
+
59
+ # --- Headers ---------------------------------------------------------------------------------------
60
+
61
+ def heading(text: str, note: str | None = None) -> Text:
62
+ """A section header in the portal's pixel-heading style: a slim foil marker + uppercase gold
63
+ title, left-aligned, with an optional dim note (e.g. a count) trailing it. The marker gives every
64
+ titled block across the CLI the same on-theme spine. Print it on its own line above the content."""
65
+ t = Text()
66
+ t.append("▍ ", style="slab.foil")
67
+ t.append(text.upper(), style="slab.heading")
68
+ if note:
69
+ t.append(f" {note}", style="slab.dim")
70
+ return t
71
+
72
+
73
+ # --- Cards (tables + panels) -----------------------------------------------------------------------
74
+
75
+ def slab_table(columns: list[tuple[str, dict]], *, foil: bool = False) -> Table:
76
+ """Build a card-styled table. `columns` is a list of `(header, column_kwargs)` — headers are
77
+ uppercased to match the portal's `.table-header` (uppercase, muted). `foil=True` swaps the slate
78
+ border for the gold "hero card" border."""
79
+ table = Table(
80
+ box=CARD_BOX,
81
+ border_style="slab.foil_dim" if foil else "slab.border",
82
+ header_style="slab.label",
83
+ title=None,
84
+ pad_edge=True,
85
+ expand=False,
86
+ )
87
+ for name, kwargs in columns:
88
+ table.add_column(name.upper(), **kwargs)
89
+ return table
90
+
91
+
92
+ def slab_panel(body: RenderableType, *, foil: bool = True, title: str | None = None) -> Panel:
93
+ """Wrap a renderable in a card. Foil (gold) border by default — that's the "hero card" the portal
94
+ reserves for headline stats; pass `foil=False` for an ordinary slate card. Padding is tight
95
+ (no blank top/bottom line) so a card hugs its content instead of reading as a near-empty box."""
96
+ return Panel(
97
+ body,
98
+ box=CARD_BOX,
99
+ border_style="slab.foil_dim" if foil else "slab.border",
100
+ title=title,
101
+ title_align="left",
102
+ padding=(0, 2),
103
+ expand=False, # hug content — a card sized to its data reads far better than a half-empty bar
104
+ )
105
+
106
+
107
+ # --- Sections: the canonical "titled block" every multi-part output composes from -----------------
108
+
109
+ def section(title: str, body: RenderableType, *, note: str | None = None, foil: bool = False) -> Group:
110
+ """A titled card: a blank line for rhythm, the foil-marked heading, then `body` in a slate (or
111
+ foil) card. THE reusable unit for command output — build every section of every command from this
112
+ and the whole CLI shares one spacing + heading language. Restyle here, everything follows."""
113
+ return Group(Text(), heading(title, note), slab_panel(body, foil=foil))
114
+
115
+
116
+ def titled(title: str, renderable: RenderableType, *, note: str | None = None) -> Group:
117
+ """Like `section` but for content that draws its own frame (a `slab_table`, a chart): blank line +
118
+ marked heading + the renderable as-is, with no extra card around it. Use for titled tables so
119
+ they get the same spacing + heading as `section` cards without a double border."""
120
+ return Group(Text(), heading(title, note), renderable)
121
+
122
+
123
+ def kv_grid(rows: list[tuple[str, RenderableType]], *, label_style: str = "slab.label") -> Table:
124
+ """An aligned two-column label/value block — the reusable replacement for hand-padded strings so
125
+ values always line up. Values may be plain strings, markup strings, or Rich renderables."""
126
+ grid = Table.grid(padding=(0, 3))
127
+ grid.add_column(style=label_style, justify="left")
128
+ grid.add_column(justify="left")
129
+ for label, value in rows:
130
+ grid.add_row(label, value)
131
+ return grid
132
+
133
+
134
+ # --- Value formatters ------------------------------------------------------------------------------
135
+
136
+ def money(v) -> str:
137
+ """`$1,234.56`, or an em-dash when absent."""
138
+ return f"${v:,.2f}" if v is not None else "—"
139
+
140
+
141
+ def signed_money(v) -> str:
142
+ """Colored, signed money for P&L: green gain / red loss, em-dash when absent."""
143
+ if v is None:
144
+ return "[slab.dim]—[/]"
145
+ color = "slab.gain" if v >= 0 else "slab.loss"
146
+ sign = "+" if v >= 0 else "-"
147
+ return f"[{color}]{sign}{money(abs(float(v)))}[/]"
148
+
149
+
150
+ def pct_coverage(pct: float) -> str:
151
+ """A coverage/health percentage, colored by band: green ≥75, gold ≥40, red below."""
152
+ color = "slab.gain" if pct >= 75 else ("slab.foil" if pct >= 40 else "slab.loss")
153
+ return f"[{color}]{pct:.0f}%[/]"
154
+
155
+
156
+ _SPARK_TICKS = "▁▂▃▄▅▆▇█"
157
+
158
+
159
+ def sparkline(values: list, *, style: str = "slab.foil", width: int = 24) -> str:
160
+ """A one-line trend as unicode blocks (`▁▂▅▇█`) — the compact CLI stand-in for the portal's
161
+ portfolio chart. Series longer than `width` are evenly downsampled (the dashboard payload's
162
+ embedded trend is daily, ~90 points). Returns markup, or '' when there's nothing to plot
163
+ (<2 points)."""
164
+ nums = [float(v) for v in values if v is not None]
165
+ if len(nums) < 2:
166
+ return ""
167
+ if len(nums) > width:
168
+ step = (len(nums) - 1) / (width - 1)
169
+ nums = [nums[round(i * step)] for i in range(width)]
170
+ lo, hi = min(nums), max(nums)
171
+ span = hi - lo
172
+ if not span:
173
+ ticks = _SPARK_TICKS[3] * len(nums) # dead-flat: a mid line, not empty
174
+ else:
175
+ ticks = "".join(_SPARK_TICKS[min(7, int((v - lo) / span * 7))] for v in nums)
176
+ return f"[{style}]{ticks}[/]"
177
+
178
+
179
+ def bar(value: float, maxv: float, width: int = 14, *, style: str = "slab.foil") -> str:
180
+ """A compact filled/track meter (`████····`) as a markup string — the reusable bar for
181
+ distributions, completion, and breakdowns. Filled cells take `style`; the track is muted."""
182
+ filled = round((value / maxv) * width) if maxv else 0
183
+ filled = max(0, min(width, filled))
184
+ return f"[{style}]{'█' * filled}[/][slab.muted]{'·' * (width - filled)}[/]"
@@ -0,0 +1,70 @@
1
+ Metadata-Version: 2.3
2
+ Name: slab-cli
3
+ Version: 0.1.0
4
+ Summary: CLI for the slab trading-card API — catalog, collect, and track your cards from the terminal.
5
+ Author: dev_jeb
6
+ Requires-Dist: slab-schemas>=0.1.0
7
+ Requires-Dist: httpx>=0.27
8
+ Requires-Dist: rich>=13.0
9
+ Requires-Dist: inquirerpy>=0.3
10
+ Requires-Python: >=3.13
11
+ Description-Content-Type: text/markdown
12
+
13
+ # slab-cli
14
+
15
+ A terminal client for the [slab](https://api.slab.dev-jeb.com) trading-card API — browse the
16
+ catalog, price cards against real sales comps, and track your own collection from the command line.
17
+
18
+ ```bash
19
+ pip install slab-cli
20
+ ```
21
+
22
+ This installs the `slab` command.
23
+
24
+ ## Setup
25
+
26
+ The CLI talks to the hosted slab API. You need an **API key** — mint one from the
27
+ [slab portal](https://app.slab.dev-jeb.com) (sign in → **API Keys** → create). Then:
28
+
29
+ ```bash
30
+ export SLAB_API_KEY=sk_live_... # your key (required)
31
+ # optional:
32
+ export SLAB_API_URL=https://api.slab.dev-jeb.com # default; override for a self-hosted API
33
+ export SLAB_COLLECTOR=<collector-uuid> # your active collector, reused across commands
34
+ ```
35
+
36
+ ## Quickstart
37
+
38
+ ```bash
39
+ # Catalog + pricing (no collector needed)
40
+ slab card search --subject "Connor Bedard" # find cards
41
+ slab card price <card-uuid> # fair-market value from sales comps
42
+ slab card comps <card-uuid> # the underlying recent sales
43
+ slab set "OPC Platinum" # a set's sealed FMV + top cards
44
+
45
+ # Your collection
46
+ slab collector create # one-time: create your collector profile
47
+ slab collection add # add an owned copy (grade, cost, acquisition)
48
+ slab collection cost # log grading/shipping costs on a copy
49
+ slab break new # record a box/case break
50
+ slab collection dashboard # portfolio value, cost basis, P&L
51
+ slab collection search # search + filter your copies
52
+ ```
53
+
54
+ Run `slab` with no arguments for the full command list, or `slab <group>` (e.g. `slab collection`)
55
+ to see a group's verbs.
56
+
57
+ ## Configuration reference
58
+
59
+ | Env var | Purpose | Default |
60
+ | --------------- | ------------------------------------------ | -------------------------------- |
61
+ | `SLAB_API_KEY` | API key for authentication (**required**) | — |
62
+ | `SLAB_API_URL` | Base URL of the slab API | `https://api.slab.dev-jeb.com` |
63
+ | `SLAB_COLLECTOR`| Active collector UUID, reused per command | — |
64
+
65
+ ## What it depends on
66
+
67
+ Requests and responses are typed against [`slab-schemas`](https://pypi.org/project/slab-schemas/) —
68
+ the same pydantic wire contract the API itself uses — so the CLI and API can never drift.
69
+
70
+ Requires Python 3.13+.
@@ -0,0 +1,28 @@
1
+ slab_cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ slab_cli/banner.py,sha256=Y8daw7PNgC2LL4Gp0CDtqPNzEYAMEpWN5WqpKmMY8NU,3690
3
+ slab_cli/client.py,sha256=QzRW0qqd_yf3DF7GytMkPuApu9cshvE2fFuYyKYhRwg,15928
4
+ slab_cli/commands/__init__.py,sha256=Mzy3oCKEj9ldoTSekQhaIpaSEVr8AmN05ogV1MqmQnM,237
5
+ slab_cli/commands/breaks.py,sha256=hV-wghXIxjIZWghFtPmIt2iCDeEGy85tHx4C2flcZPo,5553
6
+ slab_cli/commands/catalog.py,sha256=cCBFFcz6n-cMXE_nd7l8_iHXP0EBr5JGPOoc6LqS9gw,3092
7
+ slab_cli/commands/collection.py,sha256=dwqBemFtpDaYRNn2U2SwryMap3tQvUiBspkZzTk1BR4,28429
8
+ slab_cli/commands/custom_sets.py,sha256=lQVOqKlppTVduPbMQ5Y524jS9RnI3zGjqfMxTt7Ml6c,18639
9
+ slab_cli/commands/export.py,sha256=Gqv1q0bsB1AyI--_N-k2GsIWwvl_DZqTCz08CkR37QI,5236
10
+ slab_cli/commands/lots.py,sha256=btRzZdxINbsgRFdGwednaTp5fkUMX0iq52vFgVf5v2w,4969
11
+ slab_cli/commands/pricing.py,sha256=bW_c23DF0MKueU73eGLUrIdc2w7Xi9FlwivlQGvJKng,14522
12
+ slab_cli/commands/registry.py,sha256=amLA2_jpLIutam3HGiKhKZgk17z9-4RsP1PBaDDeIHw,7323
13
+ slab_cli/commands/setup.py,sha256=sI3ML7KJNMpjWg4qUVNAgUXYwvlPs8KMAl7zaN-01hA,848
14
+ slab_cli/config.py,sha256=k-g0LNd1QZL_bq_fdPtI-_gp928kt7ECeMEk8AzeUpE,546
15
+ slab_cli/context.py,sha256=bv8M7qw0GAObBlmG0pY7iAWVtDf9LRTvVfoG4HNvPBw,1634
16
+ slab_cli/display.py,sha256=zC5OlI-9_XmfFTMAh-nv-oyYS5Eqtj2X_41B1_LjHAU,49269
17
+ slab_cli/flags.py,sha256=q7o4EEPCkGu4w4VXgoY2ZOGybeV0r5IdCB61fS8KCBI,8550
18
+ slab_cli/help.py,sha256=ByGAY9ncvnzU8AlkXeFdkvIOFAZn_2yMLrgLnssZUTI,3706
19
+ slab_cli/main.py,sha256=FRqPAJZ3wbI18BKfO3xFIwr_U5-izeo0qRbGEHI6nC4,3623
20
+ slab_cli/paging.py,sha256=P02o6GY3ewB8Xnj-8EAGddKnWspil8T1-qC13lpOC1c,1825
21
+ slab_cli/picker.py,sha256=-8emLfy7TsKnHhEKQAA27o-9nQiBUy02btWSZ0flQwI,3248
22
+ slab_cli/prompts.py,sha256=vy4dwuL3ed3V5Yqn9QbX9SpYUUXMNw56WX8PymLXGFE,23452
23
+ slab_cli/sources.py,sha256=rmmA6CTBfZP6IrTRvbGqC5Hd53Eo_LRXPQInXDeCE_M,890
24
+ slab_cli/theme.py,sha256=AxeadKoij-KzN_vyQB08yKzYOeleK71A7c2tAu3ffzA,8084
25
+ slab_cli-0.1.0.dist-info/WHEEL,sha256=iHtWm8nRfs0VRdCYVXocAWFW8ppjHL-uTJkAdZJKOBM,80
26
+ slab_cli-0.1.0.dist-info/entry_points.txt,sha256=R5AFsQ2rL_F_OKAT75PeytsSHtl3BR4eTK8fpJR0yr0,45
27
+ slab_cli-0.1.0.dist-info/METADATA,sha256=WrqhEAItcTm1jgm4LhEuU7PO_7DikLPmelemEpme3JY,2904
28
+ slab_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.9.30
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ slab = slab_cli.main:main
3
+