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.3.0
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.
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.)
- 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
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(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()
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(12345).details()
print(player.first_name)
# Client automatically closed
Core Resources
Players
# 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()
Directors
# 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()
Tournaments
# 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()
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
# 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)
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.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}")
Collect All Results
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
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(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}")
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.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}")
Migration from 0.2.x
Quick Reference
| 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") |
Query Builder Migration
# 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()
Callable Pattern Changes
# 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.
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.3.0.tar.gz.
File metadata
- Download URL: ifpa_api-0.3.0.tar.gz
- Upload date:
- Size: 47.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fa926d5d8a0474f6db61d3232be26716e4119be2bd45ed0bffe3d7df3c86c8c1
|
|
| MD5 |
2308a2089f563b0b5b99d8feda1961d7
|
|
| BLAKE2b-256 |
8fd96e1d96f7e13b6947f94632f9a4c2387aa8d4bde6d20e792872fef64f36ac
|
Provenance
The following attestation bundles were made for ifpa_api-0.3.0.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.3.0.tar.gz -
Subject digest:
fa926d5d8a0474f6db61d3232be26716e4119be2bd45ed0bffe3d7df3c86c8c1 - Sigstore transparency entry: 708396368
- Sigstore integration time:
-
Permalink:
johnsosoka/ifpa-api-python@b3de24c327e39944094e9f1d9ccc1a08d1ec9d39 -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/johnsosoka
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@b3de24c327e39944094e9f1d9ccc1a08d1ec9d39 -
Trigger Event:
push
-
Statement type:
File details
Details for the file ifpa_api-0.3.0-py3-none-any.whl.
File metadata
- Download URL: ifpa_api-0.3.0-py3-none-any.whl
- Upload date:
- Size: 62.1 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 |
0a70cddcb44a8248908518c697a63aaf487ead4ca33976fb1f1ab631b81f4043
|
|
| MD5 |
6380a913d82ec533c7877c1db5f7a095
|
|
| BLAKE2b-256 |
0afaacaad755bda7fa44705c7418abadd3e3113c48a57035fc7889308fe40e1b
|
Provenance
The following attestation bundles were made for ifpa_api-0.3.0-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.3.0-py3-none-any.whl -
Subject digest:
0a70cddcb44a8248908518c697a63aaf487ead4ca33976fb1f1ab631b81f4043 - Sigstore transparency entry: 708396371
- Sigstore integration time:
-
Permalink:
johnsosoka/ifpa-api-python@b3de24c327e39944094e9f1d9ccc1a08d1ec9d39 -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/johnsosoka
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@b3de24c327e39944094e9f1d9ccc1a08d1ec9d39 -
Trigger Event:
push
-
Statement type: