korb 0.2.7__tar.gz → 0.2.9__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 (39) hide show
  1. {korb-0.2.7 → korb-0.2.9}/.gitignore +5 -0
  2. korb-0.2.9/PKG-INFO +203 -0
  3. korb-0.2.9/README.md +178 -0
  4. korb-0.2.9/korb/__init__.py +6 -0
  5. {korb-0.2.7 → korb-0.2.9}/korb/__main__.py +69 -116
  6. {korb-0.2.7 → korb-0.2.9}/korb/core.py +29 -16
  7. {korb-0.2.7 → korb-0.2.9}/korb/ergebnisse.py +2 -4
  8. {korb-0.2.7 → korb-0.2.9}/korb/predict.py +78 -79
  9. {korb-0.2.7 → korb-0.2.9}/korb/schedule.py +34 -65
  10. {korb-0.2.7 → korb-0.2.9}/korb/standings.py +4 -4
  11. {korb-0.2.7 → korb-0.2.9}/korb/team.py +106 -61
  12. {korb-0.2.7 → korb-0.2.9}/pyproject.toml +9 -7
  13. korb-0.2.9/tests/conftest.py +40 -0
  14. {korb-0.2.7 → korb-0.2.9}/tests/test_cli.py +4 -34
  15. {korb-0.2.7 → korb-0.2.9}/tests/test_core.py +3 -2
  16. {korb-0.2.7 → korb-0.2.9}/tests/test_download.py +1 -3
  17. {korb-0.2.7 → korb-0.2.9}/tests/test_predict.py +10 -12
  18. {korb-0.2.7 → korb-0.2.9}/tests/test_schedule.py +6 -5
  19. {korb-0.2.7 → korb-0.2.9}/tests/test_standings.py +3 -3
  20. {korb-0.2.7 → korb-0.2.9}/tests/test_team.py +21 -4
  21. korb-0.2.7/.coverage +0 -0
  22. korb-0.2.7/PKG-INFO +0 -426
  23. korb-0.2.7/README.md +0 -401
  24. korb-0.2.7/korb/__init__.py +0 -6
  25. korb-0.2.7/korb/skills/SKILL_LEAGUE_TOP_N_ANALYSIS.md +0 -100
  26. korb-0.2.7/korb/skills/SKILL_TEAM_ANALYSIS.md +0 -120
  27. korb-0.2.7/korb/skills/__init__.py +0 -28
  28. korb-0.2.7/tests/conftest.py +0 -22
  29. {korb-0.2.7 → korb-0.2.9}/.github/workflows/release.yml +0 -0
  30. {korb-0.2.7 → korb-0.2.9}/.github/workflows/test.yml +0 -0
  31. {korb-0.2.7 → korb-0.2.9}/CHANGELOG.md +0 -0
  32. {korb-0.2.7 → korb-0.2.9}/LICENSE +0 -0
  33. {korb-0.2.7 → korb-0.2.9}/files/.gitkeep +0 -0
  34. {korb-0.2.7 → korb-0.2.9}/korb/py.typed +0 -0
  35. {korb-0.2.7 → korb-0.2.9}/tests/__init__.py +0 -0
  36. {korb-0.2.7 → korb-0.2.9}/tests/fixtures/ergebnisse_minimal.html +0 -0
  37. {korb-0.2.7 → korb-0.2.9}/tests/fixtures/spielplan_finalized.html +0 -0
  38. {korb-0.2.7 → korb-0.2.9}/tests/fixtures/spielplan_minimal.html +0 -0
  39. {korb-0.2.7 → korb-0.2.9}/tests/test_ergebnisse.py +0 -0
@@ -6,6 +6,11 @@ __pycache__/
6
6
  dist/
7
7
  build/
8
8
 
9
+ # Coverage
10
+ .coverage
11
+ .coverage.*
12
+ htmlcov/
13
+
9
14
  # Lock file
10
15
  uv.lock
11
16
 
