fablazing-cli 0.2.2__tar.gz → 0.4.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 (22) hide show
  1. {fablazing_cli-0.2.2 → fablazing_cli-0.4.0}/CHANGELOG.md +21 -0
  2. {fablazing_cli-0.2.2 → fablazing_cli-0.4.0}/PKG-INFO +1 -1
  3. {fablazing_cli-0.2.2 → fablazing_cli-0.4.0}/pyproject.toml +1 -1
  4. fablazing_cli-0.4.0/src/fablazing_cli/__init__.py +1 -0
  5. {fablazing_cli-0.2.2 → fablazing_cli-0.4.0}/src/fablazing_cli/commands/card.py +66 -16
  6. {fablazing_cli-0.2.2 → fablazing_cli-0.4.0}/src/fablazing_cli/commands/hero.py +56 -0
  7. {fablazing_cli-0.2.2 → fablazing_cli-0.4.0}/src/fablazing_cli/commands/matchup.py +5 -0
  8. {fablazing_cli-0.2.2 → fablazing_cli-0.4.0}/src/fablazing_cli/commands/meta.py +6 -6
  9. fablazing_cli-0.4.0/src/fablazing_cli/commands/packages.py +62 -0
  10. fablazing_cli-0.4.0/src/fablazing_cli/commands/player.py +52 -0
  11. {fablazing_cli-0.2.2 → fablazing_cli-0.4.0}/src/fablazing_cli/commands/tournaments.py +13 -13
  12. {fablazing_cli-0.2.2 → fablazing_cli-0.4.0}/src/fablazing_cli/main.py +5 -3
  13. {fablazing_cli-0.2.2 → fablazing_cli-0.4.0}/src/fablazing_cli/reference.py +24 -0
  14. fablazing_cli-0.2.2/src/fablazing_cli/__init__.py +0 -1
  15. {fablazing_cli-0.2.2 → fablazing_cli-0.4.0}/.gitignore +0 -0
  16. {fablazing_cli-0.2.2 → fablazing_cli-0.4.0}/README.md +0 -0
  17. {fablazing_cli-0.2.2 → fablazing_cli-0.4.0}/src/fablazing_cli/client.py +0 -0
  18. {fablazing_cli-0.2.2 → fablazing_cli-0.4.0}/src/fablazing_cli/commands/__init__.py +0 -0
  19. {fablazing_cli-0.2.2 → fablazing_cli-0.4.0}/src/fablazing_cli/commands/auth.py +0 -0
  20. {fablazing_cli-0.2.2 → fablazing_cli-0.4.0}/src/fablazing_cli/config.py +0 -0
  21. {fablazing_cli-0.2.2 → fablazing_cli-0.4.0}/src/fablazing_cli/output.py +0 -0
  22. {fablazing_cli-0.2.2 → fablazing_cli-0.4.0}/src/fablazing_cli/resolve.py +0 -0
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.0
4
+
5
+ - `fabz packages`: same-turn card pairs with lift, share, win rate and
6
+ typical assembly turn - new API endpoint
7
+ - `fabz player search` / `player results`: paper tournament players
8
+ - `fabz hero cards` now shows play/block/pitch usage rates and an
9
+ equipment table
10
+ - Documented sub-format ids ("Open") in the reference
11
+ - API: /llms.txt primer on the API domain; unhandled 500s are now
12
+ recorded server-side for review
13
+
14
+ ## 0.3.0
15
+
16
+ - `fabz hero cards`: most-used deck cards for a hero vs the whole field
17
+ (frequency, win rate, Wilson lower bound, copies) - new API endpoint
18
+ - `fabz card pov`: a card's win-rate impact for a hero, overall and per
19
+ opponent - new API endpoint
20
+ - 95% confidence intervals on matchup and hero-cards win rates
21
+ - Tabular API endpoints accept ?output=csv (+ ?api_key= for spreadsheet
22
+ IMPORTDATA use)
23
+
3
24
  ## 0.2.2
4
25
 
5
26
  - Reference: name the exact `meets_threshold` field on tech candidates and
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fablazing-cli
3
- Version: 0.2.2
3
+ Version: 0.4.0
4
4
  Summary: Fablazing analytics from your terminal - Flesh and Blood matchups, meta, cards, and tournaments.
