Metadata-Version: 2.4
Name: arccos-api
Version: 0.5.2
Summary: Unofficial Python CLI and client library for the Arccos Golf API
Author: Paul Frederiksen
License-Expression: MIT
Project-URL: Homepage, https://github.com/pfrederiksen/arccos-api
Project-URL: Repository, https://github.com/pfrederiksen/arccos-api
Project-URL: Issues, https://github.com/pfrederiksen/arccos-api/issues
Project-URL: PyPI, https://pypi.org/project/arccos-api/
Project-URL: Changelog, https://github.com/pfrederiksen/arccos-api/blob/main/CHANGELOG.md
Keywords: golf,arccos,api,cli,golf-stats
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: End Users/Desktop
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests<3,>=2.31
Requires-Dist: click<9,>=8.1
Requires-Dist: rich<16,>=13.0
Provides-Extra: keyring
Requires-Dist: keyring<27,>=25; extra == "keyring"
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: pytest-cov; extra == "dev"
Requires-Dist: ruff; extra == "dev"
Requires-Dist: mypy; extra == "dev"
Requires-Dist: types-requests; extra == "dev"
Requires-Dist: openapi-spec-validator<0.8,>=0.7; extra == "dev"
Requires-Dist: build>=0.10.0; extra == "dev"
Requires-Dist: twine>=4.0.0; extra == "dev"
Dynamic: license-file

<p align="center">
  <img src="assets/banner.png" alt="arccos-api banner" width="100%">
</p>

# arccos-api

[![PyPI](https://img.shields.io/pypi/v/arccos-api?label=PyPI)](https://pypi.org/project/arccos-api/)

Unofficial Python client and CLI for the [Arccos Golf](https://arccosgolf.com) platform.

Arccos provides no public API. This library gives you read-only programmatic access to your own golf data — rounds, handicap, club distances, pace of play, and more.

> **Unofficial.** Not affiliated with Arccos Golf LLC. Use with your own account only. Respect their servers (no aggressive polling).

> **Read-only by design.** This client only sends `GET` requests to the Arccos data API. It cannot create, edit, or delete rounds, shots, clubs, courses, or account data. Authentication necessarily sends `POST` requests to the separate Arccos authentication service to log in and refresh tokens; those requests only obtain credentials and do not modify golf data.

## Installation

Install the `arccos-api` distribution from [PyPI](https://pypi.org/project/arccos-api/):

```bash
pip install arccos-api
```

Or install the CLI with Homebrew:

```bash
brew install pfrederiksen/tap/arccos-api
```

Or install from source:

```bash
git clone https://github.com/pfrederiksen/arccos-api.git
cd arccos-api
pip install -e .
```

Requires Python >= 3.11. Dependencies: `requests`, `click`, `rich`.

## CLI

### Quick start

```bash
arccos login       # authenticate and save credentials
arccos rounds      # your recent rounds
arccos handicap    # handicap breakdown
arccos clubs       # smart club distances
```

### Commands

| Command | Description |
|---------|-------------|
| `arccos login` | Authenticate and securely cache credentials |
| `arccos rounds` | List recent rounds with date, score, +/-, course name |
| `arccos round <id>` | Hole-by-hole detail (score, putts, FIR, GIR) with totals |
| `arccos handicap` | Handicap breakdown by category (overall, driving, approach, etc.) |
| `arccos clubs` | Smart club distances with make/model, range, and shot count |
| `arccos bests` | All-time personal bests (lowest score, longest drive, etc.) |
| `arccos overview` | Performance summary — scoring avg + handicap breakdown |
| `arccos scoring` | Scoring trend with visual bar chart |
| `arccos compare <a> <b>` | Side-by-side hole-by-hole round comparison |
| `arccos courses` | List courses you've played (or `--search` to filter) |
| `arccos pace` | Pace of play analysis by course (color-coded) |
| `arccos stats` | Strokes gained analysis (defaults to latest round) |
| `arccos stats --dashboard` | Strokes gained analysis from the current dashboard endpoint |
| `arccos tour-analytics` | Tour analytics summary/facet data |
| `arccos trends` | Handicap trend chart over recent rounds |
| `arccos club-shots <id>` | Shot history for a specific club |
| `arccos shot-dispersion [id]` | Shot dispersion for the bag or a specific club |
| `arccos up-and-down` | Scramble / strokes-to-get-down stats |
| `arccos hole-image <course> <hole>` | Hole feature metadata and dashboard image URLs |
| `arccos export` | Export rounds to JSON, CSV, or NDJSON (`--detail` for hole data) |
| `arccos completions <shell>` | Generate bash/zsh/fish completion script |
| `arccos logout` | Clear cached credentials |
| `arccos doctor` | Check configuration, credentials, and API connectivity |

Every command supports `--json` for raw JSON output and `--help` for usage info.

### Usage examples

```bash
# Rounds
arccos rounds                         # last 20 rounds (with +/- and course names)
arccos rounds -n 50                   # last 50
arccos rounds --after 2025-01-01      # filter by date
arccos rounds --course "pebble"       # filter by course name (substring)
arccos rounds --json                  # raw JSON

# Round detail
arccos round 12345678                 # hole-by-hole breakdown

# Handicap
arccos handicap                       # category breakdown
arccos handicap --history             # revision history

# Club distances
arccos clubs                          # active clubs with make/model
arccos clubs --after 2025-01-01       # distances from this year only

# Performance
arccos bests                          # all-time personal bests
arccos overview                       # scoring avg + handicap breakdown
arccos scoring                        # scoring trend with bar chart
arccos stats --dashboard              # dashboard SGA, same endpoint as web app
arccos stats --dashboard --latest     # dashboard SGA for your latest round
arccos trends                         # handicap trend chart
arccos up-and-down                    # scramble stats
arccos tour-analytics --facet driving # tour analytics by facet
arccos tour-analytics --summary       # tour analytics summary

# Compare rounds
arccos compare 12345678 12345679      # side-by-side hole comparison

# Courses
arccos courses                        # all courses played
arccos courses --search "torrey"      # filter by name or city

# Club detail
arccos club-shots 1                   # shot history for club ID 1
arccos shot-dispersion                # bag-level dispersion data
arccos shot-dispersion 1 --json       # raw dispersion for club ID 1

# Hole imagery metadata
arccos hole-image 10769 7 -v 11       # image + metadata URLs and hole features

# Shell completions
eval "$(arccos completions bash)"     # enable tab completion
eval "$(arccos completions zsh)"      # for zsh

# Pace of play
arccos pace                           # all rounds, slowest courses first
arccos pace -n 20                     # last 20 rounds only

# Export
arccos export -f csv -o rounds.csv    # export to CSV file
arccos export --detail                # include hole-by-hole data
arccos export -f json                 # dump JSON to stdout
arccos export -f ndjson               # newline-delimited JSON
```

### Example output

```
$ arccos clubs
            Smart Club Distances
╭──────┬──────────────────┬───────┬─────────┬──────────┬───────╮
│ Club │ Model            │ Smart │ Longest │    Range │ Shots │
├──────┼──────────────────┼───────┼─────────┼──────────┼───────┤
│ Dr   │ Example Driver   │  252y │    291y │ 245–263y │   312 │
│ 3w   │ Example Fairway  │  228y │    248y │ 220–235y │    87 │
│ 5w   │ Example Fairway  │  208y │    231y │ 200–215y │   143 │
│ 4h   │ Example Hybrid   │  195y │    214y │ 188–202y │    61 │
│ 6i   │ Example Iron     │  172y │    190y │ 165–179y │    94 │
│ 7i   │ Example Iron     │  160y │    178y │ 153–167y │   118 │
│ 8i   │ Example Iron     │  148y │    165y │ 141–155y │   105 │
│ 9i   │ Example Iron     │  136y │    152y │ 130–142y │    97 │
│ Pw   │ Example Wedge    │  124y │    140y │ 118–130y │    82 │
│ 50   │ Vokey SM10       │  108y │    125y │ 102–114y │    76 │
│ 54   │ Vokey SM10       │   88y │    105y │  82–94y  │   201 │
│ 58   │ Vokey SM10       │   64y │     88y │  58–70y  │   168 │
╰──────┴──────────────────┴───────┴─────────┴──────────┴───────╯

$ arccos round 12345678
╭─── Round 12345678 ────╮
│ Date:   2026-01-15    │
│ Course: Example GC    │
│ Score:  79 (+7)       │
│ Holes:  18            │
╰───────────────────────╯
            Hole-by-Hole
╭──────┬───────┬───────┬─────┬─────╮
│ Hole │ Score │ Putts │ FIR │ GIR │
├──────┼───────┼───────┼─────┼─────┤
│    1 │     4 │     2 │  T  │  T  │
│    2 │     5 │     2 │  F  │  F  │
│    3 │     3 │     1 │     │  T  │
│  ... │   ... │   ... │ ... │ ... │
├──────┼───────┼───────┼─────┼─────┤
│  Tot │    79 │    31 │     │     │
╰──────┴───────┴───────┴─────┴─────╯

$ arccos handicap
  Handicap Breakdown
╭────────────┬───────╮
│ Category   │   HCP │
├────────────┼───────┤
│ Overall    │ -12.4 │
│ Driving    │ -15.1 │
│ Approach   │ -18.7 │
│ Short Game │ -14.3 │
│ Putting    │  -6.2 │
╰────────────┴───────╯

$ arccos pace -n 10
Pace of Play — 10 rounds across 6 courses   Overall avg: 4h 22m

                  By Course (slowest first)
╭───┬──────────┬───────────────────────┬────────╮
│   │ Avg Time │ Course                │ Rounds │
├───┼──────────┼───────────────────────┼────────┤
│ ● │   5h 10m │ Example Golf Club A    │      2 │
│ ● │   4h 45m │ Example Golf Club B    │      3 │
│ ● │   3h 50m │ Example Golf Club C    │      2 │
│ ● │   3h 35m │ Chambers Bay          │      1 │
╰───┴──────────┴───────────────────────┴────────╯
```

### Environment variables

Skip the login prompt by setting:

```bash
export ARCCOS_EMAIL="you@example.com"
export ARCCOS_PASSWORD="your_password"
arccos rounds
```

Other environment variables:

```bash
export ARCCOS_TIMEOUT=30       # request timeout in seconds (default: 15)
export ARCCOS_LOG_LEVEL=DEBUG  # enable debug logging (DEBUG, INFO, WARNING)
export ARCCOS_PROFILE=work     # select a named credential profile
export ARCCOS_KEYRING=true     # use the operating-system keyring
```

> **Security note:** Environment variables may be visible in process listings (`ps`) and shell history. Prefer `arccos login` for interactive use.

## Python Library

```python
from arccos import ArccosClient

client = ArccosClient(email="you@example.com", password="your_password")
# Credentials are cached in the platform config directory and refresh silently.

# Rounds
rounds = client.rounds.list(limit=10)
for r in rounds:
    print(f"{r['startTime'][:10]}  score={r['noOfShots']}  {r.get('courseName', '')}")

# Round detail (includes embedded hole-by-hole data)
rd = client.rounds.get(rounds[0]["roundId"])
for hole in rd["holes"]:
    print(f"  Hole {hole['holeId']}: {hole['noOfShots']} shots, {hole['putts']} putts")

# Handicap breakdown
hcp = client.handicap.current()
print(f"Overall: {hcp['userHcp']:.1f}")
print(f"Driving: {hcp['driveHcp']:.1f}")

# Club distances (returns clubId as bag slot — see bag API for name mapping)
for club in client.clubs.smart_distances():
    dist = club["smartDistance"]["distance"]
    print(f"  Club {club['clubId']}: {dist:.0f}y")

# Bag configuration (maps clubId → clubType + make/model)
profile = client.profile()
bag = client.clubs.bag(str(profile["bagId"]))
for club in bag["clubs"]:
    if club.get("isDeleted") != "T":
        print(f"  {club['clubMakeOther']} {club['clubModelOther']}")

# Current dashboard club/SGA analytics endpoints
club_inventory = client.clubs.list()
shot_dispersion = client.clubs.shot_dispersion()
dashboard_sga = client.stats.dashboard_analysis(goal_hcp=5, no_of_rounds=20)
player_profile = client.stats.player_profile()
tour_summary = client.stats.tour_analytics_summary()

# Course / hole imagery metadata
hole_features = client.courses.hole_features(10769, 7, version=11)
image_url = client.courses.hole_image_url(10769, 7, 11, "2026-07")
metadata_url = client.courses.hole_metadata_url(10769, 7, 11, "2026-07")

# Courses played
for course in client.courses.played():
    print(f"  {course.get('name', course['courseId'])}")

# Pace of play analysis
pace = client.rounds.pace_of_play()
print(f"Overall avg: {pace['overall_avg_display']}")
for c in pace["course_averages"][:5]:
    print(f"  {c['avg_display']}  {c['course']}")

# Personal bests
bests = client.stats.personal_bests()

# Iterate ALL rounds (auto-pagination)
for r in client.rounds.iter_all():
    print(r["roundId"], r["noOfShots"])

# Course search
results = client.courses.search("Example Golf Club")
```

## API Reference

Full OpenAPI 3.1 spec: [`docs/openapi.yaml`](docs/openapi.yaml)

### Authentication

All auth calls go to `https://authentication.arccosgolf.com`.

These are the only non-GET Arccos interactions in this project. They obtain an access key and JWT; they do not write golf or account data.

| Step | Endpoint | Body | Returns |
|------|----------|------|---------|
| 1. Get access key | `POST /accessKeys` | `{"email", "password", "signedInByFacebook": "F"}` | `{"userId", "accessKey", "secret"}` |
| 2. Get JWT | `POST /tokens` | `{"userId", "accessKey"}` | `{"userId", "token"}` |

The JWT expires in ~3 hours. The `accessKey` is valid for ~180 days. Refresh by calling `POST /tokens` again.

### Endpoints

All data calls go to `https://api.arccosgolf.com` with `Authorization: Bearer <token>`.

The data client enforces `GET` only and exposes no `POST`, `PUT`, `PATCH`, or `DELETE` methods.

| Resource | Method | Path |
|----------|--------|------|
| User profile | GET | `/users/{userId}` (includes `bagId`, `bags[]`) |
| User details | GET | `/users/{userId}/details` |
| Bag / clubs | GET | `/users/{userId}/bags/{bagId}` (club config with make/model) |
| Club inventory | GET | `/v6/users/{userId}/clubs` |
| Rounds | GET | `/users/{userId}/rounds?offSet=0&limit=200&roundType=flagship` |
| Round detail | GET | `/users/{userId}/rounds/{roundId}` (includes `holes[]` with `shots[]`) |
| Handicap | GET | `/users/{userId}/handicaps/latest` |
| Handicap history | GET | `/users/{userId}/handicaps?rounds=20` |
| Smart distances | GET | `/v4/clubs/user/{userId}/smart-distances` |
| Shot dispersion | GET | `/v3/user/{userId}/clubs/dispersion` |
| Club shot dispersion | GET | `/v3/user/{userId}/clubs/{clubId}/dispersion` |
| Courses played | GET | `/users/{userId}/coursesPlayed` |
| Course metadata | GET | `/courses/{courseId}?courseVersion=1` |
| Hole feature metadata | GET | `/courses/{courseId}/holes/{holeId}?courseVersion=1` |
| Course search | GET | `/v2/courses?search={query}` |
| Club shots | GET | `/users/{userId}/bags/{bagId}/clubs/{clubId}/shots` |
| Dashboard SGA | GET | `/sga/getDashboardAnalysis/{userId}` |
| SGA player profile | GET | `/sga/playerProfile/{userId}` |
| Tour analytics | GET | `/users/{userId}/tourAnalytics` |
| Tour analytics summary | GET | `/users/{userId}/tourAnalyticsSummary` |
| Tour analytics ranked | GET | `/users/{userId}/tourAnalyticsRanked` |
| Personal bests | GET | `/users/{userId}/personalBests?tags=allTimeBest` |
| Strokes to get down | GET | `/v2/sga/strokes-to-get-down` |

### Key data structures

**Round** (from `GET /users/{id}/rounds/{roundId}`):
```json
{
  "roundId": 12345678,
  "courseId": 12450,
  "courseVersion": 4,
  "startTime": "2026-01-15T15:30:00.000000Z",
  "endTime": "2026-01-15T20:15:00.000000Z",
  "noOfShots": 79,
  "noOfHoles": 18,
  "holes": [
    {
      "holeId": 1, "noOfShots": 4, "putts": 2,
      "isGir": "T", "isFairWay": "T",
      "shots": [{"clubId": 1, "clubType": 1, "distance": 255.0}]
    }
  ]
}
```

**Smart distance** (from `GET /v4/clubs/user/{id}/smart-distances`):
```json
{
  "clubId": 1,
  "smartDistance": {"distance": 252.3, "unit": "yd"},
  "longest": {"distance": 291.0, "unit": "yd"},
  "range": {"low": 245.0, "high": 263.0, "unit": "yd"},
  "usage": {"count": 312}
}
```

**Bag club** (from `GET /users/{id}/bags/{bagId}`):
```json
{
  "clubId": 1, "clubType": 1,
  "clubMakeOther": "Example", "clubModelOther": "Model 1",
  "isDeleted": "F"
}
```

> `clubId` is a user-specific bag slot. Resolve to a display name via: bag's `clubType` → standard name (1=Dr, 2=3w, 3=5w, 5=4i, ..., 11=Pw, 12=Putter, 43=Aw, 53=56°).

## Development

```bash
python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"

pytest                  # tests with coverage (87%+, 203 tests)
ruff check .            # lint
mypy arccos/            # type check
openapi-spec-validator docs/openapi.yaml
```

Coverage report: `htmlcov/index.html`. Minimum threshold: 80% (configured in `pyproject.toml`).
The suite includes sanitized response-contract fixtures so upstream schema drift
is caught without making live API calls.

## Security

- Data access is read-only: the client only sends `GET` requests to `api.arccosgolf.com`. The only `POST` requests are the required login/token-refresh calls to `authentication.arccosgolf.com`; no golf data is created, changed, or deleted.
- Credentials are cached in the platform configuration directory (mode `0600` — owner-only read/write). Existing `~/.arccos_creds.json` files migrate automatically. Named profiles are available through `ARCCOS_PROFILE` or `--profile`.
- Optional OS-keyring storage is available with `pip install 'arccos-api[keyring]'` and `ARCCOS_KEYRING=true` (or `--keyring` for login/logout/doctor).
- All API communication uses HTTPS with TLS certificate verification.
- `arccos logout` removes the local credential file but does **not** revoke server-side tokens. If you suspect your credentials were compromised, change your Arccos password.
- The export command restricts output paths to your home directory or current working directory.
- Security issues should be reported privately as described in [`SECURITY.md`](SECURITY.md).

## Disclaimer

Not affiliated with, endorsed by, or connected to Arccos Golf LLC.
Use your own account credentials only. This project is for personal data access and research.

MIT License.
