Metadata-Version: 2.4
Name: tacoscore
Version: 0.1.1
Summary: TacosScore - football analytics from Sofascore. Typed models, DataFrames, and rate-limited API access for analysts.
Project-URL: Homepage, https://github.com/Tariq-15/TacosScore
Project-URL: Repository, https://github.com/Tariq-15/TacosScore
Project-URL: Issues, https://github.com/Tariq-15/TacosScore/issues
Author-email: "Md. Tariquzzaman" <tariquzzaman01@gmail.com>
License-Expression: MIT
License-File: LICENSE
Keywords: analytics,football,soccer,sofascore,sports-data,tacoscore,world-cup,xg
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering :: Information Analysis
Classifier: Typing :: Typed
Requires-Python: >=3.9
Requires-Dist: curl-cffi>=0.6.0
Requires-Dist: httpx>=0.24.0
Provides-Extra: analysis
Requires-Dist: ipykernel>=6.0; extra == 'analysis'
Requires-Dist: jupyter>=1.0; extra == 'analysis'
Requires-Dist: pandas>=2.0; extra == 'analysis'
Provides-Extra: dev
Requires-Dist: build>=1.0; extra == 'dev'
Requires-Dist: mypy>=1.0; extra == 'dev'
Requires-Dist: pkginfo>=1.12.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
Requires-Dist: pytest>=7.0; extra == 'dev'
Requires-Dist: respx>=0.20; extra == 'dev'
Requires-Dist: ruff>=0.1.0; extra == 'dev'
Requires-Dist: twine>=6.0; extra == 'dev'
Description-Content-Type: text/markdown

﻿<p align="center">
  <a href="https://imgur.com/ovkBcDL">
    <img src="https://i.imgur.com/ovkBcDL.png" alt="TacosScore — Python Football Data Library" width="420"/>
  </a>
</p>

<h1 align="center">TacosScore</h1>

<p align="center">
  <strong>Python Football Data Library</strong> — typed models, pandas DataFrames, rate-limited API access, and a full CLI built for analysts.
</p>

<p align="center">
  <a href="https://pypi.org/project/tacoscore/">PyPI</a> ·
  <a href="https://github.com/Tariq-15/TacosScore">GitHub</a> ·
  <a href="./docs/sofascore-api-reference.md">API Reference</a> ·
  <a href="./LICENSE">MIT License</a>
</p>

---

**Author:** [Md. Tariquzzamana](mailto:tariquzzaman01@gmail.com)  
**Version:** 0.1.0  
**Python:** 3.9+

