Metadata-Version: 2.5
Name: football-tournament
Version: 0.2.0
Summary: A clean, reusable Python package for managing football tournaments
License: MIT License
        
        Copyright (c) 2026
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Requires-Python: >=3.12
Requires-Dist: pydantic>=2.0
Provides-Extra: dev
Requires-Dist: mypy>=1.0; extra == 'dev'
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.4; extra == 'dev'
Description-Content-Type: text/markdown

# Football Tournament Engine (`football-tournament`)

A production-ready, reusable Python library for managing football tournaments.

`football-tournament` is designed as a **standalone domain engine**. It encapsulates all core tournament management logic (team registration, stadium management, match generation, scheduling, conflict detection, result recording, league standings, and knockout bracket progression) without coupling to any web framework, ORM, database, or UI layer.

It can be seamlessly integrated into REST APIs (FastAPI, Django), university systems, mobile applications, or CLI tools.

---

## Features

- **Multiple Tournament Formats**:
  - **League / Table**: Round-robin (single and double round-robin).
  - **Knockout**: Single-elimination brackets with automatic bye handling for non-power-of-two team counts.
  - **Group + Knockout**: Group stage round-robins followed by automatic qualification and knockout bracket generation.
- **Robust Scheduling Engine**:
  - Manual and automatic scheduling based on configurable time windows (`SchedulingConfig`).
  - Strict conflict detection: prevents stadium double-booking, team overlapping matches, self-play, and invalid time slots.
- **Standings Calculator**:
  - Computes Played, Won, Drawn, Lost, Goals For, Goals Against, Goal Difference, and Points.
  - Configurable points system and tie-breaking rules (`points`, `goal_difference`, `goals_for`,
    `goals_against`, `wins`, `played`, `head_to_head`, `name`).
- **Results & Knockout Progression**:
  - Records scores and optional penalty shoot-outs; penalty results decide the winner of a drawn knockout tie.
  - Winners are auto-advanced into the next round and the bracket is exposed as ordered rounds via `get_bracket()`.
  - Matches can be cancelled; cancelled matches cannot be re-scored.
- **Tournament Lifecycle & Validation**:
  - State management (`DRAFT`, `REGISTRATION`, `SCHEDULED`, `ONGOING`, `COMPLETED`, `CANCELLED`)
    with an enforced state machine via `set_status()`; tournaments auto-complete when every match is done.
  - Comprehensive Pydantic validation, config validation, and custom domain exceptions.
- **Persistence**:
  - `save_tournament()` / `load_tournament()` (or the `engine.save()` / `engine.load()` convenience
    wrappers) serialize a full tournament to a JSON file that survives process restarts.
- **Framework & Database Independent**:
  - Works entirely in memory with pure Python objects.
  - Easily adaptable to SQLAlchemy, SQLModel, PostgreSQL, SQLite, or Supabase via persistence adapters.

---

## Installation

```bash
pip install football-tournament
```

For development (testing and linting):

```bash
pip install -e ".[dev]"
```

---

## Quick Start

```python
from datetime import date, time
from football_tournament import (
    TournamentEngine,
    TournamentFormat,
    Team,
    Stadium,
    SchedulingConfig,
)

# 1. Initialize engine
engine = TournamentEngine()

# 2. Create tournament
tournament = engine.create_tournament(
    name="University Football Tournament",
    format=TournamentFormat.LEAGUE,
)

# 3. Register teams
teams = [
    Team(name="Software Engineering"),
    Team(name="Computer Science"),
    Team(name="Electrical Engineering"),
    Team(name="Civil Engineering"),
]
for team in teams:
    engine.add_team(tournament, team)

# 4. Add stadium
stadium = Stadium(name="Main Stadium", location="Adama", capacity=5000)
engine.add_stadium(tournament, stadium)

# 5. Generate matches
matches = engine.generate_matches(tournament)

# 6. Automatically schedule matches
config = SchedulingConfig(
    start_date=date(2026, 10, 1),
    end_date=date(2026, 10, 5),
    daily_start=time(14, 0),
    daily_end=time(18, 0),
)
engine.auto_schedule(tournament, config)

# 7. Record match result
engine.set_match_result(tournament, matches[0].id, home_score=2, away_score=1)

# 8. View standings
standings = engine.get_standings(tournament)
for rec in standings:
    print(f"{rec.team.name}: {rec.points} pts (GD: {rec.goal_difference})")
```

