fplkit 1.4.1__tar.gz → 2.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fplkit-1.4.1 → fplkit-2.0.0}/.agents/TOOLS.md +2 -2
- {fplkit-1.4.1 → fplkit-2.0.0}/CHANGELOG.md +17 -1
- {fplkit-1.4.1 → fplkit-2.0.0}/CLAUDE.md +1 -1
- {fplkit-1.4.1 → fplkit-2.0.0}/PKG-INFO +1 -1
- {fplkit-1.4.1 → fplkit-2.0.0}/docs/command-reference.md +1 -1
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/_version.py +2 -2
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/agents/data/scout.py +8 -2
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/cli/_helpers.py +5 -1
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/cli/player.py +11 -3
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/cli/stats.py +10 -5
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/models/player.py +2 -2
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/prompts/scout.py +23 -8
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/scraper/fpl_prices.py +38 -15
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_agents_data.py +40 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_cli_format_suppression.py +2 -2
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_cli_player.py +44 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_cli_stats.py +56 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_scraper.py +76 -2
- {fplkit-1.4.1 → fplkit-2.0.0}/.agents/README.md +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/.agents/skills/gw-prep/SKILL.md +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/.agents/skills/gw-prep/references/output-template.md +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/.agents/skills/gw-prep/references/rules.md +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/.agents/skills/gw-prep/scripts/bench_order.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/.agents/skills/gw-prep/scripts/extract_classic_squad.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/.agents/skills/gw-prep/scripts/starting_xi.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/.agents/skills/gw-prep/scripts/transfer_eval.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/.agents/skills/gw-prep/scripts/validate_draft_waivers.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/.agents/skills/squad-builder/SKILL.md +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/.agents/skills/squad-builder/references/output-template.md +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/.agents/skills/squad-builder/references/rules.md +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/.agents/skills/update-gw-prep/SKILL.md +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/.claude/output-styles/fpl-mate.md +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/.claude/settings.json +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/.env.example +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/.github/workflows/ci.yml +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/.github/workflows/release.yml +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/.gitignore +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/AGENTS.md +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/LICENSE +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/README.md +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/cliff.toml +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/config/team_managers.yaml +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/config/team_ratings.yaml +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/config/team_ratings_overrides.yaml +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/docs/architecture.md +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/docs/custom-analysis.md +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/docs/fpl-rules.md +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/docs/images/fpl-player-demo-v1-0.png +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/__init__.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/agents/__init__.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/agents/action/__init__.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/agents/action/waiver.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/agents/analysis/__init__.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/agents/analysis/bench_order.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/agents/analysis/captain.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/agents/analysis/squad_analyzer.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/agents/analysis/starting_xi.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/agents/analysis/stats.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/agents/analysis/transfer_eval.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/agents/base.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/agents/common.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/agents/data/__init__.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/agents/data/fixture.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/agents/data/price.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/agents/orchestration/__init__.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/agents/orchestration/report.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/api/__init__.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/api/core_insights.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/api/dataset_fetcher.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/api/football_data.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/api/fpl.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/api/fpl_draft.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/api/historical.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/api/historical_types.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/api/providers/__init__.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/api/providers/_models.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/api/providers/anthropic.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/api/providers/openai_compat.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/api/providers/perplexity.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/api/understat.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/api/vaastav.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/cli/__init__.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/cli/_banner.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/cli/_context.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/cli/_fines.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/cli/_fines_config.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/cli/_json.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/cli/_league_recap_data.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/cli/_league_recap_types.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/cli/_plan_grid.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/cli/_review_analysis.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/cli/_review_classic.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/cli/_review_draft.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/cli/_review_summarisation.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/cli/allocate.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/cli/captain.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/cli/chips.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/cli/credentials.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/cli/differentials.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/cli/fdr.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/cli/fixtures.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/cli/history.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/cli/init.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/cli/league.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/cli/league_recap.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/cli/preview.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/cli/price_changes.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/cli/price_history.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/cli/ratings.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/cli/review.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/cli/sell_prices.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/cli/squad.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/cli/status.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/cli/targets.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/cli/transfer_eval.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/cli/waivers.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/cli/xg.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/config/defaults.yaml +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/config/fixture_predictions.yaml +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/constants.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/models/__init__.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/models/chip_plan.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/models/fixture.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/models/team.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/models/types.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/parsers/__init__.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/parsers/recommendations.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/paths.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/prompts/__init__.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/prompts/league_recap.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/prompts/review.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/scraper/__init__.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/season.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/services/__init__.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/services/fixture_predictions.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/services/matchup.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/services/player_prior.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/services/player_scoring.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/services/squad_allocator.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/services/team_form.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/services/team_ratings.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/services/team_ratings_prior.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/templates/gw_league_recap.md.j2 +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/templates/gw_preview.md.j2 +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/templates/gw_review.md.j2 +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/utils/__init__.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/utils/text.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/fpl_cli/utils/time.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/pyproject.toml +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/pyrightconfig.json +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/requirements.lock +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/__init__.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/conftest.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/fixtures/classic_squad_fixture.md +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/fixtures/recommendations_fixture.md +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/fixtures/validate_draft_waivers/recs_clean.md +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/fixtures/validate_draft_waivers/squad_grid.json +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/fixtures/validate_draft_waivers/waivers.json +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_agents_action.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_agents_analysis.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_agents_analysis_squad.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_agents_base.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_agents_data_price.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_agents_orchestration.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_allocate.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_api_client.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_api_core_insights.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_api_dataset_fetcher.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_api_draft.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_api_football_data.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_api_historical.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_api_understat.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_api_vaastav.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_bench_order_script.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_classic_squad_extraction.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_cli_allocate.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_cli_banner.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_cli_captain.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_cli_chips.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_cli_credentials.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_cli_custom_analysis.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_cli_differentials.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_cli_fdr.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_cli_fdr_blanks.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_cli_fixtures.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_cli_format.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_cli_history.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_cli_init.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_cli_json.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_cli_plan_grid.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_cli_preview.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_cli_price_history.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_cli_review.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_cli_sell_prices.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_cli_squad.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_cli_status.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_cli_targets.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_cli_transfer_eval.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_cli_waivers.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_cli_warnings_stderr.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_cli_xg.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_common.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_common_scoring.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_data_prep_harmonisation.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_fines.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_fines_config.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_fixture_predictions.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_fpl_prices.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_init.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_init_fines.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_league_recap.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_matchup_service.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_models.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_paths.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_player_prior.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_player_resolution.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_player_scoring.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_providers.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_recommendations_parser.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_report_agent.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_review.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_rolling_pts_per_m.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_season.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_sell_prices.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_squad_allocator.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_starting_xi_agent.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_stats.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_team_ratings.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_team_ratings_prior.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_transfer_eval_agent.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_utils_text.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_utils_time.py +0 -0
- {fplkit-1.4.1 → fplkit-2.0.0}/tests/test_validate_draft_waivers.py +0 -0
|
@@ -27,8 +27,8 @@ Errors use `emit_json_error()` with `{"command", "error"}` shape.
|
|
|
27
27
|
| `init` | Set up fpl-cli with your FPL IDs and optional features | No | General | No | direct-api |
|
|
28
28
|
| `status` | Show FPL gameweek status and upcoming deadlines | Yes | General | No | direct-api |
|
|
29
29
|
| `fixtures` | Show fixtures for a gameweek | Yes | General | No | direct-api |
|
|
30
|
-
| `player` | Look up a player's stats, xG, ownership and fixture run. JSON includes `ep_next` (predicted pts next GW) and `ep_this` (current GW); panel shows `xPts` (ep_next). `--detail` (`-d`) shows GW-by-GW performance and, for FWD/MID with sufficient history, xGI sustainability (per-match GI-xGI divergence -> form modifier). With `custom_analysis`: JSON adds `info.adjusted_npxg_per_90` (fixture-adjusted) and `info.raw_npxg_per_90` (Understat season avg); panel shows `adj. npxG/90: X.XXX (raw: Y.YYY)` | Yes | General | No | direct-api |
|
|
31
|
-
| `stats` | List players with filtering and sorting. `--value` adds quality/value per £m columns; `--window N` sets rolling lookback (3-10) for `rolling_pts_per_m`. Sortable by `ep_next`/`ep_this` (FPL predicted points); both in JSON output | Yes | General | No | direct-api |
|
|
30
|
+
| `player` | Look up a player's stats, xG, ownership and fixture run. JSON includes `ep_next` (predicted pts next GW) and `ep_this` (current GW), emitting `null` when FPL has no projection; panel shows `xPts` (ep_next) when available, omits the segment when FPL has no projection. `--detail` (`-d`) shows GW-by-GW performance and, for FWD/MID with sufficient history, xGI sustainability (per-match GI-xGI divergence -> form modifier). With `custom_analysis`: JSON adds `info.adjusted_npxg_per_90` (fixture-adjusted) and `info.raw_npxg_per_90` (Understat season avg); panel shows `adj. npxG/90: X.XXX (raw: Y.YYY)` | Yes | General | No | direct-api |
|
|
31
|
+
| `stats` | List players with filtering and sorting. `--value` adds quality/value per £m columns; `--window N` sets rolling lookback (3-10) for `rolling_pts_per_m`. Sortable by `ep_next`/`ep_this` (FPL predicted points); both in JSON output (emit `null` when FPL has no projection; table renders `—`). Players with no projection sort to the bottom in either direction | Yes | General | No | direct-api |
|
|
32
32
|
| `history` | Show historical player performance across seasons | Yes | General | No | direct-api |
|
|
33
33
|
| `league` | Show live league standings for Classic and Draft leagues | No | General | No | direct-api |
|
|
34
34
|
| `fdr` | Analyse fixture difficulty - easy runs, blanks, doubles | Yes | General | No | via-agent |
|
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
|
+
## [1.4.2] - 2026-05-24
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
- feat(scout): bind player clubs to current-season reference (#40)
|
|
9
|
+
- feat(sell-prices refresh): Add retry logic to /api/me/ polling with hydration race handling (#39)
|
|
10
|
+
|
|
11
|
+
### Scout
|
|
12
|
+
|
|
13
|
+
- enforce player_reference as a strict allowlist (#41)
|
|
14
|
+
|
|
15
|
+
## [1.4.1] - 2026-05-17
|
|
16
|
+
|
|
17
|
+
### Refactoring
|
|
18
|
+
|
|
19
|
+
- refactor FPL scraper to fetch API directly instead of intercepting (#37)
|
|
20
|
+
|
|
4
21
|
## [1.4.0] - 2026-05-17
|
|
5
22
|
|
|
6
23
|
### Bug Fixes
|
|
@@ -109,4 +126,3 @@ All notable changes to this project will be documented in this file.
|
|
|
109
126
|
|
|
110
127
|
- CI pipeline and PyPI publish for v1.0.0 (#1)
|
|
111
128
|
|
|
112
|
-
|
|
@@ -3,7 +3,7 @@ fpl-cli - CLI tool for Fantasy Premier League analysis (classic + draft).
|
|
|
3
3
|
|
|
4
4
|
## Setup & Dev
|
|
5
5
|
```bash
|
|
6
|
-
source .venv/bin/activate && pip install -e ".[dev]"
|
|
6
|
+
source .venv/bin/activate && pip install -e ".[dev]" # local only — web sessions install globally via setup script
|
|
7
7
|
ruff check fpl_cli/ # Lint
|
|
8
8
|
pyright fpl_cli/ # Type check
|
|
9
9
|
pytest tests/ # Tests
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fplkit
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
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
|
|
@@ -379,7 +379,7 @@ The FPL API provides gameweek point predictions for every player:
|
|
|
379
379
|
| **ep_next** | FPL's predicted points for the next gameweek. Shown as `xPts` on the Points/PPG panel line. Available as a `--sort` field in `fpl stats`. |
|
|
380
380
|
| **ep_this** | FPL's predicted points for the current gameweek (freezes after GW deadline, enabling predicted-vs-actual comparison with `event_points`). JSON only - not shown in the panel. |
|
|
381
381
|
|
|
382
|
-
Both fields
|
|
382
|
+
Both fields are `None` on the `Player` model when FPL provides no projection (e.g. unavailable/injured players, end of season). The Rich panel for `fpl player` omits the `xPts` segment of the Points line in that case, and the `fpl stats` table renders the cell as `—`. JSON output emits `null` when FPL provides no projection, so consumers can distinguish missing data from a genuine `0.0` projection — `info.ep_next`/`info.ep_this` for `fpl player`, and the top-level `ep_next`/`ep_this` record fields in `fpl stats --format json`.
|
|
383
383
|
|
|
384
384
|
#### Player Detail Flags
|
|
385
385
|
|
|
@@ -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 = '
|
|
22
|
-
__version_tuple__ = version_tuple = (
|
|
21
|
+
__version__ = version = '2.0.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 0, 0)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -49,10 +49,16 @@ class ScoutAgent(Agent):
|
|
|
49
49
|
Returns:
|
|
50
50
|
Formatted position reference string.
|
|
51
51
|
"""
|
|
52
|
-
# Filter to relevant players: ownership > 1% OR form > 3 OR minutes > 500
|
|
52
|
+
# Filter to relevant players: ownership > 1% OR form > 3 OR minutes > 500.
|
|
53
|
+
# Also include any player with active news (e.g. injury return, late-fitness
|
|
54
|
+
# flags) so post-injury returnees with low minutes still appear in the
|
|
55
|
+
# reference and don't force the LLM to guess their club.
|
|
53
56
|
relevant = [
|
|
54
57
|
p for p in players
|
|
55
|
-
if p.selected_by_percent > 1.0
|
|
58
|
+
if p.selected_by_percent > 1.0
|
|
59
|
+
or p.form > 3.0
|
|
60
|
+
or p.minutes > 500
|
|
61
|
+
or bool(p.news)
|
|
56
62
|
]
|
|
57
63
|
|
|
58
64
|
# Group by position
|
|
@@ -268,5 +268,9 @@ def _format_sort_value(field: str, value) -> str:
|
|
|
268
268
|
if fmt == "pct":
|
|
269
269
|
return f"{value:.1f}%"
|
|
270
270
|
if field in _PLAYERS_FLOAT_FIELDS:
|
|
271
|
-
|
|
271
|
+
# Missing-data UX: render em dash. A table column can't be omitted
|
|
272
|
+
# per-row, so we substitute a visible placeholder. The Rich panel
|
|
273
|
+
# in `fpl player` drops the whole segment instead — different
|
|
274
|
+
# surface, different right answer.
|
|
275
|
+
return f"{value:.1f}" if value is not None else "—"
|
|
272
276
|
return str(value)
|
|
@@ -288,8 +288,8 @@ def player_command(
|
|
|
288
288
|
"form": float(p.form),
|
|
289
289
|
"total_points": p.total_points,
|
|
290
290
|
"points_per_game": float(p.points_per_game),
|
|
291
|
-
"ep_next":
|
|
292
|
-
"ep_this":
|
|
291
|
+
"ep_next": p.ep_next,
|
|
292
|
+
"ep_this": p.ep_this,
|
|
293
293
|
"goals_scored": p.goals_scored,
|
|
294
294
|
"assists": p.assists,
|
|
295
295
|
"expected_assists": float(p.expected_assists),
|
|
@@ -423,11 +423,19 @@ def player_command(
|
|
|
423
423
|
|
|
424
424
|
# Build panel lines conditionally
|
|
425
425
|
is_gk = p.position_name == "GK"
|
|
426
|
+
# Missing-data UX: drop the xPts segment entirely. The panel is
|
|
427
|
+
# a single-row free-form layout, so removing the label reads cleaner
|
|
428
|
+
# than rendering "xPts: —". The stats table takes the opposite
|
|
429
|
+
# approach (em dash) because per-row column omission isn't an option.
|
|
426
430
|
lines = [
|
|
427
431
|
f"[bold]{p.web_name}[/bold] ({p.full_name})",
|
|
428
432
|
f"Team: {team_name} | Position: {p.position_name}",
|
|
429
433
|
f"Price: £{p.price:.1f}m | Form: {p.form:.1f}",
|
|
430
|
-
|
|
434
|
+
(
|
|
435
|
+
f"Points: {p.total_points} | PPG: {p.points_per_game:.1f} | xPts: {p.ep_next:.1f}"
|
|
436
|
+
if p.ep_next is not None
|
|
437
|
+
else f"Points: {p.total_points} | PPG: {p.points_per_game:.1f}"
|
|
438
|
+
),
|
|
431
439
|
]
|
|
432
440
|
rv, rc = rolling_scores.get(p.id, (None, None))
|
|
433
441
|
if rv is not None:
|
|
@@ -280,6 +280,8 @@ def stats_command(
|
|
|
280
280
|
effective_sort = "total_points"
|
|
281
281
|
|
|
282
282
|
# Sort
|
|
283
|
+
# Null-scored players sort to bottom regardless of direction
|
|
284
|
+
bottom = float("-inf") if not reverse else float("inf")
|
|
283
285
|
if effective_sort in _VALUE_SORT_FIELDS:
|
|
284
286
|
if effective_sort == "rolling_pts_per_m":
|
|
285
287
|
rolling_score_map: dict[int, float | None] = {
|
|
@@ -290,8 +292,6 @@ def stats_command(
|
|
|
290
292
|
score_map = quality_map
|
|
291
293
|
else:
|
|
292
294
|
score_map = value_map
|
|
293
|
-
# Null-scored players sort to bottom regardless of direction
|
|
294
|
-
bottom = float("-inf") if not reverse else float("inf")
|
|
295
295
|
|
|
296
296
|
def _value_key(p: Player) -> float:
|
|
297
297
|
v = score_map.get(p.id)
|
|
@@ -300,7 +300,12 @@ def stats_command(
|
|
|
300
300
|
filtered.sort(key=_value_key, reverse=not reverse)
|
|
301
301
|
else:
|
|
302
302
|
attr = _SORT_FIELD_ALIASES.get(effective_sort, effective_sort)
|
|
303
|
-
|
|
303
|
+
|
|
304
|
+
def _attr_key(p: Player) -> float:
|
|
305
|
+
v = getattr(p, attr)
|
|
306
|
+
return v if v is not None else bottom
|
|
307
|
+
|
|
308
|
+
filtered.sort(key=_attr_key, reverse=not reverse)
|
|
304
309
|
|
|
305
310
|
# Limit
|
|
306
311
|
filtered = filtered[:limit]
|
|
@@ -366,8 +371,8 @@ def stats_command(
|
|
|
366
371
|
"defensive_contribution_per_90": float(p.defensive_contribution_per_90),
|
|
367
372
|
"form_per_m": float(p.value_form),
|
|
368
373
|
"pts_per_m": float(p.value_season),
|
|
369
|
-
"ep_next":
|
|
370
|
-
"ep_this":
|
|
374
|
+
"ep_next": p.ep_next,
|
|
375
|
+
"ep_this": p.ep_this,
|
|
371
376
|
**(
|
|
372
377
|
{
|
|
373
378
|
"quality_score": quality_map.get(p.id),
|
|
@@ -90,8 +90,8 @@ class Player(BaseModel):
|
|
|
90
90
|
expected_goals_conceded: float = Field(default=0.0, alias="expected_goals_conceded")
|
|
91
91
|
|
|
92
92
|
# FPL predicted points
|
|
93
|
-
ep_next: float =
|
|
94
|
-
ep_this: float =
|
|
93
|
+
ep_next: float | None = None
|
|
94
|
+
ep_this: float | None = None
|
|
95
95
|
|
|
96
96
|
# Defensive
|
|
97
97
|
defensive_contribution: int = 0
|
|
@@ -34,11 +34,16 @@ ALWAYS:
|
|
|
34
34
|
- Flag confidence level (High/Medium/Low) based on source agreement and recency
|
|
35
35
|
- Interpret signals for the user - don't just report raw quotes
|
|
36
36
|
- Note when trusted voices disagree with mainstream opinion
|
|
37
|
+
- Source every player's current club from the <player_reference> block - never infer it from prior-season knowledge
|
|
38
|
+
- Treat <player_reference> as a strict allowlist: every player you name anywhere in the output (BUY tables, SELL tables, Differential Whispers, Contrarian Take) MUST appear in it
|
|
37
39
|
|
|
38
40
|
NEVER:
|
|
39
41
|
- Repeat statistical analysis (xG, form scores, PPG) - the user has this
|
|
40
42
|
- Give generic advice without specific sourcing
|
|
41
43
|
- Present rumour as fact without flagging uncertainty
|
|
44
|
+
- Guess a player's club from memory; player transfers between seasons make prior knowledge unreliable
|
|
45
|
+
- Surface players who are not in <player_reference>, even if community chatter mentions them - they are not in this season's FPL pool (likely transferred abroad, retired, or never were in the PL)
|
|
46
|
+
- Emit placeholder rows like "PlayerX? — not listed in your FPL pool, so skipping". Silently exclude out-of-pool names instead; return fewer rows (or omit the section) rather than padding
|
|
42
47
|
</rules>"""
|
|
43
48
|
|
|
44
49
|
SCOUT_USER_PROMPT_TEMPLATE = """Surface qualitative FPL intelligence for Gameweek {gameweek}.
|
|
@@ -51,12 +56,21 @@ This query runs 24-30 hours before deadline, after most press conferences.
|
|
|
51
56
|
3. Check r/FantasyPL for community momentum - who's being quietly accumulated, what the RMT thread is converging on
|
|
52
57
|
4. Identify narrative breaks - players being over-sold or under-bought based on reactive sentiment
|
|
53
58
|
5. Note any blank/double GW planning implications from @BenCrellin
|
|
59
|
+
|
|
60
|
+
When scanning external sources, treat any player name you don't find in <player_reference> as out-of-pool: they're not in this season's FPL game (transferred abroad, retired, or wrong league) and must not appear anywhere in your output, even if the community is discussing them.
|
|
54
61
|
</research_focus>
|
|
55
62
|
|
|
56
|
-
<
|
|
57
|
-
|
|
63
|
+
<player_reference>
|
|
64
|
+
Authoritative directory of player → (club, position) for THIS season, sourced from live FPL data. Each entry is formatted as `Name (CLUB)`.
|
|
65
|
+
|
|
66
|
+
Use this as the ONLY source of truth for both:
|
|
67
|
+
- The "Club" column in your output tables (clubs change between seasons; do NOT use prior-season knowledge)
|
|
68
|
+
- The position grouping (DEF / MID / FWD)
|
|
69
|
+
|
|
70
|
+
If a player you want to recommend is not listed here, either omit them or explicitly flag "club unknown" rather than guessing.
|
|
71
|
+
|
|
58
72
|
{position_reference}
|
|
59
|
-
</
|
|
73
|
+
</player_reference>
|
|
60
74
|
|
|
61
75
|
<unavailable_players>
|
|
62
76
|
CRITICAL: The following players are INJURED, SUSPENDED, or otherwise UNAVAILABLE according to current FPL data. Do NOT recommend any of these players under any circumstances, even if web sources suggest them (those sources may be outdated).
|
|
@@ -70,24 +84,24 @@ CRITICAL: The following players are INJURED, SUSPENDED, or otherwise UNAVAILABLE
|
|
|
70
84
|
|
|
71
85
|
| Player | Club | Signal | Source | Confidence |
|
|
72
86
|
|--------|------|--------|--------|------------|
|
|
73
|
-
[
|
|
87
|
+
[Up to 3 players from <player_reference> with qualitative buy signals - what do the watchers see that stats don't show yet? Fewer rows are fine; omit the section entirely if no one in the pool qualifies. Do NOT pad with out-of-pool names.]
|
|
74
88
|
|
|
75
89
|
**MIDFIELDERS**
|
|
76
90
|
|
|
77
91
|
| Player | Club | Signal | Source | Confidence |
|
|
78
92
|
|--------|------|--------|--------|------------|
|
|
79
|
-
[
|
|
93
|
+
[Up to 3 players from <player_reference> with qualitative buy signals. Fewer or zero is acceptable.]
|
|
80
94
|
|
|
81
95
|
**FORWARDS**
|
|
82
96
|
|
|
83
97
|
| Player | Club | Signal | Source | Confidence |
|
|
84
98
|
|--------|------|--------|--------|------------|
|
|
85
|
-
[
|
|
99
|
+
[Up to 2 players from <player_reference> with qualitative buy signals. Fewer or zero is acceptable.]
|
|
86
100
|
|
|
87
101
|
## SELL Signals
|
|
88
102
|
| Player | Club | Signal Type | Detail | Confidence |
|
|
89
103
|
|--------|------|-------------|--------|------------|
|
|
90
|
-
[
|
|
104
|
+
[Up to 5 players from <player_reference> to avoid - categorise as: Rotation Risk, Eye-Test Warning, or Narrative Trap. Fewer rows are fine; omit if no one qualifies.]
|
|
91
105
|
|
|
92
106
|
## Strategic Intel
|
|
93
107
|
- **Blank/Double GW Watch:** Any planning implications from Ben Crellin or fixture news
|
|
@@ -100,7 +114,8 @@ CRITICAL: The following players are INJURED, SUSPENDED, or otherwise UNAVAILABLE
|
|
|
100
114
|
- Confidence = High when multiple trusted sources agree + recent (last 48h); Medium when single source or older; Low when speculative/emerging
|
|
101
115
|
- Prioritise actionable, time-sensitive intel over comprehensive coverage
|
|
102
116
|
- If a trusted voice contradicts the crowd, highlight it
|
|
103
|
-
- CRITICAL: Always check <
|
|
117
|
+
- CRITICAL: Always check <player_reference> before filling the Club column or assigning a player to DEFENDERS/MIDFIELDERS/FORWARDS. The reference reflects current-season clubs; your prior knowledge does not.
|
|
118
|
+
- CRITICAL: <player_reference> is the complete allowlist of in-pool players. If a name you're considering is not in it, drop the row — never emit "Player? — not in pool" placeholders, and never pad to hit a row count.
|
|
104
119
|
</quality_requirements>"""
|
|
105
120
|
|
|
106
121
|
|
|
@@ -217,27 +217,40 @@ class FPLPriceScraper:
|
|
|
217
217
|
finally:
|
|
218
218
|
await browser.close()
|
|
219
219
|
|
|
220
|
+
# `or {}` on each step: FPL briefly returns {"player": null} in the post-login
|
|
221
|
+
# window before the session is fully hydrated. .get(key, {}) only defaults on
|
|
222
|
+
# missing keys, not on explicit nulls, so chain through `or {}` to be safe.
|
|
223
|
+
_ME_RETRY_ATTEMPTS = 5
|
|
224
|
+
_ME_RETRY_DELAY_MS = 1000
|
|
225
|
+
|
|
220
226
|
async def _fetch_my_team(self, page) -> dict | None:
|
|
221
227
|
"""Fetch /api/me/ and /api/my-team/{id}/ from the authenticated session.
|
|
222
228
|
|
|
223
229
|
Returns the my-team payload, or None if either call fails (in which case the
|
|
224
230
|
caller falls back to DOM extraction).
|
|
225
231
|
"""
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
232
|
+
me: dict | None = None
|
|
233
|
+
entry_id = None
|
|
234
|
+
for attempt in range(self._ME_RETRY_ATTEMPTS):
|
|
235
|
+
try:
|
|
236
|
+
me = await page.evaluate(
|
|
237
|
+
"async () => (await fetch('/api/me/', {credentials: 'include'})).json()"
|
|
238
|
+
)
|
|
239
|
+
except Exception as e: # noqa: BLE001 — scraper resilience
|
|
240
|
+
logger.debug("Failed to fetch /api/me/: %s", e)
|
|
241
|
+
return None
|
|
242
|
+
entry_id = ((me or {}).get("player") or {}).get("entry")
|
|
243
|
+
if entry_id:
|
|
244
|
+
break
|
|
245
|
+
if attempt < self._ME_RETRY_ATTEMPTS - 1:
|
|
246
|
+
await page.wait_for_timeout(self._ME_RETRY_DELAY_MS)
|
|
233
247
|
|
|
234
|
-
# `or {}` on each step: FPL briefly returns {"player": null} in the post-login
|
|
235
|
-
# window before the session is fully hydrated. .get(key, {}) only defaults on
|
|
236
|
-
# missing keys, not on explicit nulls, so chain through `or {}` to be safe.
|
|
237
|
-
player = (me or {}).get("player") or {}
|
|
238
|
-
entry_id = player.get("entry")
|
|
239
248
|
if not entry_id:
|
|
240
|
-
logger.debug(
|
|
249
|
+
logger.debug(
|
|
250
|
+
"No entry id in /api/me/ after %d retries: %s",
|
|
251
|
+
self._ME_RETRY_ATTEMPTS,
|
|
252
|
+
me,
|
|
253
|
+
)
|
|
241
254
|
return None
|
|
242
255
|
|
|
243
256
|
try:
|
|
@@ -380,12 +393,22 @@ class FPLPriceScraper:
|
|
|
380
393
|
|
|
381
394
|
async def _extract_finances(self, page, my_entry_response: dict | None = None) -> TeamFinances:
|
|
382
395
|
"""Extract financial data - tries intercepted API data first, falls back to DOM."""
|
|
383
|
-
|
|
396
|
+
fallback_reason: str | None = None
|
|
397
|
+
if my_entry_response is None:
|
|
398
|
+
fallback_reason = (
|
|
399
|
+
"API /api/me/ returned no entry id after retries (post-login hydration race?)"
|
|
400
|
+
)
|
|
401
|
+
else:
|
|
384
402
|
finances = await self._extract_from_intercepted(page, my_entry_response)
|
|
385
403
|
if finances and not finances.is_suspect:
|
|
386
404
|
return finances
|
|
405
|
+
fallback_reason = (
|
|
406
|
+
"API /api/my-team/ returned no usable squad data; fell back to DOM extraction"
|
|
407
|
+
)
|
|
387
408
|
|
|
388
|
-
|
|
409
|
+
finances = await self._extract_via_dom(page)
|
|
410
|
+
finances.extraction_errors.append(fallback_reason)
|
|
411
|
+
return finances
|
|
389
412
|
|
|
390
413
|
async def _extract_from_intercepted(self, page, my_entry_response: dict) -> TeamFinances | None:
|
|
391
414
|
"""Build TeamFinances from intercepted /api/my-team/ response data."""
|
|
@@ -1206,6 +1206,46 @@ Sources:
|
|
|
1206
1206
|
assert isinstance(result.data["content_clean"], str)
|
|
1207
1207
|
assert isinstance(result.data["citations"], list)
|
|
1208
1208
|
|
|
1209
|
+
def test_build_position_reference_includes_news_flagged_players(self, agent):
|
|
1210
|
+
"""Low-minutes players with active news (e.g. injury returnees) must
|
|
1211
|
+
appear in the reference so the LLM doesn't have to guess their club."""
|
|
1212
|
+
from fpl_cli.models.player import PlayerPosition
|
|
1213
|
+
from tests.conftest import make_player
|
|
1214
|
+
|
|
1215
|
+
# A defender just back from injury: low ownership, low form, low
|
|
1216
|
+
# minutes — would be excluded by the old filter, but has news.
|
|
1217
|
+
returnee = make_player(
|
|
1218
|
+
id=99,
|
|
1219
|
+
web_name="Returnee",
|
|
1220
|
+
team_id=7,
|
|
1221
|
+
position=PlayerPosition.DEFENDER,
|
|
1222
|
+
selected_by_percent=0.3,
|
|
1223
|
+
form=1.0,
|
|
1224
|
+
minutes=120,
|
|
1225
|
+
news="Back in training, hopeful for involvement",
|
|
1226
|
+
)
|
|
1227
|
+
ref = agent.build_position_reference([returnee], {7: "MCI"})
|
|
1228
|
+
assert "Returnee (MCI)" in ref
|
|
1229
|
+
|
|
1230
|
+
def test_build_position_reference_excludes_irrelevant_players(self, agent):
|
|
1231
|
+
"""Players with no news, no minutes, no form, and tiny ownership
|
|
1232
|
+
should still be excluded to keep the reference compact."""
|
|
1233
|
+
from fpl_cli.models.player import PlayerPosition
|
|
1234
|
+
from tests.conftest import make_player
|
|
1235
|
+
|
|
1236
|
+
irrelevant = make_player(
|
|
1237
|
+
id=100,
|
|
1238
|
+
web_name="Bench",
|
|
1239
|
+
team_id=7,
|
|
1240
|
+
position=PlayerPosition.DEFENDER,
|
|
1241
|
+
selected_by_percent=0.1,
|
|
1242
|
+
form=0.0,
|
|
1243
|
+
minutes=0,
|
|
1244
|
+
news="",
|
|
1245
|
+
)
|
|
1246
|
+
ref = agent.build_position_reference([irrelevant], {7: "MCI"})
|
|
1247
|
+
assert "Bench" not in ref
|
|
1248
|
+
|
|
1209
1249
|
async def test_run_cleans_citations_correctly(self, agent):
|
|
1210
1250
|
"""Test that citations are properly cleaned from content."""
|
|
1211
1251
|
response_with_citations = LLMResponse(
|
|
@@ -248,8 +248,8 @@ class TestPlayerFormatSuppression:
|
|
|
248
248
|
player.direct_freekicks_order = None
|
|
249
249
|
player.defensive_contribution_per_90 = 0.0
|
|
250
250
|
player.penalties_saved = 0
|
|
251
|
-
player.ep_next =
|
|
252
|
-
player.ep_this =
|
|
251
|
+
player.ep_next = None
|
|
252
|
+
player.ep_this = None
|
|
253
253
|
return player
|
|
254
254
|
|
|
255
255
|
def _mock_team(self):
|
|
@@ -729,6 +729,50 @@ class TestPlayerQualityValueScores:
|
|
|
729
729
|
assert "xPts: 8.5" in result.output
|
|
730
730
|
assert "ep_this" not in result.output
|
|
731
731
|
|
|
732
|
+
def test_rich_panel_omits_ep_next_when_none(self):
|
|
733
|
+
client, fixture_agent, ratings_svc = _make_mocks()
|
|
734
|
+
client.get_players = AsyncMock(return_value=[
|
|
735
|
+
make_player(id=308, web_name="Salah", first_name="Mohamed",
|
|
736
|
+
second_name="Salah", team_id=1,
|
|
737
|
+
position=PlayerPosition.MIDFIELDER,
|
|
738
|
+
ep_next=None),
|
|
739
|
+
])
|
|
740
|
+
result = _run([], client, fixture_agent, ratings_svc)
|
|
741
|
+
assert result.exit_code == 0, result.output
|
|
742
|
+
assert "xPts" not in result.output
|
|
743
|
+
# Rest of the Points line must still render — confirms only the xPts
|
|
744
|
+
# segment was dropped, not the entire row.
|
|
745
|
+
assert "Points:" in result.output
|
|
746
|
+
assert "PPG:" in result.output
|
|
747
|
+
|
|
748
|
+
def test_rich_panel_shows_ep_next_when_zero(self):
|
|
749
|
+
# Guards against a future `if p.ep_next:` regression that would
|
|
750
|
+
# swallow genuine 0.0 projections (distinct from None).
|
|
751
|
+
client, fixture_agent, ratings_svc = _make_mocks()
|
|
752
|
+
client.get_players = AsyncMock(return_value=[
|
|
753
|
+
make_player(id=308, web_name="Salah", first_name="Mohamed",
|
|
754
|
+
second_name="Salah", team_id=1,
|
|
755
|
+
position=PlayerPosition.MIDFIELDER,
|
|
756
|
+
ep_next=0.0),
|
|
757
|
+
])
|
|
758
|
+
result = _run([], client, fixture_agent, ratings_svc)
|
|
759
|
+
assert result.exit_code == 0, result.output
|
|
760
|
+
assert "xPts: 0.0" in result.output
|
|
761
|
+
|
|
762
|
+
def test_json_ep_next_none_serialises_as_null(self):
|
|
763
|
+
client, fixture_agent, ratings_svc = _make_mocks()
|
|
764
|
+
client.get_players = AsyncMock(return_value=[
|
|
765
|
+
make_player(id=308, web_name="Salah", first_name="Mohamed",
|
|
766
|
+
second_name="Salah", team_id=1,
|
|
767
|
+
position=PlayerPosition.MIDFIELDER,
|
|
768
|
+
ep_next=None, ep_this=None),
|
|
769
|
+
])
|
|
770
|
+
result = _run_json([], client, fixture_agent, ratings_svc)
|
|
771
|
+
assert result.exit_code == 0, result.output
|
|
772
|
+
info = json.loads(result.output)["data"][0]["info"]
|
|
773
|
+
assert info["ep_next"] is None
|
|
774
|
+
assert info["ep_this"] is None
|
|
775
|
+
|
|
732
776
|
def test_gk_uses_without_xgi_weights(self):
|
|
733
777
|
"""GK quality_score should differ from MID due to without_xgi path."""
|
|
734
778
|
client, fixture_agent, ratings_svc = _make_mocks()
|
|
@@ -235,6 +235,47 @@ class TestPlayersSort:
|
|
|
235
235
|
assert result.exit_code == 0, result.output
|
|
236
236
|
assert result.output.index("Fit") < result.output.index("Injured")
|
|
237
237
|
|
|
238
|
+
def test_sort_ep_next_none_sorts_to_bottom(self):
|
|
239
|
+
players = [
|
|
240
|
+
make_player(id=1, web_name="HasValue", team_id=1, ep_next=5.0),
|
|
241
|
+
make_player(id=2, web_name="NullEp", team_id=1, ep_next=None),
|
|
242
|
+
]
|
|
243
|
+
client = _make_client(players, _sample_teams())
|
|
244
|
+
result = _run(["--sort", "ep_next"], client=client)
|
|
245
|
+
assert result.exit_code == 0, result.output
|
|
246
|
+
assert result.output.index("HasValue") < result.output.index("NullEp")
|
|
247
|
+
|
|
248
|
+
def test_sort_ep_next_all_none_does_not_crash(self):
|
|
249
|
+
players = [
|
|
250
|
+
make_player(id=1, web_name="Alpha", team_id=1, ep_next=None),
|
|
251
|
+
make_player(id=2, web_name="Beta", team_id=1, ep_next=None),
|
|
252
|
+
]
|
|
253
|
+
client = _make_client(players, _sample_teams())
|
|
254
|
+
result = _run(["--sort", "ep_next"], client=client)
|
|
255
|
+
assert result.exit_code == 0, result.output
|
|
256
|
+
|
|
257
|
+
def test_sort_ep_next_none_sorts_to_bottom_in_ascending(self):
|
|
258
|
+
# --reverse activates the float("inf") sentinel branch of the sort
|
|
259
|
+
players = [
|
|
260
|
+
make_player(id=1, web_name="HasValue", team_id=1, ep_next=5.0),
|
|
261
|
+
make_player(id=2, web_name="NullEp", team_id=1, ep_next=None),
|
|
262
|
+
]
|
|
263
|
+
client = _make_client(players, _sample_teams())
|
|
264
|
+
result = _run(["--sort", "ep_next", "--reverse"], client=client)
|
|
265
|
+
assert result.exit_code == 0, result.output
|
|
266
|
+
assert result.output.index("HasValue") < result.output.index("NullEp")
|
|
267
|
+
|
|
268
|
+
def test_sort_ep_next_none_renders_em_dash_in_table(self):
|
|
269
|
+
# Exercises the _format_sort_value None -> "—" branch
|
|
270
|
+
players = [
|
|
271
|
+
make_player(id=1, web_name="HasValue", team_id=1, ep_next=5.0),
|
|
272
|
+
make_player(id=2, web_name="NullEp", team_id=1, ep_next=None),
|
|
273
|
+
]
|
|
274
|
+
client = _make_client(players, _sample_teams())
|
|
275
|
+
result = _run(["--sort", "ep_next"], client=client)
|
|
276
|
+
assert result.exit_code == 0, result.output
|
|
277
|
+
assert "—" in result.output
|
|
278
|
+
|
|
238
279
|
|
|
239
280
|
class TestPlayersErrors:
|
|
240
281
|
def test_invalid_team_shows_valid_options(self):
|
|
@@ -294,6 +335,21 @@ class TestPlayersJsonFormat:
|
|
|
294
335
|
assert "Salah" in result.output
|
|
295
336
|
assert result.output.strip()[0] != "["
|
|
296
337
|
|
|
338
|
+
def test_json_ep_next_none_serialises_as_null(self):
|
|
339
|
+
players = [
|
|
340
|
+
make_player(
|
|
341
|
+
id=1, web_name="NullEp", team_id=1,
|
|
342
|
+
position=PlayerPosition.MIDFIELDER,
|
|
343
|
+
ep_next=None, ep_this=None,
|
|
344
|
+
),
|
|
345
|
+
]
|
|
346
|
+
client = _make_client(players, _sample_teams())
|
|
347
|
+
result = _run(["--format", "json"], client=client)
|
|
348
|
+
assert result.exit_code == 0, result.output
|
|
349
|
+
record = json.loads(result.output)["data"][0]
|
|
350
|
+
assert record["ep_next"] is None
|
|
351
|
+
assert record["ep_this"] is None
|
|
352
|
+
|
|
297
353
|
|
|
298
354
|
class TestStatsDraftOwnership:
|
|
299
355
|
"""Tests for auto-enabled draft ownership column based on format."""
|