fplkit 1.4.1__tar.gz → 1.4.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (233) hide show
  1. {fplkit-1.4.1 → fplkit-1.4.2}/CHANGELOG.md +6 -2
  2. {fplkit-1.4.1 → fplkit-1.4.2}/CLAUDE.md +1 -1
  3. {fplkit-1.4.1 → fplkit-1.4.2}/PKG-INFO +1 -1
  4. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/_version.py +2 -2
  5. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/agents/data/scout.py +8 -2
  6. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/prompts/scout.py +23 -8
  7. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/scraper/fpl_prices.py +38 -15
  8. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_agents_data.py +40 -0
  9. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_scraper.py +76 -2
  10. {fplkit-1.4.1 → fplkit-1.4.2}/.agents/README.md +0 -0
  11. {fplkit-1.4.1 → fplkit-1.4.2}/.agents/TOOLS.md +0 -0
  12. {fplkit-1.4.1 → fplkit-1.4.2}/.agents/skills/gw-prep/SKILL.md +0 -0
  13. {fplkit-1.4.1 → fplkit-1.4.2}/.agents/skills/gw-prep/references/output-template.md +0 -0
  14. {fplkit-1.4.1 → fplkit-1.4.2}/.agents/skills/gw-prep/references/rules.md +0 -0
  15. {fplkit-1.4.1 → fplkit-1.4.2}/.agents/skills/gw-prep/scripts/bench_order.py +0 -0
  16. {fplkit-1.4.1 → fplkit-1.4.2}/.agents/skills/gw-prep/scripts/extract_classic_squad.py +0 -0
  17. {fplkit-1.4.1 → fplkit-1.4.2}/.agents/skills/gw-prep/scripts/starting_xi.py +0 -0
  18. {fplkit-1.4.1 → fplkit-1.4.2}/.agents/skills/gw-prep/scripts/transfer_eval.py +0 -0
  19. {fplkit-1.4.1 → fplkit-1.4.2}/.agents/skills/gw-prep/scripts/validate_draft_waivers.py +0 -0
  20. {fplkit-1.4.1 → fplkit-1.4.2}/.agents/skills/squad-builder/SKILL.md +0 -0
  21. {fplkit-1.4.1 → fplkit-1.4.2}/.agents/skills/squad-builder/references/output-template.md +0 -0
  22. {fplkit-1.4.1 → fplkit-1.4.2}/.agents/skills/squad-builder/references/rules.md +0 -0
  23. {fplkit-1.4.1 → fplkit-1.4.2}/.agents/skills/update-gw-prep/SKILL.md +0 -0
  24. {fplkit-1.4.1 → fplkit-1.4.2}/.claude/output-styles/fpl-mate.md +0 -0
  25. {fplkit-1.4.1 → fplkit-1.4.2}/.claude/settings.json +0 -0
  26. {fplkit-1.4.1 → fplkit-1.4.2}/.env.example +0 -0
  27. {fplkit-1.4.1 → fplkit-1.4.2}/.github/workflows/ci.yml +0 -0
  28. {fplkit-1.4.1 → fplkit-1.4.2}/.github/workflows/release.yml +0 -0
  29. {fplkit-1.4.1 → fplkit-1.4.2}/.gitignore +0 -0
  30. {fplkit-1.4.1 → fplkit-1.4.2}/AGENTS.md +0 -0
  31. {fplkit-1.4.1 → fplkit-1.4.2}/LICENSE +0 -0
  32. {fplkit-1.4.1 → fplkit-1.4.2}/README.md +0 -0
  33. {fplkit-1.4.1 → fplkit-1.4.2}/cliff.toml +0 -0
  34. {fplkit-1.4.1 → fplkit-1.4.2}/config/team_managers.yaml +0 -0
  35. {fplkit-1.4.1 → fplkit-1.4.2}/config/team_ratings.yaml +0 -0
  36. {fplkit-1.4.1 → fplkit-1.4.2}/config/team_ratings_overrides.yaml +0 -0
  37. {fplkit-1.4.1 → fplkit-1.4.2}/docs/architecture.md +0 -0
  38. {fplkit-1.4.1 → fplkit-1.4.2}/docs/command-reference.md +0 -0
  39. {fplkit-1.4.1 → fplkit-1.4.2}/docs/custom-analysis.md +0 -0
  40. {fplkit-1.4.1 → fplkit-1.4.2}/docs/fpl-rules.md +0 -0
  41. {fplkit-1.4.1 → fplkit-1.4.2}/docs/images/fpl-player-demo-v1-0.png +0 -0
  42. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/__init__.py +0 -0
  43. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/agents/__init__.py +0 -0
  44. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/agents/action/__init__.py +0 -0
  45. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/agents/action/waiver.py +0 -0
  46. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/agents/analysis/__init__.py +0 -0
  47. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/agents/analysis/bench_order.py +0 -0
  48. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/agents/analysis/captain.py +0 -0
  49. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/agents/analysis/squad_analyzer.py +0 -0
  50. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/agents/analysis/starting_xi.py +0 -0
  51. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/agents/analysis/stats.py +0 -0
  52. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/agents/analysis/transfer_eval.py +0 -0
  53. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/agents/base.py +0 -0
  54. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/agents/common.py +0 -0
  55. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/agents/data/__init__.py +0 -0
  56. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/agents/data/fixture.py +0 -0
  57. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/agents/data/price.py +0 -0
  58. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/agents/orchestration/__init__.py +0 -0
  59. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/agents/orchestration/report.py +0 -0
  60. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/api/__init__.py +0 -0
  61. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/api/core_insights.py +0 -0
  62. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/api/dataset_fetcher.py +0 -0
  63. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/api/football_data.py +0 -0
  64. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/api/fpl.py +0 -0
  65. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/api/fpl_draft.py +0 -0
  66. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/api/historical.py +0 -0
  67. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/api/historical_types.py +0 -0
  68. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/api/providers/__init__.py +0 -0
  69. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/api/providers/_models.py +0 -0
  70. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/api/providers/anthropic.py +0 -0
  71. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/api/providers/openai_compat.py +0 -0
  72. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/api/providers/perplexity.py +0 -0
  73. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/api/understat.py +0 -0
  74. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/api/vaastav.py +0 -0
  75. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/cli/__init__.py +0 -0
  76. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/cli/_banner.py +0 -0
  77. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/cli/_context.py +0 -0
  78. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/cli/_fines.py +0 -0
  79. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/cli/_fines_config.py +0 -0
  80. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/cli/_helpers.py +0 -0
  81. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/cli/_json.py +0 -0
  82. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/cli/_league_recap_data.py +0 -0
  83. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/cli/_league_recap_types.py +0 -0
  84. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/cli/_plan_grid.py +0 -0
  85. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/cli/_review_analysis.py +0 -0
  86. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/cli/_review_classic.py +0 -0
  87. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/cli/_review_draft.py +0 -0
  88. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/cli/_review_summarisation.py +0 -0
  89. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/cli/allocate.py +0 -0
  90. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/cli/captain.py +0 -0
  91. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/cli/chips.py +0 -0
  92. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/cli/credentials.py +0 -0
  93. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/cli/differentials.py +0 -0
  94. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/cli/fdr.py +0 -0
  95. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/cli/fixtures.py +0 -0
  96. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/cli/history.py +0 -0
  97. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/cli/init.py +0 -0
  98. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/cli/league.py +0 -0
  99. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/cli/league_recap.py +0 -0
  100. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/cli/player.py +0 -0
  101. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/cli/preview.py +0 -0
  102. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/cli/price_changes.py +0 -0
  103. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/cli/price_history.py +0 -0
  104. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/cli/ratings.py +0 -0
  105. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/cli/review.py +0 -0
  106. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/cli/sell_prices.py +0 -0
  107. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/cli/squad.py +0 -0
  108. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/cli/stats.py +0 -0
  109. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/cli/status.py +0 -0
  110. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/cli/targets.py +0 -0
  111. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/cli/transfer_eval.py +0 -0
  112. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/cli/waivers.py +0 -0
  113. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/cli/xg.py +0 -0
  114. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/config/defaults.yaml +0 -0
  115. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/config/fixture_predictions.yaml +0 -0
  116. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/constants.py +0 -0
  117. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/models/__init__.py +0 -0
  118. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/models/chip_plan.py +0 -0
  119. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/models/fixture.py +0 -0
  120. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/models/player.py +0 -0
  121. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/models/team.py +0 -0
  122. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/models/types.py +0 -0
  123. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/parsers/__init__.py +0 -0
  124. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/parsers/recommendations.py +0 -0
  125. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/paths.py +0 -0
  126. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/prompts/__init__.py +0 -0
  127. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/prompts/league_recap.py +0 -0
  128. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/prompts/review.py +0 -0
  129. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/scraper/__init__.py +0 -0
  130. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/season.py +0 -0
  131. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/services/__init__.py +0 -0
  132. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/services/fixture_predictions.py +0 -0
  133. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/services/matchup.py +0 -0
  134. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/services/player_prior.py +0 -0
  135. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/services/player_scoring.py +0 -0
  136. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/services/squad_allocator.py +0 -0
  137. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/services/team_form.py +0 -0
  138. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/services/team_ratings.py +0 -0
  139. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/services/team_ratings_prior.py +0 -0
  140. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/templates/gw_league_recap.md.j2 +0 -0
  141. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/templates/gw_preview.md.j2 +0 -0
  142. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/templates/gw_review.md.j2 +0 -0
  143. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/utils/__init__.py +0 -0
  144. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/utils/text.py +0 -0
  145. {fplkit-1.4.1 → fplkit-1.4.2}/fpl_cli/utils/time.py +0 -0
  146. {fplkit-1.4.1 → fplkit-1.4.2}/pyproject.toml +0 -0
  147. {fplkit-1.4.1 → fplkit-1.4.2}/pyrightconfig.json +0 -0
  148. {fplkit-1.4.1 → fplkit-1.4.2}/requirements.lock +0 -0
  149. {fplkit-1.4.1 → fplkit-1.4.2}/tests/__init__.py +0 -0
  150. {fplkit-1.4.1 → fplkit-1.4.2}/tests/conftest.py +0 -0
  151. {fplkit-1.4.1 → fplkit-1.4.2}/tests/fixtures/classic_squad_fixture.md +0 -0
  152. {fplkit-1.4.1 → fplkit-1.4.2}/tests/fixtures/recommendations_fixture.md +0 -0
  153. {fplkit-1.4.1 → fplkit-1.4.2}/tests/fixtures/validate_draft_waivers/recs_clean.md +0 -0
  154. {fplkit-1.4.1 → fplkit-1.4.2}/tests/fixtures/validate_draft_waivers/squad_grid.json +0 -0
  155. {fplkit-1.4.1 → fplkit-1.4.2}/tests/fixtures/validate_draft_waivers/waivers.json +0 -0
  156. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_agents_action.py +0 -0
  157. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_agents_analysis.py +0 -0
  158. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_agents_analysis_squad.py +0 -0
  159. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_agents_base.py +0 -0
  160. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_agents_data_price.py +0 -0
  161. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_agents_orchestration.py +0 -0
  162. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_allocate.py +0 -0
  163. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_api_client.py +0 -0
  164. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_api_core_insights.py +0 -0
  165. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_api_dataset_fetcher.py +0 -0
  166. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_api_draft.py +0 -0
  167. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_api_football_data.py +0 -0
  168. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_api_historical.py +0 -0
  169. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_api_understat.py +0 -0
  170. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_api_vaastav.py +0 -0
  171. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_bench_order_script.py +0 -0
  172. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_classic_squad_extraction.py +0 -0
  173. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_cli_allocate.py +0 -0
  174. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_cli_banner.py +0 -0
  175. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_cli_captain.py +0 -0
  176. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_cli_chips.py +0 -0
  177. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_cli_credentials.py +0 -0
  178. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_cli_custom_analysis.py +0 -0
  179. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_cli_differentials.py +0 -0
  180. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_cli_fdr.py +0 -0
  181. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_cli_fdr_blanks.py +0 -0
  182. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_cli_fixtures.py +0 -0
  183. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_cli_format.py +0 -0
  184. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_cli_format_suppression.py +0 -0
  185. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_cli_history.py +0 -0
  186. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_cli_init.py +0 -0
  187. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_cli_json.py +0 -0
  188. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_cli_plan_grid.py +0 -0
  189. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_cli_player.py +0 -0
  190. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_cli_preview.py +0 -0
  191. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_cli_price_history.py +0 -0
  192. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_cli_review.py +0 -0
  193. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_cli_sell_prices.py +0 -0
  194. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_cli_squad.py +0 -0
  195. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_cli_stats.py +0 -0
  196. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_cli_status.py +0 -0
  197. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_cli_targets.py +0 -0
  198. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_cli_transfer_eval.py +0 -0
  199. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_cli_waivers.py +0 -0
  200. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_cli_warnings_stderr.py +0 -0
  201. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_cli_xg.py +0 -0
  202. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_common.py +0 -0
  203. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_common_scoring.py +0 -0
  204. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_data_prep_harmonisation.py +0 -0
  205. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_fines.py +0 -0
  206. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_fines_config.py +0 -0
  207. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_fixture_predictions.py +0 -0
  208. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_fpl_prices.py +0 -0
  209. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_init.py +0 -0
  210. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_init_fines.py +0 -0
  211. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_league_recap.py +0 -0
  212. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_matchup_service.py +0 -0
  213. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_models.py +0 -0
  214. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_paths.py +0 -0
  215. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_player_prior.py +0 -0
  216. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_player_resolution.py +0 -0
  217. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_player_scoring.py +0 -0
  218. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_providers.py +0 -0
  219. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_recommendations_parser.py +0 -0
  220. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_report_agent.py +0 -0
  221. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_review.py +0 -0
  222. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_rolling_pts_per_m.py +0 -0
  223. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_season.py +0 -0
  224. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_sell_prices.py +0 -0
  225. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_squad_allocator.py +0 -0
  226. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_starting_xi_agent.py +0 -0
  227. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_stats.py +0 -0
  228. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_team_ratings.py +0 -0
  229. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_team_ratings_prior.py +0 -0
  230. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_transfer_eval_agent.py +0 -0
  231. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_utils_text.py +0 -0
  232. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_utils_time.py +0 -0
  233. {fplkit-1.4.1 → fplkit-1.4.2}/tests/test_validate_draft_waivers.py +0 -0
