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/
Quality of Life Improvements - Enhanced debugging, pagination, and error handling:
from ifpa_api import (
IfpaClient,
IfpaApiError,
SeriesPlayerNotFoundError,
TournamentNotLeagueError,
)
# 1. Enhanced Error Messages - Full request context in exceptions
try:
player = client.player(99999).details()
except IfpaApiError as e:
print(e) # "[404] Resource not found (URL: https://api.ifpapinball.com/player/99999)"
print(e.request_url) # Direct access to URL
print(e.request_params) # Direct access to query parameters
# 2. Pagination Helpers - Automatic pagination for large result sets
for player in client.player.query().country("US").iterate(limit=100):
print(f"{player.first_name} {player.last_name}")
all_players = client.player.query().country("US").state("WA").get_all()
# 3. Semantic Exceptions - Clear, specific errors for common scenarios
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}")
# 4. Better Validation Messages - Helpful hints for validation errors
# Input error now shows: "Invalid parameter 'country': Input should be a valid string
# Hint: Country code should be a 2-letter string like 'US' or 'CA'"Query Builder Pattern - Build complex queries with a fluent, type-safe interface:
# Immutable query builders allow reuse
us_players = client.player.query().country("US")
wa_results = us_players.state("WA").limit(25).get()
or_results = us_players.state("OR").limit(25).get() # Base query unchanged
# Chain filters naturally
tournaments = client.tournament.query("Championship") \
.country("US") \
.date_range("2024-01-01", "2024-12-31") \
.limit(50) \
.get()
# Filter without search terms
results = client.player.query() \
.tournament("PAPA") \
.position(1) \
.get()Unified Callable Pattern - All resources now follow the same intuitive pattern:
# Individual resource access
player = client.player(12345).details()
director = client.director(456).details()
tournament = client.tournament(789).details()
# Collection queries
players = client.player.query("John").get()
directors = client.director.query("Josh").get()
tournaments = client.tournament.query("PAPA").get()
# Series operations
standings = client.series("NACS").standings()Breaking Changes: Users upgrading from 0.2.x should review the Migration Guide.
- 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.)
- 36 API Endpoints: Complete coverage of IFPA API v2.1 across 6 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(2643).details()
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}")
# Query players with filters
results = client.player.query("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 results
tournament = client.tournament(67890).details()
print(f"{tournament.tournament_name}")
print(f"Date: {tournament.event_date}")
print(f"Players: {tournament.tournament_stats.total_players}")
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.query().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(12345).details()
print(player.first_name)
# Client automatically closed# Query with filters
results = client.player.query("Smith") \
.country("US") \
.tournament("PAPA") \
.position(1) \
.limit(25) \
.get()
# Individual player operations
from ifpa_api.models.common import RankingSystem, ResultType
player = client.player(12345).details()
results = client.player(12345).results(RankingSystem.MAIN, ResultType.ACTIVE)
pvp = client.player(12345).pvp(67890) # Head-to-head comparison
history = client.player(12345).history()# Query for directors
results = client.director.query("Josh") \
.city("Seattle") \
.state("WA") \
.get()
# Individual director operations
director = client.director(1533).details()
tournaments = client.director(1533).tournaments(TimePeriod.PAST)
# Collection operations
country_dirs = client.director.country_directors()# Query with date range
results = client.tournament.query("Championship") \
.country("US") \
.date_range("2024-01-01", "2024-12-31") \
.limit(50) \
.get()
# Individual tournament operations
tournament = client.tournament(12345).details()
results = client.tournament(12345).results()
formats = client.tournament(12345).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()# Series operations
standings = client.series("NACS").standings()
card = client.series("PAPA").player_card(12345, region_code="OH")
regions = client.series("IFPA").regions(region_code="R1")
# List all series
all_series = client.series.list()
active_only = client.series.list(active=True)# 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.query().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.query("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.query().country("US").state("WA").get_all()
print(f"Total players: {len(all_players)}")
# Safety limit to prevent excessive memory usage
try:
results = client.player.query().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(99999999).details()
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.query("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.3.0 |
|---|---|
client.tournaments |
client.tournament |
client.player.search("name") |
client.player.query("name").get() |
client.tournament(id).get() |
client.tournament(id).details() |
client.series_handle("CODE") |
client.series("CODE") |
# Before (0.2.x)
results = client.player.search(name="John", country="US")
# After (0.3.0)
results = client.player.query("John").country("US").get()
# New capabilities - query reuse
base_query = client.player.query().country("US")
wa_players = base_query.state("WA").get()
or_players = base_query.state("OR").get()
# Filter without search term
winners = client.player.query().tournament("PAPA").position(1).get()# Before (0.2.x)
tournament = client.tournament(12345).get()
standings = client.series_handle("NACS").standings()
# After (0.3.0)
tournament = client.tournament(12345).details()
standings = client.series("NACS").standings()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.