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.
Files changed (20) hide show
  1. fablazing_cli-0.2.0/CHANGELOG.md +21 -0
  2. {fablazing_cli-0.1.0 → fablazing_cli-0.2.0}/PKG-INFO +8 -1
  3. {fablazing_cli-0.1.0 → fablazing_cli-0.2.0}/README.md +7 -0
  4. {fablazing_cli-0.1.0 → fablazing_cli-0.2.0}/pyproject.toml +1 -1
  5. fablazing_cli-0.2.0/src/fablazing_cli/__init__.py +1 -0
  6. {fablazing_cli-0.1.0 → fablazing_cli-0.2.0}/src/fablazing_cli/commands/auth.py +8 -5
  7. {fablazing_cli-0.1.0 → fablazing_cli-0.2.0}/src/fablazing_cli/commands/hero.py +2 -2
  8. {fablazing_cli-0.1.0 → fablazing_cli-0.2.0}/src/fablazing_cli/commands/matchup.py +2 -2
  9. {fablazing_cli-0.1.0 → fablazing_cli-0.2.0}/src/fablazing_cli/commands/meta.py +38 -0
  10. {fablazing_cli-0.1.0 → fablazing_cli-0.2.0}/src/fablazing_cli/config.py +1 -1
  11. {fablazing_cli-0.1.0 → fablazing_cli-0.2.0}/src/fablazing_cli/main.py +26 -1
  12. {fablazing_cli-0.1.0 → fablazing_cli-0.2.0}/src/fablazing_cli/reference.py +13 -4
  13. {fablazing_cli-0.1.0 → fablazing_cli-0.2.0}/src/fablazing_cli/resolve.py +17 -0
  14. fablazing_cli-0.1.0/src/fablazing_cli/__init__.py +0 -1
  15. {fablazing_cli-0.1.0 → fablazing_cli-0.2.0}/.gitignore +0 -0
  16. {fablazing_cli-0.1.0 → fablazing_cli-0.2.0}/src/fablazing_cli/client.py +0 -0
  17. {fablazing_cli-0.1.0 → fablazing_cli-0.2.0}/src/fablazing_cli/commands/__init__.py +0 -0
  18. {fablazing_cli-0.1.0 → fablazing_cli-0.2.0}/src/fablazing_cli/commands/card.py +0 -0
  19. {fablazing_cli-0.1.0 → fablazing_cli-0.2.0}/src/fablazing_cli/commands/tournaments.py +0 -0
  20. {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.1.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.1.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 configured key against the API."""
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
- masked = key[:15] + "..."
41
- heroes = client.get("/heroes")
42
- count = heroes.get("meta", {}).get("count", "?")
43
- console.print(f"[green]OK[/green] key {masked} against {api_url()} ({count} heroes visible)")
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)
@@ -6,7 +6,7 @@ from pathlib import Path
6
6
  from platformdirs import user_cache_dir, user_config_dir
7
7
 
8
8
  APP_NAME = "fablazing"
9
- DEFAULT_API_URL = "https://fablazing.com/api/v1"
9
+ DEFAULT_API_URL = "https://api.fablazingdata.com/api/v1"
10
10
 
11
11
 
12
12
  def config_path() -> Path:
@@ -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 .output import console
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` checks it.
14
- - Keys have a daily quota and a per-minute limit. Hitting either exits
15
- with code 3.
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