Skip to main content

Typed Python client for the IFPA (International Flipper Pinball Association) API

Project description

IFPA API Client

Development Status PyPI version Python versions License: MIT CI codecov Documentation Code style: black

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_results when using .get_all() to prevent memory issues
  • Default batch size is 100 items per request; adjust with limit parameter 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

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

ifpa_api-0.3.0.tar.gz (47.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

ifpa_api-0.3.0-py3-none-any.whl (62.1 kB view details)

Uploaded Python 3

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

Hashes for ifpa_api-0.3.0.tar.gz
Algorithm Hash digest
SHA256 fa926d5d8a0474f6db61d3232be26716e4119be2bd45ed0bffe3d7df3c86c8c1
MD5 2308a2089f563b0b5b99d8feda1961d7
BLAKE2b-256 8fd96e1d96f7e13b6947f94632f9a4c2387aa8d4bde6d20e792872fef64f36ac

See more details on using hashes here.

Provenance

The following attestation bundles were made for ifpa_api-0.3.0.tar.gz:

Publisher: publish.yml on johnsosoka/ifpa-api-python

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for ifpa_api-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 0a70cddcb44a8248908518c697a63aaf487ead4ca33976fb1f1ab631b81f4043
MD5 6380a913d82ec533c7877c1db5f7a095
BLAKE2b-256 0afaacaad755bda7fa44705c7418abadd3e3113c48a57035fc7889308fe40e1b

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page