### Knockout with penalties, bracket and persistence

```python
from football_tournament import TournamentEngine, TournamentFormat, Team

engine = TournamentEngine()

tournament = engine.create_tournament(name="Champions Cup", format=TournamentFormat.KNOCKOUT)
for name in ["Real Madrid", "Bayern", "Liverpool", "PSG"]:
    engine.add_team(tournament, Team(name=name))

engine.generate_matches(tournament)

# First semifinal ends level; penalties decide the winner.
sf = tournament.matches[0]
engine.set_match_result(
    tournament, sf.id, home_score=1, away_score=1,
    home_penalties=4, away_penalties=3,   # home team advances
)

# The winner of that semifinal is now slotted into the Final.
import json
bracket = engine.get_bracket(tournament)          # list[Round]
print([round.name for round in bracket])          # e.g. ["Semifinal", "Final"]

# Persist the whole tournament to disk and reload it later.
path = engine.save(tournament, "champions_cup.json")
reloaded = engine.load(path)
assert reloaded.name == "Champions Cup"
```

See `engine.get_standings(tournament, tiebreakers=["points", "head_to_head"])` for
custom tie-breaking, and `engine.cancel_match(...)` / `tournament.set_status(...)` for
cancellation and lifecycle control.

---

## Architecture Overview

```text
football_tournament/
├── models/         # Domain entities (Tournament, Team, Stadium, Match, Group, Round)
├── enums/          # Controlled vocabularies (TournamentFormat, MatchStatus, TournamentStatus)
├── formats/        # Format strategies (League, Knockout, Group + Knockout)
├── scheduling/     # Scheduler, conflict detectors, time slots, scheduling config
├── standings/      # Standings calculator and ranking rules
├── exceptions/     # Custom domain errors
├── persistence.py  # JSON save/load helpers
└── engine.py       # High-level TournamentEngine facade
```

### Separation of Responsibilities
1. **Match Generation**: Creates fixture pairings without date/time/stadium bindings.
2. **Scheduling**: Assigns time slots and stadiums while enforcing strict conflict rules.
3. **Results**: Records scores and updates match status.
4. **Standings**: Computes table statistics from completed matches.

---

## Integrating with FastAPI

Because the package is framework-independent, integrating it into FastAPI is straightforward:

```python
from fastapi import FastAPI, HTTPException
from football_tournament import TournamentEngine, TournamentFormat, Team, Stadium

app = FastAPI()
engine = TournamentEngine()

# In-memory store for demonstration (replace with database adapter in production)
tournaments = {}

@app.post("/tournaments")
def create_tournament(name: str, format: TournamentFormat):
    tournament = engine.create_tournament(name=name, format=format)
    tournaments[tournament.id] = tournament
    return {"id": tournament.id, "name": tournament.name, "format": tournament.format}

@app.post("/tournaments/{tournament_id}/teams")
def add_team(tournament_id: str, name: str):
    if tournament_id not in tournaments:
        raise HTTPException(status_code=404, detail="Tournament not found")
    t = tournaments[tournament_id]
    team = Team(name=name)
    engine.add_team(t, team)
    return {"team_id": team.id, "name": team.name}
```

---

## Running Tests

Run the test suite with `pytest`:

```bash
PYTHONPATH=src pytest
```

---

## Building & Publishing

### Build Wheel and Source Distribution

```bash
pip install build
python -m build
```

### Publish to PyPI

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

---

## License

MIT License. See [LICENSE](LICENSE) for details.
