fablazing-cli 0.3.0__tar.gz → 0.4.1__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.3.0 → fablazing_cli-0.4.1}/CHANGELOG.md +19 -0
- {fablazing_cli-0.3.0 → fablazing_cli-0.4.1}/PKG-INFO +8 -1
- {fablazing_cli-0.3.0 → fablazing_cli-0.4.1}/README.md +7 -0
- {fablazing_cli-0.3.0 → fablazing_cli-0.4.1}/pyproject.toml +1 -1
- fablazing_cli-0.4.1/src/fablazing_cli/__init__.py +1 -0
- {fablazing_cli-0.3.0 → fablazing_cli-0.4.1}/src/fablazing_cli/commands/hero.py +18 -6
- {fablazing_cli-0.3.0 → fablazing_cli-0.4.1}/src/fablazing_cli/commands/matchup.py +2 -0
- fablazing_cli-0.4.1/src/fablazing_cli/commands/packages.py +62 -0
- fablazing_cli-0.4.1/src/fablazing_cli/commands/player.py +52 -0
- {fablazing_cli-0.3.0 → fablazing_cli-0.4.1}/src/fablazing_cli/main.py +4 -1
- {fablazing_cli-0.3.0 → fablazing_cli-0.4.1}/src/fablazing_cli/reference.py +22 -3
- fablazing_cli-0.3.0/src/fablazing_cli/__init__.py +0 -1
- {fablazing_cli-0.3.0 → fablazing_cli-0.4.1}/.gitignore +0 -0
- {fablazing_cli-0.3.0 → fablazing_cli-0.4.1}/src/fablazing_cli/client.py +0 -0
- {fablazing_cli-0.3.0 → fablazing_cli-0.4.1}/src/fablazing_cli/commands/__init__.py +0 -0
- {fablazing_cli-0.3.0 → fablazing_cli-0.4.1}/src/fablazing_cli/commands/auth.py +0 -0
- {fablazing_cli-0.3.0 → fablazing_cli-0.4.1}/src/fablazing_cli/commands/card.py +0 -0
- {fablazing_cli-0.3.0 → fablazing_cli-0.4.1}/src/fablazing_cli/commands/meta.py +0 -0
- {fablazing_cli-0.3.0 → fablazing_cli-0.4.1}/src/fablazing_cli/commands/tournaments.py +0 -0
- {fablazing_cli-0.3.0 → fablazing_cli-0.4.1}/src/fablazing_cli/config.py +0 -0
- {fablazing_cli-0.3.0 → fablazing_cli-0.4.1}/src/fablazing_cli/output.py +0 -0
- {fablazing_cli-0.3.0 → fablazing_cli-0.4.1}/src/fablazing_cli/resolve.py +0 -0
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.1
|
|
4
|
+
|
|
5
|
+
- `--sub-format` flag on matchup (repeatable) - the API capability was
|
|
6
|
+
documented but unreachable from the CLI
|
|
7
|
+
- Reference: honest per-command format coverage (meta and aggregate
|
|
8
|
+
commands don't cover gage; matchup and card commands do)
|
|
9
|
+
- README: troubleshooting note for interrupted pip upgrades
|
|
10
|
+
|
|
11
|
+
## 0.4.0
|
|
12
|
+
|
|
13
|
+
- `fabz packages`: same-turn card pairs with lift, share, win rate and
|
|
14
|
+
typical assembly turn - new API endpoint
|
|
15
|
+
- `fabz player search` / `player results`: paper tournament players
|
|
16
|
+
- `fabz hero cards` now shows play/block/pitch usage rates and an
|
|
17
|
+
equipment table
|
|
18
|
+
- Documented sub-format ids ("Open") in the reference
|
|
19
|
+
- API: /llms.txt primer on the API domain; unhandled 500s are now
|
|
20
|
+
recorded server-side for review
|
|
21
|
+
|
|
3
22
|
## 0.3.0
|
|
4
23
|
|
|
5
24
|
- `fabz hero cards`: most-used deck cards for a hero vs the whole field
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fablazing-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.1
|
|
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
|
|
@@ -58,6 +58,13 @@ one. Copy count filters work on matchup includes:
|
|
|
58
58
|
Tables on a terminal. JSON when piped or with `--json`, CSV with `--csv`.
|
|
59
59
|
Exit codes: 0 ok, 1 api error, 2 usage error, 3 quota exceeded.
|
|
60
60
|
|
|
61
|
+
## Troubleshooting
|
|
62
|
+
|
|
63
|
+
If an upgrade leaves `fabz` broken (interrupted pip uninstall can strand
|
|
64
|
+
an empty `fablazing_cli` directory and `~ablazing_cli` backup folders):
|
|
65
|
+
|
|
66
|
+
pip install --force-reinstall fablazing-cli
|
|
67
|
+
|
|
61
68
|
## Tab completion
|
|
62
69
|
|
|
63
70
|
fablazing --install-completion
|
|
@@ -43,6 +43,13 @@ one. Copy count filters work on matchup includes:
|
|
|
43
43
|
Tables on a terminal. JSON when piped or with `--json`, CSV with `--csv`.
|
|
44
44
|
Exit codes: 0 ok, 1 api error, 2 usage error, 3 quota exceeded.
|
|
45
45
|
|
|
46
|
+
## Troubleshooting
|
|
47
|
+
|
|
48
|
+
If an upgrade leaves `fabz` broken (interrupted pip uninstall can strand
|
|
49
|
+
an empty `fablazing_cli` directory and `~ablazing_cli` backup folders):
|
|
50
|
+
|
|
51
|
+
pip install --force-reinstall fablazing-cli
|
|
52
|
+
|
|
46
53
|
## Tab completion
|
|
47
54
|
|
|
48
55
|
fablazing --install-completion
|
|
@@ -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.1"
|
|
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.1"
|
|
@@ -69,7 +69,9 @@ def cards(
|
|
|
69
69
|
pct(c.get("win_rate")),
|
|
70
70
|
pct(c.get("wilson_lower_bound")),
|
|
71
71
|
f"{c.get('copies_avg', 0):.1f}",
|
|
72
|
-
|
|
72
|
+
pct(c.get("play_rate")),
|
|
73
|
+
pct(c.get("block_rate")),
|
|
74
|
+
pct(c.get("pitch_rate")),
|
|
73
75
|
]
|
|
74
76
|
for c in data.get("cards", [])
|
|
75
77
|
]
|
|
@@ -79,11 +81,21 @@ def cards(
|
|
|
79
81
|
f"{format_type}, {days}d: {data.get('total_games', 0)} games, "
|
|
80
82
|
f"WR {pct(data.get('overall_win_rate'))} (95% CI {pct(ci[0])}-{pct(ci[1])})"
|
|
81
83
|
]
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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)
|
|
87
99
|
|
|
88
100
|
|
|
89
101
|
@app.command()
|
|
@@ -11,6 +11,7 @@ def matchup(
|
|
|
11
11
|
hero1: str = typer.Argument(..., help="Deck 1 hero (id or name)", autocompletion=resolve.complete_hero),
|
|
12
12
|
hero2: str = typer.Argument(..., help="Deck 2 hero (id or name)", autocompletion=resolve.complete_hero),
|
|
13
13
|
format_type: str = typer.Option("cc", "--format"),
|
|
14
|
+
sub_formats: List[int] = typer.Option([], "--sub-format", help="Restrict to sub-format ids (repeatable), e.g. --sub-format 0 --sub-format 1 for cc without Open"),
|
|
14
15
|
days: int = typer.Option(30, "--days", min=1, max=365),
|
|
15
16
|
start_date: str = typer.Option(None, "--start", help="YYYY-MM-DD (overrides --days)"),
|
|
16
17
|
end_date: str = typer.Option(None, "--end", help="YYYY-MM-DD"),
|
|
@@ -31,6 +32,7 @@ def matchup(
|
|
|
31
32
|
h2 = resolve.hero_id(hero2)
|
|
32
33
|
params = {
|
|
33
34
|
"format": format_type,
|
|
35
|
+
"sub_formats": sub_formats or None,
|
|
34
36
|
"days": days,
|
|
35
37
|
"start_date": start_date,
|
|
36
38
|
"end_date": end_date,
|
|
@@ -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
|
+
)
|
|
@@ -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
|
|
@@ -46,7 +47,9 @@ app.add_typer(hero.app, name="hero")
|
|
|
46
47
|
app.add_typer(meta.app, name="meta")
|
|
47
48
|
app.add_typer(card.app, name="card")
|
|
48
49
|
app.add_typer(tournaments.app, name="tournaments")
|
|
50
|
+
app.add_typer(player.app, name="player")
|
|
49
51
|
app.command()(matchup)
|
|
52
|
+
app.command()(packages)
|
|
50
53
|
|
|
51
54
|
|
|
52
55
|
@app.command()
|
|
@@ -23,7 +23,12 @@ analytics and paper tournament results for competitive Flesh and Blood.
|
|
|
23
23
|
different cards and are counted separately everywhere.
|
|
24
24
|
- Names are accepted anywhere an id is. Ambiguous names error out and
|
|
25
25
|
list the matching ids (exit code 2).
|
|
26
|
-
- Formats: `cc` (default), `blitz`, `ll`, `sage`, `gage
|
|
26
|
+
- Formats: `cc` (default), `blitz`, `ll`, `sage`, `gage` - but coverage
|
|
27
|
+
is per command. matchup, hero cards, card pov and card analytics
|
|
28
|
+
accept all five. meta snapshot/movers cover cc, blitz, ll, sage.
|
|
29
|
+
hero aggregate and meta matrix cover cc, blitz, ll only (their
|
|
30
|
+
pre-computed stores don't process the rest). gage is a tiny corpus
|
|
31
|
+
(a few hundred games total) - expect thin samples everywhere.
|
|
27
32
|
|
|
28
33
|
## Data notes
|
|
29
34
|
- win_rate fields are fractions between 0 and 1.
|
|
@@ -57,18 +62,32 @@ analytics and paper tournament results for competitive Flesh and Blood.
|
|
|
57
62
|
- `fablazing hero list` / `hero show EVO001`
|
|
58
63
|
- `fablazing hero aggregate EVO001 --period 14d --source online`
|
|
59
64
|
- `fablazing hero cards MON119 --days 30` - most-used deck cards for a
|
|
60
|
-
hero vs the whole field
|
|
61
|
-
|
|
65
|
+
hero vs the whole field: per-card WR, Wilson lower bound, copies, and
|
|
66
|
+
play/block/pitch usage rates, plus an equipment table (live
|
|
67
|
+
aggregation cached up to 4h)
|
|
62
68
|
- `fablazing card pov MON119 "Dread Screamer"` - does running the card
|
|
63
69
|
change the hero's win rate, overall and per opponent (cached up to 4h)
|
|
64
70
|
- `fablazing card search "sink"` / `card show sink_below_red`
|
|
65
71
|
- `fablazing card trends sink_below_red --days 30`
|
|
66
72
|
- `fablazing card synergies sink_below_red`
|
|
67
73
|
- `fablazing card popular --days 7`
|
|
74
|
+
- `fablazing packages EVO001 --min-lift 1.5 --sort lift` - distinctive
|
|
75
|
+
same-turn card pairs: games together, lift over chance, win rate,
|
|
76
|
+
typical assembly turn. `--opponent` ranks vs one matchup, `--seed`
|
|
77
|
+
filters to pairs containing a card.
|
|
78
|
+
- `fablazing player search "Emily"` / `player results <id>` - paper
|
|
79
|
+
tournament players and their event records.
|
|
68
80
|
- `fablazing tournaments list --type Calling`
|
|
69
81
|
- `fablazing tournaments show <id>` / `standings <id>` / `decklists <id>`
|
|
70
82
|
- `fablazing reference` prints this text.
|
|
71
83
|
|
|
84
|
+
## Sub-formats ("Open")
|
|
85
|
+
Each format family maps to internal ids: cc = 0,1 plus Open 4; sage =
|
|
86
|
+
14,15 plus Open 16; blitz = 2,3; ll = 8,11; gage = 18. All ids in a
|
|
87
|
+
family are included by default. Narrow with repeatable `--sub-format`
|
|
88
|
+
on matchup (e.g. `--sub-format 0 --sub-format 1` for cc without Open);
|
|
89
|
+
the API parameter is `sub_formats`.
|
|
90
|
+
|
|
72
91
|
## Output
|
|
73
92
|
- Tables on a terminal, JSON when piped or with --json (the JSON is the
|
|
74
93
|
full API payload, a superset of the tables), CSV of the primary table
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.3.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|