fablazing-cli 0.3.0__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.3.0 → fablazing_cli-0.4.0}/CHANGELOG.md +11 -0
  2. {fablazing_cli-0.3.0 → fablazing_cli-0.4.0}/PKG-INFO +1 -1
  3. {fablazing_cli-0.3.0 → 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.3.0 → fablazing_cli-0.4.0}/src/fablazing_cli/commands/hero.py +18 -6
  6. fablazing_cli-0.4.0/src/fablazing_cli/commands/packages.py +62 -0
  7. fablazing_cli-0.4.0/src/fablazing_cli/commands/player.py +52 -0
  8. {fablazing_cli-0.3.0 → fablazing_cli-0.4.0}/src/fablazing_cli/main.py +4 -1
  9. {fablazing_cli-0.3.0 → fablazing_cli-0.4.0}/src/fablazing_cli/reference.py +16 -2
  10. fablazing_cli-0.3.0/src/fablazing_cli/__init__.py +0 -1
  11. {fablazing_cli-0.3.0 → fablazing_cli-0.4.0}/.gitignore +0 -0
  12. {fablazing_cli-0.3.0 → fablazing_cli-0.4.0}/README.md +0 -0
  13. {fablazing_cli-0.3.0 → fablazing_cli-0.4.0}/src/fablazing_cli/client.py +0 -0
  14. {fablazing_cli-0.3.0 → fablazing_cli-0.4.0}/src/fablazing_cli/commands/__init__.py +0 -0
  15. {fablazing_cli-0.3.0 → fablazing_cli-0.4.0}/src/fablazing_cli/commands/auth.py +0 -0
  16. {fablazing_cli-0.3.0 → fablazing_cli-0.4.0}/src/fablazing_cli/commands/card.py +0 -0
  17. {fablazing_cli-0.3.0 → fablazing_cli-0.4.0}/src/fablazing_cli/commands/matchup.py +0 -0
  18. {fablazing_cli-0.3.0 → fablazing_cli-0.4.0}/src/fablazing_cli/commands/meta.py +0 -0
  19. {fablazing_cli-0.3.0 → fablazing_cli-0.4.0}/src/fablazing_cli/commands/tournaments.py +0 -0
  20. {fablazing_cli-0.3.0 → fablazing_cli-0.4.0}/src/fablazing_cli/config.py +0 -0
  21. {fablazing_cli-0.3.0 → fablazing_cli-0.4.0}/src/fablazing_cli/output.py +0 -0
  22. {fablazing_cli-0.3.0 → fablazing_cli-0.4.0}/src/fablazing_cli/resolve.py +0 -0
@@ -1,5 +1,16 @@
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
+
3
14
  ## 0.3.0
4
15
 
5
16
  - `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.0
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.3.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"
@@ -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
- f"${c['tcg_market_price']:.2f}" if c.get("tcg_market_price") else "-",
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
- emit(
83
- payload,
84
- tables=[("Cards", ["Card", "Freq", "N", "WR", "Wilson LB", "Copies", "Price"], rows)],
85
- summary=summary,
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()
@@ -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()
@@ -57,18 +57,32 @@ analytics and paper tournament results for competitive Flesh and Blood.
57
57
  - `fablazing hero list` / `hero show EVO001`
58
58
  - `fablazing hero aggregate EVO001 --period 14d --source online`
59
59
  - `fablazing hero cards MON119 --days 30` - most-used deck cards for a
60
- hero vs the whole field, with per-card WR and Wilson lower bound
61
- (deck cards only, no equipment; live aggregation cached up to 4h)
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)
62
63
  - `fablazing card pov MON119 "Dread Screamer"` - does running the card
63
64
  change the hero's win rate, overall and per opponent (cached up to 4h)
64
65
  - `fablazing card search "sink"` / `card show sink_below_red`
65
66
  - `fablazing card trends sink_below_red --days 30`
66
67
  - `fablazing card synergies sink_below_red`
67
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.
68
75
  - `fablazing tournaments list --type Calling`
69
76
  - `fablazing tournaments show <id>` / `standings <id>` / `decklists <id>`
70
77
  - `fablazing reference` prints this text.
71
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
+
72
86
  ## Output
73
87
  - Tables on a terminal, JSON when piped or with --json (the JSON is the
74
88
  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