korb-0.2.9/PKG-INFO ADDED
@@ -0,0 +1,203 @@
1
+ Metadata-Version: 2.4
2
+ Name: korb
3
+ Version: 0.2.9
4
+ Summary: A CLI toolkit for DBB basketball league analysis
5
+ Project-URL: Homepage, https://github.com/malvavisc0/korb
6
+ Project-URL: Repository, https://github.com/malvavisc0/korb
7
+ Project-URL: Issues, https://github.com/malvavisc0/korb/issues
8
+ Author-email: Malvavisc0 <207609879+malvavisc0@users.noreply.github.com>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: basketball,cli,dbb,german-basketball,sports-analytics
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: End Users/Desktop
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Requires-Python: >=3.10
24
+ Description-Content-Type: text/markdown
25
+
26
+ <p align="center">
27
+ <strong>🏀 korb</strong><br>
28
+ <em>A CLI toolkit for DBB basketball league analysis</em>
29
+ </p>
30
+
31
+ <p align="center">
32
+ <img src="https://img.shields.io/badge/python-3.10%2B-blue?logo=python&logoColor=white" alt="Python 3.10+">
33
+ <img src="https://img.shields.io/badge/deps-zero-brightgreen" alt="Zero dependencies">
34
+ <img src="https://img.shields.io/badge/license-MIT-green" alt="MIT License">
35
+ </p>
36
+
37
+ ---
38
+
39
+ **korb** is a zero-dependency Python CLI that parses HTML from the
40
+ [DBB](https://www.basketball-bund.net) (Deutscher Basketball Bund) legacy JSP
41
+ platform and provides standings, results, schedules, predictions, and more.
42
+
43
+ ## Features
44
+
45
+ - 📊 **Standings** — full league table with points, differentials, averages
46
+ - 🏀 **Team** — game-by-game results, sparklines, quality metrics
47
+ - 📋 **Ergebnisse** — all completed game results with optional team filter
48
+ - 📅 **Schedule** — with back-to-back detection, filtering, pending games
49
+ - 🔮 **Predict** — efficiency-model-based final standings forecast
50
+ - 🥇 **Top** — quick leaderboard with ASCII bar chart
51
+ - 📥 **Download** — fetch fresh HTML data directly from basketball-bund.net
52
+ - 🔧 **`--json`** — machine-readable output for all commands
53
+
54
+ ## Installation
55
+
56
+ ```bash
57
+ # With uv (recommended)
58
+ git clone https://github.com/malvavisc0/korb && cd korb
59
+ uv sync
60
+
61
+ # With pip
62
+ pip install .
63
+ ```
64
+
65
+ After installation, the `korb` CLI is available inside the virtual environment.
66
+ Use `uv run korb` to invoke it, or activate the venv first.
67
+
68
+ ## Quick Start
69
+
70
+ ```bash
71
+ # Download league data
72
+ uv run korb --ligaid 51187 download
73
+
74
+ # View standings
75
+ uv run korb --ligaid 51187 standings
76
+
77
+ # Download fresh data + predict in one go
78
+ uv run korb --ligaid 51187 --download predict
79
+ ```
80
+
81
+ ## Finding your Liga ID
82
+
83
+ The `--ligaid` value comes from the `liga_id` parameter in the DBB league URL:
84
+
85
+ ```
86
+ https://www.basketball-bund.net/index.jsp?Action=103&liga_id=51187
87
+ ^^^^^
88
+ this is your liga ID
89
+ ```
90
+
91
+ ## Commands
92
+
93
+ ### `download` — Fetch HTML data
94
+
95
+ ```bash
96
+ uv run korb --ligaid 12345 download
97
+ uv run korb download --all # refresh all previously downloaded leagues
98
+ ```
99
+
100
+ Saves `ergebnisse.html` and `spielplan.html` into `files/<ligaid>/`.
101
+
102
+ ### `standings` — League table
103
+
104
+ ```bash
105
+ uv run korb --ligaid 12345 standings
106
+ ```
107
+
108
+ ### `ergebnisse` — Game results
109
+
110
+ ```bash
111
+ uv run korb --ligaid 12345 ergebnisse
112
+ uv run korb --ligaid 12345 ergebnisse --team "Hawks"
113
+ ```
114
+
115
+ ### `team` — Deep dive on a single team
116
+
117
+ ```bash
118
+ uv run korb --ligaid 12345 team "Thunder"
119
+ uv run korb --ligaid 12345 team "Thunder" --bars --last-k 5 --metrics
120
+ ```
121
+
122
+ ### `schedule` — Game calendar
123
+
124
+ ```bash
125
+ uv run korb --ligaid 12345 schedule --pending
126
+ uv run korb --ligaid 12345 schedule --team "Hawks" --pending --b2b
127
+ ```
128
+
129
+ ### `predict` — Forecast final standings
130
+
131
+ ```bash
132
+ uv run korb --ligaid 12345 predict
133
+ ```
134
+
135
+ ### `top` — Quick leaderboard
136
+
137
+ ```bash
138
+ uv run korb --ligaid 12345 top -n 5
139
+ ```
140
+
141
+ ### Global flags
142
+
143
+ | Flag | Description |
144
+ |---|---|
145
+ | `--json` | Output as JSON instead of formatted tables |
146
+ | `--download`, `-d` | Fetch fresh data before running the command |
147
+ | `--ligaid`, `-l` | Liga ID (resolves file paths automatically) |
148
+ | `--results`, `-r` | Explicit path to HTML results file |
149
+ | `--schedule`, `-s` | Explicit path to HTML schedule file |
150
+ | `--version`, `-V` | Show version |
151
+
152
+ ## How Predictions Work
153
+
154
+ The `predict` command estimates final standings using a **multiplicative efficiency model**:
155
+
156
+ | Factor | How it works |
157
+ |---|---|
158
+ | **Offensive rating** | Team's scored points vs. league average (>1.0 = above avg) |
159
+ | **Defensive rating** | Points allowed vs. league average (>1.0 = worse defense) |
160
+ | **Recency weighting** | 60-day half-life — recent games count more |
161
+ | **Recent form** | Last 5 games blended at 30% weight into ratings |
162
+ | **Home advantage** | 3% scoring boost applied symmetrically |
163
+ | **B2B fatigue** | ≤36h between games → 5% offense/defense penalty |
164
+ | **New teams** | <3 games → ratings blended toward league average |
165
+ | **No draws** | Ties broken by home advantage (basketball has OT) |
166
+
167
+ ## Project Structure
168
+
169
+ ```
170
+ korb/
171
+ ├── __init__.py # Package marker & version
172
+ ├── __main__.py # CLI entry point & download command
173
+ ├── core.py # Shared models, HTML parsing, utilities
174
+ ├── ergebnisse.py # Game results viewer & filter
175
+ ├── predict.py # Multiplicative efficiency prediction model
176
+ ├── schedule.py # HTML schedule parser & filters
177
+ ├── standings.py # Standings calculator
178
+ └── team.py # Team results viewer & metrics
179
+ ```
180
+
181
+ **Requirements:** Python 3.10+ · [uv](https://docs.astral.sh/uv/) · No runtime dependencies
182
+
183
+ > **Note:** Downloaded HTML files and `--ligaid` paths resolve relative to your
184
+ > current working directory (`files/<ligaid>/`). Run `korb` from the project root
185
+ > or pass explicit `--results` / `--schedule` paths.
186
+
187
+ ## Development
188
+
189
+ ```bash
190
+ # Install with dev tools
191
+ uv sync --group dev
192
+
193
+ # Lint & format
194
+ uv run ruff check korb/ tests/
195
+ uv run ruff format korb/ tests/
196
+
197
+ # Run tests
198
+ uv run pytest
199
+ ```
200
+
201
+ ## License
202
+
203
+ [MIT](LICENSE)
korb-0.2.9/README.md ADDED
@@ -0,0 +1,178 @@
1
+ <p align="center">
2
+ <strong>🏀 korb</strong><br>
3
+ <em>A CLI toolkit for DBB basketball league analysis</em>
4
+ </p>
5
+
6
+ <p align="center">
7
+ <img src="https://img.shields.io/badge/python-3.10%2B-blue?logo=python&logoColor=white" alt="Python 3.10+">
8
+ <img src="https://img.shields.io/badge/deps-zero-brightgreen" alt="Zero dependencies">
9
+ <img src="https://img.shields.io/badge/license-MIT-green" alt="MIT License">
10
+ </p>
11
+
12
+ ---
13
+
14
+ **korb** is a zero-dependency Python CLI that parses HTML from the
15
+ [DBB](https://www.basketball-bund.net) (Deutscher Basketball Bund) legacy JSP
16
+ platform and provides standings, results, schedules, predictions, and more.
17
+
18
+ ## Features
19
+
20
+ - 📊 **Standings** — full league table with points, differentials, averages
21
+ - 🏀 **Team** — game-by-game results, sparklines, quality metrics
22
+ - 📋 **Ergebnisse** — all completed game results with optional team filter
23
+ - 📅 **Schedule** — with back-to-back detection, filtering, pending games
24
+ - 🔮 **Predict** — efficiency-model-based final standings forecast
25
+ - 🥇 **Top** — quick leaderboard with ASCII bar chart
26
+ - 📥 **Download** — fetch fresh HTML data directly from basketball-bund.net
27
+ - 🔧 **`--json`** — machine-readable output for all commands
28
+
29
+ ## Installation
30
+
31
+ ```bash
32
+ # With uv (recommended)
33
+ git clone https://github.com/malvavisc0/korb && cd korb
34
+ uv sync
35
+
36
+ # With pip
37
+ pip install .
38
+ ```
39
+
40
+ After installation, the `korb` CLI is available inside the virtual environment.
41
+ Use `uv run korb` to invoke it, or activate the venv first.
42
+
43
+ ## Quick Start
44
+
45
+ ```bash
46
+ # Download league data
47
+ uv run korb --ligaid 51187 download
48
+
49
+ # View standings
50
+ uv run korb --ligaid 51187 standings
51
+
52
+ # Download fresh data + predict in one go
53
+ uv run korb --ligaid 51187 --download predict
54
+ ```
55
+
56
+ ## Finding your Liga ID
57
+
58
+ The `--ligaid` value comes from the `liga_id` parameter in the DBB league URL:
59
+
60
+ ```
61
+ https://www.basketball-bund.net/index.jsp?Action=103&liga_id=51187
62
+ ^^^^^
63
+ this is your liga ID
64
+ ```
65
+
66
+ ## Commands
67
+
68
+ ### `download` — Fetch HTML data
69
+
70
+ ```bash
71
+ uv run korb --ligaid 12345 download
72
+ uv run korb download --all # refresh all previously downloaded leagues
73
+ ```
74
+
75
+ Saves `ergebnisse.html` and `spielplan.html` into `files/<ligaid>/`.
76
+
77
+ ### `standings` — League table
78
+
79
+ ```bash
80
+ uv run korb --ligaid 12345 standings
81
+ ```
82
+
83
+ ### `ergebnisse` — Game results
84
+
85
+ ```bash
86
+ uv run korb --ligaid 12345 ergebnisse
87
+ uv run korb --ligaid 12345 ergebnisse --team "Hawks"
88
+ ```
89
+
90
+ ### `team` — Deep dive on a single team
91
+
92
+ ```bash
93
+ uv run korb --ligaid 12345 team "Thunder"
94
+ uv run korb --ligaid 12345 team "Thunder" --bars --last-k 5 --metrics
95
+ ```
96
+
97
+ ### `schedule` — Game calendar
98
+
99
+ ```bash
100
+ uv run korb --ligaid 12345 schedule --pending
101
+ uv run korb --ligaid 12345 schedule --team "Hawks" --pending --b2b
102
+ ```
103
+
104
+ ### `predict` — Forecast final standings
105
+
106
+ ```bash
107
+ uv run korb --ligaid 12345 predict
108
+ ```
109
+
110
+ ### `top` — Quick leaderboard
111
+
112
+ ```bash
113
+ uv run korb --ligaid 12345 top -n 5
114
+ ```
115
+
116
+ ### Global flags
117
+
118
+ | Flag | Description |
119
+ |---|---|
120
+ | `--json` | Output as JSON instead of formatted tables |
121
+ | `--download`, `-d` | Fetch fresh data before running the command |
122
+ | `--ligaid`, `-l` | Liga ID (resolves file paths automatically) |
123
+ | `--results`, `-r` | Explicit path to HTML results file |
124
+ | `--schedule`, `-s` | Explicit path to HTML schedule file |
125
+ | `--version`, `-V` | Show version |
126
+
127
+ ## How Predictions Work
128
+
129
+ The `predict` command estimates final standings using a **multiplicative efficiency model**:
130
+
131
+ | Factor | How it works |
132
+ |---|---|
133
+ | **Offensive rating** | Team's scored points vs. league average (>1.0 = above avg) |
134
+ | **Defensive rating** | Points allowed vs. league average (>1.0 = worse defense) |
135
+ | **Recency weighting** | 60-day half-life — recent games count more |
136
+ | **Recent form** | Last 5 games blended at 30% weight into ratings |
137
+ | **Home advantage** | 3% scoring boost applied symmetrically |
138
+ | **B2B fatigue** | ≤36h between games → 5% offense/defense penalty |
139
+ | **New teams** | <3 games → ratings blended toward league average |
140
+ | **No draws** | Ties broken by home advantage (basketball has OT) |
141
+
142
+ ## Project Structure
143
+
144
+ ```
145
+ korb/
146
+ ├── __init__.py # Package marker & version
147
+ ├── __main__.py # CLI entry point & download command
148
+ ├── core.py # Shared models, HTML parsing, utilities
149
+ ├── ergebnisse.py # Game results viewer & filter
150
+ ├── predict.py # Multiplicative efficiency prediction model
151
+ ├── schedule.py # HTML schedule parser & filters
152
+ ├── standings.py # Standings calculator
153
+ └── team.py # Team results viewer & metrics
154
+ ```
155
+
156
+ **Requirements:** Python 3.10+ · [uv](https://docs.astral.sh/uv/) · No runtime dependencies
157
+
158
+ > **Note:** Downloaded HTML files and `--ligaid` paths resolve relative to your
159
+ > current working directory (`files/<ligaid>/`). Run `korb` from the project root
160
+ > or pass explicit `--results` / `--schedule` paths.
161
+
162
+ ## Development
163
+
164
+ ```bash
165
+ # Install with dev tools
166
+ uv sync --group dev
167
+
168
+ # Lint & format
169
+ uv run ruff check korb/ tests/
170
+ uv run ruff format korb/ tests/
171
+
172
+ # Run tests
173
+ uv run pytest
174
+ ```
175
+
176
+ ## License
177
+
178
+ [MIT](LICENSE)
@@ -0,0 +1,6 @@
1
+ """Basketball league analysis package for DBB (Deutscher Basketball Bund).
2
+
3
+ Target: DBB Version ≤11.50.1-2f7f327 (legacy JSP platform).
4
+ """
5
+
6
+ __version__ = "0.2.9"
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env python3
2
2
  """Basketball league CLI for DBB (Deutscher Basketball Bund) data.
3
3
 
4
- Target: DBB Version ≤11.50.0-623b018 (legacy JSP platform).
4
+ Target: DBB Version ≤11.50.1-2f7f327 (legacy JSP platform).
5
5
  """
6
6
 
7
7
  import argparse
@@ -26,8 +26,7 @@ from .schedule import (
26
26
  parse_schedule,
27
27
  print_schedule,
28
28
  )
29
- from .skills import SKILL_MAP, get_skill_text
30
- from .standings import calculate_standings, print_table
29
+ from .standings import Standing, calculate_standings, print_table
31
30
  from .team import get_team_results, print_bars, print_metrics, print_results
32
31
 
33
32
 
@@ -39,7 +38,7 @@ def _json_out(data: object) -> None:
39
38
  def _liga_meta(
40
39
  league_info: LeagueInfo,
41
40
  ligaid: int | None,
42
- ) -> dict:
41
+ ) -> dict[str, object]:
43
42
  """Build liga metadata dict for JSON output.
44
43
 
45
44
  Args:
@@ -55,18 +54,41 @@ def _liga_meta(
55
54
  }
56
55
 
57
56
 
57
+ def _resolve_path(
58
+ explicit: str | None, ligaid: int | None, filename: str, flag: str, label: str
59
+ ) -> str:
60
+ """Resolve an HTML path from an explicit flag value or --ligaid.
61
+
62
+ Exits with an error if neither is provided.
63
+ """
64
+ if explicit is not None:
65
+ return explicit
66
+ if ligaid is None:
67
+ print(
68
+ f"Error: pass {flag} PATH or --ligaid LIGAID for {label}.",
69
+ file=sys.stderr,
70
+ )
71
+ sys.exit(1)
72
+ return str(Path("files") / str(ligaid) / filename)
73
+
74
+
75
+ def _resolve_results(args: argparse.Namespace, label: str) -> str:
76
+ """Resolve the results HTML path from --results or --ligaid."""
77
+ return _resolve_path(
78
+ args.results, args.ligaid, "ergebnisse.html", "--results", label
79
+ )
80
+
81
+
82
+ def _resolve_schedule(args: argparse.Namespace, label: str) -> str:
83
+ """Resolve the schedule HTML path from --schedule or --ligaid."""
84
+ return _resolve_path(
85
+ args.schedule, args.ligaid, "spielplan.html", "--schedule", label
86
+ )
87
+
88
+
58
89
  def cmd_standings(args: argparse.Namespace) -> None:
59
90
  """Handle 'standings' subcommand."""
60
- fp = args.results
61
- if fp is None:
62
- if args.ligaid is None:
63
- print(
64
- "Error: pass --results PATH or --ligaid LIGAID " "for standings.",
65
- file=sys.stderr,
66
- )
67
- sys.exit(1)
68
- fp = str(Path("files") / str(args.ligaid) / "ergebnisse.html")
69
-
91
+ fp = _resolve_results(args, "standings")
70
92
  standings, league_info = calculate_standings(fp)
71
93
  if args.json:
72
94
  _json_out(
@@ -81,16 +103,7 @@ def cmd_standings(args: argparse.Namespace) -> None:
81
103
 
82
104
  def cmd_team(args: argparse.Namespace) -> None:
83
105
  """Handle 'team' subcommand."""
84
- fp = args.results
85
- if fp is None:
86
- if args.ligaid is None:
87
- print(
88
- "Error: pass --results PATH or --ligaid LIGAID " "for team results.",
89
- file=sys.stderr,
90
- )
91
- sys.exit(1)
92
- fp = str(Path("files") / str(args.ligaid) / "ergebnisse.html")
93
-
106
+ fp = _resolve_results(args, "team results")
94
107
  results, league_info = get_team_results(args.name, fp)
95
108
  if args.json:
96
109
  data = [r.to_dict() for r in results]
@@ -113,19 +126,9 @@ def cmd_team(args: argparse.Namespace) -> None:
113
126
 
114
127
  def cmd_ergebnisse(args: argparse.Namespace) -> None:
115
128
  """Handle 'ergebnisse' subcommand."""
116
- fp = args.results
117
- if fp is None:
118
- if args.ligaid is None:
119
- print(
120
- "Error: pass --results PATH or --ligaid LIGAID " "for ergebnisse.",
121
- file=sys.stderr,
122
- )
123
- sys.exit(1)
124
- fp = str(Path("files") / str(args.ligaid) / "ergebnisse.html")
125
-
129
+ fp = _resolve_results(args, "ergebnisse")
126
130
  games, league_info = read_games(fp)
127
131
  filtered = filter_ergebnisse(games, team=args.team)
128
- # Sort chronologically (oldest first) for consistent output
129
132
  filtered.sort(key=lambda g: g.date)
130
133
  if args.json:
131
134
  _json_out(
@@ -149,16 +152,7 @@ def cmd_ergebnisse(args: argparse.Namespace) -> None:
149
152
 
150
153
  def cmd_schedule(args: argparse.Namespace) -> None:
151
154
  """Handle 'schedule' subcommand."""
152
- html = args.schedule
153
- if html is None:
154
- if args.ligaid is None:
155
- print(
156
- "Error: pass --schedule PATH or --ligaid LIGAID for schedule.",
157
- file=sys.stderr,
158
- )
159
- sys.exit(1)
160
- html = str(Path("files") / str(args.ligaid) / "spielplan.html")
161
-
155
+ html = _resolve_schedule(args, "schedule")
162
156
  games, league_info = parse_schedule(html)
163
157
  filtered = filter_schedule(
164
158
  games, show_all=args.all, pending=args.pending, team=args.team
@@ -191,7 +185,7 @@ def cmd_predict(args: argparse.Namespace) -> None:
191
185
  sp = sp or str(base / "spielplan.html")
192
186
 
193
187
  # Check if season is finalized
194
- finalized, pending_count = is_season_finalized(sp)
188
+ finalized, _pending_count = is_season_finalized(sp)
195
189
  if finalized:
196
190
  print(
197
191
  "Error: Season is finalized (no pending games). "
@@ -229,32 +223,8 @@ def _blocks(value: int, scale: int) -> str:
229
223
  return "█" * max(0, value // scale)
230
224
 
231
225
 
232
- def cmd_top(args: argparse.Namespace) -> None:
233
- """Handle 'top' subcommand."""
234
-
235
- fp = args.results
236
- if fp is None:
237
- if args.ligaid is None:
238
- print(
239
- "Error: pass --results PATH or --ligaid LIGAID for top.",
240
- file=sys.stderr,
241
- )
242
- sys.exit(1)
243
- fp = str(Path("files") / str(args.ligaid) / "ergebnisse.html")
244
-
245
- standings, league_info = calculate_standings(fp)
246
- top = standings[: args.n]
247
-
248
- if args.json:
249
- _json_out(
250
- {
251
- **_liga_meta(league_info, args.ligaid),
252
- "top": [s.to_dict() for s in top],
253
- }
254
- )
255
- return
256
-
257
- tw = max(4, max(len(s.name) for s in top))
226
+ def _print_top_table(top: list[Standing], tw: int) -> None:
227
+ """Print the ranked top-teams table."""
258
228
  print("\n" + "=" * 70)
259
229
  print(" Top Teams — current table")
260
230
  print("=" * 70)
@@ -264,11 +234,12 @@ def cmd_top(args: argparse.Namespace) -> None:
264
234
  print("-" * len(hdr))
265
235
  for i, s in enumerate(top, 1):
266
236
  st = s.stats
267
- diff = st.diff
268
- diff_str = f"+{diff}" if diff > 0 else str(diff)
237
+ diff_str = f"+{st.diff}" if st.diff > 0 else str(st.diff)
269
238
  print(f"{i:>2} {s.name:<{tw}} {st.gp:>2} {st.pts:>3} {diff_str:>5}")
270
239
 
271
- # Points bar chart (ASCII)
240
+
241
+ def _print_points_chart(top: list[Standing], tw: int) -> None:
242
+ """Print the ASCII points bar chart for the top teams."""
272
243
  pts_vals = [s.stats.pts for s in top]
273
244
  max_pts = max(pts_vals) if pts_vals else 0
274
245
  # Keep chart readable: 1 block per 1–2 points depending on scale.
@@ -276,12 +247,31 @@ def cmd_top(args: argparse.Namespace) -> None:
276
247
  print("\nStandings Points (scaled)")
277
248
  print("─────────────────────────")
278
249
  for s in top:
279
- st = s.stats
280
- pts = st.pts
250
+ pts = s.stats.pts
281
251
  print(f"{s.name:<{tw}} {_blocks(pts, scale):<22} {pts}")
282
252
  print(f"(scale: 1 block ≈ {scale} pts)")
283
253
 
284
254
 
255
+ def cmd_top(args: argparse.Namespace) -> None:
256
+ """Handle 'top' subcommand."""
257
+ fp = _resolve_results(args, "top")
258
+ standings, league_info = calculate_standings(fp)
259
+ top = standings[: args.n]
260
+
261
+ if args.json:
262
+ _json_out(
263
+ {
264
+ **_liga_meta(league_info, args.ligaid),
265
+ "top": [s.to_dict() for s in top],
266
+ }
267
+ )
268
+ return
269
+
270
+ tw = max(4, max(len(s.name) for s in top))
271
+ _print_top_table(top, tw)
272
+ _print_points_chart(top, tw)
273
+
274
+
285
275
  _BASE = "https://www.basketball-bund.net/public"
286
276
  _RESULTS_URL = (
287
277
  f"{_BASE}/ergebnisse.jsp?print=1"
@@ -393,9 +383,8 @@ def _discover_league_ids() -> list[int]:
393
383
  return []
394
384
  ids: list[int] = []
395
385
  for entry in sorted(root.iterdir()):
396
- if entry.is_dir() and entry.name.isdigit():
397
- if any(entry.glob("*.html")):
398
- ids.append(int(entry.name))
386
+ if entry.is_dir() and entry.name.isdigit() and any(entry.glob("*.html")):
387
+ ids.append(int(entry.name))
399
388
  return ids
400
389
 
401
390
 
@@ -423,22 +412,6 @@ def cmd_download(args: argparse.Namespace) -> None:
423
412
  _download(args.ligaid)
424
413
 
425
414
 
426
- def cmd_skill(args: argparse.Namespace) -> None:
427
- """Handle 'skill' subcommand."""
428
- if args.list_skills:
429
- for name, filename in SKILL_MAP.items():
430
- print(f" {name:12s} {filename}")
431
- return
432
- if args.name is None:
433
- print("Error: pass a skill name or --list", file=sys.stderr)
434
- sys.exit(1)
435
- try:
436
- print(get_skill_text(args.name))
437
- except ValueError as exc:
438
- print(f"Error: {exc}", file=sys.stderr)
439
- sys.exit(1)
440
-
441
-
442
415
  def main() -> None:
443
416
  """Entry point for CLI."""
444
417
  parser = argparse.ArgumentParser(
@@ -582,28 +555,8 @@ def main() -> None:
582
555
  )
583
556
  p_dl.set_defaults(func=cmd_download)
584
557
 
585
- p_sk = subs.add_parser(
586
- "skill",
587
- help="Print a skill prompt or list available skills",
588
- )
589
- p_sk.add_argument(
590
- "name",
591
- nargs="?",
592
- default=None,
593
- help="Skill name: analysis or prediction",
594
- )
595
- p_sk.add_argument(
596
- "--list",
597
- "-l",
598
- action="store_true",
599
- dest="list_skills",
600
- help="List available skill names and descriptions",
601
- )
602
- p_sk.set_defaults(func=cmd_skill)
603
-
604
558
  args = parser.parse_args()
605
559
 
606
- # Pre-command download hook
607
560
  if args.download and args.command != "download":
608
561
  if args.ligaid is None:
609
562
  print(