openmatchkit 0.2.1__tar.gz → 0.3.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.
Files changed (55) hide show
  1. {openmatchkit-0.2.1 → openmatchkit-0.3.0}/CHANGELOG.md +16 -0
  2. openmatchkit-0.3.0/PKG-INFO +435 -0
  3. openmatchkit-0.3.0/README.md +400 -0
  4. {openmatchkit-0.2.1 → openmatchkit-0.3.0}/pyproject.toml +1 -1
  5. {openmatchkit-0.2.1 → openmatchkit-0.3.0}/src/openmatchkit/__init__.py +12 -0
  6. {openmatchkit-0.2.1 → openmatchkit-0.3.0}/src/openmatchkit/cli.py +140 -0
  7. {openmatchkit-0.2.1 → openmatchkit-0.3.0}/src/openmatchkit/client.py +405 -1
  8. {openmatchkit-0.2.1 → openmatchkit-0.3.0}/src/openmatchkit/http.py +1 -1
  9. {openmatchkit-0.2.1 → openmatchkit-0.3.0}/src/openmatchkit/models.py +74 -1
  10. {openmatchkit-0.2.1 → openmatchkit-0.3.0}/tests/test_client.py +63 -0
  11. openmatchkit-0.2.1/PKG-INFO +0 -192
  12. openmatchkit-0.2.1/README.md +0 -157
  13. {openmatchkit-0.2.1 → openmatchkit-0.3.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  14. {openmatchkit-0.2.1 → openmatchkit-0.3.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  15. {openmatchkit-0.2.1 → openmatchkit-0.3.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  16. {openmatchkit-0.2.1 → openmatchkit-0.3.0}/.github/ISSUE_TEMPLATE/source_adapter.md +0 -0
  17. {openmatchkit-0.2.1 → openmatchkit-0.3.0}/.github/pull_request_template.md +0 -0
  18. {openmatchkit-0.2.1 → openmatchkit-0.3.0}/.github/workflows/ci.yml +0 -0
  19. {openmatchkit-0.2.1 → openmatchkit-0.3.0}/.github/workflows/publish.yml +0 -0
  20. {openmatchkit-0.2.1 → openmatchkit-0.3.0}/.gitignore +0 -0
  21. {openmatchkit-0.2.1 → openmatchkit-0.3.0}/CODE_OF_CONDUCT.md +0 -0
  22. {openmatchkit-0.2.1 → openmatchkit-0.3.0}/CONTRIBUTING.md +0 -0
  23. {openmatchkit-0.2.1 → openmatchkit-0.3.0}/DATA_SOURCES.md +0 -0
  24. {openmatchkit-0.2.1 → openmatchkit-0.3.0}/LICENSE +0 -0
  25. {openmatchkit-0.2.1 → openmatchkit-0.3.0}/PRIVACY.md +0 -0
  26. {openmatchkit-0.2.1 → openmatchkit-0.3.0}/ROADMAP.md +0 -0
  27. {openmatchkit-0.2.1 → openmatchkit-0.3.0}/SECURITY.md +0 -0
  28. {openmatchkit-0.2.1 → openmatchkit-0.3.0}/docs/detailed-data.md +0 -0
  29. {openmatchkit-0.2.1 → openmatchkit-0.3.0}/docs/index.md +0 -0
  30. {openmatchkit-0.2.1 → openmatchkit-0.3.0}/docs/prediction.md +0 -0
  31. {openmatchkit-0.2.1 → openmatchkit-0.3.0}/docs/sources.md +0 -0
  32. {openmatchkit-0.2.1 → openmatchkit-0.3.0}/examples/fixtures_demo.py +0 -0
  33. {openmatchkit-0.2.1 → openmatchkit-0.3.0}/examples/live_demo.py +0 -0
  34. {openmatchkit-0.2.1 → openmatchkit-0.3.0}/examples/prediction_demo.py +0 -0
  35. {openmatchkit-0.2.1 → openmatchkit-0.3.0}/src/openmatchkit/exceptions.py +0 -0
  36. {openmatchkit-0.2.1 → openmatchkit-0.3.0}/src/openmatchkit/export.py +0 -0
  37. {openmatchkit-0.2.1 → openmatchkit-0.3.0}/src/openmatchkit/prediction/__init__.py +0 -0
  38. {openmatchkit-0.2.1 → openmatchkit-0.3.0}/src/openmatchkit/prediction/elo.py +0 -0
  39. {openmatchkit-0.2.1 → openmatchkit-0.3.0}/src/openmatchkit/prediction/poisson.py +0 -0
  40. {openmatchkit-0.2.1 → openmatchkit-0.3.0}/src/openmatchkit/sources/__init__.py +0 -0
  41. {openmatchkit-0.2.1 → openmatchkit-0.3.0}/src/openmatchkit/sources/base.py +0 -0
  42. {openmatchkit-0.2.1 → openmatchkit-0.3.0}/src/openmatchkit/sources/football_data_uk.py +0 -0
  43. {openmatchkit-0.2.1 → openmatchkit-0.3.0}/src/openmatchkit/sources/json_file.py +0 -0
  44. {openmatchkit-0.2.1 → openmatchkit-0.3.0}/src/openmatchkit/sources/openfootball.py +0 -0
  45. {openmatchkit-0.2.1 → openmatchkit-0.3.0}/src/openmatchkit/sources/public_html.py +0 -0
  46. {openmatchkit-0.2.1 → openmatchkit-0.3.0}/tests/fixtures/sample_detailed_source.json +0 -0
  47. {openmatchkit-0.2.1 → openmatchkit-0.3.0}/tests/fixtures/sample_football_data.csv +0 -0
  48. {openmatchkit-0.2.1 → openmatchkit-0.3.0}/tests/fixtures/sample_openfootball.json +0 -0
  49. {openmatchkit-0.2.1 → openmatchkit-0.3.0}/tests/test_detailed_data.py +0 -0
  50. {openmatchkit-0.2.1 → openmatchkit-0.3.0}/tests/test_export.py +0 -0
  51. {openmatchkit-0.2.1 → openmatchkit-0.3.0}/tests/test_football_data_uk.py +0 -0
  52. {openmatchkit-0.2.1 → openmatchkit-0.3.0}/tests/test_http.py +0 -0
  53. {openmatchkit-0.2.1 → openmatchkit-0.3.0}/tests/test_models.py +0 -0
  54. {openmatchkit-0.2.1 → openmatchkit-0.3.0}/tests/test_openfootball.py +0 -0
  55. {openmatchkit-0.2.1 → openmatchkit-0.3.0}/tests/test_prediction.py +0 -0
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.3.0 - 2026-06-12
4
+
5
+ - Added source capability and source health models.
6
+ - Added match search/filter helpers.
7
+ - Added upcoming and recent result helpers.
8
+ - Added match lookup and match summary helpers.
9
+ - Added team form and head-to-head summaries.
10
+ - Added CLI commands for new features.
11
+ - Expanded README with detailed function-by-function JSON examples.
12
+ - Increased tests from 16 to 19.
13
+
14
+ ## v0.2.2 - 2026-06-12
15
+
16
+ - Updated README and PyPI project description to use `pip install openmatchkit`.
17
+ - Added PyPI version badge.
18
+
3
19
  ## v0.2.1 - 2026-06-12
4
20
 
5
21
  - Added GitHub Actions trusted publishing workflow for PyPI.
@@ -0,0 +1,435 @@
1
+ Metadata-Version: 2.4
2
+ Name: openmatchkit
3
+ Version: 0.3.0
4
+ Summary: Unofficial open-source football match data toolkit using public sources.
5
+ Project-URL: Homepage, https://github.com/patilprashan246/openmatchkit
6
+ Project-URL: Repository, https://github.com/patilprashan246/openmatchkit
7
+ Project-URL: Issues, https://github.com/patilprashan246/openmatchkit/issues
8
+ Author: Prashant Patil
9
+ License: MIT License
10
+ Keywords: cli,fixtures,football,open-data,prediction,scores,soccer,world-cup
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Education
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Requires-Python: >=3.10
24
+ Requires-Dist: beautifulsoup4>=4.12
25
+ Requires-Dist: httpx>=0.27
26
+ Requires-Dist: lxml>=5.0
27
+ Requires-Dist: pydantic>=2.7
28
+ Requires-Dist: typer>=0.12
29
+ Provides-Extra: dev
30
+ Requires-Dist: build>=1.2; extra == 'dev'
31
+ Requires-Dist: pytest>=8.0; extra == 'dev'
32
+ Requires-Dist: ruff>=0.5; extra == 'dev'
33
+ Requires-Dist: twine>=5.1; extra == 'dev'
34
+ Description-Content-Type: text/markdown
35
+
36
+ # openmatchkit
37
+
38
+ [![CI](https://github.com/patilprashan246/openmatchkit/actions/workflows/ci.yml/badge.svg)](https://github.com/patilprashan246/openmatchkit/actions/workflows/ci.yml)
39
+ [![PyPI](https://img.shields.io/pypi/v/openmatchkit)](https://pypi.org/project/openmatchkit/)
40
+ [![Release](https://img.shields.io/github/v/release/patilprashan246/openmatchkit)](https://github.com/patilprashan246/openmatchkit/releases)
41
+ [![Python](https://img.shields.io/badge/python-3.10%2B-blue)](pyproject.toml)
42
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
43
+
44
+ Unofficial open-source football match data toolkit for fixtures, results, best-effort live score adapters, standings, exports, and simple prediction models.
45
+
46
+ > This project is unofficial and not affiliated with FIFA, any league, club, broadcaster, federation, or data provider. It does not provide official FIFA data.
47
+
48
+ ## Why openmatchkit?
49
+
50
+ openmatchkit gives Python developers a clean, source-attributed way to work with public football match facts without paid API keys. It starts with open/public datasets, keeps scraping optional and conservative, and returns structured JSON-ready models.
51
+
52
+ ## Install
53
+
54
+ ```bash
55
+ pip install openmatchkit
56
+ ```
57
+
58
+ From GitHub:
59
+
60
+ ```bash
61
+ pip install git+https://github.com/patilprashan246/openmatchkit.git@v0.3.0
62
+ ```
63
+
64
+ For local development:
65
+
66
+ ```bash
67
+ python -m venv .venv
68
+ .venv\Scripts\activate
69
+ python -m pip install --upgrade pip
70
+ python -m pip install -e ".[dev]"
71
+ pytest
72
+ ```
73
+
74
+ ## Quick Start
75
+
76
+ ```python
77
+ from openmatchkit import MatchClient
78
+
79
+ client = MatchClient()
80
+ next_match = client.next_match(competition="worldcup", season="2026")
81
+
82
+ print(next_match.model_dump(mode="json"))
83
+ ```
84
+
85
+ ```bash
86
+ openmatch next-match --competition worldcup --season 2026
87
+ ```
88
+
89
+ ## Python API
90
+
91
+ ```python
92
+ from openmatchkit import MatchClient
93
+
94
+ client = MatchClient()
95
+
96
+ fixtures = client.fixtures(competition="worldcup", season="2026")
97
+ results = client.results(competition="worldcup", season="2026")
98
+ live = client.live_scores()
99
+ next_match = client.next_match(competition="worldcup", season="2026")
100
+ prediction = client.predict(home="Mexico", away="South Africa", history=results)
101
+
102
+ client.export_json(fixtures, "fixtures.json")
103
+ client.export_csv(fixtures, "fixtures.csv")
104
+ ```
105
+
106
+ League datasets from OpenFootball can be fetched with codes such as:
107
+
108
+ ```python
109
+ matches = client.fixtures(competition="en.1", season="2015-16")
110
+ ```
111
+
112
+ ## Functionality Reference
113
+
114
+ All public models are Pydantic models. Use `.model_dump(mode="json")` when you want JSON-ready dictionaries, or use the CLI commands when you want JSON printed directly.
115
+
116
+ ### Source Metadata
117
+
118
+ ```python
119
+ client.source_capabilities()
120
+ client.source_health()
121
+ ```
122
+
123
+ Returns source capability and local health metadata:
124
+
125
+ ```json
126
+ [
127
+ {
128
+ "source": "openfootball",
129
+ "fixtures": true,
130
+ "results": true,
131
+ "live_scores": false,
132
+ "scoreboards": true,
133
+ "player_history": false,
134
+ "standings": true,
135
+ "prediction_history": true,
136
+ "notes": [
137
+ "OpenFootball provides fixtures/results; detailed player data is unavailable."
138
+ ]
139
+ }
140
+ ]
141
+ ```
142
+
143
+ ### Fixtures, Results, Search, Upcoming, Recent
144
+
145
+ ```python
146
+ fixtures = client.fixtures("worldcup", "2026")
147
+ results = client.results("worldcup", "2026")
148
+ matches = client.search_matches("worldcup", "2026", team="Canada", status="scheduled")
149
+ upcoming = client.upcoming_matches("worldcup", "2026", team="Canada", limit=3)
150
+ recent = client.recent_results("worldcup", "2026", limit=5)
151
+ next_match = client.next_match("worldcup", "2026")
152
+ match = client.match_by_id(match_id="...", competition="worldcup", season="2026")
153
+ ```
154
+
155
+ Each item is a `Match`:
156
+
157
+ ```json
158
+ {
159
+ "match_id": "ded80f9a2a529045",
160
+ "competition": "World Cup 2026",
161
+ "season": "2026",
162
+ "round": "Matchday 2",
163
+ "group": "Group B",
164
+ "kickoff": "2026-06-12T15:00:00-04:00",
165
+ "home": {"name": "Canada", "code": null, "country": null},
166
+ "away": {"name": "Bosnia & Herzegovina", "code": null, "country": null},
167
+ "score": {"home": null, "away": null},
168
+ "status": "scheduled",
169
+ "venue": "Toronto",
170
+ "source": "openfootball",
171
+ "source_url": "https://raw.githubusercontent.com/openfootball/worldcup.json/master/2026/worldcup.json",
172
+ "fetched_at": "2026-06-12T10:46:21.427460Z"
173
+ }
174
+ ```
175
+
176
+ ### Scoreboards and Match Summaries
177
+
178
+ ```python
179
+ summary = client.match_summary(match_id="detail-1", competition="Demo Cup", season="2026")
180
+ scoreboard = client.scoreboard(match_id="detail-1")
181
+ scoreboards = client.scoreboards(competition="Demo Cup", season="2026")
182
+ live_boards = client.live_scoreboards()
183
+ ```
184
+
185
+ `Scoreboard` can include match clock, lineups, events, team stats, and player stats when the configured source provides them:
186
+
187
+ ```json
188
+ {
189
+ "match": {
190
+ "match_id": "detail-1",
191
+ "competition": "Demo Cup",
192
+ "home": {"name": "Team A"},
193
+ "away": {"name": "Team B"},
194
+ "score": {"home": 1, "away": 0},
195
+ "status": "live"
196
+ },
197
+ "clock": {"minute": 45, "added_time": 2, "period": "first_half", "display": "45+2"},
198
+ "events": [
199
+ {
200
+ "event_type": "goal",
201
+ "team": "Team A",
202
+ "player": "Alex Demo",
203
+ "assist": "Sam Creator",
204
+ "minute": 34,
205
+ "home_score": 1,
206
+ "away_score": 0
207
+ }
208
+ ],
209
+ "team_stats": [
210
+ {"team": "Team A", "possession": 58.4, "shots": 7, "shots_on_target": 3}
211
+ ],
212
+ "player_stats": [
213
+ {"player": {"name": "Alex Demo", "team": "Team A"}, "goals": 1, "minutes_played": 45}
214
+ ],
215
+ "source_notes": ["Demo fixture representing an authorized detailed public JSON feed."]
216
+ }
217
+ ```
218
+
219
+ ### Teams, Standings, Form, Head-to-Head
220
+
221
+ ```python
222
+ teams = client.teams("worldcup", "2026")
223
+ table = client.standings("worldcup", "2026", group="Group A")
224
+ form = client.team_form("Mexico", "worldcup", "2026", last=5)
225
+ h2h = client.head_to_head("Mexico", "South Africa", "worldcup", "2026")
226
+ ```
227
+
228
+ `TeamForm` returns recent results from the team perspective:
229
+
230
+ ```json
231
+ {
232
+ "team": "Mexico",
233
+ "competition": "worldcup",
234
+ "season": "2026",
235
+ "requested_last": 5,
236
+ "played": 1,
237
+ "wins": 1,
238
+ "draws": 0,
239
+ "losses": 0,
240
+ "goals_for": 2,
241
+ "goals_against": 0,
242
+ "goal_difference": 2,
243
+ "points": 3,
244
+ "form": ["W"],
245
+ "matches": [
246
+ {
247
+ "opponent": "South Africa",
248
+ "home_away": "home",
249
+ "team_score": 2,
250
+ "opponent_score": 0,
251
+ "result": "W"
252
+ }
253
+ ]
254
+ }
255
+ ```
256
+
257
+ `HeadToHeadSummary` returns aggregate history plus latest matches:
258
+
259
+ ```json
260
+ {
261
+ "team_a": "Mexico",
262
+ "team_b": "South Africa",
263
+ "matches": 1,
264
+ "team_a_wins": 1,
265
+ "draws": 0,
266
+ "team_b_wins": 0,
267
+ "team_a_goals": 2,
268
+ "team_b_goals": 0,
269
+ "latest_matches": []
270
+ }
271
+ ```
272
+
273
+ ### Player History
274
+
275
+ Player history depends on a source that legally provides player-level public match facts. The default OpenFootball adapter does not include player stats, but `JsonFileSource` supports detailed authorized feeds:
276
+
277
+ ```python
278
+ from openmatchkit import MatchClient
279
+ from openmatchkit.sources.json_file import JsonFileSource
280
+
281
+ client = MatchClient(sources=[JsonFileSource("tests/fixtures/sample_detailed_source.json")])
282
+ history = client.player_history("Alex Demo")
283
+ print(history.model_dump(mode="json"))
284
+ ```
285
+
286
+ ```json
287
+ {
288
+ "player": {"name": "Alex Demo", "team": "Team A", "position": "FW", "shirt_number": 9},
289
+ "totals": {
290
+ "appearances": 1,
291
+ "starts": 1,
292
+ "minutes_played": 45,
293
+ "goals": 1,
294
+ "assists": 0,
295
+ "yellow_cards": 1,
296
+ "red_cards": 0
297
+ },
298
+ "appearances": [
299
+ {
300
+ "match_id": "detail-1",
301
+ "team": "Team A",
302
+ "opponent": "Team B",
303
+ "home_away": "home",
304
+ "goals": 1,
305
+ "source": "demo_public_feed"
306
+ }
307
+ ]
308
+ }
309
+ ```
310
+
311
+ ### Prediction
312
+
313
+ ```python
314
+ history = client.results("worldcup", "2026")
315
+ prediction = client.predict(home="Mexico", away="South Africa", history=history)
316
+ ```
317
+
318
+ Returns an educational baseline only:
319
+
320
+ ```json
321
+ {
322
+ "home": "Mexico",
323
+ "away": "South Africa",
324
+ "home_win_probability": 0.45,
325
+ "draw_probability": 0.27,
326
+ "away_win_probability": 0.28,
327
+ "expected_home_goals": 1.2,
328
+ "expected_away_goals": 0.9,
329
+ "model": "simple_poisson",
330
+ "training_matches": 2,
331
+ "note": "Educational baseline only. Not betting, financial, or professional advice."
332
+ }
333
+ ```
334
+
335
+ ### Export
336
+
337
+ ```python
338
+ client.export_json(fixtures, "fixtures.json")
339
+ client.export_csv(fixtures, "fixtures.csv")
340
+ ```
341
+
342
+ JSON export keeps full nested model data. CSV export flattens match-level fields for spreadsheet tools.
343
+
344
+ ## CLI
345
+
346
+ ```bash
347
+ openmatch sources
348
+ openmatch health
349
+ openmatch fixtures --competition worldcup --season 2026
350
+ openmatch search --competition worldcup --season 2026 --team Canada --status scheduled
351
+ openmatch results --competition en.1 --season 2015-16 --format csv --output results.csv
352
+ openmatch upcoming --competition worldcup --season 2026 --team Canada --limit 3
353
+ openmatch recent --competition worldcup --season 2026 --limit 5
354
+ openmatch next-match --competition worldcup --season 2026
355
+ openmatch match-summary --match-id detail-1 --data-file tests/fixtures/sample_detailed_source.json
356
+ openmatch standings --competition worldcup --season 2026
357
+ openmatch team-form --team Mexico --competition worldcup --season 2026
358
+ openmatch head-to-head --team-a Mexico --team-b "South Africa" --competition worldcup --season 2026
359
+ openmatch predict --home Mexico --away "South Africa" --competition worldcup --season 2026
360
+ ```
361
+
362
+ Detailed JSON feeds can expose richer scoreboards and player history:
363
+
364
+ ```bash
365
+ openmatch scoreboards --data-file tests/fixtures/sample_detailed_source.json
366
+ openmatch scoreboard --match-id detail-1 --data-file tests/fixtures/sample_detailed_source.json
367
+ openmatch live-scoreboards --data-file tests/fixtures/sample_detailed_source.json
368
+ openmatch player-history --player "Alex Demo" --data-file tests/fixtures/sample_detailed_source.json
369
+ ```
370
+
371
+ ```python
372
+ from openmatchkit import MatchClient
373
+ from openmatchkit.sources.json_file import JsonFileSource
374
+
375
+ client = MatchClient(sources=[JsonFileSource("tests/fixtures/sample_detailed_source.json")])
376
+ scoreboard = client.scoreboard(match_id="detail-1")
377
+ history = client.player_history("Alex Demo")
378
+ print(scoreboard.model_dump(mode="json"))
379
+ print(history.model_dump(mode="json"))
380
+ ```
381
+
382
+ ## Example Output
383
+
384
+ ```json
385
+ {
386
+ "competition": "World Cup 2026",
387
+ "home": {"name": "Canada"},
388
+ "away": {"name": "Bosnia & Herzegovina"},
389
+ "status": "scheduled",
390
+ "source": "openfootball"
391
+ }
392
+ ```
393
+
394
+ ## Features
395
+
396
+ - Unified match, scoreboard, player history, standings, and prediction models
397
+ - OpenFootball JSON adapter
398
+ - Football-Data.co.uk CSV adapter
399
+ - Local detailed JSON adapter for authorized/public detailed feeds
400
+ - Generic optional public HTML adapter
401
+ - Safe HTTP client with robots.txt checks, caching, user agent, and per-origin delay
402
+ - Fixtures, results, search, upcoming/recent matches, next match, live score adapter interface
403
+ - Source capabilities and health metadata
404
+ - Team info, grouped standings, team form, and head-to-head summaries
405
+ - Detailed scoreboards: clock, events, lineups, team stats, and player match stats when provided
406
+ - Player histories aggregated from player-level source data
407
+ - Simple Poisson prediction baseline and small Elo rating helper
408
+ - JSON and CSV export helpers
409
+ - Offline tests using local fixtures
410
+
411
+ ## Current Status
412
+
413
+ - Stable enough for demos, research, education, and early open-source feedback
414
+ - Not official live FIFA data
415
+ - True live/player-level data requires a lawful source adapter that provides those facts
416
+ - Public roadmap: [ROADMAP.md](ROADMAP.md)
417
+ - Release notes: [CHANGELOG.md](CHANGELOG.md)
418
+
419
+ ## Source and legal policy
420
+
421
+ The package code is MIT licensed. Third-party data sources have their own licenses and terms. Optional scraping adapters must respect Terms of Service, robots.txt, caching, and rate limits. Player-level data should only come from public or authorized sources and should contain public sporting facts, not private or sensitive information.
422
+
423
+ See [DATA_SOURCES.md](DATA_SOURCES.md), [PRIVACY.md](PRIVACY.md), and [CONTRIBUTING.md](CONTRIBUTING.md).
424
+
425
+ ## Development
426
+
427
+ ```bash
428
+ python -m pip install -e ".[dev]"
429
+ ruff check .
430
+ ruff format .
431
+ pytest
432
+ python -m build
433
+ ```
434
+
435
+ The prediction output is an educational baseline only. It is not betting, financial, or professional advice.