5
5
  Project-URL: Homepage, https://fablazing.com
6
6
  Author: Fablazing
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "fablazing-cli"
7
- version = "0.2.2"
7
+ version = "0.4.0"
8
8
  description = "Fablazing analytics from your terminal - Flesh and Blood matchups, meta, cards, and tournaments."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -0,0 +1 @@
1
+ __version__ = "0.4.0"
@@ -1,4 +1,4 @@
1
- """Card search, metadata, trends, synergies, popularity."""
1
+ """card search / show / trends / synergies / popular"""
2
2
  import typer
3
3
 
4
4
  from .. import client, resolve
@@ -11,11 +11,11 @@ app = typer.Typer(help="Card catalog and analytics")
11
11
  def search(
12
12
  query: str = typer.Argument(..., help="Part of a card name"),
13
13
  limit: int = typer.Option(25, "--limit", min=1, max=100),
14
- json_: bool = typer.Option(False, "--json"),
15
- csv_: bool = typer.Option(False, "--csv"),
14
+ as_json: bool = typer.Option(False, "--json"),
15
+ as_csv: bool = typer.Option(False, "--csv"),
16
16
  ):
17
17
  """Search cards and equipment by name."""
18
- set_mode(json_, csv_)
18
+ set_mode(as_json, as_csv)
19
19
  payload = client.get("/cards/search", {"q": query, "limit": limit})