> **Disclaimer:** TacosScore is **not affiliated with Sofascore**. The Sofascore API is **undocumented** and may change without notice. Use responsibly for research and personal analysis. See [Operational considerations](#operational-considerations).

---

## Table of contents

- [Why TacosScore?](#why-tacoscore)
- [Features](#features)
- [Installation](#installation)
- [Quick start](#quick-start)
- [World Cup 2026 extraction example](#world-cup-2026-extraction-example)
- [Finding matches and IDs](#finding-matches-and-ids)
- [Client configuration](#client-configuration)
- [API methods reference](#api-methods-reference)
- [Full-match pipeline](#full-match-pipeline)
- [Data models](#data-models)
- [Player statistics](#player-statistics)
- [Pandas and DataFrames](#pandas-and-dataframes)
- [Analytics extraction](#analytics-extraction)
- [Coordinate systems](#coordinate-systems)
- [Command-line interface](#command-line-interface)
- [Interactive help](#interactive-help)
- [Async client](#async-client)
- [Serialization and export](#serialization-and-export)
- [Exceptions](#exceptions)
- [Project structure](#project-structure)
- [Development and testing](#development-and-testing)
- [Operational considerations](#operational-considerations)
- [License](#license)

---

## Why TacosScore?

Sofascore exposes rich match data — xG, shot maps, heatmaps, action streams, ~60 per-player stats — but the API is reverse-engineered, JSON-heavy, and protected by Akamai. TacosScore gives you:

| Problem | TacosScore solution |
|--------|---------------------|
| Raw JSON with UI clutter | Typed `dataclass` models + `strip_ui_fields()` |
| 403 errors from plain HTTP | `curl_cffi` Chrome TLS impersonation |
| Rate-limit bans | Built-in rate limiter + exponential backoff on 429 |
| Inconsistent field names | Snake_case Python fields mapped from camelCase API |
| Notebook / warehouse workflows | `.to_dataframe()` on models + standalone helpers |
| Discoverability | `tacoscore help` interactive guide + full CLI |

Inspired by the breadth of tools like Match ScraperFC, TacosScore focuses on **football analytics only** — media streams, highlights, betting UI, and comment feeds are intentionally excluded.

---

## Features

- **Sync and async clients** — `TacosScoreClient` and `AsyncTacosScoreClient`
- **Match-level data** — metadata, lineups, team stats, incidents, graph, H2H, pregame form, match shotmap
- **Player-level spatial data** — heatmap, action stream (rating-breakdown), per-player shotmap
- **Tournament discovery** — rounds, round fixtures, standings, season-wide fetch
- **Profiles** — team, player, season stats, transfers, rankings, fixture lists
- **`fetch_full_match()`** — orchestrated multi-endpoint pipeline with smart skips
- **CLI** — every major endpoint as a subcommand with `--raw`, `--pretty`, `--analytics-only`
- **Interactive help** — arrow-key browser (`tacoscore help`)
- **132+ tests** — parsers, client, CLI, extraction rules

---

## Installation

### From PyPI

```bash
pip install tacoscore
```

### With pandas and Jupyter (recommended for analysts)

```bash
pip install "tacoscore[analysis]"
```

### From source (development)

```bash
git clone https://github.com/Tariq-15/TacosScore.git
cd TacosScore
pip install -e ".[dev,analysis]"
```

### Optional dependency groups

| Extra | Packages | Use case |
|-------|----------|----------|
| *(default)* | `curl_cffi`, `httpx` | Core client + CLI |
| `analysis` | `pandas`, `jupyter`, `ipykernel` | DataFrames and notebooks |
| `dev` | `pytest`, `ruff`, `mypy`, `respx`, … | Contributing and CI |

---

## Quick start

### Python — one match

```python
from tacoscore import TacosScoreClient

client = TacosScoreClient()

# Team stats and full lineups (~60 stats per player)
stats = client.event_statistics(15186861)
lineups = client.event_lineups(15186861)

print(f"{lineups.home.formation} vs {lineups.away.formation}")
print(f"Possession: {stats.by_period['ALL']['ballPossession'].home_value}%")

# Spatial data for one player
heatmap = client.player_heatmap(15186861, 868812)
actions = client.player_actions(15186861, 868812)
shots   = client.player_shotmap(15186861, 868812)

for shot in shots.shots:
    if shot.shot_type == "goal":
        print(f"Goal {shot.minute}' — xG={shot.xg:.3f}")

client.close()
```

### Python — full match in one call

```python
match = client.fetch_full_match(15186861)

print(match.event_detail)          # venue, referee, score
print(match.team_statistics)       # by period
print(match.lineups.home.players)  # all home players + stats
print(match.match_shotmap)         # every shot in the match
print(match.player_data[868812])   # heatmap, actions, shotmap per player
```

### CLI — fastest way to inspect JSON

```bash
tacoscore lineups 15186861 --pretty
tacoscore event-stats 15186861
tacoscore fetch-match 15186861 --out match.json --pretty
tacoscore help
```

### Import name (TacosScore Client)

```python
from tacoscore import TacosScoreClient

client = TacosScoreClient()
```

The old names `SofascoreClient` and `AsyncSofascoreClient` still work as **aliases** for backward compatibility, but new code should use **`TacosScoreClient`** and **`AsyncTacosScoreClient`**.

---

## World Cup 2026 extraction example

Use this ready-to-copy flow to extract World Cup 2026 data using `58210` as requested.

```python
from tacoscore import TacosScoreClient

client = TacosScoreClient()

# World Cup 2026 extraction example
# tournament_id = 16, 58210 used here for World Cup 2026 season data
rounds = client.tournament_rounds(16, 58210)
round_6 = client.round_events(16, 58210, 6, slug="round-of-32")

for match in round_6.events:
    event_id = match.event_id
    full = client.fetch_full_match(
        event_id,
        include_match_shotmap=True,
        include_h2h=True,
        include_pregame_form=True,
    )
    print(
        event_id,
        full.lineups.home.team.name,
        "vs",
        full.lineups.away.team.name,
        "| players:",
        len(full.player_data),
    )

client.close()
```

CLI version:

```bash
tacoscore rounds 16 58210
tacoscore round-events 16 58210 6 --slug round-of-32 --table
tacoscore fetch-match 15186861 --pretty --out wc2026_match.json
```

---

## Finding matches and IDs

Sofascore uses numeric IDs everywhere. You typically discover **`event_id`** values from tournament rounds or team fixture lists.

### Common tournament IDs

| Tournament | `tournament_id` | Example `season_id` |
|------------|-----------------|---------------------|
| FIFA World Cup | `16` | `58210` (2026) |
| Premier League | `17` | varies by season |
| UEFA Champions League | `7` | varies by season |

### Workflow: list rounds → list matches → fetch match

```python
from tacoscore import TacosScoreClient

client = TacosScoreClient()

# 1. All rounds in a season
rounds = client.tournament_rounds(16, 58210)
print(rounds.current_round)
for r in rounds.rounds:
    print(r.round, r.name, r.slug)

# 2. Matches in a knockout round (slug required for named rounds)
r32 = rounds.by_slug("round-of-32")
matches = client.round_events(16, 58210, r32.round, r32.slug)

for m in matches.events:
    print(m.event_id, m.home_team.name, "vs", m.away_team.name)

# 3. Use event_id for any match endpoint
event_id = matches.events[0].event_id
lineups = client.event_lineups(event_id)
```

### CLI equivalent

```bash
tacoscore rounds 16 58210
tacoscore round-events 16 58210 6 --slug round-of-32 --table
```

The `--table` flag prints `event_id | Home vs Away` per line instead of JSON.

### Extract event ID from a Sofascore URL

Match URLs look like:

```
https://www.sofascore.com/scotland-brazil/xxxxx#id:15186861
```

The number after `id:` is the **`event_id`**.

---

## Client configuration

```python
from tacoscore import TacosScoreClient

client = TacosScoreClient(
    rate_limit_seconds=1.5,   # minimum gap between requests (default 1.5)
    rate_jitter_seconds=0.5,  # random extra delay (default 0.5)
    timeout=30.0,             # per-request timeout in seconds
    max_retries=3,            # retries on HTTP 429 with backoff
    user_agent=None,          # optional custom User-Agent
)
```

| Parameter | Default | Description |
|-----------|---------|-------------|
| `rate_limit_seconds` | `1.5` | Minimum seconds between API calls |
| `rate_jitter_seconds` | `0.5` | Random jitter added to spacing |
| `timeout` | `30.0` | Request timeout |
| `max_retries` | `3` | Retries on rate-limit (429) responses |
| `user_agent` | built-in | Override User-Agent header |

Always call `client.close()` when done (or use `AsyncTacosScoreClient` as a context manager).

---

## API methods reference

Base URL: `https://www.sofascore.com/api/v1`

### Match endpoints

| Python method | Returns | API path | CLI command |
|---------------|---------|----------|-------------|
| `event(event_id)` | `EventDetail` | `GET event/{id}` | `tacoscore event ID` |
| `event_lineups(event_id)` | `Lineups` | `GET event/{id}/lineups` | `tacoscore lineups ID` |
| `event_statistics(event_id)` | `TeamStatistics` | `GET event/{id}/statistics` | `tacoscore event-stats ID` |
| `event_incidents(event_id)` | `Incidents` | `GET event/{id}/incidents` | `tacoscore incidents ID` |
| `event_graph(event_id)` | `MatchGraph` | `GET event/{id}/graph` | `tacoscore graph ID` |
| `event_managers(event_id)` | `EventManagers` | `GET event/{id}/managers` | `tacoscore managers ID` |
| `event_shotmap(event_id)` | `Shotmap` | `GET event/{id}/shotmap` | `tacoscore event-shotmap ID` |
| `event_h2h(event_id)` | `HeadToHead` | `GET event/{id}/h2h` | `tacoscore h2h ID` |
| `event_pregame_form(event_id)` | `PregameForm` | `GET event/{id}/pregame-form` | `tacoscore pregame-form ID` |

### Player endpoints (per match)

| Python method | Returns | API path | CLI command |
|---------------|---------|----------|-------------|
| `player_heatmap(event_id, player_id)` | `Heatmap` | `GET event/{id}/player/{pid}/heatmap` | `tacoscore heatmap ID PID` |
| `player_actions(event_id, player_id)` | `ActionStream` | `GET event/{id}/player/{pid}/rating-breakdown` | `tacoscore actions ID PID` |
| `player_shotmap(event_id, player_id)` | `Shotmap` | `GET event/{id}/shotmap/player/{pid}` | `tacoscore shotmap ID PID` |
| `player_statistics(event_id, player_id)` | `SinglePlayerStats` | `GET event/{id}/player/{pid}/statistics` | `tacoscore player-stats ID PID` |

> **Tip:** Prefer `event_lineups()` when you need stats for the entire squad. `player_statistics()` returns the same block for one player only.

### Tournament and standings

| Python method | Returns | CLI command |
|---------------|---------|-------------|
| `tournament_rounds(tournament_id, season_id)` | `RoundList` | `tacoscore rounds TID SID` |
| `round_events(tid, sid, round_num, slug=None)` | `MatchList` | `tacoscore round-events TID SID ROUND [--slug]` |
| `season_events(tid, sid)` | `dict[int, MatchList]` | *(Python only — one request per round)* |
| `tournament_standings(tid, sid, table_type="total")` | `Standings` | `tacoscore standings TID SID [--type]` |

`table_type` for standings: `"total"`, `"home"`, or `"away"`.

### Profiles and history

| Python method | Returns | CLI command |
|---------------|---------|-------------|
| `team(team_id)` | `TeamProfile` | `tacoscore team ID` |
| `player(player_id)` | `PlayerProfile` | `tacoscore player ID` |
| `player_season_statistics(pid, tid, sid)` | `SeasonPlayerStatistics` | `tacoscore player-season-stats PID TID SID` |
| `team_events(team_id, page=0)` | `MatchList` | `tacoscore team-events ID [--page]` |
| `team_events_next(team_id, page=0)` | `MatchList` | *(Python only)* |
| `team_rankings(team_id)` | `TeamRankings` | `tacoscore team-rankings ID` |
| `player_transfer_history(player_id)` | `TransferHistory` | *(Python only)* |
| `player_events_last(player_id, page=0)` | `MatchList` | *(Python only)* |

### Low-level access

```python
raw = client.get_raw("event/15186861/lineups")
```

Any CLI subcommand also supports `--raw` to bypass parsers and emit original API JSON.

---

## Full-match pipeline

`fetch_full_match()` implements the recommended analytics order from [`docs/sofascore-api-reference.md`](./docs/sofascore-api-reference.md) (Section 10).

### Fetch order

```
event → lineups → statistics → incidents → managers → graph
  → h2h → pregame_form → match_shotmap
  → per-player: heatmap → actions → shotmap
```

### Parameters

```python
match = client.fetch_full_match(
    event_id=15186861,
    skip_no_minutes=True,           # skip spatial calls for 0-minute players
    skip_no_shots=True,             # skip shotmap when total_shots == 0
    skip_sparse_heatmap=True,       # skip heatmap when touches <= threshold
    min_touches_for_heatmap=5,      # minimum touches for heatmap
    include_match_shotmap=True,     # fetch event/{id}/shotmap
    include_h2h=True,
    include_pregame_form=True,
)
```

| Parameter | Default | Effect |
|-----------|---------|--------|
| `skip_no_minutes` | `True` | No heatmap/actions/shotmap for unused subs |
| `skip_no_shots` | `True` | No per-player shotmap when `total_shots == 0` |
| `skip_sparse_heatmap` | `True` | No heatmap when `touches <= min_touches` |
| `min_touches_for_heatmap` | `5` | Touch threshold for heatmap |
| `include_match_shotmap` | `True` | Fetch all shots in the match |
| `include_h2h` | `True` | Head-to-head aggregates |
| `include_pregame_form` | `True` | Pre-match form strings |

### `FullMatch` structure

```python
match.event_id            # int
match.event_detail        # EventDetail | None
match.team_statistics     # TeamStatistics
match.lineups             # Lineups
match.incidents           # Incidents | None
match.graph               # MatchGraph | None
match.managers            # EventManagers | None
match.head_to_head        # HeadToHead | None
match.pregame_form        # PregameForm | None
match.match_shotmap       # Shotmap | None
match.player_data         # dict[int, PlayerMatchData]
```

Each `PlayerMatchData` holds:

```python
pmd = match.player_data[868812]
pmd.player_id   # int
pmd.heatmap     # Heatmap | None
pmd.actions     # ActionStream | None
pmd.shotmap     # Shotmap | None
```

### CLI

```bash
tacoscore fetch-match 15186861 --out match.json --pretty
tacoscore fetch-match 15186861 --include-bench --fetch-empty-shotmaps
```

---

## Data models

All models are immutable-style `dataclasses` with typed fields. Parsers map Sofascore camelCase JSON to snake_case Python.

### Core match models

| Model | Description |
|-------|-------------|
| `EventDetail` | Teams, score, status, venue, referee, attendance |
| `Lineups` | Confirmed flag, home/away `LineupSide` (formation + players) |
| `LineupEntry` | `Player`, shirt number, position, captain, `PlayerStats` |
| `TeamStatistics` | Stats keyed by period (`ALL`, `1ST`, `2ND`) then stat name |
| `Incidents` | Goals, cards, subs, period markers |
| `MatchGraph` | Minute-by-minute attack momentum |
| `Shotmap` / `Shot` | xG, coordinates, body part, situation, outcome |
| `Heatmap` / `HeatmapPoint` | Normalized touch coordinates |
| `ActionStream` / `Action` | Passes, dribbles, defensive actions, carries |
| `HeadToHead` | Team and manager duel records |
| `PregameForm` | Form strings and table context |

### Discovery models

| Model | Description |
|-------|-------------|
| `RoundList` / `Round` | Season rounds with optional knockout slug |
| `MatchList` / `MatchSummary` | Fixture lists with `event_id` |
| `Standings` / `StandingRow` | League table rows |
| `TeamProfile` / `PlayerProfile` | Bio, market value, venue |
| `SeasonPlayerStatistics` | Season aggregate stats |
| `TransferHistory` / `Transfer` | Career transfers |
| `TeamRankings` | FIFA-style ranking rows |

### Accessing raw JSON

Every parsed model stores the original payload:

```python
lineups = client.event_lineups(15186861)
lineups.raw                    # full API dict on Lineups
lineups.home.players[0].stats.raw  # per-player raw stats block
```

Use `--include-raw` on the CLI to include `raw` fields in JSON output.

---

## Player statistics

`PlayerStats` exposes **~60 fields** per player. Sofascore omits zero values in JSON; TacosScore defaults missing fields to `0` / `0.0`.

### Categories

| Category | Example fields |
|----------|----------------|
| **Passing** | `total_pass`, `accurate_pass`, `key_pass`, `goal_assist`, `expected_assists` |
| **Shooting** | `total_shots`, `goals`, `expected_goals`, `expected_goals_on_target`, `big_chance_created` |
| **Dribbling** | `total_contest`, `won_contest`, `dispossessed` |
| **Duels** | `duel_won`, `duel_lost`, `aerial_won`, `aerial_lost` |
| **Defending** | `total_tackle`, `interception_won`, `ball_recovery`, `error_lead_to_a_goal` |
| **Fouls** | `fouls`, `was_fouled`, `total_offside` |
| **Goalkeeping** | `saves`, `goals_prevented`, `keeper_save_value` |
| **Physical** | `top_speed`, `kilometers_covered`, `number_of_sprints` |
| **Ball carries** | `ball_carries_count`, `total_progression`, `progressive_ball_carries_count` |
| **Engagement** | `touches`, `minutes_played`, `possession_lost_ctrl` |
| **Rating** | `rating`, `shot_value_normalized`, `pass_value_normalized`, `goalkeeper_value_normalized` |

### Example — top xG in a match

```python
lineups = client.event_lineups(15186861)

rows = []
for side in (lineups.home, lineups.away):
    for entry in side.players:
        s = entry.stats
        rows.append((entry.player.name, s.expected_goals, s.minutes_played))

for name, xg, mins in sorted(rows, key=lambda r: r[1], reverse=True)[:5]:
    print(f"{name:20s}  xG={xg:.2f}  ({mins} min)")
```

---

## Pandas and DataFrames

Install the analysis extra:

```bash
pip install "tacoscore[analysis]"
```

### Method on models (`.to_dataframe()`)

Most models expose a convenience method:

```python
lineups = client.event_lineups(15186861)
df = lineups.to_dataframe()   # player_id is the first column

stats_df = client.event_statistics(15186861).to_dataframe()  # long format by period
shots_df = client.player_shotmap(15186861, 868812).to_dataframe(player_id=868812)
```

### Module-level helpers

```python
from tacoscore import (
    lineups_to_dataframe,
    team_stats_to_dataframe,
    player_stats_to_dataframe,
    heatmap_to_dataframe,
    actions_to_dataframe,
    shotmap_to_dataframe,
    incidents_to_dataframe,
    graph_to_dataframe,
    matches_to_dataframe,
    rounds_to_dataframe,
    standings_to_dataframe,
    season_player_stats_to_dataframe,
)

df = lineups_to_dataframe(lineups)
```

| Helper | Input model | Typical use |
|--------|-------------|-------------|
| `lineups_to_dataframe` | `Lineups` | One row per player, all stat columns |
| `team_stats_to_dataframe` | `TeamStatistics` | Long format: period × stat × home/away |
| `shotmap_to_dataframe` | `Shotmap` | Shot-level xG, coords, body part |
| `heatmap_to_dataframe` | `Heatmap` | Touch coordinates |
| `actions_to_dataframe` | `ActionStream` | Pass/dribble/defensive events |
| `matches_to_dataframe` | `MatchList` | Fixture discovery tables |
| `standings_to_dataframe` | `Standings` | League table |

### Notebook and explore script

```bash
jupyter notebook notebooks/tacoscore_methods.ipynb
python scripts/explore_data.py --show-raw
```

---

## Analytics extraction

The `tacoscore.extraction` module encodes rules from the [Sofascore API Reference](./docs/sofascore-api-reference.md) — which fields to keep, drop, and when to skip endpoints.

### Strip UI-only JSON keys

```python
from tacoscore import strip_ui_fields

clean = strip_ui_fields(client.get_raw("event/15186861/lineups"))
```

Dropped keys include: `fieldTranslations`, `userCount`, `teamColors`, `logo`, `draw`, `directStreamUrl`, `thumbnailUrl`, and other web/UI fields. See `UI_DROP_KEYS` in `extraction.py`.

### Pipeline constants

```python
from tacoscore import MATCH_EXTRACTION_ORDER, ANALYTICS_EVENT_ENDPOINTS

print(MATCH_EXTRACTION_ORDER)
# ('event', 'lineups', 'statistics', 'incidents', 'managers', 'graph', ...)

from tacoscore.extraction import should_fetch_shotmap, should_fetch_heatmap

should_fetch_shotmap(total_shots=0)   # False
should_fetch_heatmap(touches=3)       # False (default min_touches=5)
```

### CLI — analytics-only JSON

```bash
tacoscore lineups 15186861 --analytics-only --pretty
```

This removes UI keys from serialized output before writing to stdout or `--out`.

### Intentionally excluded endpoints

Media and web-only paths are **not** implemented as typed methods:

| Suffix | Reason |
|--------|--------|
| `/media` | Video streams (m3u8), thumbnails |
| `/highlights` | Video highlights |
| `/news` | Editorial content |
| `/tv-channels` | Broadcast listings |
| `/comments` | User comments |

Use `get_raw()` only if you explicitly need these; do not use them in analytics pipelines.

---

## Coordinate systems

Sofascore uses two 0–100 grids plus a separate goal-mouth frame. TacosScore documents them in `coordinates.py` and provides **StatsBomb-aligned** conversion in `statsbomb.py`.

| System | Used by | X axis | Y axis |
|--------|---------|--------|--------|
| **A** | Heatmap, rating-breakdown | 0 = own goal → 100 = opposition goal | 0/100 = touchlines |
| **B** | Shotmap `playerCoordinates` | Distance from opposition goal | Lateral |
| **Goal mouth** | `goalMouthCoordinates` | Always 0 (on goal line) | Lateral `y`, height `z` |

### StatsBomb 120×80 (recommended for plotting)

| Rule | Value |
|------|--------|
| Origin | `(0, 0)` = bottom-left corner |
| Pitch size | 120 × 80 |
| Home goal (default) | `x = 120` when `home_attacks_left_to_right=True` |

```python
from tacoscore import TacosScoreClient
from tacoscore.statsbomb import convert_shotmap_to_statsbomb

client = TacosScoreClient()
shots = client.event_shotmap(15186861)

# Each shot: location (where struck) + end_location (goal mouth)
for spatial in convert_shotmap_to_statsbomb(shots, home_attacks_left_to_right=True):
    print(spatial.location, spatial.end_location)

# DataFrame with sb_x, sb_y, sb_end_x, sb_end_y, sb_end_z columns
df = shots.to_statsbomb_dataframe(home_attacks_left_to_right=True)
```

Low-level converters:

```python
from tacoscore.statsbomb import (
    system_a_to_statsbomb,   # heatmap / actions
    system_b_to_statsbomb,   # shot take position
    goal_mouth_to_statsbomb,  # goal frame (y, z) -> end_location
)
```

### Metres on FIFA pitch (105×68)

```python
from tacoscore.coordinates import system_a_to_pitch, system_b_to_pitch

px, py = system_a_to_pitch(63.0, 91.0)
px, py = system_b_to_pitch({"x": 8.1, "y": 51}, attack_right=True)
```

Note: `coordinates.to_statsbomb()` is a **simple scale** on System A only. Use `tacoscore.statsbomb` for shotmap and mixed-team plots.

---

## Command-line interface

After install, `tacoscore` is on your PATH.

### Global options

```bash
tacoscore [--version]
        [--rate-limit SEC] [--jitter SEC]
        [--max-retries N] [--timeout SEC]
        [--user-agent UA]
        COMMAND ...
```

### Per-command options

| Flag | Description |
|------|-------------|
| `--out PATH` | Write JSON to file instead of stdout |
| `--raw` | Emit original API JSON (skip parsers) |
| `--pretty` | Pretty-print JSON (2-space indent) |
| `--include-raw` | Include each model's `raw` field in output |
| `--analytics-only` | Strip UI-only Sofascore keys from output |

### All subcommands

```bash
# Match
tacoscore event EVENT_ID
tacoscore lineups EVENT_ID
tacoscore event-stats EVENT_ID
tacoscore incidents EVENT_ID
tacoscore graph EVENT_ID
tacoscore managers EVENT_ID
tacoscore h2h EVENT_ID
tacoscore pregame-form EVENT_ID
tacoscore event-shotmap EVENT_ID
tacoscore fetch-match EVENT_ID [--include-bench] [--fetch-empty-shotmaps]

# Player (per match)
tacoscore heatmap EVENT_ID PLAYER_ID
tacoscore actions EVENT_ID PLAYER_ID
tacoscore shotmap EVENT_ID PLAYER_ID
tacoscore player-stats EVENT_ID PLAYER_ID

# Tournament
tacoscore rounds TOURNAMENT_ID SEASON_ID
tacoscore round-events TID SID ROUND [--slug SLUG] [--table]
tacoscore standings TID SID [--type total|home|away]

# Profiles
tacoscore team TEAM_ID
tacoscore player PLAYER_ID
tacoscore player-season-stats PID TID SID
tacoscore team-events TEAM_ID [--page N]
tacoscore team-rankings TEAM_ID

# Help
tacoscore help [TOPIC] [--all] [--no-interactive]
```

### Examples

```bash
# Pretty JSON to stdout
tacoscore lineups 15186861 --pretty

# Save full match bundle
tacoscore fetch-match 15186861 --out wc_match.json --pretty

# Raw API passthrough
tacoscore heatmap 15186861 868812 --raw

# List knockout fixtures
tacoscore round-events 16 58210 6 --slug round-of-32 --table
```

---

## Interactive help

Browse all methods with an arrow-key UI (like an in-terminal notebook index):

```bash
tacoscore help
```

| Key | Action |
|-----|--------|
| ↑ / ↓ | Navigate list |
| Enter | Open section or method detail |
| Backspace | Go back |
| q / Esc | Quit |

```bash
# One topic (plain text)
tacoscore help fetch_full_match
tacoscore help player_heatmap

# Full catalog
tacoscore help --all
```

### From Python

```python
from tacoscore import show_help, get_help_entry, list_help_entries

show_help()                        # interactive when run in a TTY
show_help("event_lineups")         # print one entry
show_help(interactive=False)       # print entire catalog

entry = get_help_entry("fetch_full_match")
print(entry.signature)
print(entry.example_cli)
```

---

## Async client

```python
import asyncio
from tacoscore import AsyncTacosScoreClient

async def main():
    async with AsyncTacosScoreClient() as client:
        stats = await client.event_statistics(15186861)
        lineups = await client.event_lineups(15186861)
        match = await client.fetch_full_match(15186861)

asyncio.run(main())
```

`AsyncTacosScoreClient` mirrors every method on `TacosScoreClient`. Rate limiting is shared per client instance.

---

## Serialization and export

```python
from tacoscore._serialize import to_jsonable

match = client.fetch_full_match(15186861)
payload = to_jsonable(match, include_raw=False, analytics_only=True)

import json
with open("match_clean.json", "w", encoding="utf-8") as f:
    json.dump(payload, f, indent=2, ensure_ascii=False, default=str)
```

The CLI uses the same serializer internally.

---

## Exceptions

```python
from tacoscore import (
    SofascoreError,   # base class
    APIError,         # HTTP errors (4xx/5xx)
    RateLimitError,   # 429 after retries exhausted
    NotFoundError,    # 404
    ParseError,       # JSON shape unexpected
)

try:
    match = client.fetch_full_match(99999999)
except NotFoundError:
    print("Match not found")
except RateLimitError:
    print("Rate limited — increase rate_limit_seconds")
except SofascoreError as e:
    print(f"API error: {e}")
```

---

## Project structure

```
TacosScore/
├── src/tacoscore/
│   ├── client.py              # TacosScoreClient (sync)
│   ├── async_client.py        # AsyncTacosScoreClient
│   ├── cli.py                 # tacoscore CLI entry point
│   ├── extraction.py          # strip_ui_fields, pipeline rules
│   ├── help_catalog.py        # Method documentation catalog
│   ├── interactive_help.py    # Arrow-key help browser
│   ├── dataframe.py           # Pandas export helpers
│   ├── coordinates.py         # Metres + legacy scale-only to_statsbomb
│   ├── statsbomb.py           # StatsBomb 120×80 coordinate conversion
│   ├── _serialize.py          # JSON serialization
│   ├── models/                # Typed dataclasses
│   └── parsers/               # API JSON → models
├── docs/
│   └── sofascore-api-reference.md   # Field catalog + KEEP/DROP rules
├── notebooks/
│   └── tacoscore_methods.ipynb      # Exploration notebook
├── scripts/
│   └── explore_data.py              # Live API exploration
├── tests/                           # pytest suite (132+ tests)
├── pyproject.toml
├── README.md
├── LICENSE
└── TacosScore.png
```

---

## Development and testing

```bash
git clone https://github.com/Tariq-15/TacosScore.git
cd TacosScore
pip install -e ".[dev,analysis]"

# Run tests
pytest

# Lint
ruff check src tests

# Type check
mypy src/tacoscore
```

Tests use `respx` to mock HTTP — no live API calls in CI.

### Build and publish (maintainers)

```bash
pip install build twine
python -m build
twine upload dist/*
```

---

## Operational considerations

| Topic | Guidance |
|-------|----------|
| **Akamai / 403** | Plain `httpx` or `requests` often get blocked. TacosScore uses `curl_cffi` with Chrome TLS impersonation by default. |
| **Rate limits** | Default 1.5 s between requests. Aggressive scraping will get you IP-blocked. Use `fetch_full_match()` sparingly on large batches. |
| **Undocumented API** | Field names and shapes can change. Every model keeps `.raw` for forward compatibility. |
| **Legal / ToS** | Sofascore's terms may restrict automated access. This library is for **research and personal analysis**. Not authorized for commercial scraping or redistribution of Sofascore data. |
| **National teams** | Some team/player sub-endpoints return 404 for certain national-team IDs. |


---

## Related documentation

| Document | Contents |
|----------|----------|
| [`docs/sofascore-api-reference.md`](./docs/sofascore-api-reference.md) | Full endpoint catalog, field KEEP/DROP/UNKNOWN tables, pipeline spec |
| `tacoscore help` | Interactive in-terminal method guide |
| [`notebooks/tacoscore_methods.ipynb`](./notebooks/tacoscore_methods.ipynb) | Hands-on examples |

---

## License

MIT License — Copyright (c) 2026 **Md. Tariquzzaman**.

See [LICENSE](./LICENSE) for full text.

---

<p align="center">
  <sub>Built for football analysts who want typed data, not raw JSON soup.</sub>
</p>
