fablazing-cli 0.1.0__tar.gz → 0.2.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.0/CHANGELOG.md +21 -0
- {fablazing_cli-0.1.0 → fablazing_cli-0.2.0}/PKG-INFO +8 -1
- {fablazing_cli-0.1.0 → fablazing_cli-0.2.0}/README.md +7 -0
- {fablazing_cli-0.1.0 → fablazing_cli-0.2.0}/pyproject.toml +1 -1
- fablazing_cli-0.2.0/src/fablazing_cli/__init__.py +1 -0
- {fablazing_cli-0.1.0 → fablazing_cli-0.2.0}/src/fablazing_cli/commands/auth.py +8 -5
- {fablazing_cli-0.1.0 → fablazing_cli-0.2.0}/src/fablazing_cli/commands/hero.py +2 -2
- {fablazing_cli-0.1.0 → fablazing_cli-0.2.0}/src/fablazing_cli/commands/matchup.py +2 -2
- {fablazing_cli-0.1.0 → fablazing_cli-0.2.0}/src/fablazing_cli/commands/meta.py +38 -0
- {fablazing_cli-0.1.0 → fablazing_cli-0.2.0}/src/fablazing_cli/config.py +1 -1
- {fablazing_cli-0.1.0 → fablazing_cli-0.2.0}/src/fablazing_cli/main.py +26 -1
- {fablazing_cli-0.1.0 → fablazing_cli-0.2.0}/src/fablazing_cli/reference.py +13 -4
- {fablazing_cli-0.1.0 → fablazing_cli-0.2.0}/src/fablazing_cli/resolve.py +17 -0
- fablazing_cli-0.1.0/src/fablazing_cli/__init__.py +0 -1
- {fablazing_cli-0.1.0 → fablazing_cli-0.2.0}/.gitignore +0 -0
- {fablazing_cli-0.1.0 → fablazing_cli-0.2.0}/src/fablazing_cli/client.py +0 -0
- {fablazing_cli-0.1.0 → fablazing_cli-0.2.0}/src/fablazing_cli/commands/__init__.py +0 -0
- {fablazing_cli-0.1.0 → fablazing_cli-0.2.0}/src/fablazing_cli/commands/card.py +0 -0
- {fablazing_cli-0.1.0 → fablazing_cli-0.2.0}/src/fablazing_cli/commands/tournaments.py +0 -0
- {fablazing_cli-0.1.0 → fablazing_cli-0.2.0}/src/fablazing_cli/output.py +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
- `fabz meta matrix`: matchup matrix for the top-N heroes (win rate +
|
|
6
|
+
sample size per pairing) in a single API call
|
|
7
|
+
- `fabz auth status` now shows quota: used/remaining today, reset time,
|
|
8
|
+
burst limit
|
|
9
|
+
- Aggregated endpoints report `data_through` and their refresh schedule;
|
|
10
|
+
documented which commands are realtime vs daily in the reference
|
|
11
|
+
|
|
12
|
+
## 0.1.1
|
|
13
|
+
|
|
14
|
+
- Default API URL fixed to https://api.fablazingdata.com/api/v1
|
|
15
|
+
- Tab completion for hero names (after `--install-completion`)
|
|
16
|
+
- One-time first-run tip about installing completion
|
|
17
|
+
|
|
18
|
+
## 0.1.0
|
|
19
|
+
|
|
20
|
+
- First release: matchup, meta, hero, card, tournaments, auth, reference
|
|
21
|
+
- Table/JSON/CSV output, name-to-id resolution, shell completion support
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fablazing-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.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
|
|
@@ -65,3 +65,10 @@ Exit codes: 0 ok, 1 api error, 2 usage error, 3 quota exceeded.
|
|
|
65
65
|
## Reference
|
|
66
66
|
|
|
67
67
|
`fablazing reference` prints the full command and data reference.
|
|
68
|
+
|
|
69
|
+
## Versioning
|
|
70
|
+
|
|
71
|
+
Semver. The JSON output shape (`schema_version`, `meta`, `data`), exit
|
|
72
|
+
codes, and existing command names/flags only change with a major version
|
|
73
|
+
bump. New commands and new response fields can appear in minor versions.
|
|
74
|
+
See CHANGELOG.md.
|
|
@@ -50,3 +50,10 @@ Exit codes: 0 ok, 1 api error, 2 usage error, 3 quota exceeded.
|
|
|
50
50
|
## Reference
|
|
51
51
|
|
|
52
52
|
`fablazing reference` prints the full command and data reference.
|
|
53
|
+
|
|
54
|
+
## Versioning
|
|
55
|
+
|
|
56
|
+
Semver. The JSON output shape (`schema_version`, `meta`, `data`), exit
|
|
57
|
+
codes, and existing command names/flags only change with a major version
|
|
58
|
+
bump. New commands and new response fields can appear in minor versions.
|
|
59
|
+
See CHANGELOG.md.
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "fablazing-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.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.2.0"
|
|
@@ -33,11 +33,14 @@ def set_url(url: str = typer.Argument(..., help="API base URL (for self-hosted/d
|
|
|
33
33
|
|
|
34
34
|
@app.command()
|
|
35
35
|
def status():
|
|
36
|
-
"""Verify the
|
|
36
|
+
"""Verify the key and show today's usage and limits."""
|
|
37
37
|
key = api_key()
|
|
38
38
|
if not key:
|
|
39
39
|
fail("no API key configured. Run: fablazing auth set-key")
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
data = client.get("/me")["data"]
|
|
41
|
+
console.print(f"[green]OK[/green] key {data.get('key_prefix', '?')}... against {api_url()}")
|
|
42
|
+
console.print(
|
|
43
|
+
f"today: {data.get('used_today', '?')}/{data.get('daily_quota', '?')} requests used, "
|
|
44
|
+
f"{data.get('remaining_today', '?')} left, resets {data.get('resets_at_utc', '?')}"
|
|
45
|
+
)
|
|
46
|
+
console.print(f"burst limit: {data.get('burst_per_minute', '?')} requests/minute")
|
|
@@ -25,7 +25,7 @@ def list_heroes(
|
|
|
25
25
|
|
|
26
26
|
@app.command()
|
|
27
27
|
def show(
|
|
28
|
-
hero: str = typer.Argument(..., help="Hero id or name"),
|
|
28
|
+
hero: str = typer.Argument(..., help="Hero id or name", autocompletion=resolve.complete_hero),
|
|
29
29
|
json_: bool = typer.Option(False, "--json"),
|
|
30
30
|
csv_: bool = typer.Option(False, "--csv"),
|
|
31
31
|
):
|
|
@@ -44,7 +44,7 @@ def show(
|
|
|
44
44
|
|
|
45
45
|
@app.command()
|
|
46
46
|
def aggregate(
|
|
47
|
-
hero: str = typer.Argument(..., help="Hero id or name"),
|
|
47
|
+
hero: str = typer.Argument(..., help="Hero id or name", autocompletion=resolve.complete_hero),
|
|
48
48
|
format_type: str = typer.Option("cc", "--format"),
|
|
49
49
|
source: str = typer.Option("online", "--source", help="online | paper"),
|
|
50
50
|
period: str = typer.Option("14d", "--period", help="7d | 14d | 30d"),
|
|
@@ -8,8 +8,8 @@ from ..output import emit, pct, set_mode
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
def matchup(
|
|
11
|
-
hero1: str = typer.Argument(..., help="Deck 1 hero (id or name)"),
|
|
12
|
-
hero2: str = typer.Argument(..., help="Deck 2 hero (id or name)"),
|
|
11
|
+
hero1: str = typer.Argument(..., help="Deck 1 hero (id or name)", autocompletion=resolve.complete_hero),
|
|
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
14
|
days: int = typer.Option(30, "--days", min=1, max=365),
|
|
15
15
|
start_date: str = typer.Option(None, "--start", help="YYYY-MM-DD (overrides --days)"),
|
|
@@ -74,3 +74,41 @@ def movers(
|
|
|
74
74
|
tables=[("Movers", ["Hero", "ID", "Trend", "Change", "Now", "Avg"], rows)],
|
|
75
75
|
summary=[f"[bold]Meta movers[/bold] - {format_type}/{source}, last {days}d, by {sort_by}"],
|
|
76
76
|
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@app.command()
|
|
80
|
+
def matrix(
|
|
81
|
+
format_type: str = typer.Option("cc", "--format"),
|
|
82
|
+
source: str = typer.Option("online", "--source"),
|
|
83
|
+
period: str = typer.Option("14d", "--period", help="7d | 14d | 30d"),
|
|
84
|
+
top: int = typer.Option(10, "--top", min=2, max=40, help="How many heroes"),
|
|
85
|
+
json_: bool = typer.Option(False, "--json"),
|
|
86
|
+
csv_: bool = typer.Option(False, "--csv"),
|
|
87
|
+
):
|
|
88
|
+
"""Matchup matrix for the top-N most played heroes: win rate and sample
|
|
89
|
+
size for every pairing, one API call."""
|
|
90
|
+
set_mode(json_, csv_)
|
|
91
|
+
payload = client.get(
|
|
92
|
+
"/meta/matrix",
|
|
93
|
+
{"format": format_type, "source": source, "period": period, "top": top},
|
|
94
|
+
)
|
|
95
|
+
heroes = payload["data"]["heroes"]
|
|
96
|
+
ids = [h["hero_id"] for h in heroes]
|
|
97
|
+
|
|
98
|
+
columns = ["Hero", "Games"] + ids
|
|
99
|
+
rows = []
|
|
100
|
+
for h in heroes:
|
|
101
|
+
cells = []
|
|
102
|
+
for opp in ids:
|
|
103
|
+
m = h["matchups"].get(opp)
|
|
104
|
+
cells.append(f"{m['win_rate'] * 100:.0f}%/{m['total_matches']}" if m else "-")
|
|
105
|
+
name = h.get("hero_name", h["hero_id"])
|
|
106
|
+
label = name if len(name) <= 22 else name[:20] + ".."
|
|
107
|
+
rows.append([f"{label} ({h['hero_id']})", h.get("total_matches", "")] + cells)
|
|
108
|
+
|
|
109
|
+
meta_info = payload.get("meta", {})
|
|
110
|
+
summary = [
|
|
111
|
+
f"[bold]Matchup matrix[/bold] - {format_type}/{source}/{period}, top {top}, "
|
|
112
|
+
f"cells are row-hero WR / games (data through {str(meta_info.get('data_through', '?'))[:10]})"
|
|
113
|
+
]
|
|
114
|
+
emit(payload, tables=[("Matrix", columns, rows)], summary=summary)
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
"""Fablazing CLI entry point. `fablazing` and `fabz` both land here."""
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
|
|
2
5
|
import typer
|
|
3
6
|
|
|
4
7
|
from . import __version__
|
|
5
8
|
from .commands import auth, card, hero, meta, tournaments
|
|
6
9
|
from .commands.matchup import matchup
|
|
7
|
-
from .
|
|
10
|
+
from .config import load_config, save_config
|
|
11
|
+
from .output import console, err_console
|
|
8
12
|
from .reference import REFERENCE
|
|
9
13
|
|
|
10
14
|
app = typer.Typer(
|
|
@@ -17,6 +21,27 @@ app = typer.Typer(
|
|
|
17
21
|
context_settings={"help_option_names": ["-h", "--help"]},
|
|
18
22
|
)
|
|
19
23
|
|
|
24
|
+
@app.callback()
|
|
25
|
+
def _main():
|
|
26
|
+
_completion_tip()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _completion_tip():
|
|
30
|
+
"""One-time hint about tab completion. Skipped during shell completion
|
|
31
|
+
itself and when not on a terminal."""
|
|
32
|
+
if any(k.endswith("_COMPLETE") for k in os.environ):
|
|
33
|
+
return
|
|
34
|
+
cfg = load_config()
|
|
35
|
+
if cfg.get("completion_tip_shown") or not sys.stderr.isatty():
|
|
36
|
+
return
|
|
37
|
+
err_console.print(
|
|
38
|
+
"[dim]tip: run `fablazing --install-completion` once to enable tab "
|
|
39
|
+
"completion (takes effect in a new terminal)[/dim]"
|
|
40
|
+
)
|
|
41
|
+
cfg["completion_tip_shown"] = True
|
|
42
|
+
save_config(cfg)
|
|
43
|
+
|
|
44
|
+
|
|
20
45
|
app.add_typer(auth.app, name="auth")
|
|
21
46
|
app.add_typer(hero.app, name="hero")
|
|
22
47
|
app.add_typer(meta.app, name="meta")
|
|
@@ -7,12 +7,14 @@ Terminal client for the Fablazing API: matchup stats, meta breakdowns, card
|
|
|
7
7
|
analytics and paper tournament results for competitive Flesh and Blood.
|
|
8
8
|
`fablazing` and `fabz` are the same binary.
|
|
9
9
|
|
|
10
|
-
## Auth
|
|
10
|
+
## Auth and limits
|
|
11
11
|
- `fablazing auth set-key fbl_live_...` stores your key, or set the
|
|
12
12
|
FABLAZING_API_KEY env var.
|
|
13
|
-
- `fablazing auth status`
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
- `fablazing auth status` shows usage: requests used/left today, reset
|
|
14
|
+
time, burst limit.
|
|
15
|
+
- Standard keys: 2000 requests/day, 60/minute (per-key overrides exist).
|
|
16
|
+
Every response carries X-RateLimit-Limit / -Remaining / -Reset headers.
|
|
17
|
+
Exceeding a limit exits with code 3 and a Retry-After.
|
|
16
18
|
|
|
17
19
|
## IDs and formats
|
|
18
20
|
- Hero ids look like `EVO001` (set code + number).
|
|
@@ -33,6 +35,11 @@ analytics and paper tournament results for competitive Flesh and Blood.
|
|
|
33
35
|
- Tech cards are Wilson-scored against the matchup baseline, so a lucky
|
|
34
36
|
streak on a small sample doesn't make the list.
|
|
35
37
|
- Tournament standings and decklists come from official paper events.
|
|
38
|
+
- Freshness: `matchup` queries live match data (ingested every ~5
|
|
39
|
+
minutes). Meta snapshot/movers and card trends/popular are daily
|
|
40
|
+
aggregates (previous day processed at 03:00 / 02:00 UTC) - their
|
|
41
|
+
responses carry `data_through` in meta so you can see exactly how
|
|
42
|
+
fresh the numbers are.
|
|
36
43
|
|
|
37
44
|
## Commands
|
|
38
45
|
- `fablazing matchup EVO001 "Jarl" --days 30`
|
|
@@ -41,6 +48,8 @@ analytics and paper tournament results for competitive Flesh and Blood.
|
|
|
41
48
|
cards only, not equipment). `--no-dynamics` skips game length stats.
|
|
42
49
|
- `fablazing meta snapshot --format cc --days 14`
|
|
43
50
|
- `fablazing meta movers --days 14`
|
|
51
|
+
- `fablazing meta matrix --top 10` - win rate + sample size for every
|
|
52
|
+
pairing of the top-N heroes, in one call (never loop matchup for this)
|
|
44
53
|
- `fablazing hero list` / `hero show EVO001`
|
|
45
54
|
- `fablazing hero aggregate EVO001 --period 14d --source online`
|
|
46
55
|
- `fablazing card search "sink"` / `card show sink_below_red`
|
|
@@ -61,6 +61,23 @@ def hero_id(text: str) -> str:
|
|
|
61
61
|
fail(f"ambiguous hero '{text}' - did you mean: {options}", 2)
|
|
62
62
|
|
|
63
63
|
|
|
64
|
+
def complete_hero(incomplete: str):
|
|
65
|
+
"""Tab completion for hero arguments. Reads the local cache only; a
|
|
66
|
+
completer must never hit the network or it makes TAB feel broken."""
|
|
67
|
+
path = cache_path(_HEROES_CACHE_FILE)
|
|
68
|
+
try:
|
|
69
|
+
heroes = json.loads(path.read_text(encoding="utf-8"))["heroes"]
|
|
70
|
+
except (OSError, ValueError, KeyError):
|
|
71
|
+
return
|
|
72
|
+
query = incomplete.lower()
|
|
73
|
+
for h in heroes:
|
|
74
|
+
name = h.get("hero_name", "")
|
|
75
|
+
if name.lower().startswith(query):
|
|
76
|
+
yield (name, h["hero_id"])
|
|
77
|
+
elif h["hero_id"].lower().startswith(query):
|
|
78
|
+
yield (h["hero_id"], name)
|
|
79
|
+
|
|
80
|
+
|
|
64
81
|
def card_id(text: str, allow_spec: bool = False) -> str:
|
|
65
82
|
"""Resolve a card name or id; optionally preserve a :gte2-style suffix."""
|
|
66
83
|
candidate = text.strip()
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|