20
20
  rows = [
21
21
  [
@@ -34,11 +34,11 @@ def search(
34
34
  @app.command()
35
35
  def show(
36
36
  card: str = typer.Argument(..., help="Card id or name"),
37
- json_: bool = typer.Option(False, "--json"),
38
- csv_: bool = typer.Option(False, "--csv"),
37
+ as_json: bool = typer.Option(False, "--json"),
38
+ as_csv: bool = typer.Option(False, "--csv"),
39
39
  ):
40
40
  """Show one card's metadata."""
41
- set_mode(json_, csv_)
41
+ set_mode(as_json, as_csv)
42
42
  card_id = resolve.card_id(card)
43
43
  payload = client.get(f"/cards/{card_id}")
44
44
  data = payload["data"]
@@ -60,11 +60,11 @@ def trends(
60
60
  card: str = typer.Argument(..., help="Card id or name"),
61
61
  format_type: str = typer.Option("cc", "--format"),
62
62
  days: int = typer.Option(30, "--days", min=1, max=365),
63
- json_: bool = typer.Option(False, "--json"),
64
- csv_: bool = typer.Option(False, "--csv"),
63
+ as_json: bool = typer.Option(False, "--json"),
64
+ as_csv: bool = typer.Option(False, "--csv"),
65
65
  ):
66
66
  """Daily play-rate time series."""
67
- set_mode(json_, csv_)
67
+ set_mode(as_json, as_csv)
68
68
  card_id = resolve.card_id(card)
69
69
  payload = client.get(f"/cards/{card_id}/trends", {"format": format_type, "days": days})
70
70
  data = payload["data"]
@@ -92,11 +92,11 @@ def synergies(
92
92
  card: str = typer.Argument(..., help="Card id or name"),
93
93
  format_type: str = typer.Option("cc", "--format"),
94
94
  limit: int = typer.Option(20, "--limit", min=1, max=50),
95
- json_: bool = typer.Option(False, "--json"),
96
- csv_: bool = typer.Option(False, "--csv"),
95
+ as_json: bool = typer.Option(False, "--json"),
96
+ as_csv: bool = typer.Option(False, "--csv"),
97
97
  ):
98
98
  """Co-occurrence synergy partners, with scores."""
99
- set_mode(json_, csv_)
99
+ set_mode(as_json, as_csv)
100
100
  card_id = resolve.card_id(card)
101
101
  payload = client.get(f"/cards/{card_id}/synergies", {"format": format_type, "limit": limit})
102
102
  rows = [
@@ -112,16 +112,66 @@ def synergies(
112
112
  emit(payload, tables=[(f"Synergies for {card_id}", ["ID", "Name", "Score", "Co-occur", "Price"], rows)])
113
113
 
114
114
 
115
+ @app.command()
116
+ def pov(
117
+ hero: str = typer.Argument(..., help="Hero id or name", autocompletion=resolve.complete_hero),
118
+ card: str = typer.Argument(..., help="Card id or name"),
119
+ format_type: str = typer.Option("cc", "--format"),
120
+ days: int = typer.Option(30, "--days", min=1, max=90),
121
+ top: int = typer.Option(12, "--top", min=1, max=50, help="Matchup rows"),
122
+ as_json: bool = typer.Option(False, "--json"),
123
+ as_csv: bool = typer.Option(False, "--csv"),
124
+ ):
125
+ """Card point of view: does running this card change the hero's win
126
+ rate, overall and against which opponents."""
127
+ set_mode(as_json, as_csv)
128
+ hero_id = resolve.hero_id(hero)
129
+ card_id = resolve.card_id(card)
130
+ payload = client.get(
131
+ f"/card-pov/{hero_id}/{card_id}", {"format": format_type, "days": days}
132
+ )
133
+ data = payload["data"]
134
+ overall = data.get("overall_card_impact") or {}
135
+ summary = [
136
+ f"[bold]{card_id}[/bold] on [bold]{hero_id}[/bold] - {format_type}, {days}d, "
137
+ f"{data.get('total_matches_analyzed', overall.get('total_matches', '?'))} matches"
138
+ ]
139
+ if overall:
140
+ summary.append(
141
+ f"presence {pct(overall.get('presence_rate'))} | "
142
+ f"WR with {pct(overall.get('card_win_rate'))} vs without {pct(overall.get('without_card_win_rate'))} "
143
+ f"(impact {overall.get('presence_impact', 0) * 100:+.1f}%)"
144
+ )
145
+ impacts = data.get("matchup_impacts") or []
146
+ rows = [
147
+ [
148
+ m.get("opponent_hero_name", m.get("opponent_hero_id", "")),
149
+ m.get("total_matches", ""),
150
+ pct(m.get("card_presence_rate")),
151
+ pct(m.get("base_win_rate")),
152
+ pct(m.get("card_win_rate")),
153
+ pct(m.get("without_card_win_rate")),
154
+ f"{m.get('presence_impact', 0) * 100:+.1f}%",
155
+ ]
156
+ for m in impacts[:top]
157
+ ]
158
+ emit(
159
+ payload,
160
+ tables=[("Per-matchup impact", ["Opponent", "N", "Presence", "Base WR", "With", "Without", "Impact"], rows)],
161
+ summary=summary,
162
+ )
163
+
164
+
115
165
  @app.command()
116
166
  def popular(
117
167
  format_type: str = typer.Option("cc", "--format"),
118
168
  days: int = typer.Option(7, "--days", min=1, max=365),
119
169
  limit: int = typer.Option(25, "--limit", min=1, max=100),
120
- json_: bool = typer.Option(False, "--json"),
121
- csv_: bool = typer.Option(False, "--csv"),
170
+ as_json: bool = typer.Option(False, "--json"),
171
+ as_csv: bool = typer.Option(False, "--csv"),
122
172
  ):
123
173
  """Most-played cards over the window."""
124
- set_mode(json_, csv_)
174
+ set_mode(as_json, as_csv)
125
175
  payload = client.get("/cards/popular", {"format": format_type, "days": days, "limit": limit})
126
176
  rows = [
127
177
  [
@@ -42,6 +42,62 @@ def show(
42
42
  emit(payload, tables=[(data.get("hero_name", hero_id), ["Field", "Value"], rows)])
43
43
 
44
44
 
45
+ @app.command()
46
+ def cards(
47
+ hero: str = typer.Argument(..., help="Hero id or name", autocompletion=resolve.complete_hero),
48
+ format_type: str = typer.Option("cc", "--format"),
49
+ days: int = typer.Option(30, "--days", min=1, max=90),
50
+ min_freq: float = typer.Option(0.02, "--min-freq", help="Minimum deck frequency"),
51
+ limit: int = typer.Option(30, "--limit", min=1, max=300),
52
+ json_: bool = typer.Option(False, "--json"),
53
+ csv_: bool = typer.Option(False, "--csv"),
54
+ ):
55
+ """Most-used deck cards for a hero across the whole field: frequency,
56
+ win rate, Wilson lower bound, average copies."""
57
+ set_mode(json_, csv_)
58
+ hero_id = resolve.hero_id(hero)
59
+ payload = client.get(
60
+ f"/heroes/{hero_id}/cards",
61
+ {"format": format_type, "days": days, "min_frequency": min_freq, "limit": limit},
62
+ )
63
+ data = payload["data"]
64
+ rows = [
65
+ [
66
+ c.get("card_id", ""),
67
+ pct(c.get("frequency")),
68
+ c.get("matches", ""),
69
+ pct(c.get("win_rate")),
70
+ pct(c.get("wilson_lower_bound")),
71
+ f"{c.get('copies_avg', 0):.1f}",
72
+ pct(c.get("play_rate")),
73
+ pct(c.get("block_rate")),
74
+ pct(c.get("pitch_rate")),
75
+ ]
76
+ for c in data.get("cards", [])
77
+ ]
78
+ ci = data.get("overall_win_rate_ci") or [None, None]
79
+ summary = [
80
+ f"[bold]{resolve.hero_names().get(hero_id, hero_id)}[/bold] vs the field - "
81
+ f"{format_type}, {days}d: {data.get('total_games', 0)} games, "
82
+ f"WR {pct(data.get('overall_win_rate'))} (95% CI {pct(ci[0])}-{pct(ci[1])})"
83
+ ]
84
+ tables = [("Cards", ["Card", "Freq", "N", "WR", "Wilson LB", "Copies", "Play", "Block", "Pitch"], rows)]
85
+ equipment = data.get("equipment") or []
86
+ if equipment:
87
+ equipment_rows = [
88
+ [
89
+ e.get("card_id", ""),
90
+ pct(e.get("frequency")),
91
+ e.get("matches", ""),
92
+ pct(e.get("win_rate")),
93
+ pct(e.get("wilson_lower_bound")),
94
+ ]
95
+ for e in equipment[:15]
96
+ ]
97
+ tables.append(("Equipment", ["Item", "Freq", "N", "WR", "Wilson LB"], equipment_rows))
98
+ emit(payload, tables=tables, summary=summary)
99
+
100
+
45
101
  @app.command()
46
102
  def aggregate(
47
103
  hero: str = typer.Argument(..., help="Hero id or name", autocompletion=resolve.complete_hero),
@@ -50,6 +50,11 @@ def matchup(
50
50
  summary = [
51
51
  f"[bold]{name1}[/bold] vs [bold]{name2}[/bold] - {format_type}, {total} matches"
52
52
  ]
53
+ ci1, ci2 = data.get("deck1_win_rate_ci"), data.get("deck2_win_rate_ci")
54
+ if ci1 and ci2:
55
+ summary.append(
56
+ f"95% CI: {name1} {pct(ci1[0])}-{pct(ci1[1])}, {name2} {pct(ci2[0])}-{pct(ci2[1])}"
57
+ )
53
58
  if total == 0:
54
59
  emit(payload, summary=summary + ["No matches found for these filters."])
55
60
  return
@@ -58,17 +58,17 @@ def movers(
58
58
  {"days": days, "format": format_type, "source": source, "limit": limit, "sort_by": sort_by},
59
59
  )
60
60
  arrows = {"up": "↑", "down": "↓", "stable": "→"}
61
- rows = [
62
- [
61
+ rows = []
62
+ for h in payload["data"].get("heroes", []):
63
+ trend = h.get("trend_direction", "")
64
+ rows.append([
63
65
  h.get("hero_name", h.get("hero_id", "")),
64
66
  h.get("hero_id", ""),
65
- arrows.get(h.get("trend_direction", ""), h.get("trend_direction", "")),
67
+ arrows.get(trend, trend),
66
68
  f"{h.get('percent_change', 0):+.1f}%",
67
69
  f"{h.get('current_popularity', 0):.1f}%",
68
70
  f"{h.get('avg_popularity', 0):.1f}%",
69
- ]
70
- for h in payload["data"].get("heroes", [])
71
- ]
71
+ ])
72
72
  emit(
73
73
  payload,
74
74
  tables=[("Movers", ["Hero", "ID", "Trend", "Change", "Now", "Avg"], rows)],
@@ -0,0 +1,62 @@
1
+ """the packages command: same-turn card pairs with lift"""
2
+ from typing import Optional
3
+
4
+ import typer
5
+
6
+ from .. import client, resolve
7
+ from ..output import emit, set_mode
8
+
9
+
10
+ def packages(
11
+ hero: str = typer.Argument(..., help="Hero id or name", autocompletion=resolve.complete_hero),
12
+ format_type: str = typer.Option("cc", "--format"),
13
+ opponent: Optional[str] = typer.Option(None, "--opponent", help="Rank against this opponent only"),
14
+ seed: Optional[str] = typer.Option(None, "--seed", help="Only packages containing this card"),
15
+ min_lift: float = typer.Option(1.4, "--min-lift"),
16
+ min_games: int = typer.Option(10, "--min-games"),
17
+ sort: str = typer.Option("usage", "--sort", help="usage | lift | win_rate | turn"),
18
+ limit: int = typer.Option(20, "--limit", min=1, max=200),
19
+ as_json: bool = typer.Option(False, "--json"),
20
+ as_csv: bool = typer.Option(False, "--csv"),
21
+ ):
22
+ """Distinctive same-turn card pairs for a hero: how often they land
23
+ together, the lift over chance, and the win rate when they do."""
24
+ set_mode(as_json, as_csv)
25
+ hero_id = resolve.hero_id(hero)
26
+ params = {
27
+ "format": format_type,
28
+ "min_lift": min_lift,
29
+ "min_games": min_games,
30
+ "sort": sort,
31
+ "limit": limit,
32
+ }
33
+ if opponent:
34
+ params["opponent"] = resolve.hero_id(opponent)
35
+ if seed:
36
+ params["seed_card"] = resolve.card_id(seed)
37
+ payload = client.get(f"/packages/{hero_id}", params)
38
+ data = payload["data"]
39
+
40
+ rows = []
41
+ for p in data.get("packages", []):
42
+ pair = p.get("cards") or []
43
+ # the packages store reports pct_games/win_rate in 0-100 units,
44
+ # unlike the rest of the API's 0-1 fractions
45
+ rows.append([
46
+ " + ".join(pair),
47
+ p.get("games_together", ""),
48
+ f"{p.get('pct_games', 0):.1f}%",
49
+ f"{p.get('lift', 0):.2f}x",
50
+ f"{p.get('win_rate', 0):.1f}%",
51
+ p.get("typical_turn", ""),
52
+ ])
53
+ corpus = data.get("corpus") or {}
54
+ summary = [
55
+ f"[bold]{data.get('hero_name', hero_id)}[/bold] packages - {format_type}, "
56
+ f"{corpus.get('games', '?')} games in corpus, sorted by {sort}"
57
+ ]
58
+ emit(
59
+ payload,
60
+ tables=[("Packages", ["Pair", "Games", "Share", "Lift", "WR", "Turn"], rows)],
61
+ summary=summary,
62
+ )
@@ -0,0 +1,52 @@
1
+ """paper tournament players: search + results"""
2
+ import typer
3
+
4
+ from .. import client
5
+ from ..output import emit, set_mode
6
+
7
+ app = typer.Typer(help="Paper tournament players")
8
+
9
+
10
+ @app.command()
11
+ def search(
12
+ query: str = typer.Argument(..., help="Part of a player name"),
13
+ limit: int = typer.Option(25, "--limit", min=1, max=100),
14
+ as_json: bool = typer.Option(False, "--json"),
15
+ as_csv: bool = typer.Option(False, "--csv"),
16
+ ):
17
+ """Search tournament players by name."""
18
+ set_mode(as_json, as_csv)
19
+ payload = client.get("/players/search", {"q": query, "limit": limit})
20
+ rows = [
21
+ [p.get("id", ""), p.get("player_name", ""), p.get("country", "")]
22
+ for p in payload["data"]
23
+ ]
24
+ emit(payload, tables=[("Players", ["ID", "Name", "Country"], rows)])
25
+
26
+
27
+ @app.command()
28
+ def results(
29
+ player_id: str = typer.Argument(..., help="Player id (from `player search`)"),
30
+ limit: int = typer.Option(25, "--limit", min=1, max=200),
31
+ as_json: bool = typer.Option(False, "--json"),
32
+ as_csv: bool = typer.Option(False, "--csv"),
33
+ ):
34
+ """A player's paper tournament results."""
35
+ set_mode(as_json, as_csv)
36
+ payload = client.get(f"/players/{player_id}/results", {"limit": limit})
37
+ rows = []
38
+ for r in payload["data"]:
39
+ wins, losses = r.get("wins"), r.get("losses")
40
+ record = f"{wins}-{losses}" if wins is not None and losses is not None else "-"
41
+ rows.append([
42
+ (r.get("created_at") or "")[:10],
43
+ r.get("tournament_name", ""),
44
+ r.get("format", ""),
45
+ r.get("hero_played") or "-",
46
+ record,
47
+ r.get("standing") or "-",
48
+ ])
49
+ emit(
50
+ payload,
51
+ tables=[("Results", ["Date", "Tournament", "Format", "Hero", "W-L", "Standing"], rows)],
52
+ )
@@ -1,4 +1,4 @@
1
- """Tournament events, final standings, decklists."""
1
+ """tournament events, standings, decklists"""
2
2
  import typer
3
3
 
4
4
  from .. import client
@@ -15,11 +15,11 @@ def list_tournaments(
15
15
  has_decklists: bool = typer.Option(None, "--has-decklists/--no-decklists"),
16
16
  limit: int = typer.Option(25, "--limit", min=1, max=100),
17
17
  offset: int = typer.Option(0, "--offset", min=0),
18
- json_: bool = typer.Option(False, "--json"),
19
- csv_: bool = typer.Option(False, "--csv"),
18
+ as_json: bool = typer.Option(False, "--json"),
19
+ as_csv: bool = typer.Option(False, "--csv"),
20
20
  ):
21
21
  """List tournaments, newest first."""
22
- set_mode(json_, csv_)
22
+ set_mode(as_json, as_csv)
23
23
  payload = client.get(
24
24
  "/tournaments",
25
25
  {
@@ -53,11 +53,11 @@ def list_tournaments(
53
53
  @app.command()
54
54
  def show(
55
55
  event_id: str = typer.Argument(..., help="Tournament id (from `tournaments list`)"),
56
- json_: bool = typer.Option(False, "--json"),
57
- csv_: bool = typer.Option(False, "--csv"),
56
+ as_json: bool = typer.Option(False, "--json"),
57
+ as_csv: bool = typer.Option(False, "--csv"),
58
58
  ):
59
59
  """Show one tournament's details."""
60
- set_mode(json_, csv_)
60
+ set_mode(as_json, as_csv)
61
61
  payload = client.get(f"/tournaments/{event_id}")
62
62
  data = payload["data"]
63
63
  rows = [
@@ -74,11 +74,11 @@ def show(
74
74
  @app.command()
75
75
  def standings(
76
76
  event_id: str = typer.Argument(...),
77
- json_: bool = typer.Option(False, "--json"),
78
- csv_: bool = typer.Option(False, "--csv"),
77
+ as_json: bool = typer.Option(False, "--json"),
78
+ as_csv: bool = typer.Option(False, "--csv"),
79
79
  ):
80
80
  """Winner + top 8 from official final standings."""
81
- set_mode(json_, csv_)
81
+ set_mode(as_json, as_csv)
82
82
  payload = client.get(f"/tournaments/{event_id}/standings")
83
83
  data = payload["data"]
84
84
  rows = [
@@ -98,11 +98,11 @@ def standings(
98
98
  @app.command()
99
99
  def decklists(
100
100
  event_id: str = typer.Argument(...),
101
- json_: bool = typer.Option(False, "--json"),
102
- csv_: bool = typer.Option(False, "--csv"),
101
+ as_json: bool = typer.Option(False, "--json"),
102
+ as_csv: bool = typer.Option(False, "--csv"),
103
103
  ):
104
104
  """Submitted decklists for a tournament."""
105
- set_mode(json_, csv_)
105
+ set_mode(as_json, as_csv)
106
106
  payload = client.get(f"/tournaments/{event_id}/decklists")
107
107
  rows = [
108
108
  [
@@ -5,8 +5,9 @@ import sys
5
5
  import typer
6
6
 
7
7
  from . import __version__
8
- from .commands import auth, card, hero, meta, tournaments
8
+ from .commands import auth, card, hero, meta, player, tournaments
9
9
  from .commands.matchup import matchup
10
+ from .commands.packages import packages
10
11
  from .config import load_config, save_config
11
12
  from .output import console, err_console
12
13
  from .reference import REFERENCE
@@ -27,8 +28,7 @@ def _main():
27
28
 
28
29
 
29
30
  def _completion_tip():
30
- """One-time hint about tab completion. Skipped during shell completion
31
- itself and when not on a terminal."""
31
+ # one-time nudge; skip when running as a shell completer or piped
32
32
  if any(k.endswith("_COMPLETE") for k in os.environ):
33
33
  return
34
34
  cfg = load_config()
@@ -47,7 +47,9 @@ app.add_typer(hero.app, name="hero")
47
47
  app.add_typer(meta.app, name="meta")
48
48
  app.add_typer(card.app, name="card")
49
49
  app.add_typer(tournaments.app, name="tournaments")
50
+ app.add_typer(player.app, name="player")
50
51
  app.command()(matchup)
52
+ app.command()(packages)
51
53
 
52
54
 
53
55
  @app.command()
@@ -56,18 +56,42 @@ analytics and paper tournament results for competitive Flesh and Blood.
56
56
  pairing of the top-N heroes, in one call (never loop matchup for this)
57
57
  - `fablazing hero list` / `hero show EVO001`
58
58
  - `fablazing hero aggregate EVO001 --period 14d --source online`
59
+ - `fablazing hero cards MON119 --days 30` - most-used deck cards for a
60
+ hero vs the whole field: per-card WR, Wilson lower bound, copies, and
61
+ play/block/pitch usage rates, plus an equipment table (live
62
+ aggregation cached up to 4h)
63
+ - `fablazing card pov MON119 "Dread Screamer"` - does running the card
64
+ change the hero's win rate, overall and per opponent (cached up to 4h)
59
65
  - `fablazing card search "sink"` / `card show sink_below_red`
60
66
  - `fablazing card trends sink_below_red --days 30`
61
67
  - `fablazing card synergies sink_below_red`
62
68
  - `fablazing card popular --days 7`
69
+ - `fablazing packages EVO001 --min-lift 1.5 --sort lift` - distinctive
70
+ same-turn card pairs: games together, lift over chance, win rate,
71
+ typical assembly turn. `--opponent` ranks vs one matchup, `--seed`
72
+ filters to pairs containing a card.
73
+ - `fablazing player search "Emily"` / `player results <id>` - paper
74
+ tournament players and their event records.
63
75
  - `fablazing tournaments list --type Calling`
64
76
  - `fablazing tournaments show <id>` / `standings <id>` / `decklists <id>`
65
77
  - `fablazing reference` prints this text.
66
78
 
79
+ ## Sub-formats ("Open")
80
+ Each format family maps to internal ids: cc = 0,1 plus Open 4; sage =
81
+ 14,15 plus Open 16; blitz = 2,3; ll = 8,11; gage = 18. All ids in a
82
+ family are included by default. The API's matchup endpoint accepts
83
+ `sub_formats` (repeatable int) to narrow, e.g. sub_formats=0&sub_formats=1
84
+ for cc without Open.
85
+
67
86
  ## Output
68
87
  - Tables on a terminal, JSON when piped or with --json (the JSON is the
69
88
  full API payload, a superset of the tables), CSV of the primary table
70
89
  with --csv. Data goes to stdout, errors to stderr.
90
+ - Matchup and hero-cards responses include 95% Wilson confidence
91
+ intervals (`*_win_rate_ci`) next to the win rates.
92
+ - Spreadsheets can skip the CLI: tabular API endpoints accept
93
+ `?output=csv&api_key=fbl_live_...` for Google Sheets IMPORTDATA
94
+ (careful: the key is then visible in the sheet).
71
95
 
72
96
  ## Exit codes
73
97
  0 ok, 1 api error, 2 usage error, 3 quota exceeded
@@ -1 +0,0 @@
1
- __version__ = "0.2.2"
File without changes
File without changes