Usage Guide¶
This comprehensive guide covers all the features and capabilities of RepeaterBook.
API Client¶
RepeaterBookAPI¶
The RepeaterBookAPI class provides access to the RepeaterBook.com API.
Basic Usage¶
from repeaterbook.services import RepeaterBookAPI
from datetime import timedelta
# Create an API client with default settings
api = RepeaterBookAPI()
# Custom configuration
api = RepeaterBookAPI(
max_cache_age=timedelta(hours=2), # Cache responses for 2 hours
max_count=5000, # Expected max results (default: 3500)
)
Downloading Repeater Data¶
The download() method fetches repeater data from the API:
import asyncio
from repeaterbook.models import ExportQuery
import pycountry
async def download_example():
api = RepeaterBookAPI()
# Download by country
germany = pycountry.countries.get(name="Germany")
repeaters = await api.download(
query=ExportQuery(countries={germany})
)
# Download by multiple countries
countries = {
pycountry.countries.get(name="France"),
pycountry.countries.get(name="Belgium"),
}
repeaters = await api.download(
query=ExportQuery(countries=countries)
)
# Download by state (USA) - use FIPS codes
usa = pycountry.countries.get(alpha_2="US")
repeaters = await api.download(
query=ExportQuery(
countries={usa},
state_ids={"06", "41", "53"} # CA, OR, WA
)
)
return repeaters
repeaters = asyncio.run(download_example())
Caching¶
The API client automatically caches responses to reduce load on the RepeaterBook servers and improve performance:
- Default cache directory:
.repeaterbook_cache/ - Default cache TTL: 3600 seconds (1 hour)
- Cache is based on the query parameters
# First call downloads from API (slow)
repeaters1 = await api.download(query=ExportQuery(countries={brazil}))
# Second call uses cache (fast)
repeaters2 = await api.download(query=ExportQuery(countries={brazil}))
# Different query downloads from API
repeaters3 = await api.download(query=ExportQuery(countries={argentina}))
Progress Bars¶
Long downloads automatically display progress bars using tqdm:
# Progress bar shows automatically for large downloads
# Downloading repeaters: 100%|████████████| 1234/1234 [00:05<00:00, 245.67it/s]
repeaters = await api.download(query=ExportQuery(countries={usa}))
Export Queries¶
The ExportQuery dataclass specifies what data to download:
from repeaterbook.models import ExportQuery
import pycountry
# By country
query = ExportQuery(
countries={pycountry.countries.get(name="Japan")}
)
# By country and state (use FIPS codes)
query = ExportQuery(
countries={pycountry.countries.get(alpha_2="US")},
state_ids={"48", "40", "35"} # TX, OK, NM
)
# Multiple countries
query = ExportQuery(
countries={
pycountry.countries.get(name="Australia"),
pycountry.countries.get(name="New Zealand"),
}
)
Database Operations¶
RepeaterBook Class¶
The RepeaterBook class manages a local SQLite database of repeaters:
from repeaterbook import RepeaterBook
from anyio import Path
# Default database file (./repeaterbook.db)
rb = RepeaterBook()
# Custom database file name
rb = RepeaterBook(database="my_repeaters.db")
# Custom working directory
rb = RepeaterBook(working_dir=Path("/tmp"), database="repeaters.db")
Populating the Database¶
Use populate() to add repeaters to the database:
# Add repeaters (merges, doesn't duplicate)
rb.populate(repeaters)
# Populate from API directly
from repeaterbook.services import RepeaterBookAPI
import pycountry
api = RepeaterBookAPI()
italy = pycountry.countries.get(name="Italy")
repeaters = await api.download(query=ExportQuery(countries={italy}))
rb.populate(repeaters)
The populate() method intelligently merges data:
- Uses the id field to detect duplicates
- Updates existing records if they've changed
- Adds new records
Querying Repeaters¶
The query() method accepts SQLAlchemy filter expressions:
from repeaterbook import Repeater
from repeaterbook.models import Status, Use
# Simple queries
operational = rb.query(Repeater.operational_status == Status.ON_AIR)
open_repeaters = rb.query(Repeater.use_membership == Use.OPEN)
# Multiple conditions (AND)
results = rb.query(
Repeater.operational_status == Status.ON_AIR,
Repeater.use_membership == Use.OPEN,
Repeater.dmr_capable == True
)
# OR conditions
digital = rb.query(
(Repeater.dmr_capable | Repeater.apco_p_25_capable | Repeater.nxdn_capable)
)
# Complex queries
from sqlmodel import or_, and_
results = rb.query(
and_(
Repeater.operational_status == Status.ON_AIR,
or_(
Repeater.dmr_capable == True,
Repeater.apco_p_25_capable == True
)
)
)
Geographic Queries¶
Defining Locations¶
Use the provided utility types for geographic data:
from repeaterbook.utils import LatLon, Radius
# Define a point
location = LatLon(lat=51.5074, lon=-0.1278) # London
# Define a search radius
radius = Radius(
origin=LatLon(lat=51.5074, lon=-0.1278),
distance=50 # kilometers
)
Square Bounding Box¶
The square() function creates a bounding box query:
from repeaterbook.queries import square
from repeaterbook.utils import LatLon, Radius
# Define search area
radius = Radius(
origin=LatLon(lat=51.5074, lon=-0.1278),
distance=50
)
# Get repeaters in bounding box
repeaters = rb.query(square(radius))
This is very fast as it uses simple latitude/longitude comparisons.
Distance Filtering¶
For precise distance calculations, use filter_radius():
from repeaterbook.queries import filter_radius
# Get repeaters in bounding box
candidates = rb.query(square(radius))
# Filter by actual distance (uses Haversine formula)
# filter_radius returns repeaters sorted by distance from origin
nearby = filter_radius(candidates, radius)
# Results are already sorted by distance
# If you need the distance value for display, calculate it:
from haversine import haversine
for rep in nearby[:10]:
distance = haversine(radius.origin, (rep.latitude, rep.longitude), unit=radius.unit)
print(f"{distance:.1f}km - {rep.callsign}")
Distance Units¶
The haversine library supports multiple units:
from haversine import Unit
# Kilometers (default)
radius = Radius(origin=location, distance=50)
# Miles
radius = Radius(origin=location, distance=30, unit=Unit.MILES)
# Nautical miles
radius = Radius(origin=location, distance=25, unit=Unit.NAUTICAL_MILES)
Frequency and Band Queries¶
Band Filtering¶
The Bands enum defines common amateur radio bands:
from repeaterbook.queries import Bands, band
# Available bands
# Bands.M_10 # 10 meters (28-29.7 MHz)
# Bands.M_6 # 6 meters (50-54 MHz)
# Bands.M_4 # 4 meters (70-70.5 MHz)
# Bands.M_2 # 2 meters (144-148 MHz)
# Bands.CM_70 # 70 centimeters (420-450 MHz)
# Bands.CM_33 # 33 centimeters (902-928 MHz)
# Bands.CM_23 # 23 centimeters (1240-1300 MHz)
# Bands.CM_13 # 13 centimeters (2300-2450 MHz)
# Bands.CM_3 # 3 centimeters (10000-10500 MHz)
# Query single band
vhf_repeaters = rb.query(band(Bands.M_2))
# Query multiple bands
vhf_uhf = rb.query(band(Bands.M_2, Bands.CM_70))
Frequency Range Queries¶
For custom frequency ranges:
# Repeaters between 145.0 and 146.0 MHz
results = rb.query(
Repeater.frequency >= 145.0,
Repeater.frequency <= 146.0
)
# Sort by frequency
sorted_results = sorted(results, key=lambda r: r.frequency)
Digital Mode Queries¶
Capability Flags¶
The Repeater model includes capability flags for different digital modes:
# DMR repeaters
dmr = rb.query(Repeater.dmr_capable == True)
# P25 repeaters
p25 = rb.query(Repeater.apco_p_25_capable == True)
# NXDN repeaters
nxdn = rb.query(Repeater.nxdn_capable == True)
# Analog repeaters
analog = rb.query(Repeater.analog_capable == True)
# Any digital mode
digital = rb.query(
(Repeater.dmr_capable | Repeater.apco_p_25_capable | Repeater.nxdn_capable)
)
# Dual mode (analog + digital)
dual_mode = rb.query(
Repeater.analog_capable == True,
(Repeater.dmr_capable | Repeater.apco_p_25_capable | Repeater.nxdn_capable)
)
DMR Specific Data¶
DMR-capable repeaters include additional fields:
dmr_repeaters = rb.query(Repeater.dmr_capable == True)
for rep in dmr_repeaters:
print(f"{rep.callsign}:")
print(f" DMR ID: {rep.dmr_id}")
print(f" Color Code: {rep.dmr_color_code}")
P25 Specific Data¶
p25_repeaters = rb.query(Repeater.apco_p_25_capable == True)
for rep in p25_repeaters:
print(f"{rep.callsign}:")
print(f" NAC: {rep.p_25_nac}")
Filtering by Access¶
Membership Types¶
from repeaterbook.models import Use
# Open repeaters (no membership required)
open_repeaters = rb.query(Repeater.use_membership == Use.OPEN)
# Private repeaters (membership required)
private = rb.query(Repeater.use_membership == Use.PRIVATE)
# Closed repeaters (restricted access)
closed = rb.query(Repeater.use_membership == Use.CLOSED)
CTCSS Tones¶
Filter by required access tones:
# Repeaters with CTCSS tone
with_tone = rb.query(Repeater.pl_ctcss_uplink != None)
# Specific CTCSS tone
tone_110_9 = rb.query(Repeater.pl_ctcss_uplink == 110.9)
# No tone required
no_tone = rb.query(Repeater.pl_ctcss_uplink == None)
Status Filtering¶
Operational Status¶
from repeaterbook.models import Status
# On-air repeaters
on_air = rb.query(Repeater.operational_status == Status.ON_AIR)
# Off-air repeaters
off_air = rb.query(Repeater.operational_status == Status.OFF_AIR)
# Unknown status
unknown = rb.query(Repeater.operational_status == Status.UNKNOWN)
Emergency Services¶
# Repeaters with ARES support
ares = rb.query(Repeater.ares != None)
# Repeaters with RACES support
races = rb.query(Repeater.races != None)
# Repeaters with SKYWARN support
skywarn = rb.query(Repeater.skywarn != None)
# Repeaters with CANWARN support
canwarn = rb.query(Repeater.canwarn != None)
# Any emergency services
emergency = rb.query(
(Repeater.ares != None) |
(Repeater.races != None) |
(Repeater.skywarn != None) |
(Repeater.canwarn != None)
)
Combining Queries¶
Complex Search Example¶
Find the best repeaters for a specific use case:
from repeaterbook.queries import square, filter_radius, band, Bands
from repeaterbook.utils import LatLon, Radius
from repeaterbook.models import Status, Use
# Location: Chicago, IL
chicago = LatLon(lat=41.8781, lon=-87.6298)
radius = Radius(origin=chicago, distance=100) # 100 km
# Find: Nearby, open, operational, DMR-capable repeaters on 70cm
results = rb.query(
square(radius),
Repeater.operational_status == Status.ON_AIR,
Repeater.use_membership == Use.OPEN,
Repeater.dmr_capable == True,
band(Bands.CM_70)
)
# Filter by actual distance
# filter_radius returns repeaters sorted by distance
nearby = filter_radius(results, radius)
# Display results
from haversine import haversine
for rep in nearby[:10]:
distance = haversine(radius.origin, (rep.latitude, rep.longitude), unit=radius.unit)
print(f"{distance:5.1f}km - {rep.frequency:.4f} MHz - {rep.callsign}")
print(f" Location: {rep.location_nearest_city}")
print(f" DMR ID: {rep.dmr_id}, CC: {rep.dmr_color_code}")
print(f" Tone: {rep.pl_ctcss_uplink or 'None'}")
print()
Data Export¶
Export to Pandas¶
import pandas as pd
# Query repeaters
results = rb.query(Repeater.operational_status == Status.ON_AIR)
# Convert to DataFrame
data = [r.model_dump() for r in results]
df = pd.DataFrame(data)
# Analyze
print(df.describe())
print(df.groupby('use_membership').size())
# Export to CSV
df.to_csv('repeaters.csv', index=False)
Export to JSON¶
import json
results = rb.query(band(Bands.M_2))
# Convert to JSON
data = [r.model_dump() for r in results]
with open('repeaters.json', 'w') as f:
json.dump(data, f, indent=2, default=str)
Export for Radio Programming¶
# Format for Chirp or other programming software
results = rb.query(
square(radius),
Repeater.operational_status == Status.ON_AIR
)
nearby = filter_radius(results, radius)
# Create CSV in Chirp format
import csv
with open('chirp_import.csv', 'w', newline='') as f:
writer = csv.writer(f)
writer.writerow(['Location', 'Name', 'Frequency', 'Duplex', 'Offset',
'Tone', 'rToneFreq', 'cToneFreq', 'DtcsCode', 'Comment'])
# filter_radius returns repeaters sorted by distance
# Calculate distance for each repeater for display
from haversine import haversine
for rep in nearby:
distance = haversine(radius.origin, (rep.latitude, rep.longitude), unit=radius.unit)
writer.writerow([
rep.id,
rep.callsign,
rep.frequency,
'+' if rep.input_frequency < rep.frequency else '-',
abs(rep.frequency - rep.input_frequency),
'Tone' if rep.pl_ctcss_uplink else '',
rep.pl_ctcss_uplink or '',
rep.pl_ctcss_tsq_downlink or '',
'', # DCS code not available
f"{rep.location_nearest_city} - {distance:.1f}km"
])
Working with Repeater Objects¶
Repeater Model Fields¶
Key fields available on Repeater objects:
# Identification
rep.id # Unique RepeaterBook ID
rep.callsign # Repeater callsign
rep.location_nearest_city # City/location description
# Frequency
rep.frequency # Output frequency (MHz)
rep.input_frequency # Input frequency (MHz)
# Access (CTCSS tones)
rep.pl_ctcss_uplink # Input CTCSS/PL tone (Hz)
rep.pl_ctcss_tsq_downlink # Output CTCSS/TSQ tone (Hz)
# Status
rep.operational_status # ON_AIR, OFF_AIR, UNKNOWN
rep.use_membership # OPEN, PRIVATE, CLOSED
# Emergency Services (string fields)
rep.ares # ARES support indicator
rep.races # RACES support indicator
rep.skywarn # SKYWARN support indicator
rep.canwarn # CANWARN support indicator
# Capabilities
rep.analog_capable # Boolean
rep.dmr_capable # Boolean
rep.apco_p_25_capable # Boolean (P25)
rep.nxdn_capable # Boolean
rep.tetra_capable # Boolean
# Digital mode details
rep.dmr_id # DMR radio ID
rep.dmr_color_code # DMR color code (0-15)
rep.p_25_nac # P25 NAC code
# Location
rep.latitude # Latitude (degrees)
rep.longitude # Longitude (degrees)
rep.precise # Precise location flag
# Notes
rep.notes # Additional information
Checking Capabilities¶
def describe_repeater(rep):
"""Print a detailed description of a repeater."""
print(f"=== {rep.callsign} ===")
print(f"Frequency: {rep.frequency:.4f} MHz ({rep.input_frequency:.4f} MHz)")
print(f"Location: {rep.location_nearest_city}")
print(f"Coordinates: {rep.latitude:.4f}, {rep.longitude:.4f}")
# Access
if rep.pl_ctcss_uplink:
print(f"CTCSS: {rep.pl_ctcss_uplink} Hz")
# Modes
modes = []
if rep.analog_capable:
modes.append("FM")
if rep.dmr_capable:
modes.append(f"DMR (CC{rep.dmr_color_code})")
if rep.apco_p_25_capable:
modes.append(f"P25 (NAC ${rep.p_25_nac:03X})")
if rep.nxdn_capable:
modes.append("NXDN")
print(f"Modes: {', '.join(modes)}")
print(f"Status: {rep.operational_status.value}")
print(f"Access: {rep.use_membership.value}")
if rep.notes:
print(f"Notes: {rep.notes}")
# Example usage
results = rb.query(Repeater.callsign == "W6CX")
if results:
describe_repeater(results[0])
Performance Tips¶
Use Bounding Box First¶
Always use square() before filter_radius() to reduce the number of distance calculations:
# Good: Fast
candidates = rb.query(square(radius))
nearby = filter_radius(candidates, radius)
# Bad: Slow (calculates distance for ALL repeaters)
all_repeaters = rb.query()
nearby = filter_radius(all_repeaters, radius)
Limit Query Results¶
For large result sets, consider using additional filters to narrow down results:
# Query with multiple filters to reduce result size
results = rb.query(
Repeater.operational_status == Status.ON_AIR,
Repeater.use_membership == Use.OPEN,
band(Bands.M_2)
)
# Or use Python slicing on results
results = rb.query(Repeater.operational_status == Status.ON_AIR)[:100]
Cache API Responses¶
The API client automatically caches, but you can customize:
from datetime import timedelta
# Longer cache for stable data
api = RepeaterBookAPI(max_cache_age=timedelta(hours=24))
# Shorter cache for frequently changing data
api = RepeaterBookAPI(max_cache_age=timedelta(minutes=30))
Reuse Database Connection¶
# Create once
rb = RepeaterBook(database="repeaters.db")
# Reuse for multiple queries
results1 = rb.query(band(Bands.M_2))
results2 = rb.query(band(Bands.CM_70))
results3 = rb.query(Repeater.dmr_capable == True)
Error Handling¶
RepeaterBook provides custom exceptions for robust error handling:
from repeaterbook import (
RepeaterBookError,
RepeaterBookAPIError,
RepeaterBookCacheError,
RepeaterBookValidationError,
)
try:
repeaters = await api.download(query=ExportQuery(countries={brazil}))
except RepeaterBookAPIError as e:
# API returned an error response
print(f"API error: {e}")
except RepeaterBookValidationError as e:
# Invalid response format or data
print(f"Validation error: {e}")
except RepeaterBookCacheError as e:
# Cache read/write failed
print(f"Cache error: {e}")
except RepeaterBookError as e:
# Catch all library errors
print(f"RepeaterBook error: {e}")
Exception Types¶
| Exception | Description |
|---|---|
RepeaterBookError |
Base exception for all library errors |
RepeaterBookAPIError |
API returned an error response |
RepeaterBookValidationError |
Invalid data or response format |
RepeaterBookCacheError |
Cache operations failed |
Data Validation¶
The Repeater model includes built-in validation:
# These will raise ValueError if invalid:
# - Latitude must be between -90 and 90
# - Longitude must be between -180 and 180
# - Frequency must be positive
Logging¶
RepeaterBook uses loguru for logging:
from loguru import logger
# Enable debug logging
logger.add("repeaterbook.log", level="DEBUG")
# Now operations will be logged
repeaters = await api.download(query=ExportQuery(countries={brazil}))
Next Steps¶
- Examples - Real-world use cases and patterns
- Architecture - Understanding the internals
- FAQ - Common questions and troubleshooting
- API Reference - Complete API documentation