Note: This is an unofficial client library, not affiliated with or endorsed by IFPA.
A typed Python client for the IFPA (International Flipper Pinball Association) API. Access player rankings, tournament data, and statistics through a clean, type-safe Python interface with Pydantic validation.
Complete documentation: https://johnsosoka.github.io/ifpa-api-python/
GitHub Pages Documentation - Professional documentation hosting:
The complete documentation is now hosted on GitHub Pages for improved accessibility and discoverability. Visit https://johnsosoka.github.io/ifpa-api-python/ for guides, API reference, and examples.
Type-Safe Enums - Enhanced type safety for rankings and tournaments:
from ifpa_api import IfpaClient, RankingDivision, TournamentSearchType
client = IfpaClient(api_key="your-api-key")
# Rankings with type-safe enum
rankings = client.rankings.women(
tournament_type=RankingDivision.OPEN,
count=50
)
# Tournament search with type-safe enum
tournaments = (client.tournament.search("Championship")
.tournament_type(TournamentSearchType.WOMEN)
.country("US")
.get())
# IDE autocomplete shows available options
# - RankingDivision.OPEN / RankingDivision.WOMEN
# - TournamentSearchType.OPEN / WOMEN / YOUTH / LEAGUE
# Strings still work (backward compatible)
rankings = client.rankings.women(tournament_type="OPEN", count=50)Benefits:
- Type safety: Catch invalid values at development time
- IDE autocomplete: Discover available division types
- Self-documenting: Clear what values are valid
- No breaking changes: Existing code continues to work
This release includes stats resource with 10 endpoints, type-safe enums for stats parameters, enhanced error messages, pagination helpers, and query builder pattern. See CHANGELOG for details.
- Full Type Safety: Complete type hints for IDE autocompletion and static analysis
- Pydantic Validation: Request and response validation with helpful error hints
- Query Builder Pattern: Composable, immutable queries with method chaining
- Automatic Pagination: Memory-efficient iteration with
.iterate()and.get_all() - Enhanced Error Context: All exceptions include request URLs and parameters for debugging
- Semantic Exceptions: Domain-specific errors (PlayersNeverMetError, SeriesPlayerNotFoundError, etc.)
- 46 API Endpoints: Complete coverage of IFPA API v2.1 across 7 resources
- 99% Test Coverage: Comprehensive unit and integration tests
- Context Manager Support: Automatic resource cleanup
- Clear Error Handling: Structured exception hierarchy for different failure modes
pip install ifpa-apiRequires Python 3.11 or higher.
from ifpa_api import IfpaClient
# Initialize with API key
client = IfpaClient(api_key='your-api-key-here')
# Get player profile and rankings
player = client.player.get(2643)
print(f"{player.first_name} {player.last_name}")
print(f"WPPR Rank: {player.player_stats.current_wppr_rank}")
print(f"WPPR Points: {player.player_stats.current_wppr_value}")
# Search for players with filters
results = client.player.search("John") \
.country("US") \
.state("CA") \
.limit(10) \
.get()
for player in results.search:
print(f"{player.first_name} {player.last_name} - {player.city}")
# Get tournament details
tournament = client.tournament.get(67890)
print(f"{tournament.tournament_name}")
print(f"Date: {tournament.event_date}")
print(f"Players: {tournament.tournament_stats.total_players}")
# Get tournament results
results = client.tournament(67890).results()
for result in results.results[:5]:
print(f"{result.position}. {result.player_name}: {result.points} pts")
# Automatic pagination for large datasets
for player in client.player.search().country("US").iterate(limit=100):
print(f"{player.first_name} {player.last_name}")
# Close client when done
client.close()Set IFPA_API_KEY to avoid passing the key in code:
export IFPA_API_KEY='your-api-key-here'from ifpa_api import IfpaClient
# API key automatically loaded from environment
client = IfpaClient()from ifpa_api import IfpaClient
with IfpaClient(api_key='your-api-key-here') as client:
player = client.player.get(12345)
print(player.first_name)
# Client automatically closed# Search with filters
results = client.player.search("Smith") \
.country("US") \
.tournament("PAPA") \
.position(1) \
.limit(25) \
.get()
# Convenience methods for individual players
player = client.player.get(12345)
player = client.player.get_or_none(12345) # Returns None if not found
if client.player.exists(12345):
print("Player exists!")
# Get first search result
first = client.player.search("Smith").first()
maybe_first = client.player.search("Rare Name").first_or_none()
# Individual player operations (using context)
from ifpa_api.models.common import RankingSystem, ResultType
results = client.player(12345).results(RankingSystem.MAIN, ResultType.ACTIVE)
pvp = client.player(12345).pvp(67890) # Head-to-head comparison
history = client.player(12345).history()# Search for directors
results = client.director.search("Josh") \
.city("Seattle") \
.state("WA") \
.get()
# Convenience methods for individual directors
director = client.director.get(1533)
director = client.director.get_or_none(1533) # Returns None if not found
if client.director.exists(1533):
print("Director exists!")
# Individual director operations (using context)
from ifpa_api.models.common import TimePeriod
tournaments = client.director(1533).tournaments(TimePeriod.PAST)
# Collection operations
country_dirs = client.director.list_country_directors()# Search with date range
results = client.tournament.search("Championship") \
.country("US") \
.date_range("2024-01-01", "2024-12-31") \
.limit(50) \
.get()
# Convenience methods for individual tournaments
tournament = client.tournament.get(12345)
tournament = client.tournament.get_or_none(12345) # Returns None if not found
if client.tournament.exists(12345):
print("Tournament exists!")
# Individual tournament operations (using context)
results = client.tournament(12345).results()
formats = client.tournament(12345).formats()
league = client.tournament(12345).league() # For league-format tournaments
# List all tournament formats
all_formats = client.tournament.list_formats()# Various ranking types
wppr = client.rankings.wppr(count=100)
women = client.rankings.women(count=50)
youth = client.rankings.youth(count=50)
country = client.rankings.by_country("US", count=100)
# Age-based rankings
seniors = client.rankings.age_based(50, 59, count=50)
# Custom rankings and lists
countries = client.rankings.country_list()
custom_systems = client.rankings.custom_list()# Convenience methods for series
standings = client.series.get("NACS")
standings = client.series.get_or_none("NACS") # Returns None if not found
if client.series.exists("NACS"):
print("Series exists!")
# Individual series operations (using context)
from ifpa_api.models.common import TimePeriod
card = client.series("PAPA").player_card(12345, region_code="OH")
regions = client.series("IFPA").regions(region_code="R1", year=2024)
overview = client.series("NACS").overview(time_period=TimePeriod.CURRENT)
# List all series
all_series = client.series.list_series()
active_only = client.series.list_series(active_only=True)from ifpa_api import IfpaClient, StatsRankType, SystemCode, MajorTournament
client = IfpaClient()
# Get overall IFPA statistics
stats = client.stats.overall(system_code=SystemCode.OPEN)
print(f"Active players: {stats.stats.active_player_count:,}")
print(f"Tournaments this year: {stats.stats.tournament_count_this_year:,}")
# Get top point earners for a time period
points = client.stats.points_given_period(
rank_type=StatsRankType.OPEN,
start_date="2024-01-01",
end_date="2024-12-31",
limit=25
)
for player in points.stats[:10]:
print(f"{player.first_name} {player.last_name}: {player.wppr_points} pts")
# Get largest tournaments
tournaments = client.stats.largest_tournaments(
rank_type=StatsRankType.OPEN,
country_code="US"
)
for tourney in tournaments.stats[:10]:
print(f"{tourney.tournament_name}: {tourney.player_count} players")
# Get player counts by country (women's rankings)
country_stats = client.stats.country_players(rank_type=StatsRankType.WOMEN)
for country in country_stats.stats[:10]:
print(f"{country.country_name}: {country.player_count:,} players")
# Get most active players in a time period
active_players = client.stats.events_attended_period(
rank_type=StatsRankType.OPEN,
start_date="2024-01-01",
end_date="2024-12-31",
country_code="US",
limit=25
)Type Safety: Stats methods accept typed enums (e.g., StatsRankType.WOMEN) or strings for backwards compatibility.
# Get countries and states
countries = client.reference.countries()
states = client.reference.state_provs(country_code="US")The SDK provides two methods for handling large result sets with automatic pagination:
Use .iterate() to process results one at a time without loading everything into memory:
# Iterate through all US players efficiently
for player in client.player.search().country("US").iterate(limit=100):
print(f"{player.first_name} {player.last_name} - {player.city}")
# Process each player individually
# Iterate through tournament results with filters
for tournament in client.tournament.search("Championship").country("US").iterate():
print(f"{tournament.tournament_name} - {tournament.event_date}")Use .get_all() when you need all results in a list:
# Get all players from Washington state
all_players = client.player.search().country("US").state("WA").get_all()
print(f"Total players: {len(all_players)}")
# Safety limit to prevent excessive memory usage
try:
results = client.player.search().country("US").get_all(max_results=1000)
except ValueError as e:
print(f"Too many results: {e}")Best Practices:
- Use
.iterate()for large datasets or when processing items one at a time - Use
.get_all()for smaller datasets when you need the complete list - Always set
max_resultswhen using.get_all()to prevent memory issues - Default batch size is 100 items per request; adjust with
limitparameter if needed
The SDK provides a structured exception hierarchy with enhanced error context for debugging.
from ifpa_api import IfpaClient, IfpaApiError, MissingApiKeyError
try:
client = IfpaClient() # Raises if no API key found
player = client.player.get(99999999)
except MissingApiKeyError:
print("No API key provided or found in environment")
except IfpaApiError as e:
print(f"API error [{e.status_code}]: {e.message}")
print(f"Request URL: {e.request_url}")
print(f"Request params: {e.request_params}")The SDK raises domain-specific exceptions for common error scenarios:
from ifpa_api import (
IfpaClient,
PlayersNeverMetError,
SeriesPlayerNotFoundError,
TournamentNotLeagueError,
)
client = IfpaClient(api_key='your-api-key')
# Players who have never competed together
try:
comparison = client.player(12345).pvp(67890)
except PlayersNeverMetError as e:
print(f"Players {e.player_id} and {e.opponent_id} have never met in competition")
# Player not found in series
try:
card = client.series("PAPA").player_card(12345, "OH")
except SeriesPlayerNotFoundError as e:
print(f"Player {e.player_id} has no results in {e.series_code} series")
print(f"Region: {e.region_code}")
# Non-league tournament
try:
league = client.tournament(12345).league()
except TournamentNotLeagueError as e:
print(f"Tournament {e.tournament_id} is not a league-format tournament")IfpaError (base)
├── MissingApiKeyError - No API key provided
├── IfpaApiError - API returned error (has status_code, response_body, request_url, request_params)
│ ├── PlayersNeverMetError - Players have never competed together
│ ├── SeriesPlayerNotFoundError - Player not found in series/region
│ └── TournamentNotLeagueError - Tournament is not a league format
└── IfpaClientValidationError - Request validation failed (includes helpful hints)
All API errors (v0.3.0+) include full request context:
try:
results = client.player.search("John").country("INVALID").get()
except IfpaApiError as e:
# Access error details
print(f"Status: {e.status_code}")
print(f"Message: {e.message}")
print(f"URL: {e.request_url}")
print(f"Params: {e.request_params}")
print(f"Response: {e.response_body}")| 0.2.x | 0.4.0 (Preferred) |
|---|---|
client.tournaments |
client.tournament |
client.player.search("name") |
client.player.search("name").get() |
client.player(id).details() |
client.player.get(id) |
client.tournament(id).get() |
client.tournament.get(id) |
client.series_handle("CODE") |
client.series.get("CODE") |
client.director.country_directors() |
client.director.list_country_directors() |
# Before (0.2.x)
results = client.player.search(name="John", country="US")
# After (0.4.0 - Preferred)
results = client.player.search("John").country("US").get()
# New capabilities - query reuse with immutable pattern
base_query = client.player.search().country("US")
wa_players = base_query.state("WA").get()
or_players = base_query.state("OR").get() # Original query unchanged
# Filter without search term
winners = client.player.search().tournament("PAPA").position(1).get()
# Convenience methods for getting first result
first = client.player.search("Smith").first()
maybe_first = client.player.search("Rare").first_or_none()# Before (0.2.x and 0.3.0)
tournament = client.tournament(12345).details()
player = client.player(12345).details()
standings = client.series_handle("NACS").standings()
# After (0.4.0 - Preferred)
tournament = client.tournament.get(12345)
player = client.player.get(12345)
standings = client.series.get("NACS")
# New convenience methods
player = client.player.get_or_none(12345) # Returns None instead of raising
if client.player.exists(12345):
print("Player exists!")See the CHANGELOG for complete migration details.
# Clone and install dependencies
git clone https://github.com/johnsosoka/ifpa-api-python.git
cd ifpa-api-python
poetry install
# Install pre-commit hooks
poetry run pre-commit install
# Set API key for integration tests
export IFPA_API_KEY='your-api-key'# Run unit tests (no API key required)
poetry run pytest tests/unit/ -v
# Run all tests including integration (requires API key)
poetry run pytest -v
# Run with coverage
poetry run pytest --cov=ifpa_api --cov-report=term-missing# Format code
poetry run black src tests
# Lint
poetry run ruff check src tests --fix
# Type check
poetry run mypy src
# Run all checks
poetry run pre-commit run --all-files- Documentation: https://johnsosoka.github.io/ifpa-api-python/
- PyPI Package: https://pypi.org/project/ifpa-api/
- GitHub Repository: https://github.com/johnsosoka/ifpa-api-python
- Issue Tracker: https://github.com/johnsosoka/ifpa-api-python/issues
- IFPA API Documentation: https://api.ifpapinball.com/docs
Contributions are welcome! Please see CONTRIBUTING.md for detailed guidelines on:
- Setting up your development environment
- Code quality standards (Black, Ruff, mypy)
- Writing and running tests
- Submitting pull requests
You can also contribute by:
- Reporting bugs
- Requesting features
- Providing feedback on usability and documentation
MIT License - Copyright (c) 2025 John Sosoka
See the LICENSE file for details.
Maintainer: John Sosoka | open.source@sosoka.com
Built for the worldwide pinball community.