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.
- {korb-0.2.7 → korb-0.2.9}/.gitignore +5 -0
- korb-0.2.9/PKG-INFO +203 -0
- korb-0.2.9/README.md +178 -0
- korb-0.2.9/korb/__init__.py +6 -0
- {korb-0.2.7 → korb-0.2.9}/korb/__main__.py +69 -116
- {korb-0.2.7 → korb-0.2.9}/korb/core.py +29 -16
- {korb-0.2.7 → korb-0.2.9}/korb/ergebnisse.py +2 -4
- {korb-0.2.7 → korb-0.2.9}/korb/predict.py +78 -79
- {korb-0.2.7 → korb-0.2.9}/korb/schedule.py +34 -65
- {korb-0.2.7 → korb-0.2.9}/korb/standings.py +4 -4
- {korb-0.2.7 → korb-0.2.9}/korb/team.py +106 -61
- {korb-0.2.7 → korb-0.2.9}/pyproject.toml +9 -7
- korb-0.2.9/tests/conftest.py +40 -0
- {korb-0.2.7 → korb-0.2.9}/tests/test_cli.py +4 -34
- {korb-0.2.7 → korb-0.2.9}/tests/test_core.py +3 -2
- {korb-0.2.7 → korb-0.2.9}/tests/test_download.py +1 -3
- {korb-0.2.7 → korb-0.2.9}/tests/test_predict.py +10 -12
- {korb-0.2.7 → korb-0.2.9}/tests/test_schedule.py +6 -5
- {korb-0.2.7 → korb-0.2.9}/tests/test_standings.py +3 -3
- {korb-0.2.7 → korb-0.2.9}/tests/test_team.py +21 -4
- korb-0.2.7/.coverage +0 -0
- korb-0.2.7/PKG-INFO +0 -426
- korb-0.2.7/README.md +0 -401
- korb-0.2.7/korb/__init__.py +0 -6
- korb-0.2.7/korb/skills/SKILL_LEAGUE_TOP_N_ANALYSIS.md +0 -100
- korb-0.2.7/korb/skills/SKILL_TEAM_ANALYSIS.md +0 -120
- korb-0.2.7/korb/skills/__init__.py +0 -28
- korb-0.2.7/tests/conftest.py +0 -22
- {korb-0.2.7 → korb-0.2.9}/.github/workflows/release.yml +0 -0
- {korb-0.2.7 → korb-0.2.9}/.github/workflows/test.yml +0 -0
- {korb-0.2.7 → korb-0.2.9}/CHANGELOG.md +0 -0
- {korb-0.2.7 → korb-0.2.9}/LICENSE +0 -0
- {korb-0.2.7 → korb-0.2.9}/files/.gitkeep +0 -0
- {korb-0.2.7 → korb-0.2.9}/korb/py.typed +0 -0
- {korb-0.2.7 → korb-0.2.9}/tests/__init__.py +0 -0
- {korb-0.2.7 → korb-0.2.9}/tests/fixtures/ergebnisse_minimal.html +0 -0
- {korb-0.2.7 → korb-0.2.9}/tests/fixtures/spielplan_finalized.html +0 -0
- {korb-0.2.7 → korb-0.2.9}/tests/fixtures/spielplan_minimal.html +0 -0
- {korb-0.2.7 → korb-0.2.9}/tests/test_ergebnisse.py +0 -0
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)
|
|
@@ -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.
|
|
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 .
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
233
|
-
"""
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|