@@ -1,6 +1,12 @@
1
1
  # Changelog
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
+ ## [1.4.1] - 2026-05-17
5
+
6
+ ### Refactoring
7
+
8
+ - refactor FPL scraper to fetch API directly instead of intercepting (#37)
9
+
4
10
  ## [1.4.0] - 2026-05-17
5
11
 
6
12
  ### Bug Fixes
@@ -108,5 +114,3 @@ All notable changes to this project will be documented in this file.
108
114
  ### Features
109
115
 
110
116
  - CI pipeline and PyPI publish for v1.0.0 (#1)
111
-
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: 1.4.1
3
+ Version: 1.4.2
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
@@ -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 = '1.4.1'
22
- __version_tuple__ = version_tuple = (1, 4, 1)
21
+ __version__ = version = '1.4.2'
22
+ __version_tuple__ = version_tuple = (1, 4, 2)
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 or p.form > 3.0 or p.minutes > 500
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
@@ -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
- <player_positions>
57
- IMPORTANT: Use this official FPL position reference when categorising players. Do NOT guess positions.
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
- </player_positions>
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
- [2-3 players with qualitative buy signals - what do the watchers see that stats don't show yet?]
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
- [2-3 players with qualitative buy signals]
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
- [1-2 players with qualitative buy signals]
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
- [3-5 players to avoid - categorise as: Rotation Risk, Eye-Test Warning, or Narrative Trap]
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 <player_positions> reference before assigning a player to DEFENDERS/MIDFIELDERS/FORWARDS
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
- try:
227
- me = await page.evaluate(
228
- "async () => (await fetch('/api/me/', {credentials: 'include'})).json()"
229
- )
230
- except Exception as e: # noqa: BLE001 — scraper resilience
231
- logger.debug("Failed to fetch /api/me/: %s", e)
232
- return None
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("No entry id in /api/me/ response: %s", me)
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
- if my_entry_response:
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
- return await self._extract_via_dom(page)
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(
@@ -256,26 +256,100 @@ class TestFPLPriceScraper:
256
256
  assert page.evaluate.call_args_list[1].args[1] == 5955459
257
257
 
258
258
  async def test_fetch_my_team_returns_none_on_missing_entry(self):
259
- """If /api/me/ returns no player.entry, abort and let DOM fallback take over."""
259
+ """If /api/me/ never returns a player.entry, abort and let DOM fallback take over."""
260
260
  from unittest.mock import AsyncMock, MagicMock
261
261
 
262
262
  scraper = FPLPriceScraper()
263
263
  page = MagicMock()
264
264
  page.evaluate = AsyncMock(return_value={})
265
+ page.wait_for_timeout = AsyncMock()
265
266
 
266
267
  result = await scraper._fetch_my_team(page)
267
268
  assert result is None
269
+ assert page.evaluate.call_count == scraper._ME_RETRY_ATTEMPTS
270
+ # No trailing wait after the final failed attempt
271
+ assert page.wait_for_timeout.call_count == scraper._ME_RETRY_ATTEMPTS - 1
268
272
 
269
273
  async def test_fetch_my_team_handles_null_player(self):
270
- """FPL briefly returns {'player': null} in the post-login window; must not crash."""
274
+ """FPL briefly returns {'player': null} post-login; we retry then give up cleanly."""
271
275
  from unittest.mock import AsyncMock, MagicMock
272
276
 
273
277
  scraper = FPLPriceScraper()
274
278
  page = MagicMock()
275
279
  page.evaluate = AsyncMock(return_value={"player": None})
280
+ page.wait_for_timeout = AsyncMock()
276
281
 
277
282
  result = await scraper._fetch_my_team(page)
278
283
  assert result is None
284
+ assert page.evaluate.call_count == scraper._ME_RETRY_ATTEMPTS
285
+ # No trailing wait after the final failed attempt
286
+ assert page.wait_for_timeout.call_count == scraper._ME_RETRY_ATTEMPTS - 1
287
+
288
+ async def test_fetch_my_team_retries_until_hydrated(self):
289
+ """Null player on early polls, then a real entry id: retry succeeds and fetches my-team."""
290
+ from unittest.mock import AsyncMock, MagicMock
291
+
292
+ scraper = FPLPriceScraper()
293
+ page = MagicMock()
294
+ page.evaluate = AsyncMock(side_effect=[
295
+ {"player": None},
296
+ {"player": None},
297
+ {"player": {"entry": 12345}},
298
+ {"picks": [{"element": 1, "selling_price": 50, "purchase_price": 50}],
299
+ "transfers": {"bank": 10, "limit": 1}},
300
+ ])
301
+ page.wait_for_timeout = AsyncMock()
302
+
303
+ result = await scraper._fetch_my_team(page)
304
+ assert result is not None
305
+ assert result["transfers"]["bank"] == 10
306
+ # 3 /api/me/ calls + 1 /api/my-team/ call
307
+ assert page.evaluate.call_count == 4
308
+ # And we waited between the failing /api/me/ attempts (twice)
309
+ assert page.wait_for_timeout.call_count == 2
310
+
311
+ async def test_fetch_my_team_evaluate_error_no_retry(self):
312
+ """A hard page.evaluate exception is a different failure mode - don't retry."""
313
+ from unittest.mock import AsyncMock, MagicMock
314
+
315
+ scraper = FPLPriceScraper()
316
+ page = MagicMock()
317
+ page.evaluate = AsyncMock(side_effect=RuntimeError("eval failed"))
318
+ page.wait_for_timeout = AsyncMock()
319
+
320
+ result = await scraper._fetch_my_team(page)
321
+ assert result is None
322
+ assert page.evaluate.call_count == 1
323
+ assert page.wait_for_timeout.call_count == 0
324
+
325
+ async def test_extract_finances_records_api_failure_in_errors(self):
326
+ """When the API path is skipped, the DOM fallback's result gets a hint appended."""
327
+ from unittest.mock import AsyncMock, MagicMock
328
+
329
+ scraper = FPLPriceScraper()
330
+ dom_result = TeamFinances(bank=0.0, free_transfers=0, squad=[], total_value=0.0)
331
+ with patch.object(scraper, "_extract_via_dom", AsyncMock(return_value=dom_result)):
332
+ result = await scraper._extract_finances(MagicMock(), my_entry_response=None)
333
+
334
+ assert result is dom_result
335
+ assert any("api/me" in err.lower() for err in result.extraction_errors)
336
+
337
+ async def test_extract_finances_records_intercepted_failure_in_errors(self):
338
+ """If /api/me/ succeeded but the intercepted payload is unusable, the DOM fallback still gets a hint."""
339
+ from unittest.mock import AsyncMock, MagicMock
340
+
341
+ scraper = FPLPriceScraper()
342
+ dom_result = TeamFinances(bank=0.0, free_transfers=0, squad=[], total_value=0.0)
343
+ with (
344
+ patch.object(scraper, "_extract_from_intercepted", AsyncMock(return_value=None)),
345
+ patch.object(scraper, "_extract_via_dom", AsyncMock(return_value=dom_result)),
346
+ ):
347
+ result = await scraper._extract_finances(
348
+ MagicMock(), my_entry_response={"picks": [], "transfers": {}}
349
+ )
350
+
351
+ assert result is dom_result
352
+ assert any("my-team" in err.lower() for err in result.extraction_errors)
279
353
 
280
354
  def test_cache_file_path(self):
281
355
  """Test cache file path is correct."""
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes