yardage-hub-python
A Python client for yardage-hub, an open, crowdsourced database of golf courses: where they are, and their scorecards (holes, par, handicaps, tees, ratings, slopes, yardages). It starts from OpenStreetMap, and golfers fill in the rest.
This README is a quick start. The full API documentation is at lukeheide.com/sports/yardage-hub/docs.
Install
pip install yardage-hub
Python 3.9+, no dependencies.
Get a key
Every call needs a key. Sign in at lukeheide.com, open your profile page, and generate one. It's shown once, so copy it somewhere safe; if you lose it, regenerate it. Free keys allow 1,000 calls a day and 30 a minute.
from yardage_hub import YardageHub
hub = YardageHub("yh_...") # or set YARDAGE_HUB_KEY and call YardageHub()
Quick start
# Find a course
found = hub.search_courses("pebble beach", state="CA")
course = hub.get_course(found["courses"][0]["id"])
print(course["course_name"], course["par"])
for tee in course["tee_sets"]:
print(tee["tee_name"], tee["yardage"], [(r["gender"], r["course_rating"], r["slope"]) for r in tee["ratings"]])
for hole in course["holes_data"]:
print(hole["number"], hole["par"], hole["handicap_index"], hole["yardages"], hole["pars"])
# Courses near a point (km)
near = hub.search_courses(lat=42.28, lon=-83.74, radius_km=10)
# Courses that need help from contributors near you
help = hub.courses_needing_help(lat=42.28, lon=-83.74, radius_km=25)
for c in help["courses"]:
print(c["course_name"], [n["need"] for n in c["needs"]])
A hole's pars is None, or the tees that play it at a par of their own, by tee_key
({"red": 5}). A tee's par for a hole is its entry in pars, else women_par for a women's
rating that has one, else par. A hole's handicaps works the same way for a tee's handicap, with
women_handicap_index and handicap_index.
Name search forgives abbreviations, punctuation, and typos: "st johns", "mt pleasant", and "pebel beach" all find what you mean. It reads the club and course name together, so "detroit north" finds Detroit Golf Club's North Course.
Courses at the same club share a club_name, and each keeps its own course_name ("North
Course"). get_course adds facility, the club with its address, postcode, phone, and website.
Methods
| Method | What it does |
|---|---|
search_courses(q, state, lat, lon, radius_km, kinds, page, page_size, include_closed) |
Search by name, state, or distance. kinds defaults to ["course", "par3"]; also practice, disc, mini. include_closed=True also returns closed courses |
get_course(course_id) |
One course: holes, tees and ratings, links, completeness. None for an unknown id |
course_edits(course_id, page_size, contributor_ref) |
A course's edit history, newest first |
courses_needing_help(state, lat, lon, radius_km, needs, page, page_size) |
Courses missing something a contributor can add, by state or near a point |
submit_edit(course_id, payload, note, evidence_url, contributor_ref, contributor_name) |
Propose a change (contact details, holes, tees, ratings, yardages, scorecard link) |
link_course(course_id, club_id, note, evidence_url, contributor_ref, contributor_name) |
Propose linking a course to its club |
unlink_course(course_id, note, evidence_url, contributor_ref, contributor_name) |
Propose removing a course from its club |
vote_edit(edit_id, vote, reason, contributor_ref, contributor_name) |
Back (1) or doubt (-1) a pending edit, or remove your vote (0) |
report_link(course_id, field, url, works, contributor_ref, contributor_name) |
Confirm a website or scorecard_url works, or report it dead |
report_course(course_id, kind, note, evidence_url, contributor_ref, contributor_name) |
Report a scorecard as scorecard_changed, numbers_wrong, or other |
submit_scorecard_photos(course_id, photos, note, replaces_link, contributor_ref, contributor_name) |
Send 1–4 photos of a course's scorecard; once approved they become its scorecard_url |
Each method returns the API's JSON as Python dicts and lists. The fields are described in the docs.
Writing: apply for write access
Every key can read. The last seven methods send data to a review queue, and a person reviews every edit, so they need a key with write access. New keys don't have it; a key without it gets a 403.
- Contribute on the site first. Suggest edits on course pages at lukeheide.com/sports/yardage-hub.
- Once 5 of your edits are approved, apply on your profile page. Say what you'll write, e.g. "building an app that lets people suggest edits".
- The maintainer reviews the application. Once it's granted, your key can call the write methods.
Write access can be taken back. After a denial or revocation, you can apply again in 30 days.
An edit looks like this:
hub.submit_edit(course_id, {
"course": {"phone": "(415) 561-4661", "scorecard_url": "https://example.com/scorecard.pdf"},
"holes": [{"number": 1, "par": 4, "handicap": 7}],
"tees": [{"name": "Blue", "color": "blue", "gender": "M", "rating": 70.8, "slope": 132,
"yardages": {"1": 512, "2": 410}, "pars": {"2": 5}, "handicaps": {"1": 3}}],
}, note="From the 2026 scorecard")
A tee's pars and handicaps hold only the holes where it differs from the hole; None clears one.
It's validated right away (a bad payload raises a 400) and waits as pending until it's
reviewed.
Clubs
Say which club a course belongs to. Like an edit, it waits for review:
hub.link_course(course_id, club_id, note="North and South are both Detroit Golf Club")
hub.unlink_course(course_id, note="This course isn't part of that club")
Once a link is approved, the club is a facility: search stops returning it and returns its courses
instead. An unlinked club stays a facility. submit_edit can do the same with a link section:
{"link": {"club_id": club_id}}, or None to unlink.
Scorecard photos
Send 1–4 photos of a course's card, in order (front, back, …). Each one can be a file path,
bytes, or a binary file object:
result = hub.submit_scorecard_photos(course_id, ["front.jpg", "back.jpg"],
note="Card from the pro shop", contributor_ref="user-42")
result["url"] # the photos; it becomes the course's scorecard_url once approved
Photos stay private until a reviewer approves them: until then url returns 404, for you too.
-
A photo is the card. If the course already has a
scorecard_url, the call is refused (409) unless you passreplaces_link=True. Pass it only when the user says the current link is wrong. -
JPEG, PNG, or WebP, at most 2 MB each, and at least 1000 px on the long side (500 on the short). Photos straight off a phone are 2–6 MB, so resize first: about 2400 px on the long side, JPEG quality 85. With Pillow:
from io import BytesIO from PIL import Image, ImageOps img = ImageOps.exif_transpose(Image.open("card.jpg")).convert("RGB") img.thumbnail((2400, 2400)) buf = BytesIO() img.save(buf, "JPEG", quality=85) hub.submit_scorecard_photos(course_id, buf.getvalue(), contributor_ref="user-42")
-
Metadata is removed on the server, including GPS location and orientation, so send photos upright (
exif_transposeabove does that). -
Limits: 5 photos per user a day, 200 per app, 500 in all (UTC days). An upload counts as two calls against your daily quota. App keys must pass
contributor_ref, since the per-user limit counts by it. -
Don't send cards with personal information written on them (names, scores, handicaps). Approved photos are public.
-
Junk is deleted. Photos that aren't a scorecard, can't be read, or show personal details are rejected as junk and deleted within the hour. After 3 in 30 days, that user's photo uploads are paused (403, saying until when). Links still work.
Errors
A failed call raises YardageHubError, with status, message, and retry_after (seconds,
after a 429):
from yardage_hub import YardageHubError
try:
hub.submit_edit(course_id, {"course": {"phone": "555-0100"}})
except YardageHubError as e:
if e.status == 403:
print("This key is read-only: apply for write access on your profile page")
elif e.status == 429:
print(f"Over a limit; retry in {e.retry_after} s")
else:
raise
| Status | Meaning |
|---|---|
| 400 | Invalid input; the message says what |
| 401 | Missing, wrong, or revoked key |
| 403 | A read-only key tried to write, or this user's photo uploads are paused for junk |
| 404 | Unknown course or edit |
| 409 | Scorecard photos for a course that already has a scorecard link (pass replaces_link=True), or a photo already sent |
| 413 | A scorecard photo over 2 MB |
| 415 | A scorecard photo that isn't a JPEG, PNG, or WebP |
| 422 | A scorecard photo too small to read (under 1000 px on the long side or 500 on the short) |
| 429 | Over the daily or per-minute limit, or a scorecard photo limit |
| 503 | Scorecard photo storage is nearly full; uploads are paused |
Data and license
The data contains information from OpenStreetMap, © OpenStreetMap contributors, and is available under the ODbL. Credit it wherever you show it: "© OpenStreetMap contributors, yardage-hub". For the whole dataset, download the weekly snapshots from lukhed/yardage-hub-data; please don't harvest it through the API.
This client is MIT licensed.
Metadata
Release files for yardage-hub 0.4.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| yardage_hub-0.4.2.tar.gz | 14.8 kB | Without provenance | Details for yardage_hub-0.4.2.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| yardage_hub-0.4.2-py3-none-any.whl | Python 3 | none | any | Details for yardage_hub-0.4.2-py3-none-any.whl |
Total release size: 24.7 kB
Release files / yardage_hub-0.4.2.tar.gz
| Download URL | yardage_hub-0.4.2.tar.gz |
|---|---|
| Size | 14.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
80bb077af4efa904f87c6dc9247ed7d583cf21d7f620b76eeb0266733c21a96b
|
|
BLAKE2b-256 checksum How to use checksums |
d98b501930daf8f7d24dbf3e12efd98a54939ba36ff4faaeac669c60e6c8f7a7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.2
|
Release files / yardage_hub-0.4.2-py3-none-any.whl
| Download URL | yardage_hub-0.4.2-py3-none-any.whl |
|---|---|
| Size | 9.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
6009b0b46365e50902887e7a64efaad61097862ccf146e11e2e7c96ba4e5799c
|
|
BLAKE2b-256 checksum How to use checksums |
59f86ca552b84aeba76d1c562bc6b2ee027ef44c98dbb79ef47ad3b249515ea0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.2
|
Security for yardage-hub 0.4.2
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.