fplkit 2.5.0__tar.gz → 2.6.1__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.
- {fplkit-2.5.0 → fplkit-2.6.1}/CHANGELOG.md +50 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/PKG-INFO +1 -1
- {fplkit-2.5.0 → fplkit-2.6.1}/docs/architecture.md +4 -4
- {fplkit-2.5.0 → fplkit-2.6.1}/docs/command-reference.md +31 -11
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/_version.py +2 -2
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/api/providers/_models.py +25 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/api/providers/anthropic.py +19 -1
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/api/understat.py +228 -62
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/_context.py +80 -2
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/_review_summarisation.py +52 -12
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/doctor.py +71 -27
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/doctor_providers.py +11 -3
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/ratings.py +1 -1
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/config/defaults.yaml +1 -1
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/prompts/review.py +35 -12
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/season.py +24 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/services/returnee_radar.py +7 -3
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/services/team_ratings.py +61 -6
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/templates/gw_review.md.j2 +9 -4
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_api_understat.py +235 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_doctor.py +196 -9
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_doctor_providers.py +15 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_format.py +85 -1
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_review.py +123 -7
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_paths.py +88 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_providers.py +107 -1
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_report_agent.py +62 -3
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_returnee_radar.py +41 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_review.py +43 -1
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_season.py +21 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_team_ratings.py +127 -1
- {fplkit-2.5.0 → fplkit-2.6.1}/.agents/README.md +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/.agents/TOOLS.md +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/.agents/skills/build-test-plan/SKILL.md +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/.agents/skills/build-test-plan/references/plan-template.md +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/.agents/skills/build-test-plan/references/workspace-env.md +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/.agents/skills/create-pr/SKILL.md +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/.agents/skills/gw-prep/SKILL.md +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/.agents/skills/gw-prep/references/entity-normalisation.md +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/.agents/skills/gw-prep/references/output-template.md +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/.agents/skills/gw-prep/references/rules.md +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/.agents/skills/gw-prep/scripts/_bootstrap.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/.agents/skills/gw-prep/scripts/bench_order.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/.agents/skills/gw-prep/scripts/extract_classic_squad.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/.agents/skills/gw-prep/scripts/normalise_entities.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/.agents/skills/gw-prep/scripts/starting_xi.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/.agents/skills/gw-prep/scripts/transfer_eval.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/.agents/skills/gw-prep/scripts/validate_draft_waivers.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/.agents/skills/preview-ingest/SKILL.md +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/.agents/skills/release/SKILL.md +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/.agents/skills/release-notes/SKILL.md +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/.agents/skills/squad-builder/SKILL.md +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/.agents/skills/squad-builder/references/output-template.md +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/.agents/skills/squad-builder/references/rules.md +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/.agents/skills/update-gw-prep/SKILL.md +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/.claude/output-styles/fpl-mate.md +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/.claude/settings.json +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/.env.example +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/.github/workflows/ci.yml +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/.github/workflows/pr-title.yml +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/.github/workflows/release.yml +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/.gitignore +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/AGENTS.md +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/CLAUDE.md +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/CONTRIBUTING.md +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/LICENSE +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/README.md +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/cliff.toml +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/config/team_ratings_overrides.yaml +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/docs/custom-analysis.md +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/docs/fpl-rules.md +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/docs/images/fpl-player-demo-v1-0.png +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/__init__.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/agents/__init__.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/agents/action/__init__.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/agents/action/waiver.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/agents/analysis/__init__.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/agents/analysis/bench_order.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/agents/analysis/captain.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/agents/analysis/squad_analyzer.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/agents/analysis/starting_xi.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/agents/analysis/stats.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/agents/analysis/transfer_eval.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/agents/base.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/agents/common.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/agents/data/__init__.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/agents/data/fixture.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/agents/data/price.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/agents/data/scout.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/agents/orchestration/__init__.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/agents/orchestration/report.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/api/__init__.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/api/contract.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/api/core_insights.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/api/dataset_fetcher.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/api/football_data.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/api/fpl.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/api/fpl_draft.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/api/historical.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/api/historical_types.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/api/providers/__init__.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/api/providers/_http.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/api/providers/openai_compat.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/api/providers/perplexity.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/api/vaastav.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/__init__.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/_banner.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/_fines.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/_fines_config.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/_helpers.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/_json.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/_league_recap_data.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/_league_recap_history.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/_league_recap_types.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/_plan_grid.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/_review_analysis.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/_review_classic.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/_review_draft.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/allocate.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/captain.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/chips.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/credentials.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/differentials.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/fdr.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/fixtures.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/history.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/init.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/intel.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/league.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/league_fines.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/league_recap.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/player.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/preview.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/price_changes.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/price_history.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/returnees.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/review.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/sell_prices.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/squad.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/stats.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/status.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/targets.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/transfer_eval.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/waivers.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/cli/xg.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/config/fixture_predictions.yaml +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/config/previews/EXAMPLE.yaml +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/config/team_managers.yaml +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/constants.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/models/__init__.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/models/chip_plan.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/models/fixture.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/models/league_history.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/models/player.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/models/team.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/models/types.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/parsers/__init__.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/parsers/recommendations.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/paths.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/prompts/__init__.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/prompts/league_recap.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/prompts/returnees.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/prompts/scout.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/scraper/__init__.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/scraper/fpl_prices.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/services/__init__.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/services/fixture_predictions.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/services/league_history.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/services/league_history_counters.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/services/league_history_fines.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/services/league_history_notes.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/services/matchup.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/services/player_clubs.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/services/player_prior.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/services/scoring/__init__.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/services/scoring/constants.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/services/scoring/data_prep.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/services/scoring/display.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/services/scoring/evaluation.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/services/scoring/ownership.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/services/scoring/shrinkage.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/services/scoring/signals.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/services/scoring/single_gw.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/services/scoring/value_quality.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/services/season_previews.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/services/squad_allocator.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/services/team_form.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/services/team_ratings_prior.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/templates/gw_league_recap.md.j2 +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/templates/gw_preview.md.j2 +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/utils/__init__.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/utils/files.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/utils/gameweek.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/utils/markdown.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/utils/teams.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/utils/text.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/fpl_cli/utils/time.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/pyproject.toml +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/pyrightconfig.json +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/scripts/backtest_early_season_prior_blend.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/scripts/calibrate_promoted_prior.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/scripts/calibrate_quality_ceilings.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/__init__.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/conftest.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/fixtures/classic_squad_fixture.md +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/fixtures/recommendations_fixture.md +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/fixtures/validate_draft_waivers/recs_clean.md +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/fixtures/validate_draft_waivers/squad_grid.json +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/fixtures/validate_draft_waivers/waivers.json +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/fixtures/waiver_ranked_golden.json +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_agents_action.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_agents_analysis.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_agents_analysis_squad.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_agents_base.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_agents_data.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_agents_data_price.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_agents_orchestration.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_allocate.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_api_client.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_api_core_insights.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_api_dataset_fetcher.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_api_draft.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_api_football_data.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_api_historical.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_api_vaastav.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_bench_order_script.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_calibrate_quality_ceilings.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_classic_squad_extraction.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_allocate.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_banner.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_captain.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_chips.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_context_settings.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_credentials.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_custom_analysis.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_differentials.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_failure_parity.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_failure_streams.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_fdr.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_fdr_blanks.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_fixtures.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_format_suppression.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_fpl_block.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_history.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_init.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_intel.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_json.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_json_contract.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_league.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_league_fines.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_league_recap.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_plan_grid.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_player.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_preview.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_price_changes.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_price_history.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_returnees.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_sell_prices.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_settings_warning.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_squad.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_stats.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_status.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_targets.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_transfer_eval.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_waivers.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_warnings_stderr.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_cli_xg.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_common.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_common_scoring.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_data_prep_harmonisation.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_dependencies.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_doc_consistency.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_fines.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_fines_config.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_fixture_predictions.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_fpl_prices.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_gw_prep_script_bootstrap.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_init.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_init_fines.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_league_history_counters.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_league_history_fines.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_league_history_notes.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_league_history_store.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_league_recap.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_matchup_service.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_models.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_normalise_entities.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_output_style_sync.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_player_clubs.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_player_prior.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_player_resolution.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_preseason.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_prompt_tables.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_recommendations_parser.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_report_paths.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_rolling_pts_per_m.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_scoring.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_scraper.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_season_awareness.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_season_previews.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_sell_prices.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_skill_fdr_quality_signals.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_skill_stats_thresholds.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_squad_allocator.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_starting_xi_agent.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_starting_xi_script.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_stats.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_team_ratings_prior.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_transfer_eval_agent.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_transfer_eval_script.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_utils_gameweek.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_utils_markdown.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_utils_text.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_utils_time.py +0 -0
- {fplkit-2.5.0 → fplkit-2.6.1}/tests/test_validate_draft_waivers.py +0 -0
|
@@ -1,6 +1,56 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
|
+
## [2.6.0] - 2026-09-07
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
- drop ratings update's dead -v hint for a kept prior (#311)
|
|
9
|
+
- carry the --format json envelope through a relative FPL_CLI_* rejection (#312)
|
|
10
|
+
- derive `fpl doctor`'s season from GW1's deadline like `fpl status` (#314)
|
|
11
|
+
- warn in the saved review when the summary came back empty (#313)
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
- join Understat rows for players the two sources spell differently (#315)
|
|
16
|
+
|
|
17
|
+
## [2.5.0] - 2026-09-07
|
|
18
|
+
|
|
19
|
+
### Bug Fixes
|
|
20
|
+
|
|
21
|
+
- stop the fpl-mate output style calling ep_next prior-informed (#261)
|
|
22
|
+
- restore xG enrichment for players with an apostrophe in their name (#268)
|
|
23
|
+
- keep coverage readable when one ledger file is damaged (#271)
|
|
24
|
+
- stop the review scrubber deleting sentences naming full names (#270)
|
|
25
|
+
- recap the last completed gameweek once the next one has kicked off (#274)
|
|
26
|
+
- stop fpl xg table output crashing when custom analysis is off (#275)
|
|
27
|
+
- cap httpx, pydantic and click below their next majors (#278)
|
|
28
|
+
- stop a truncated LLM summary being saved without a warning (#276)
|
|
29
|
+
- stop a draft replay erasing the league positions it recorded (#280)
|
|
30
|
+
- validate FPL_CLI_DATA_DIR and FPL_CLI_CACHE_DIR eagerly like FPL_CLI_CONFIG_DIR (#281)
|
|
31
|
+
- raise the click floor to 8.2 so the test suite can capture stderr (#283)
|
|
32
|
+
- collapse a league-wide captain tie into one recap line (#282)
|
|
33
|
+
- fill the blank Source column in a review's Standout Performers (#284)
|
|
34
|
+
- exit 1 with the real reason when review or recap cannot run (#285)
|
|
35
|
+
- point doctor's draft ID fix hints at `fpl init` (#288)
|
|
36
|
+
- keep the current table's points out of a replayed gameweek's recap (#293)
|
|
37
|
+
- report a failure the same way whatever the output format (#292)
|
|
38
|
+
- keep league positions when one manager's replay data is partial (#295)
|
|
39
|
+
- stop a replay renaming the player a stored red-card fine names (#290)
|
|
40
|
+
- ground the review's "Next Week" advice in next gameweek's fixtures (#291)
|
|
41
|
+
- rebuild the ratings prior when a football-data key appears (#289)
|
|
42
|
+
- record the club a player played for in a replayed gameweek (#296)
|
|
43
|
+
- keep `review --summarise` running when an LLM key is missing (#297)
|
|
44
|
+
- derive season labels from GW1's deadline instead of the clock (#299)
|
|
45
|
+
- correct award move direction and free-agent mislabelling (#302)
|
|
46
|
+
|
|
47
|
+
### Features
|
|
48
|
+
|
|
49
|
+
- flag stale quality-score calibration in `fpl doctor` (#298)
|
|
50
|
+
- hand the league-recap editorial every manager's transfers (#300)
|
|
51
|
+
- give the draft recap editorial every waiver and free-agent move (#303)
|
|
52
|
+
- give the season-opener recap each manager's prior FPL seasons (#304)
|
|
53
|
+
|
|
4
54
|
## [2.4.1] - 2026-09-05
|
|
5
55
|
|
|
6
56
|
### Bug Fixes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: fplkit
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.6.1
|
|
4
4
|
Summary: CLI tool for Fantasy Premier League analysis - classic and draft formats
|
|
5
5
|
Project-URL: Homepage, https://github.com/rossgroomio/fpl-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/rossgroomio/fpl-cli
|
|
@@ -434,7 +434,7 @@ classDiagram
|
|
|
434
434
|
|
|
435
435
|
All providers share the `LLMResponse` contract. `OpenAICompatProvider` supports OpenAI, Groq, Together, Ollama via configurable `base_url`. Provider selection configured in settings.
|
|
436
436
|
|
|
437
|
-
`stop_reason` carries the provider's own verdict on why generation ended, verbatim in whichever vocabulary it used — Anthropic's `stop_reason`, an OpenAI-compatible API's `finish_reason`. `stopped_early` reads it against `NORMAL_STOP_REASONS`, a single set covering both dialects (the vocabularies do not collide), and every provider announces an abnormal one at WARNING via `log_abnormal_stop` — the same reason `_http` announces its retries there, so a truncation is visible on any command that talks to an LLM. `None` means the provider said nothing, which is never treated as evidence of truncation. Without this a cut-off response and a complete one were indistinguishable downstream, which is how a saved gameweek review shipped missing a whole format's verdict at exit 0 (#266).
|
|
437
|
+
`stop_reason` carries the provider's own verdict on why generation ended, verbatim in whichever vocabulary it used — Anthropic's `stop_reason`, an OpenAI-compatible API's `finish_reason`. `stopped_early` reads it against `NORMAL_STOP_REASONS`, a single set covering both dialects (the vocabularies do not collide), and every provider announces an abnormal one at WARNING via `log_abnormal_stop` — the same reason `_http` announces its retries there, so a truncation is visible on any command that talks to an LLM. `None` means the provider said nothing, which is never treated as evidence of truncation. Without this a cut-off response and a complete one were indistinguishable downstream, which is how a saved gameweek review shipped missing a whole format's verdict at exit 0 (#266). `log_textless_response` is its companion for the other half of that failure: a reasoning model that reaches the ceiling mid-thought returns content with no text block in it at all, so the concatenated prose is `""` — honest, but indistinguishable from a model with nothing to say until the non-text blocks that did arrive are named (#306). Unlike `log_abnormal_stop` this one is Anthropic-only, because only the Messages API returns typed content blocks — an OpenAI-compatible reply carries a plain `message.content` string, so there is nothing to name when it comes back empty. What catches an empty answer whatever produced it is `check_synthesis_completeness`'s `empty` flag, one layer along, which reads the string the report would carry rather than the envelope it arrived in.
|
|
438
438
|
|
|
439
439
|
Both HTTP providers post through `_http.post_json_with_retry`, the one place their error handling lives: it retries an HTTP 429 with `RetryPolicy` (exponential backoff with jitter, `Retry-After` honoured, the whole wait bounded by one budget, each retry announced at WARNING) and raises `RateLimitError` — a `ProviderError` subclass carrying the server's `retry_after` — once the attempts are spent, and turns every other error status, a timeout, or a non-JSON body into a sanitised `ProviderError`. That is the same 429-is-transient distinction `DatasetFetcher` draws for the historical datasets. The module also holds `QueryPacer`, which keeps request starts a configured interval apart for a caller that fires several queries at once; `fpl returnees --enrich` pairs it with an in-flight cap, reads the typed error to re-query only the rate-limited subset of its shortlist once, and reports a still-refused player as rate-limited rather than unanswered.
|
|
440
440
|
|
|
@@ -555,7 +555,7 @@ fpl_cli/
|
|
|
555
555
|
│ ├── historical.py # HistoricalDataProvider (composition: vaastav + Core-Insights) + historical_season_windows() (the disjoint source allocation) + merge_season_histories()
|
|
556
556
|
│ ├── football_data.py # FootballDataClient (standings, match results)
|
|
557
557
|
│ └── providers/ # LLM provider abstraction
|
|
558
|
-
│ ├── _models.py # LLMResponse (incl. stop_reason/stopped_early), TokenUsage, NORMAL_STOP_REASONS, log_abnormal_stop, ProviderError, RateLimitError
|
|
558
|
+
│ ├── _models.py # LLMResponse (incl. stop_reason/stopped_early), TokenUsage, NORMAL_STOP_REASONS, log_abnormal_stop, log_textless_response, ProviderError, RateLimitError
|
|
559
559
|
│ ├── _http.py # Shared HTTP plumbing: 429 backoff (RetryPolicy, post_with_retry), the providers' one error path (post_json_with_retry), QueryPacer for batching callers
|
|
560
560
|
│ ├── anthropic.py # AnthropicProvider
|
|
561
561
|
│ ├── openai_compat.py # OpenAICompatProvider (OpenAI, Groq, Together, Ollama)
|
|
@@ -572,7 +572,7 @@ fpl_cli/
|
|
|
572
572
|
│ │ ├── shrinkage.py # Single-GW early-season shrinkage + the shared hold-out helpers
|
|
573
573
|
│ │ └── data_prep.py # ScoringContext / ScoringData + prepare_scoring_data()
|
|
574
574
|
│ ├── player_prior.py # Player prior (Bayesian early-season confidence)
|
|
575
|
-
│ ├── team_ratings.py # TeamRatingsService + Calculator (1-7 scale)
|
|
575
|
+
│ ├── team_ratings.py # TeamRatingsService + Calculator (1-7 scale). The service judges the file's `metadata.season` stamp against the season it is told is in progress, discarding a previous season's ratings outright (they rate relegated clubs, and serving them is worse than serving none) — which means the season it uses has to be the resolved one, not the clock: an overrunning season had its own live ratings thrown away from 1 July, dropping every fixture to a neutral 4.0 mid-season (#318). `ensure_fresh(client)` resolves it from GW1's deadline before anything else and re-reads if a property access already forced a load on the clock; `fpl doctor` passes the label it resolved; writes are stamped with the same season, so a file written during an overrun is not stamped into the future
|
|
576
576
|
│ ├── team_ratings_prior.py # Previous-season prior + Bayesian blending (cutoff GW12); every pool it reads (the PL pool from either source, the Championship division) takes full-season records only, and every club's basis is written to the cache as `inputs`. The cache is invalidated by the methodology version, the league's club list, and a one-directional provenance check (`_inputs_have_improved`) that rebuilds only when an input the cached run lacked is available now — so setting `FOOTBALL_DATA_API_KEY` reaches the promoted sides without the file being deleted (#112). A rebuild is then held to `_is_worse_than_cache` (source rank and count of clubs on an estimate) before it may replace the cached prior, and `_record_attempt` stamps the file either way so a down provider costs one retry, not one per command
|
|
577
577
|
│ ├── matchup.py # Fixture matchup scoring (0-10)
|
|
578
578
|
│ ├── fixture_predictions.py # BGW/DGW predictions from YAML + live detection, plus the point-in-time "did his club have a fixture, and how many" read a past gameweek needs
|
|
@@ -602,7 +602,7 @@ fpl_cli/
|
|
|
602
602
|
├── scraper/
|
|
603
603
|
│ └── fpl_prices.py # FPLPriceScraper (needs FPL_EMAIL/FPL_PASSWORD; behind TLS-inspecting proxies: FPL_BROWSER_IGNORE_CERTS=1 for cert MITM, or FPL_BROWSER_EXECUTABLE/CHANNEL/ARGS to swap the browser when the ClientHello itself is rejected)
|
|
604
604
|
├── paths.py # SHIPPED_CONFIG_DIR, TEMPLATE_DIR, user_config_dir(), user_data_dir(), user_cache_dir() — each user_* dir overridable via FPL_CLI_CONFIG_DIR / FPL_CLI_DATA_DIR / FPL_CLI_CACHE_DIR (absolute paths only; a relative one raises UserDirError rather than resolving against the cwd)
|
|
605
|
-
├── season.py # season_label() (+ vaastav_season() alias), previous_season_year()/previous_season_label() (the newest *completed* season, one expression for the player prior, the calibration script's default and doctor's frozen-anchor check), understat_season(), core_insights_season(), TOTAL_GAMEWEEKS, CHIP_SPLIT_GW, PROMOTED_CLUBS_PER_SEASON. season_year_from_gameweeks() derives the year from GW1's deadline rather than the July-cutover clock (#91); resolve_season_year() composes it with the clock — GW1's year while its season is still live, the clock's once that season has fully concluded and the calendar has moved past it (the close-season gap before the next one's fixtures publish, when bootstrap-static keeps serving the finished season's events untouched). FPLClient.get_season_year() wraps resolve_season_year(), and review/league-recap/preview/status pass the result through explicitly so a season overrunning the cutover (2019-20, delayed into July 2020) still resolves to the year it started; league-fines, which reads the ledger with no network, instead defaults to services/league_history.py's most_recent_captured_season() so its default agrees with whatever league-recap actually wrote
|
|
605
|
+
├── season.py # season_label() (+ vaastav_season() alias), previous_season_year()/previous_season_label() (the newest *completed* season, one expression for the player prior, the calibration script's default and doctor's frozen-anchor check), understat_season(), core_insights_season(), TOTAL_GAMEWEEKS, CHIP_SPLIT_GW, PROMOTED_CLUBS_PER_SEASON. season_year_from_gameweeks() derives the year from GW1's deadline rather than the July-cutover clock (#91); resolve_season_year() composes it with the clock — GW1's year while its season is still live, the clock's once that season has fully concluded and the calendar has moved past it (the close-season gap before the next one's fixtures publish, when bootstrap-static keeps serving the finished season's events untouched). FPLClient.get_season_year() wraps resolve_season_year(), and review/league-recap/preview/status/doctor pass the result through explicitly so a season overrunning the cutover (2019-20, delayed into July 2020) still resolves to the year it started -- doctor falls back to the clock only when the API is unreachable, and `fpl doctor --providers` reads the season off the bootstrap its FPL probe already fetched rather than downloading it twice. is_previous_season_year() is the ordered counterpart the staleness checks compare a timestamp's cutover year with: during an overrun a July timestamp lands a year *ahead* of the season it was written in, so exact comparison would call live data a previous season's (#308); league-fines, which reads the ledger with no network, instead defaults to services/league_history.py's most_recent_captured_season() so its default agrees with whatever league-recap actually wrote
|
|
606
606
|
├── constants.py # MIN_MINUTES_FOR_PER90
|
|
607
607
|
└── utils/
|
|
608
608
|
├── gameweek.py # is_opening_gameweek(gw) — shared GW1 check (transfers, waivers and league tables don't exist yet)
|
|
@@ -96,6 +96,15 @@ about: the block is read on nearly every command, so the envelope naming it can
|
|
|
96
96
|
from `fpl status` or `fpl league-recap` as readily as from `fpl league-fines`, and the
|
|
97
97
|
message names the rule and the valid set.
|
|
98
98
|
|
|
99
|
+
A relative `FPL_CLI_CONFIG_DIR` / `FPL_CLI_DATA_DIR` / `FPL_CLI_CACHE_DIR` override is
|
|
100
|
+
rejected before any command runs, so this one case has no parsed `--format` to read when it
|
|
101
|
+
fails — the CLI scans the raw command line for `--format json` instead. The envelope's
|
|
102
|
+
`command` names your subcommand where that is unambiguous (`fpl status`, or a bare group like
|
|
103
|
+
`fpl chips`), and falls back to `"fpl"` for a subgroup's own subcommand (`fpl chips timing`,
|
|
104
|
+
`fpl squad grid`, `fpl intel show`) or a line that was nothing but options — those commands
|
|
105
|
+
don't name their own envelope after either word you typed, and guessing wrong would be worse
|
|
106
|
+
than saying so.
|
|
107
|
+
|
|
99
108
|
**Table mode splits the same way.** Without `--format json` the output you asked for goes
|
|
100
109
|
to stdout and everything else goes to stderr — warnings, progress notices, and the reason
|
|
101
110
|
a command exited nonzero. `fpl squad grid 2>/dev/null` prints a grid or prints nothing;
|
|
@@ -403,7 +412,7 @@ A club that has played only one venue is rated on it. Once GW1 finishes, every c
|
|
|
403
412
|
|
|
404
413
|
Early-season results are shrunk toward that previous-season prior, by the automatic refresh and by `fpl ratings update` alike: a one-gameweek sample carries 1/7 of the weight, six gameweeks half, and the prior drops out entirely once GW12 has completed. Shrinkage is gated on how far the season has run, so a narrow window late on is not blended -- `--since-gw 30` at GW34 saves recent form alone. Inside the early-season window the weight follows the size of that window rather than the gameweek number, so `--since-gw 8` at GW10 is weighted as three gameweeks of evidence. Blended files are stamped `calculated_blended` / `understat_xg_blended` / `auto_calculated_blended` so `fpl ratings` shows that shrinkage was applied. While the sample is shorter than six gameweeks the prior still carries most of the weight, and every fixture-difficulty view says so - `Ratings are mostly last season's prior - 1 gameweek of results carries 14% of the weight`. That replaces the pre-season estimate warning, which stops applying the moment a gameweek completes.
|
|
405
414
|
|
|
406
|
-
Ratings are tied to the season that produced them. A file carried across a season boundary is ignored rather than served, because it rates the three relegated clubs and knows nothing about the three promoted ones. Ratings that cover the wrong set of clubs are called out by name — `team_ratings.yaml is missing COV, HUL, IPS and still rates BUR, WHU, WOL` — which catches a rollover that a "days old" check cannot: a file rebuilt in early August is new by date and still describes last season's league.
|
|
415
|
+
Ratings are tied to the season that produced them. A file carried across a season boundary is ignored rather than served, because it rates the three relegated clubs and knows nothing about the three promoted ones. Which season is "in progress" comes from GW1's deadline, not the July clock, so a season overrunning 1 July keeps its own ratings instead of having them discarded with a third of the run-in still to play. Ratings that cover the wrong set of clubs are called out by name — `team_ratings.yaml is missing COV, HUL, IPS and still rates BUR, WHU, WOL` — which catches a rollover that a "days old" check cannot: a file rebuilt in early August is new by date and still describes last season's league.
|
|
407
416
|
|
|
408
417
|
See [Team Ratings](custom-analysis.md#team-ratings) for calculation methodology, axes, early-season blending, pre-season estimates, and manual overrides.
|
|
409
418
|
|
|
@@ -556,7 +565,7 @@ These metrics appear in `fpl player`, `fpl xg`, and gameweek reports. If Underst
|
|
|
556
565
|
|
|
557
566
|
**When a whole club fails to join.** FPL and Understat spell some clubs differently, so a `TEAM_NAME_MAP` gap costs one club's entire squad its npxG, xGChain and every score built on them, while the other 19 look fine. The commands that enrich from the live league payload — `fpl stats --value`, `fpl player`, `fpl allocate`, `fpl xg`, `fpl targets`, `fpl differentials` — report it once per club: on stderr in table mode, and as an `understat_team_unmatched` entry in JSON `metadata.warnings`, one per club, naming both the FPL club and the Understat name it was mapped to. `fpl doctor --providers` checks the same thing ahead of time. A club missing from a *past* season's payload is not this: `fpl returnees` scores a player's current club against the season they played, so the three clubs promoted since are absent from it every year by definition, and that stays a debug line rather than a warning about a map that is fine.
|
|
558
567
|
|
|
559
|
-
**When one player fails to join.** A player whose club Understat *does* carry can still fail to match on name, and that is not always a defect: Understat has not ingested the gameweek in progress,
|
|
568
|
+
**When one player fails to join.** A player whose club Understat *does* carry can still fail to match on name, and that is not always a defect: Understat has not ingested the gameweek in progress, names the player outright differently (FPL's `A.Becker` is Understat's `Alisson`), or simply has no row for them. Those read as `null` quality and value scores rather than as zeros, so the player is ranked as "no data" rather than as bad. A name the two sources merely *spell* differently does join, through a last-resort pass that stays inside the player's club and needs their minutes to agree and no second candidate: a transliteration variant (`Yarmoliuk` and `Yehor Yarmolyuk`), a double-barrelled or middle name one source drops (`Gannon-Doak` and `Ben Doak`, `Walle Egeli` and `Sindre Egeli`), or a mononym inside a longer name (`Jair Cunha` and `Jair`). A shared word alone is never enough — `João Pedro` and `Pedro Neto` share one — so a player whose row is genuinely missing stays `null` rather than wearing a club-mate's numbers. A few percent of a club's players missing this way is normal and stays a debug line naming each one with a running rate (`No Understat row matched ... — 3 of 351 FPL players with minutes unmatched so far this run`); the rate is what to watch, since a jump in it means the two sources have stopped agreeing about names rather than that one more cameo went un-ingested. `fpl doctor --providers` reports the same rate as an `Understat name join` row, which is where to look for it without turning on debug logging. Understat serves apostrophes HTML-escaped (`O'Shea`), which used to fail every apostrophe name silently; the payload is now decoded when it is fetched.
|
|
560
569
|
|
|
561
570
|
#### Quality and Value Scores
|
|
562
571
|
|
|
@@ -769,15 +778,24 @@ on a run with no fixture data, is reported alongside the completeness findings b
|
|
|
769
778
|
stderr, and as a warning callout in the saved report.
|
|
770
779
|
|
|
771
780
|
**Incomplete summaries:** the personal analysis is checked against the sections its own prompt
|
|
772
|
-
asked for before anything is written. A response that is missing a `## `
|
|
773
|
-
terminal punctuation, or that the provider reports it stopped early on is
|
|
774
|
-
if the second attempt is no better, the
|
|
775
|
-
what is missing. Under `--debug` the detail lands in
|
|
776
|
-
alongside `research_corrections.txt`. A saved report
|
|
777
|
-
same list as a warning callout above the summary, so
|
|
778
|
-
|
|
781
|
+
asked for before anything is written. A response that comes back empty, is missing a `## `
|
|
782
|
+
heading, ends without terminal punctuation, or that the provider reports it stopped early on is
|
|
783
|
+
retried **once** — and if the second attempt is no better, the less damaged of the two stands
|
|
784
|
+
and the run says so on stderr, naming what is missing. Under `--debug` the detail lands in
|
|
785
|
+
`data/debug/synthesis_corrections.txt` alongside `research_corrections.txt`. A saved report
|
|
786
|
+
generated from such a response carries the same list as a warning callout above the summary, so
|
|
787
|
+
a verdict the model dropped never reads as one deliberately omitted. The callout does not depend
|
|
788
|
+
on there being a summary to qualify: a response that returned no text at all — the shape a hard
|
|
789
|
+
`max_tokens` truncation takes when the model was still thinking at the ceiling — is the one that
|
|
790
|
+
most needs explaining, and is reported as a single "the response is empty" finding rather than
|
|
791
|
+
as every section it happens to be short of. A verdict for a format the run has no squad data for is not counted as
|
|
779
792
|
missing — the prompt tells the model to analyse only the format it was given.
|
|
780
793
|
|
|
794
|
+
A call that never lands at all — a timeout, a 500, a 429 that outlasted its retries, a revoked
|
|
795
|
+
key — is reported through the same callout, naming the failure. The run still exits **0** with
|
|
796
|
+
the rest of the review intact, but the saved report says the analysis was attempted and lost
|
|
797
|
+
rather than leaving a reader to wonder whether the template ever had one.
|
|
798
|
+
|
|
781
799
|
**Blanks and doubles:** a zero from a player whose club had no fixture is marked `[BGW]` rather
|
|
782
800
|
than read as a choice that failed, and such a player is kept off the Blankers list entirely; a
|
|
783
801
|
player whose club played twice is marked `[DGW]`. Which clubs those were is read off the
|
|
@@ -1278,7 +1296,7 @@ Rolling a setup into a new season silently invalidates IDs and per-team files: a
|
|
|
1278
1296
|
|
|
1279
1297
|
**Data files:**
|
|
1280
1298
|
|
|
1281
|
-
- `team_ratings.yaml` — season stamp and team set vs the live league (problems here are stale, not broken: the ratings service already ignores or rebuilds bad files)
|
|
1299
|
+
- `team_ratings.yaml` — season stamp and team set vs the live league, judged against the same resolved season the report names (problems here are stale, not broken: the ratings service already ignores or rebuilds bad files)
|
|
1282
1300
|
- `team_managers.yaml` — merged shipped + user copy covers exactly the current twenty clubs
|
|
1283
1301
|
- `previews/` — optional season preview intel: a file for a club not in the current league is flagged (it loads and inflates the coverage gate), and files the loader skipped (previous season, malformed) are surfaced with `fpl intel` as the follow-up
|
|
1284
1302
|
- `team_finances.json` — `scraped_at` falls within the current season
|
|
@@ -1291,6 +1309,8 @@ Rolling a setup into a new season silently invalidates IDs and per-team files: a
|
|
|
1291
1309
|
|
|
1292
1310
|
**Environment** — which directory each of config/data/cache resolved to and whether an `FPL_CLI_*` override is in effect, plus whether `settings.yaml` exists.
|
|
1293
1311
|
|
|
1312
|
+
**Which season it is** — the season doctor reports in `metadata.season`, and judges "a previous season's" against, comes from GW1's `deadline_time` where the API answers and the July cutover only where it does not — the same rule `fpl status` uses, so the two commands cannot name different seasons for the same moment. A season that overruns 1 July (2019-20, COVID-delayed) therefore keeps its own label in both, and a `team_finances.json` scraped during that overrun reads as current rather than as a previous season's.
|
|
1313
|
+
|
|
1294
1314
|
Each finding is classified as **broken** (wrong answers now — fix it today), **stale** (self-corrects or needs one routine refresh), **skipped** (not configured/present), or **unchecked** (API unreachable). Exits non-zero when anything is broken, so it can gate scripts.
|
|
1295
1315
|
|
|
1296
1316
|
**`--providers`** checks the external data sources instead of the local setup. None of them version anything, and the tool degrades gracefully everywhere, so upstream drift otherwise surfaces as plausible but wrong output — a renamed stat field zeroes every player's xG without an error. Each probe asserts shape and volume, not just reachability, and where a column check is not the contract it runs the parser itself:
|
|
@@ -1398,7 +1418,7 @@ llm:
|
|
|
1398
1418
|
model: claude-sonnet-5
|
|
1399
1419
|
timeout: 60
|
|
1400
1420
|
query_defaults:
|
|
1401
|
-
max_tokens:
|
|
1421
|
+
max_tokens: 8000
|
|
1402
1422
|
```
|
|
1403
1423
|
|
|
1404
1424
|
### LLM Providers
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '2.
|
|
22
|
-
__version_tuple__ = version_tuple = (2,
|
|
21
|
+
__version__ = version = '2.6.1'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 6, 1)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
|
+
from collections.abc import Sequence
|
|
6
7
|
from dataclasses import dataclass, field
|
|
7
8
|
|
|
8
9
|
logger = logging.getLogger(__name__)
|
|
@@ -97,3 +98,27 @@ def log_abnormal_stop(response: LLMResponse, label: str) -> None:
|
|
|
97
98
|
"the response may be cut off",
|
|
98
99
|
label, response.stop_reason, response.usage.output_tokens,
|
|
99
100
|
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def log_textless_response(
|
|
104
|
+
response: LLMResponse, label: str, non_text_blocks: Sequence[str],
|
|
105
|
+
) -> None:
|
|
106
|
+
"""Announce a response whose content was blocks, none of them text.
|
|
107
|
+
|
|
108
|
+
A model that reasons before it answers emits its thinking first, so a
|
|
109
|
+
ceiling reached mid-thought returns content with no prose block in it at
|
|
110
|
+
all. The concatenated text is then `""` -- honest, but indistinguishable
|
|
111
|
+
downstream from a model that had nothing to say. Naming the blocks that
|
|
112
|
+
did arrive is what separates the two for anyone reading stderr (#306).
|
|
113
|
+
|
|
114
|
+
`non_text_blocks` is the types of the blocks that were *not* text, so an
|
|
115
|
+
answer of nothing but an empty text block stays silent: that is a model
|
|
116
|
+
with nothing to say, which is the case this has nothing to add to.
|
|
117
|
+
`log_abnormal_stop` has already spoken if it was cut off.
|
|
118
|
+
"""
|
|
119
|
+
if response.content or not non_text_blocks:
|
|
120
|
+
return
|
|
121
|
+
logger.warning(
|
|
122
|
+
"%s returned no text content -- the response carried only %s block(s)",
|
|
123
|
+
label, ", ".join(sorted(set(non_text_blocks))),
|
|
124
|
+
)
|
|
@@ -8,7 +8,13 @@ from typing import Any, ClassVar, Self
|
|
|
8
8
|
import httpx
|
|
9
9
|
|
|
10
10
|
from fpl_cli.api.providers._http import RetryPolicy, post_json_with_retry
|
|
11
|
-
from fpl_cli.api.providers._models import
|
|
11
|
+
from fpl_cli.api.providers._models import (
|
|
12
|
+
LLMResponse,
|
|
13
|
+
ProviderError,
|
|
14
|
+
TokenUsage,
|
|
15
|
+
log_abnormal_stop,
|
|
16
|
+
log_textless_response,
|
|
17
|
+
)
|
|
12
18
|
|
|
13
19
|
_BASE_URL = "https://api.anthropic.com/v1"
|
|
14
20
|
_PROVIDER_LABEL = "Anthropic"
|
|
@@ -81,10 +87,21 @@ class AnthropicProvider:
|
|
|
81
87
|
label=_PROVIDER_LABEL, policy=self.RETRY_POLICY,
|
|
82
88
|
)
|
|
83
89
|
|
|
90
|
+
# Only text blocks carry prose, but the types that were *not* text are
|
|
91
|
+
# worth keeping: a ceiling reached while the model was still thinking
|
|
92
|
+
# returns a lone `thinking` block, and the empty string that falls out
|
|
93
|
+
# of this loop is otherwise indistinguishable from a model with
|
|
94
|
+
# nothing to say (#306). Text blocks are deliberately not collected --
|
|
95
|
+
# a response of nothing but an empty text block *is* a model with
|
|
96
|
+
# nothing to say, and naming "text" as the reason there was no text
|
|
97
|
+
# would contradict itself.
|
|
84
98
|
content = ""
|
|
99
|
+
non_text_blocks: list[str] = []
|
|
85
100
|
for block in data.get("content", []):
|
|
86
101
|
if block.get("type") == "text":
|
|
87
102
|
content += block.get("text", "")
|
|
103
|
+
else:
|
|
104
|
+
non_text_blocks.append(str(block.get("type")))
|
|
88
105
|
|
|
89
106
|
# Normalise usage: Anthropic returns input_tokens/output_tokens directly
|
|
90
107
|
raw_usage = data.get("usage", {})
|
|
@@ -105,6 +122,7 @@ class AnthropicProvider:
|
|
|
105
122
|
stop_reason=data.get("stop_reason") or None,
|
|
106
123
|
)
|
|
107
124
|
log_abnormal_stop(response, _PROVIDER_LABEL)
|
|
125
|
+
log_textless_response(response, _PROVIDER_LABEL, non_text_blocks)
|
|
108
126
|
return response
|
|
109
127
|
|
|
110
128
|
def post_process(self, content: str) -> str:
|