Typed Python client for the IFPA (International Flipper Pinball Association) API
Project description
IFPA API Client
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/
What's New in 0.4.0
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.
Features
- 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
Installation
pip install ifpa-api
Requires Python 3.11 or higher.
Quick Start
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()
Using Environment Variable
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()
Context Manager Pattern
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
Core Resources
Players
# 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()
Directors
# 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()
Tournaments
# 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()
Rankings
# 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
# 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)
Stats
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.
Reference Data
# Get countries and states
countries = client.reference.countries()
states = client.reference.state_provs(country_code="US")
Pagination
The SDK provides two methods for handling large result sets with automatic pagination:
Memory-Efficient Iteration
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}")
Collect All Results
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
Exception Handling
The SDK provides a structured exception hierarchy with enhanced error context for debugging.
Basic Error Handling
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}")
Semantic Exceptions
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")
Exception Hierarchy
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)
Enhanced Error Context
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}")
Migration from 0.2.x
Quick Reference
| 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() |
Query Builder Migration
# 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()
Resource Access Pattern Changes
# 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.
Development
Setup
# 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'
Testing
# 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
Code Quality
# 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
Resources
- 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
Contributing
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
License
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.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ifpa_api-0.4.2.tar.gz.
File metadata
- Download URL: ifpa_api-0.4.2.tar.gz
- Upload date:
- Size: 60.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
dda832becf32d2bd65979f091d5a40133e422b680fd20d20e36070e9c9627fb2
|
|
| MD5 |
6ec5237f93167bb14eeef115b457d766
|
|
| BLAKE2b-256 |
b9cbfbb821af9bb3f01cd37c0ad58f40cdcb895a2a3d6dae22e690c09e3fe920
|
Provenance
The following attestation bundles were made for ifpa_api-0.4.2.tar.gz:
Publisher:
publish.yml on johnsosoka/ifpa-api-python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ifpa_api-0.4.2.tar.gz -
Subject digest:
dda832becf32d2bd65979f091d5a40133e422b680fd20d20e36070e9c9627fb2 - Sigstore transparency entry: 1187927804
- Sigstore integration time:
-
Permalink:
johnsosoka/ifpa-api-python@545919289fb309587c8280fc0a9626286582d09a -
Branch / Tag:
refs/tags/v0.4.2 - Owner: https://github.com/johnsosoka
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@545919289fb309587c8280fc0a9626286582d09a -
Trigger Event:
push
-
Statement type:
File details
Details for the file ifpa_api-0.4.2-py3-none-any.whl.
File metadata
- Download URL: ifpa_api-0.4.2-py3-none-any.whl
- Upload date:
- Size: 77.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
09f7696128bd7f87523b15d26a614f06c219f4b9f417b1ddcdab9034a556805d
|
|
| MD5 |
e471de108484d9a4e16d04a3d3f4a3c2
|
|
| BLAKE2b-256 |
c35e48ceba2bd11e9be7bdf3941e32f05b76ae290695596bf0fb244e98ebb098
|
Provenance
The following attestation bundles were made for ifpa_api-0.4.2-py3-none-any.whl:
Publisher:
publish.yml on johnsosoka/ifpa-api-python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ifpa_api-0.4.2-py3-none-any.whl -
Subject digest:
09f7696128bd7f87523b15d26a614f06c219f4b9f417b1ddcdab9034a556805d - Sigstore transparency entry: 1187927811
- Sigstore integration time:
-
Permalink:
johnsosoka/ifpa-api-python@545919289fb309587c8280fc0a9626286582d09a -
Branch / Tag:
refs/tags/v0.4.2 - Owner: https://github.com/johnsosoka
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@545919289fb309587c8280fc0a9626286582d09a -
Trigger Event:
push
-
Statement type: