korb 0.2.2__tar.gz → 0.2.4__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.2 → korb-0.2.4}/.coverage +0 -0
- {korb-0.2.2 → korb-0.2.4}/CHANGELOG.md +16 -0
- {korb-0.2.2 → korb-0.2.4}/PKG-INFO +18 -3
- {korb-0.2.2 → korb-0.2.4}/README.md +17 -2
- {korb-0.2.2 → korb-0.2.4}/korb/__init__.py +1 -1
- {korb-0.2.2 → korb-0.2.4}/korb/__main__.py +36 -0
- {korb-0.2.2 → korb-0.2.4}/korb/core.py +0 -4
- {korb-0.2.2 → korb-0.2.4}/korb/schedule.py +3 -3
- {korb-0.2.2 → korb-0.2.4/korb}/skills/SKILL_LEAGUE_TOP_N_ANALYSIS.md +1 -2
- {korb-0.2.2 → korb-0.2.4/korb}/skills/SKILL_TEAM_ANALYSIS.md +4 -5
- korb-0.2.4/korb/skills/__init__.py +28 -0
- {korb-0.2.2 → korb-0.2.4}/tests/test_cli.py +38 -1
- {korb-0.2.2 → korb-0.2.4}/tests/test_download.py +2 -9
- {korb-0.2.2 → korb-0.2.4}/tests/test_schedule.py +1 -1
- {korb-0.2.2 → korb-0.2.4}/.github/workflows/release.yml +0 -0
- {korb-0.2.2 → korb-0.2.4}/.github/workflows/test.yml +0 -0
- {korb-0.2.2 → korb-0.2.4}/.gitignore +0 -0
- {korb-0.2.2 → korb-0.2.4}/LICENSE +0 -0
- {korb-0.2.2 → korb-0.2.4}/files/.gitkeep +0 -0
- {korb-0.2.2 → korb-0.2.4}/korb/predict.py +0 -0
- {korb-0.2.2 → korb-0.2.4}/korb/py.typed +0 -0
- {korb-0.2.2 → korb-0.2.4}/korb/standings.py +0 -0
- {korb-0.2.2 → korb-0.2.4}/korb/team.py +0 -0
- {korb-0.2.2 → korb-0.2.4}/pyproject.toml +0 -0
- {korb-0.2.2 → korb-0.2.4}/tests/__init__.py +0 -0
- {korb-0.2.2 → korb-0.2.4}/tests/conftest.py +0 -0
- {korb-0.2.2 → korb-0.2.4}/tests/fixtures/ergebnisse_minimal.html +0 -0
- {korb-0.2.2 → korb-0.2.4}/tests/fixtures/spielplan_finalized.html +0 -0
- {korb-0.2.2 → korb-0.2.4}/tests/fixtures/spielplan_minimal.html +0 -0
- {korb-0.2.2 → korb-0.2.4}/tests/test_core.py +0 -0
- {korb-0.2.2 → korb-0.2.4}/tests/test_predict.py +0 -0
- {korb-0.2.2 → korb-0.2.4}/tests/test_standings.py +0 -0
- {korb-0.2.2 → korb-0.2.4}/tests/test_team.py +0 -0
|
Binary file
|
|
@@ -4,6 +4,22 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/).
|
|
6
6
|
|
|
7
|
+
## [0.2.3] — 2026-04-10
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `skill` subcommand — prints built-in AI skill prompts to stdout.
|
|
12
|
+
- `skill --list` / `-l` flag — lists available skill names and filenames.
|
|
13
|
+
- `korb/skills/` package with `SKILL_MAP` and `get_skill_text()` using `importlib.resources`.
|
|
14
|
+
- Two bundled skills: `analysis` (team deep-dive) and `prediction` (league top-N forecast).
|
|
15
|
+
- Skill markdown files now shipped inside the `korb` package (included in pip distributions).
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Migrated `skills/` from project root into `korb/skills/` for proper wheel inclusion.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
7
23
|
## [0.2.2] — 2026-04-09
|
|
8
24
|
|
|
9
25
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: korb
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: A CLI toolkit for DBB basketball league analysis
|
|
5
5
|
Project-URL: Homepage, https://github.com/malvavisc0/korb
|
|
6
6
|
Project-URL: Repository, https://github.com/malvavisc0/korb
|
|
@@ -47,6 +47,7 @@ A zero-dependency Python CLI that parses HTML from the **DBB** (Deutscher Basket
|
|
|
47
47
|
- 🔮 **Predictions** — efficiency-model-based final standings forecast
|
|
48
48
|
- 🥇 **Top N** — quick leaderboard with ASCII bar chart
|
|
49
49
|
- 📥 **Download** — fetch fresh HTML data directly from basketball-bund.net
|
|
50
|
+
- 🤖 **Skills** — built-in AI skill prompts for team analysis & league prediction
|
|
50
51
|
- 🔧 **JSON output** — pipe-friendly `--json` flag for all commands
|
|
51
52
|
|
|
52
53
|
---
|
|
@@ -161,6 +162,19 @@ Uses a multiplicative efficiency model with recency weighting, recent form blend
|
|
|
161
162
|
uv run korb --ligaid 12345 top -n 5
|
|
162
163
|
```
|
|
163
164
|
|
|
165
|
+
### `skill` — Print AI skill prompts
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
# List available skills
|
|
169
|
+
uv run korb skill --list
|
|
170
|
+
|
|
171
|
+
# Print a specific skill prompt
|
|
172
|
+
uv run korb skill analysis
|
|
173
|
+
uv run korb skill prediction
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Ships two built-in skills: `analysis` (team deep-dive) and `prediction` (league top-N forecast).
|
|
177
|
+
|
|
164
178
|
### `--download` — Fetch fresh data before any command
|
|
165
179
|
|
|
166
180
|
```bash
|
|
@@ -192,18 +206,19 @@ $ uv run korb --help
|
|
|
192
206
|
|
|
193
207
|
usage: korb [-h] [--version] [--results RESULTS] [--schedule SCHEDULE]
|
|
194
208
|
[--json] [--ligaid LIGAID] [--download]
|
|
195
|
-
{standings,team,schedule,predict,top,download} ...
|
|
209
|
+
{standings,team,schedule,predict,top,download,skill} ...
|
|
196
210
|
|
|
197
211
|
Basketball league analysis tools
|
|
198
212
|
|
|
199
213
|
positional arguments:
|
|
200
|
-
{standings,team,schedule,predict,top,download}
|
|
214
|
+
{standings,team,schedule,predict,top,download,skill}
|
|
201
215
|
standings Display league standings
|
|
202
216
|
team Display results for a team
|
|
203
217
|
schedule Display game schedule
|
|
204
218
|
predict Predict final standings
|
|
205
219
|
top Show top teams from standings
|
|
206
220
|
download Download results & schedule HTML
|
|
221
|
+
skill Print a skill prompt or list available skills
|
|
207
222
|
|
|
208
223
|
options:
|
|
209
224
|
-h, --help show this help message and exit
|
|
@@ -22,6 +22,7 @@ A zero-dependency Python CLI that parses HTML from the **DBB** (Deutscher Basket
|
|
|
22
22
|
- 🔮 **Predictions** — efficiency-model-based final standings forecast
|
|
23
23
|
- 🥇 **Top N** — quick leaderboard with ASCII bar chart
|
|
24
24
|
- 📥 **Download** — fetch fresh HTML data directly from basketball-bund.net
|
|
25
|
+
- 🤖 **Skills** — built-in AI skill prompts for team analysis & league prediction
|
|
25
26
|
- 🔧 **JSON output** — pipe-friendly `--json` flag for all commands
|
|
26
27
|
|
|
27
28
|
---
|
|
@@ -136,6 +137,19 @@ Uses a multiplicative efficiency model with recency weighting, recent form blend
|
|
|
136
137
|
uv run korb --ligaid 12345 top -n 5
|
|
137
138
|
```
|
|
138
139
|
|
|
140
|
+
### `skill` — Print AI skill prompts
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
# List available skills
|
|
144
|
+
uv run korb skill --list
|
|
145
|
+
|
|
146
|
+
# Print a specific skill prompt
|
|
147
|
+
uv run korb skill analysis
|
|
148
|
+
uv run korb skill prediction
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Ships two built-in skills: `analysis` (team deep-dive) and `prediction` (league top-N forecast).
|
|
152
|
+
|
|
139
153
|
### `--download` — Fetch fresh data before any command
|
|
140
154
|
|
|
141
155
|
```bash
|
|
@@ -167,18 +181,19 @@ $ uv run korb --help
|
|
|
167
181
|
|
|
168
182
|
usage: korb [-h] [--version] [--results RESULTS] [--schedule SCHEDULE]
|
|
169
183
|
[--json] [--ligaid LIGAID] [--download]
|
|
170
|
-
{standings,team,schedule,predict,top,download} ...
|
|
184
|
+
{standings,team,schedule,predict,top,download,skill} ...
|
|
171
185
|
|
|
172
186
|
Basketball league analysis tools
|
|
173
187
|
|
|
174
188
|
positional arguments:
|
|
175
|
-
{standings,team,schedule,predict,top,download}
|
|
189
|
+
{standings,team,schedule,predict,top,download,skill}
|
|
176
190
|
standings Display league standings
|
|
177
191
|
team Display results for a team
|
|
178
192
|
schedule Display game schedule
|
|
179
193
|
predict Predict final standings
|
|
180
194
|
top Show top teams from standings
|
|
181
195
|
download Download results & schedule HTML
|
|
196
|
+
skill Print a skill prompt or list available skills
|
|
182
197
|
|
|
183
198
|
options:
|
|
184
199
|
-h, --help show this help message and exit
|
|
@@ -25,6 +25,7 @@ from .schedule import (
|
|
|
25
25
|
parse_schedule,
|
|
26
26
|
print_schedule,
|
|
27
27
|
)
|
|
28
|
+
from .skills import SKILL_MAP, get_skill_text
|
|
28
29
|
from .standings import calculate_standings, print_table
|
|
29
30
|
from .team import get_team_results, print_bars, print_metrics, print_results
|
|
30
31
|
|
|
@@ -352,6 +353,22 @@ def cmd_download(args: argparse.Namespace) -> None:
|
|
|
352
353
|
_download(args.ligaid)
|
|
353
354
|
|
|
354
355
|
|
|
356
|
+
def cmd_skill(args: argparse.Namespace) -> None:
|
|
357
|
+
"""Handle 'skill' subcommand."""
|
|
358
|
+
if args.list_skills:
|
|
359
|
+
for name, filename in SKILL_MAP.items():
|
|
360
|
+
print(f" {name:12s} {filename}")
|
|
361
|
+
return
|
|
362
|
+
if args.name is None:
|
|
363
|
+
print("Error: pass a skill name or --list", file=sys.stderr)
|
|
364
|
+
sys.exit(1)
|
|
365
|
+
try:
|
|
366
|
+
print(get_skill_text(args.name))
|
|
367
|
+
except ValueError as exc:
|
|
368
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
369
|
+
sys.exit(1)
|
|
370
|
+
|
|
371
|
+
|
|
355
372
|
def main() -> None:
|
|
356
373
|
"""Entry point for CLI."""
|
|
357
374
|
parser = argparse.ArgumentParser(
|
|
@@ -478,6 +495,25 @@ def main() -> None:
|
|
|
478
495
|
)
|
|
479
496
|
p_dl.set_defaults(func=cmd_download)
|
|
480
497
|
|
|
498
|
+
p_sk = subs.add_parser(
|
|
499
|
+
"skill",
|
|
500
|
+
help="Print a skill prompt or list available skills",
|
|
501
|
+
)
|
|
502
|
+
p_sk.add_argument(
|
|
503
|
+
"name",
|
|
504
|
+
nargs="?",
|
|
505
|
+
default=None,
|
|
506
|
+
help="Skill name: analysis or prediction",
|
|
507
|
+
)
|
|
508
|
+
p_sk.add_argument(
|
|
509
|
+
"--list",
|
|
510
|
+
"-l",
|
|
511
|
+
action="store_true",
|
|
512
|
+
dest="list_skills",
|
|
513
|
+
help="List available skill names and descriptions",
|
|
514
|
+
)
|
|
515
|
+
p_sk.set_defaults(func=cmd_skill)
|
|
516
|
+
|
|
481
517
|
args = parser.parse_args()
|
|
482
518
|
|
|
483
519
|
# Pre-command download hook
|
|
@@ -182,10 +182,6 @@ def extract_league_info(html: str) -> LeagueInfo:
|
|
|
182
182
|
return LeagueInfo(name=name, number=number)
|
|
183
183
|
|
|
184
184
|
|
|
185
|
-
# Backward-compatible alias
|
|
186
|
-
extract_league_name = extract_league_info
|
|
187
|
-
|
|
188
|
-
|
|
189
185
|
def read_games(filepath: str) -> tuple[list[Game], LeagueInfo]:
|
|
190
186
|
"""Read all valid games from HTML results file.
|
|
191
187
|
|
|
@@ -126,19 +126,19 @@ class _HTMLScheduleParser(HTMLParser):
|
|
|
126
126
|
|
|
127
127
|
|
|
128
128
|
def parse_schedule(html_file: str) -> tuple[list[ScheduledGame], LeagueInfo]:
|
|
129
|
-
"""Parse HTML file into scheduled games, sorted
|
|
129
|
+
"""Parse HTML file into scheduled games, sorted chronologically.
|
|
130
130
|
|
|
131
131
|
Args:
|
|
132
132
|
html_file: Path to HTML schedule file.
|
|
133
133
|
|
|
134
134
|
Returns:
|
|
135
|
-
Tuple of (games sorted by date
|
|
135
|
+
Tuple of (games sorted by date ascending, league_info).
|
|
136
136
|
"""
|
|
137
137
|
content = read_file_safe(html_file)
|
|
138
138
|
league_info = extract_league_info(content)
|
|
139
139
|
parser = _HTMLScheduleParser()
|
|
140
140
|
parser.feed(content)
|
|
141
|
-
parser.games.sort(key=lambda g: g.date
|
|
141
|
+
parser.games.sort(key=lambda g: g.date)
|
|
142
142
|
return parser.games, league_info
|
|
143
143
|
|
|
144
144
|
|
|
@@ -66,11 +66,11 @@ uv run korb --json --ligaid <LIGA_ID> team "<TEAM_NAME>"
|
|
|
66
66
|
|
|
67
67
|
Important: in JSON mode, the CLI returns **all matching games** and does **not** apply `--last-k` slicing or `--metrics` printing logic.
|
|
68
68
|
|
|
69
|
-
So to emulate
|
|
69
|
+
So to emulate "last 5 games":
|
|
70
70
|
|
|
71
71
|
1. Take `team_json["results"]`.
|
|
72
72
|
2. Treat it as **newest-first**.
|
|
73
|
-
3. Use the first 5 items as the
|
|
73
|
+
3. Use the first 5 items as the "last 5". If there are fewer than 5 games, use all available.
|
|
74
74
|
|
|
75
75
|
Compute:
|
|
76
76
|
|
|
@@ -92,7 +92,7 @@ uv run korb --json --ligaid <LIGA_ID> schedule --pending
|
|
|
92
92
|
uv run korb --json --ligaid <LIGA_ID> predict
|
|
93
93
|
```
|
|
94
94
|
|
|
95
|
-
From `predict_json["standings"]` extract the team
|
|
95
|
+
From `predict_json["standings"]` extract the team's **predicted rank** as `index_in_list + 1`.
|
|
96
96
|
|
|
97
97
|
---
|
|
98
98
|
|
|
@@ -117,5 +117,4 @@ Write the paragraph in the selected `LANGUAGE`.
|
|
|
117
117
|
|
|
118
118
|
## Output
|
|
119
119
|
|
|
120
|
-
Return the paragraph directly. Do **not** save to a file.
|
|
121
|
-
|
|
120
|
+
Return the paragraph directly. Do **not** save to a file.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Skill definitions shipped with the korb package."""
|
|
2
|
+
|
|
3
|
+
from importlib.resources import files
|
|
4
|
+
|
|
5
|
+
SKILL_MAP: dict[str, str] = {
|
|
6
|
+
"analysis": "SKILL_TEAM_ANALYSIS.md",
|
|
7
|
+
"prediction": "SKILL_LEAGUE_TOP_N_ANALYSIS.md",
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_skill_text(name: str) -> str:
|
|
12
|
+
"""Return the markdown text of a named skill.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
name: Skill key from SKILL_MAP (e.g. "analysis", "prediction").
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
The full markdown content of the skill file.
|
|
19
|
+
|
|
20
|
+
Raises:
|
|
21
|
+
ValueError: If *name* is not in SKILL_MAP.
|
|
22
|
+
"""
|
|
23
|
+
if name not in SKILL_MAP:
|
|
24
|
+
raise ValueError(
|
|
25
|
+
f"unknown skill: {name!r}. Choose from: {', '.join(SKILL_MAP)}"
|
|
26
|
+
)
|
|
27
|
+
filename = SKILL_MAP[name]
|
|
28
|
+
return files(__package__).joinpath(filename).read_text(encoding="utf-8")
|
|
@@ -4,7 +4,13 @@ import argparse
|
|
|
4
4
|
|
|
5
5
|
import pytest
|
|
6
6
|
|
|
7
|
-
from korb.__main__ import
|
|
7
|
+
from korb.__main__ import (
|
|
8
|
+
cmd_predict,
|
|
9
|
+
cmd_schedule,
|
|
10
|
+
cmd_skill,
|
|
11
|
+
cmd_standings,
|
|
12
|
+
cmd_team,
|
|
13
|
+
)
|
|
8
14
|
|
|
9
15
|
|
|
10
16
|
def _make_args(**kwargs) -> argparse.Namespace:
|
|
@@ -167,3 +173,34 @@ class TestCmdPredict:
|
|
|
167
173
|
cmd_predict(args)
|
|
168
174
|
captured = capsys.readouterr()
|
|
169
175
|
assert "Predicted" in captured.out
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class TestCmdSkill:
|
|
179
|
+
def test_analysis_skill(self, capsys):
|
|
180
|
+
args = _make_args(name="analysis", list_skills=False)
|
|
181
|
+
cmd_skill(args)
|
|
182
|
+
out = capsys.readouterr().out
|
|
183
|
+
assert "Team Analysis" in out
|
|
184
|
+
|
|
185
|
+
def test_prediction_skill(self, capsys):
|
|
186
|
+
args = _make_args(name="prediction", list_skills=False)
|
|
187
|
+
cmd_skill(args)
|
|
188
|
+
out = capsys.readouterr().out
|
|
189
|
+
assert "League Prediction" in out
|
|
190
|
+
|
|
191
|
+
def test_list_skills(self, capsys):
|
|
192
|
+
args = _make_args(name=None, list_skills=True)
|
|
193
|
+
cmd_skill(args)
|
|
194
|
+
out = capsys.readouterr().out
|
|
195
|
+
assert "analysis" in out
|
|
196
|
+
assert "prediction" in out
|
|
197
|
+
|
|
198
|
+
def test_no_args(self):
|
|
199
|
+
args = _make_args(name=None, list_skills=False)
|
|
200
|
+
with pytest.raises(SystemExit):
|
|
201
|
+
cmd_skill(args)
|
|
202
|
+
|
|
203
|
+
def test_invalid_skill(self):
|
|
204
|
+
args = _make_args(name="nonexistent", list_skills=False)
|
|
205
|
+
with pytest.raises(SystemExit):
|
|
206
|
+
cmd_skill(args)
|
|
@@ -4,7 +4,6 @@ import gzip
|
|
|
4
4
|
import zlib
|
|
5
5
|
from unittest.mock import MagicMock, patch
|
|
6
6
|
|
|
7
|
-
|
|
8
7
|
from korb.__main__ import _DELAY_MAX, _DELAY_MIN, _HEADERS, _read_response
|
|
9
8
|
|
|
10
9
|
|
|
@@ -46,7 +45,6 @@ class TestHeaders:
|
|
|
46
45
|
assert _HEADERS["DNT"] == "1"
|
|
47
46
|
|
|
48
47
|
|
|
49
|
-
|
|
50
48
|
def _fake_response(body: bytes, encoding: str = "") -> MagicMock:
|
|
51
49
|
"""Build a mock HTTP response with the given body & encoding."""
|
|
52
50
|
resp = MagicMock()
|
|
@@ -90,7 +88,6 @@ class TestReadResponse:
|
|
|
90
88
|
assert _read_response(resp) == raw
|
|
91
89
|
|
|
92
90
|
|
|
93
|
-
|
|
94
91
|
class TestDownload:
|
|
95
92
|
"""Verify _download sends correct headers and sleeps between requests."""
|
|
96
93
|
|
|
@@ -120,9 +117,7 @@ class TestDownload:
|
|
|
120
117
|
# Use real tmp_path for file writes
|
|
121
118
|
dest_a = tmp_path / "ergebnisse.html"
|
|
122
119
|
dest_b = tmp_path / "spielplan.html"
|
|
123
|
-
mock_liga.__truediv__ = MagicMock(
|
|
124
|
-
side_effect=[dest_a, dest_b]
|
|
125
|
-
)
|
|
120
|
+
mock_liga.__truediv__ = MagicMock(side_effect=[dest_a, dest_b])
|
|
126
121
|
mock_root.__truediv__ = MagicMock(return_value=mock_liga)
|
|
127
122
|
mock_liga.mkdir = MagicMock()
|
|
128
123
|
|
|
@@ -158,9 +153,7 @@ class TestDownload:
|
|
|
158
153
|
|
|
159
154
|
dest_a = tmp_path / "ergebnisse.html"
|
|
160
155
|
dest_b = tmp_path / "spielplan.html"
|
|
161
|
-
mock_liga.__truediv__ = MagicMock(
|
|
162
|
-
side_effect=[dest_a, dest_b]
|
|
163
|
-
)
|
|
156
|
+
mock_liga.__truediv__ = MagicMock(side_effect=[dest_a, dest_b])
|
|
164
157
|
mock_root.__truediv__ = MagicMock(return_value=mock_liga)
|
|
165
158
|
mock_liga.mkdir = MagicMock()
|
|
166
159
|
|
|
@@ -66,7 +66,7 @@ class TestParseSchedule:
|
|
|
66
66
|
def test_sorting_by_date(self, spielplan_path):
|
|
67
67
|
games, _ = parse_schedule(spielplan_path)
|
|
68
68
|
for i in range(len(games) - 1):
|
|
69
|
-
assert games[i].date
|
|
69
|
+
assert games[i].date <= games[i + 1].date
|
|
70
70
|
|
|
71
71
|
def test_missing_file_raises(self, tmp_path):
|
|
72
72
|
with pytest.raises(SystemExit):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|