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.
- {fablazing_cli-0.2.2 → fablazing_cli-0.4.0}/CHANGELOG.md +21 -0
- {fablazing_cli-0.2.2 → fablazing_cli-0.4.0}/PKG-INFO +1 -1
- {fablazing_cli-0.2.2 → fablazing_cli-0.4.0}/pyproject.toml +1 -1
- fablazing_cli-0.4.0/src/fablazing_cli/__init__.py +1 -0
- {fablazing_cli-0.2.2 → fablazing_cli-0.4.0}/src/fablazing_cli/commands/card.py +66 -16
- {fablazing_cli-0.2.2 → fablazing_cli-0.4.0}/src/fablazing_cli/commands/hero.py +56 -0
- {fablazing_cli-0.2.2 → fablazing_cli-0.4.0}/src/fablazing_cli/commands/matchup.py +5 -0
- {fablazing_cli-0.2.2 → fablazing_cli-0.4.0}/src/fablazing_cli/commands/meta.py +6 -6
- fablazing_cli-0.4.0/src/fablazing_cli/commands/packages.py +62 -0
- fablazing_cli-0.4.0/src/fablazing_cli/commands/player.py +52 -0
- {fablazing_cli-0.2.2 → fablazing_cli-0.4.0}/src/fablazing_cli/commands/tournaments.py +13 -13
- {fablazing_cli-0.2.2 → fablazing_cli-0.4.0}/src/fablazing_cli/main.py +5 -3
- {fablazing_cli-0.2.2 → fablazing_cli-0.4.0}/src/fablazing_cli/reference.py +24 -0
- fablazing_cli-0.2.2/src/fablazing_cli/__init__.py +0 -1
- {fablazing_cli-0.2.2 → fablazing_cli-0.4.0}/.gitignore +0 -0
- {fablazing_cli-0.2.2 → fablazing_cli-0.4.0}/README.md +0 -0
- {fablazing_cli-0.2.2 → fablazing_cli-0.4.0}/src/fablazing_cli/client.py +0 -0
- {fablazing_cli-0.2.2 → fablazing_cli-0.4.0}/src/fablazing_cli/commands/__init__.py +0 -0
- {fablazing_cli-0.2.2 → fablazing_cli-0.4.0}/src/fablazing_cli/commands/auth.py +0 -0
- {fablazing_cli-0.2.2 → fablazing_cli-0.4.0}/src/fablazing_cli/config.py +0 -0
- {fablazing_cli-0.2.2 → fablazing_cli-0.4.0}/src/fablazing_cli/output.py +0 -0
- {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
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "fablazing-cli"
|
|
7
|
-
version = "0.
|
|
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
|
-
"""
|
|
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
|
-
|
|
15
|
-
|
|
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(
|
|
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
|
-
|
|
38
|
-
|
|
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(
|
|
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
|
-
|
|
64
|
-
|
|
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(
|
|
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
|
-
|
|
96
|
-
|
|
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(
|
|
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
|
-
|
|
121
|
-
|
|
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(
|
|
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(
|
|
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
|
-
"""
|
|
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
|
-
|
|
19
|
-
|
|
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(
|
|
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
|
-
|
|
57
|
-
|
|
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(
|
|
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
|
-
|
|
78
|
-
|
|
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(
|
|
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
|
-
|
|
102
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|