arccos-api 0.4.2__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. arccos_api-0.5.0/LICENSE +21 -0
  2. {arccos_api-0.4.2/arccos_api.egg-info → arccos_api-0.5.0}/PKG-INFO +87 -30
  3. {arccos_api-0.4.2 → arccos_api-0.5.0}/README.md +81 -27
  4. {arccos_api-0.4.2 → arccos_api-0.5.0}/arccos/__init__.py +4 -1
  5. {arccos_api-0.4.2 → arccos_api-0.5.0}/arccos/_http.py +10 -28
  6. {arccos_api-0.4.2 → arccos_api-0.5.0}/arccos/auth.py +20 -11
  7. {arccos_api-0.4.2 → arccos_api-0.5.0}/arccos/cli/__init__.py +3 -0
  8. {arccos_api-0.4.2 → arccos_api-0.5.0}/arccos/cli/auth.py +2 -1
  9. {arccos_api-0.4.2 → arccos_api-0.5.0}/arccos/cli/rounds.py +9 -3
  10. {arccos_api-0.4.2 → arccos_api-0.5.0}/arccos/cli/stats.py +477 -5
  11. {arccos_api-0.4.2 → arccos_api-0.5.0}/arccos/client.py +8 -0
  12. {arccos_api-0.4.2 → arccos_api-0.5.0}/arccos/resources/clubs.py +81 -2
  13. {arccos_api-0.4.2 → arccos_api-0.5.0}/arccos/resources/courses.py +53 -2
  14. {arccos_api-0.4.2 → arccos_api-0.5.0}/arccos/resources/stats.py +91 -0
  15. {arccos_api-0.4.2 → arccos_api-0.5.0/arccos_api.egg-info}/PKG-INFO +87 -30
  16. {arccos_api-0.4.2 → arccos_api-0.5.0}/arccos_api.egg-info/SOURCES.txt +1 -0
  17. {arccos_api-0.4.2 → arccos_api-0.5.0}/pyproject.toml +4 -3
  18. {arccos_api-0.4.2 → arccos_api-0.5.0}/tests/test_auth.py +20 -0
  19. {arccos_api-0.4.2 → arccos_api-0.5.0}/tests/test_cli.py +151 -8
  20. {arccos_api-0.4.2 → arccos_api-0.5.0}/tests/test_client.py +30 -0
  21. {arccos_api-0.4.2 → arccos_api-0.5.0}/tests/test_http.py +20 -54
  22. {arccos_api-0.4.2 → arccos_api-0.5.0}/tests/test_resources.py +121 -1
  23. {arccos_api-0.4.2 → arccos_api-0.5.0}/tests/test_security.py +45 -4
  24. {arccos_api-0.4.2 → arccos_api-0.5.0}/MANIFEST.in +0 -0
  25. {arccos_api-0.4.2 → arccos_api-0.5.0}/arccos/__main__.py +0 -0
  26. {arccos_api-0.4.2 → arccos_api-0.5.0}/arccos/cli/_helpers.py +0 -0
  27. {arccos_api-0.4.2 → arccos_api-0.5.0}/arccos/exceptions.py +0 -0
  28. {arccos_api-0.4.2 → arccos_api-0.5.0}/arccos/resources/__init__.py +0 -0
  29. {arccos_api-0.4.2 → arccos_api-0.5.0}/arccos/resources/handicap.py +0 -0
  30. {arccos_api-0.4.2 → arccos_api-0.5.0}/arccos/resources/rounds.py +0 -0
  31. {arccos_api-0.4.2 → arccos_api-0.5.0}/arccos/types.py +0 -0
  32. {arccos_api-0.4.2 → arccos_api-0.5.0}/arccos_api.egg-info/dependency_links.txt +0 -0
  33. {arccos_api-0.4.2 → arccos_api-0.5.0}/arccos_api.egg-info/entry_points.txt +0 -0
  34. {arccos_api-0.4.2 → arccos_api-0.5.0}/arccos_api.egg-info/requires.txt +0 -0
  35. {arccos_api-0.4.2 → arccos_api-0.5.0}/arccos_api.egg-info/top_level.txt +0 -0
  36. {arccos_api-0.4.2 → arccos_api-0.5.0}/setup.cfg +0 -0
  37. {arccos_api-0.4.2 → arccos_api-0.5.0}/tests/test_exceptions.py +0 -0
  38. {arccos_api-0.4.2 → arccos_api-0.5.0}/tests/test_rounds.py +0 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Paul Frederiksen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -1,18 +1,19 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arccos-api
3
- Version: 0.4.2
3
+ Version: 0.5.0
4
4
  Summary: Unofficial Python CLI and client library for the Arccos Golf API
5
5
  Author: Paul Frederiksen
6
- License: MIT
6
+ License-Expression: MIT
7
7
  Project-URL: Homepage, https://github.com/pfrederiksen/arccos-api
8
8
  Project-URL: Repository, https://github.com/pfrederiksen/arccos-api
9
9
  Project-URL: Issues, https://github.com/pfrederiksen/arccos-api/issues
10
+ Project-URL: PyPI, https://pypi.org/project/arccos-api/
11
+ Project-URL: Changelog, https://github.com/pfrederiksen/arccos-api/blob/main/CHANGELOG.md
10
12
  Keywords: golf,arccos,api,cli,golf-stats
11
13
  Classifier: Development Status :: 4 - Beta
12
14
  Classifier: Environment :: Console
13
15
  Classifier: Intended Audience :: Developers
14
16
  Classifier: Intended Audience :: End Users/Desktop
15
- Classifier: License :: OSI Approved :: MIT License
16
17
  Classifier: Programming Language :: Python :: 3
17
18
  Classifier: Programming Language :: Python :: 3.11
18
19
  Classifier: Programming Language :: Python :: 3.12
@@ -21,6 +22,7 @@ Classifier: Programming Language :: Python :: 3.14
21
22
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
23
  Requires-Python: >=3.11
23
24
  Description-Content-Type: text/markdown
25
+ License-File: LICENSE
24
26
  Requires-Dist: requests<3,>=2.31
25
27
  Requires-Dist: click<9,>=8.1
26
28
  Requires-Dist: rich<15,>=13.0
@@ -32,6 +34,7 @@ Requires-Dist: mypy; extra == "dev"
32
34
  Requires-Dist: types-requests; extra == "dev"
33
35
  Requires-Dist: build>=0.10.0; extra == "dev"
34
36
  Requires-Dist: twine>=4.0.0; extra == "dev"
37
+ Dynamic: license-file
35
38
 
36
39
  <p align="center">
37
40
  <img src="assets/banner.png" alt="arccos-api banner" width="100%">
@@ -39,18 +42,30 @@ Requires-Dist: twine>=4.0.0; extra == "dev"
39
42
 
40
43
  # arccos-api
41
44
 
45
+ [![PyPI](https://img.shields.io/pypi/v/arccos-api?label=PyPI)](https://pypi.org/project/arccos-api/)
46
+
42
47
  Unofficial Python client and CLI for the [Arccos Golf](https://arccosgolf.com) platform.
43
48
 
44
- Arccos provides no public API. This library was reverse-engineered from the Arccos Dashboard web app. It gives you full programmatic access to your own golf data — rounds, handicap, club distances, pace of play, and more.
49
+ 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.
45
50
 
46
51
  > **Unofficial.** Not affiliated with Arccos Golf LLC. Use with your own account only. Respect their servers (no aggressive polling).
47
52
 
53
+ > **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.
54
+
48
55
  ## Installation
49
56
 
57
+ Install the `arccos-api` distribution from [PyPI](https://pypi.org/project/arccos-api/):
58
+
50
59
  ```bash
51
60
  pip install arccos-api
52
61
  ```
53
62
 
63
+ Or install the CLI with Homebrew:
64
+
65
+ ```bash
66
+ brew install pfrederiksen/tap/arccos-api
67
+ ```
68
+
54
69
  Or install from source:
55
70
 
56
71
  ```bash
@@ -88,9 +103,13 @@ arccos clubs # smart club distances
88
103
  | `arccos courses` | List courses you've played (or `--search` to filter) |
89
104
  | `arccos pace` | Pace of play analysis by course (color-coded) |
90
105
  | `arccos stats` | Strokes gained analysis (defaults to latest round) |
106
+ | `arccos stats --dashboard` | Strokes gained analysis from the current dashboard endpoint |
107
+ | `arccos tour-analytics` | Tour analytics summary/facet data |
91
108
  | `arccos trends` | Handicap trend chart over recent rounds |
92
109
  | `arccos club-shots <id>` | Shot history for a specific club |
110
+ | `arccos shot-dispersion [id]` | Shot dispersion for the bag or a specific club |
93
111
  | `arccos up-and-down` | Scramble / strokes-to-get-down stats |
112
+ | `arccos hole-image <course> <hole>` | Hole feature metadata and dashboard image URLs |
94
113
  | `arccos export` | Export rounds to JSON, CSV, or NDJSON (`--detail` for hole data) |
95
114
  | `arccos completions <shell>` | Generate bash/zsh/fish completion script |
96
115
  | `arccos logout` | Clear cached credentials |
@@ -108,7 +127,7 @@ arccos rounds --course "pebble" # filter by course name (substring)
108
127
  arccos rounds --json # raw JSON
109
128
 
110
129
  # Round detail
111
- arccos round 18294051 # hole-by-hole breakdown
130
+ arccos round 12345678 # hole-by-hole breakdown
112
131
 
113
132
  # Handicap
114
133
  arccos handicap # category breakdown
@@ -122,11 +141,15 @@ arccos clubs --after 2025-01-01 # distances from this year only
122
141
  arccos bests # all-time personal bests
123
142
  arccos overview # scoring avg + handicap breakdown
124
143
  arccos scoring # scoring trend with bar chart
144
+ arccos stats --dashboard # dashboard SGA, same endpoint as web app
145
+ arccos stats --dashboard --latest # dashboard SGA for your latest round
125
146
  arccos trends # handicap trend chart
126
147
  arccos up-and-down # scramble stats
148
+ arccos tour-analytics --facet driving # tour analytics by facet
149
+ arccos tour-analytics --summary # tour analytics summary
127
150
 
128
151
  # Compare rounds
129
- arccos compare 26685289 26762935 # side-by-side hole comparison
152
+ arccos compare 12345678 12345679 # side-by-side hole comparison
130
153
 
131
154
  # Courses
132
155
  arccos courses # all courses played
@@ -134,6 +157,11 @@ arccos courses --search "torrey" # filter by name or city
134
157
 
135
158
  # Club detail
136
159
  arccos club-shots 1 # shot history for club ID 1
160
+ arccos shot-dispersion # bag-level dispersion data
161
+ arccos shot-dispersion 1 --json # raw dispersion for club ID 1
162
+
163
+ # Hole imagery metadata
164
+ arccos hole-image 10769 7 -v 11 # image + metadata URLs and hole features
137
165
 
138
166
  # Shell completions
139
167
  eval "$(arccos completions bash)" # enable tab completion
@@ -158,24 +186,24 @@ $ arccos clubs
158
186
  ╭──────┬──────────────────┬───────┬─────────┬──────────┬───────╮
159
187
  │ Club │ Model │ Smart │ Longest │ Range │ Shots │
160
188
  ├──────┼──────────────────┼───────┼─────────┼──────────┼───────┤
161
- │ Dr │ TaylorMade Qi10 │ 252y │ 291y │ 245–263y │ 312 │
162
- │ 3w │ TaylorMade Qi10 │ 228y │ 248y │ 220–235y │ 87 │
163
- │ 5w │ Callaway Paradym │ 208y │ 231y │ 200–215y │ 143 │
164
- │ 4h │ Titleist T200 │ 195y │ 214y │ 188–202y │ 61 │
165
- │ 6i │ Titleist T150 │ 172y │ 190y │ 165–179y │ 94 │
166
- │ 7i │ Titleist T150 │ 160y │ 178y │ 153–167y │ 118 │
167
- │ 8i │ Titleist T150 │ 148y │ 165y │ 141–155y │ 105 │
168
- │ 9i │ Titleist T150 │ 136y │ 152y │ 130–142y │ 97 │
169
- │ Pw │ Titleist T150 │ 124y │ 140y │ 118–130y │ 82 │
189
+ │ Dr │ Example Driver │ 252y │ 291y │ 245–263y │ 312 │
190
+ │ 3w │ Example Fairway │ 228y │ 248y │ 220–235y │ 87 │
191
+ │ 5w │ Example Fairway │ 208y │ 231y │ 200–215y │ 143 │
192
+ │ 4h │ Example Hybrid │ 195y │ 214y │ 188–202y │ 61 │
193
+ │ 6i │ Example Iron │ 172y │ 190y │ 165–179y │ 94 │
194
+ │ 7i │ Example Iron │ 160y │ 178y │ 153–167y │ 118 │
195
+ │ 8i │ Example Iron │ 148y │ 165y │ 141–155y │ 105 │
196
+ │ 9i │ Example Iron │ 136y │ 152y │ 130–142y │ 97 │
197
+ │ Pw │ Example Wedge │ 124y │ 140y │ 118–130y │ 82 │
170
198
  │ 50 │ Vokey SM10 │ 108y │ 125y │ 102–114y │ 76 │
171
199
  │ 54 │ Vokey SM10 │ 88y │ 105y │ 82–94y │ 201 │
172
200
  │ 58 │ Vokey SM10 │ 64y │ 88y │ 58–70y │ 168 │
173
201
  ╰──────┴──────────────────┴───────┴─────────┴──────────┴───────╯
174
202
 
175
- $ arccos round 18294051
176
- ╭─── Round 18294051 ────╮
177
- │ Date: 2025-09-14
178
- │ Course: Torrey Pines
203
+ $ arccos round 12345678
204
+ ╭─── Round 12345678 ────╮
205
+ │ Date: 2026-01-15
206
+ │ Course: Example GC
179
207
  │ Score: 79 (+7) │
180
208
  │ Holes: 18 │
181
209
  ╰───────────────────────╯
@@ -210,9 +238,9 @@ Pace of Play — 10 rounds across 6 courses Overall avg: 4h 22m
210
238
  ╭───┬──────────┬───────────────────────┬────────╮
211
239
  │ │ Avg Time │ Course │ Rounds │
212
240
  ├───┼──────────┼───────────────────────┼────────┤
213
- │ ● │ 5h 10m │ Bethpage Black │ 2 │
214
- │ ● │ 4h 45m │ Torrey Pines │ 3 │
215
- │ ● │ 3h 50m │ Bandon Dunes │ 2 │
241
+ │ ● │ 5h 10m │ Example Golf Club A │ 2 │
242
+ │ ● │ 4h 45m │ Example Golf Club B │ 3 │
243
+ │ ● │ 3h 50m │ Example Golf Club C │ 2 │
216
244
  │ ● │ 3h 35m │ Chambers Bay │ 1 │
217
245
  ╰───┴──────────┴───────────────────────┴────────╯
218
246
  ```
@@ -265,12 +293,24 @@ for club in client.clubs.smart_distances():
265
293
  print(f" Club {club['clubId']}: {dist:.0f}y")
266
294
 
267
295
  # Bag configuration (maps clubId → clubType + make/model)
268
- profile = client._http.get(f"/users/{client.user_id}")
296
+ profile = client.profile()
269
297
  bag = client.clubs.bag(str(profile["bagId"]))
270
298
  for club in bag["clubs"]:
271
299
  if club.get("isDeleted") != "T":
272
300
  print(f" {club['clubMakeOther']} {club['clubModelOther']}")
273
301
 
302
+ # Current dashboard club/SGA analytics endpoints
303
+ club_inventory = client.clubs.list()
304
+ shot_dispersion = client.clubs.shot_dispersion()
305
+ dashboard_sga = client.stats.dashboard_analysis(goal_hcp=5, no_of_rounds=20)
306
+ player_profile = client.stats.player_profile()
307
+ tour_summary = client.stats.tour_analytics_summary()
308
+
309
+ # Course / hole imagery metadata
310
+ hole_features = client.courses.hole_features(10769, 7, version=11)
311
+ image_url = client.courses.hole_image_url(10769, 7, 11, "2026-07")
312
+ metadata_url = client.courses.hole_metadata_url(10769, 7, 11, "2026-07")
313
+
274
314
  # Courses played
275
315
  for course in client.courses.played():
276
316
  print(f" {course.get('name', course['courseId'])}")
@@ -289,7 +329,7 @@ for r in client.rounds.iter_all():
289
329
  print(r["roundId"], r["noOfShots"])
290
330
 
291
331
  # Course search
292
- results = client.courses.search("Torrey Pines")
332
+ results = client.courses.search("Example Golf Club")
293
333
  ```
294
334
 
295
335
  ## API Reference
@@ -300,6 +340,8 @@ Full OpenAPI 3.1 spec: [`docs/openapi.yaml`](docs/openapi.yaml)
300
340
 
301
341
  All auth calls go to `https://authentication.arccosgolf.com`.
302
342
 
343
+ 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.
344
+
303
345
  | Step | Endpoint | Body | Returns |
304
346
  |------|----------|------|---------|
305
347
  | 1. Get access key | `POST /accessKeys` | `{"email", "password", "signedInByFacebook": "F"}` | `{"userId", "accessKey", "secret"}` |
@@ -311,19 +353,31 @@ The JWT expires in ~3 hours. The `accessKey` is valid for ~180 days. Refresh by
311
353
 
312
354
  All data calls go to `https://api.arccosgolf.com` with `Authorization: Bearer <token>`.
313
355
 
356
+ The data client enforces `GET` only and exposes no `POST`, `PUT`, `PATCH`, or `DELETE` methods.
357
+
314
358
  | Resource | Method | Path |
315
359
  |----------|--------|------|
316
360
  | User profile | GET | `/users/{userId}` (includes `bagId`, `bags[]`) |
361
+ | User details | GET | `/users/{userId}/details` |
317
362
  | Bag / clubs | GET | `/users/{userId}/bags/{bagId}` (club config with make/model) |
363
+ | Club inventory | GET | `/v6/users/{userId}/clubs` |
318
364
  | Rounds | GET | `/users/{userId}/rounds?offSet=0&limit=200&roundType=flagship` |
319
365
  | Round detail | GET | `/users/{userId}/rounds/{roundId}` (includes `holes[]` with `shots[]`) |
320
366
  | Handicap | GET | `/users/{userId}/handicaps/latest` |
321
367
  | Handicap history | GET | `/users/{userId}/handicaps?rounds=20` |
322
368
  | Smart distances | GET | `/v4/clubs/user/{userId}/smart-distances` |
369
+ | Shot dispersion | GET | `/v3/user/{userId}/clubs/dispersion` |
370
+ | Club shot dispersion | GET | `/v3/user/{userId}/clubs/{clubId}/dispersion` |
323
371
  | Courses played | GET | `/users/{userId}/coursesPlayed` |
324
372
  | Course metadata | GET | `/courses/{courseId}?courseVersion=1` |
373
+ | Hole feature metadata | GET | `/courses/{courseId}/holes/{holeId}?courseVersion=1` |
325
374
  | Course search | GET | `/v2/courses?search={query}` |
326
375
  | Club shots | GET | `/users/{userId}/bags/{bagId}/clubs/{clubId}/shots` |
376
+ | Dashboard SGA | GET | `/sga/getDashboardAnalysis/{userId}` |
377
+ | SGA player profile | GET | `/sga/playerProfile/{userId}` |
378
+ | Tour analytics | GET | `/users/{userId}/tourAnalytics` |
379
+ | Tour analytics summary | GET | `/users/{userId}/tourAnalyticsSummary` |
380
+ | Tour analytics ranked | GET | `/users/{userId}/tourAnalyticsRanked` |
327
381
  | Personal bests | GET | `/users/{userId}/personalBests?tags=allTimeBest` |
328
382
  | Strokes to get down | GET | `/v2/sga/strokes-to-get-down` |
329
383
 
@@ -332,11 +386,11 @@ All data calls go to `https://api.arccosgolf.com` with `Authorization: Bearer <t
332
386
  **Round** (from `GET /users/{id}/rounds/{roundId}`):
333
387
  ```json
334
388
  {
335
- "roundId": 18294051,
389
+ "roundId": 12345678,
336
390
  "courseId": 12450,
337
391
  "courseVersion": 4,
338
- "startTime": "2025-09-14T15:30:00.000000Z",
339
- "endTime": "2025-09-14T20:15:00.000000Z",
392
+ "startTime": "2026-01-15T15:30:00.000000Z",
393
+ "endTime": "2026-01-15T20:15:00.000000Z",
340
394
  "noOfShots": 79,
341
395
  "noOfHoles": 18,
342
396
  "holes": [
@@ -364,7 +418,7 @@ All data calls go to `https://api.arccosgolf.com` with `Authorization: Bearer <t
364
418
  ```json
365
419
  {
366
420
  "clubId": 1, "clubType": 1,
367
- "clubMakeOther": "TaylorMade", "clubModelOther": "Qi10",
421
+ "clubMakeOther": "Example", "clubModelOther": "Model 1",
368
422
  "isDeleted": "F"
369
423
  }
370
424
  ```
@@ -378,19 +432,22 @@ python3 -m venv .venv
378
432
  source .venv/bin/activate
379
433
  pip install -e ".[dev]"
380
434
 
381
- pytest # tests with coverage (87%+, 139 tests)
382
- ruff check arccos/ # lint
435
+ pytest # tests with coverage (87%+, 173 tests)
436
+ ruff check . # lint
383
437
  mypy arccos/ # type check
384
438
  ```
385
439
 
386
440
  Coverage report: `htmlcov/index.html`. Minimum threshold: 80% (configured in `pyproject.toml`).
441
+ Current suite: 173 tests.
387
442
 
388
443
  ## Security
389
444
 
445
+ - 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.
390
446
  - Credentials are cached in `~/.arccos_creds.json` (mode `0600` — owner-only read/write). The file contains your access key (~180-day validity) and JWT token. Keep it safe.
391
447
  - All API communication uses HTTPS with TLS certificate verification.
392
448
  - `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.
393
449
  - The export command restricts output paths to your home directory or current working directory.
450
+ - Security issues should be reported privately as described in [`SECURITY.md`](SECURITY.md).
394
451
 
395
452
  ## Disclaimer
396
453
 
@@ -4,18 +4,30 @@
4
4
 
5
5
  # arccos-api
6
6
 
7
+ [![PyPI](https://img.shields.io/pypi/v/arccos-api?label=PyPI)](https://pypi.org/project/arccos-api/)
8
+
7
9
  Unofficial Python client and CLI for the [Arccos Golf](https://arccosgolf.com) platform.
8
10
 
9
- Arccos provides no public API. This library was reverse-engineered from the Arccos Dashboard web app. It gives you full programmatic access to your own golf data — rounds, handicap, club distances, pace of play, and more.
11
+ 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.
10
12
 
11
13
  > **Unofficial.** Not affiliated with Arccos Golf LLC. Use with your own account only. Respect their servers (no aggressive polling).
12
14
 
15
+ > **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.
16
+
13
17
  ## Installation
14
18
 
19
+ Install the `arccos-api` distribution from [PyPI](https://pypi.org/project/arccos-api/):
20
+
15
21
  ```bash
16
22
  pip install arccos-api
17
23
  ```
18
24
 
25
+ Or install the CLI with Homebrew:
26
+
27
+ ```bash
28
+ brew install pfrederiksen/tap/arccos-api
29
+ ```
30
+
19
31
  Or install from source:
20
32
 
21
33
  ```bash
@@ -53,9 +65,13 @@ arccos clubs # smart club distances
53
65
  | `arccos courses` | List courses you've played (or `--search` to filter) |
54
66
  | `arccos pace` | Pace of play analysis by course (color-coded) |
55
67
  | `arccos stats` | Strokes gained analysis (defaults to latest round) |
68
+ | `arccos stats --dashboard` | Strokes gained analysis from the current dashboard endpoint |
69
+ | `arccos tour-analytics` | Tour analytics summary/facet data |
56
70
  | `arccos trends` | Handicap trend chart over recent rounds |
57
71
  | `arccos club-shots <id>` | Shot history for a specific club |
72
+ | `arccos shot-dispersion [id]` | Shot dispersion for the bag or a specific club |
58
73
  | `arccos up-and-down` | Scramble / strokes-to-get-down stats |
74
+ | `arccos hole-image <course> <hole>` | Hole feature metadata and dashboard image URLs |
59
75
  | `arccos export` | Export rounds to JSON, CSV, or NDJSON (`--detail` for hole data) |
60
76
  | `arccos completions <shell>` | Generate bash/zsh/fish completion script |
61
77
  | `arccos logout` | Clear cached credentials |
@@ -73,7 +89,7 @@ arccos rounds --course "pebble" # filter by course name (substring)
73
89
  arccos rounds --json # raw JSON
74
90
 
75
91
  # Round detail
76
- arccos round 18294051 # hole-by-hole breakdown
92
+ arccos round 12345678 # hole-by-hole breakdown
77
93
 
78
94
  # Handicap
79
95
  arccos handicap # category breakdown
@@ -87,11 +103,15 @@ arccos clubs --after 2025-01-01 # distances from this year only
87
103
  arccos bests # all-time personal bests
88
104
  arccos overview # scoring avg + handicap breakdown
89
105
  arccos scoring # scoring trend with bar chart
106
+ arccos stats --dashboard # dashboard SGA, same endpoint as web app
107
+ arccos stats --dashboard --latest # dashboard SGA for your latest round
90
108
  arccos trends # handicap trend chart
91
109
  arccos up-and-down # scramble stats
110
+ arccos tour-analytics --facet driving # tour analytics by facet
111
+ arccos tour-analytics --summary # tour analytics summary
92
112
 
93
113
  # Compare rounds
94
- arccos compare 26685289 26762935 # side-by-side hole comparison
114
+ arccos compare 12345678 12345679 # side-by-side hole comparison
95
115
 
96
116
  # Courses
97
117
  arccos courses # all courses played
@@ -99,6 +119,11 @@ arccos courses --search "torrey" # filter by name or city
99
119
 
100
120
  # Club detail
101
121
  arccos club-shots 1 # shot history for club ID 1
122
+ arccos shot-dispersion # bag-level dispersion data
123
+ arccos shot-dispersion 1 --json # raw dispersion for club ID 1
124
+
125
+ # Hole imagery metadata
126
+ arccos hole-image 10769 7 -v 11 # image + metadata URLs and hole features
102
127
 
103
128
  # Shell completions
104
129
  eval "$(arccos completions bash)" # enable tab completion
@@ -123,24 +148,24 @@ $ arccos clubs
123
148
  ╭──────┬──────────────────┬───────┬─────────┬──────────┬───────╮
124
149
  │ Club │ Model │ Smart │ Longest │ Range │ Shots │
125
150
  ├──────┼──────────────────┼───────┼─────────┼──────────┼───────┤
126
- │ Dr │ TaylorMade Qi10 │ 252y │ 291y │ 245–263y │ 312 │
127
- │ 3w │ TaylorMade Qi10 │ 228y │ 248y │ 220–235y │ 87 │
128
- │ 5w │ Callaway Paradym │ 208y │ 231y │ 200–215y │ 143 │
129
- │ 4h │ Titleist T200 │ 195y │ 214y │ 188–202y │ 61 │
130
- │ 6i │ Titleist T150 │ 172y │ 190y │ 165–179y │ 94 │
131
- │ 7i │ Titleist T150 │ 160y │ 178y │ 153–167y │ 118 │
132
- │ 8i │ Titleist T150 │ 148y │ 165y │ 141–155y │ 105 │
133
- │ 9i │ Titleist T150 │ 136y │ 152y │ 130–142y │ 97 │
134
- │ Pw │ Titleist T150 │ 124y │ 140y │ 118–130y │ 82 │
151
+ │ Dr │ Example Driver │ 252y │ 291y │ 245–263y │ 312 │
152
+ │ 3w │ Example Fairway │ 228y │ 248y │ 220–235y │ 87 │
153
+ │ 5w │ Example Fairway │ 208y │ 231y │ 200–215y │ 143 │
154
+ │ 4h │ Example Hybrid │ 195y │ 214y │ 188–202y │ 61 │
155
+ │ 6i │ Example Iron │ 172y │ 190y │ 165–179y │ 94 │
156
+ │ 7i │ Example Iron │ 160y │ 178y │ 153–167y │ 118 │
157
+ │ 8i │ Example Iron │ 148y │ 165y │ 141–155y │ 105 │
158
+ │ 9i │ Example Iron │ 136y │ 152y │ 130–142y │ 97 │
159
+ │ Pw │ Example Wedge │ 124y │ 140y │ 118–130y │ 82 │
135
160
  │ 50 │ Vokey SM10 │ 108y │ 125y │ 102–114y │ 76 │
136
161
  │ 54 │ Vokey SM10 │ 88y │ 105y │ 82–94y │ 201 │
137
162
  │ 58 │ Vokey SM10 │ 64y │ 88y │ 58–70y │ 168 │
138
163
  ╰──────┴──────────────────┴───────┴─────────┴──────────┴───────╯
139
164
 
140
- $ arccos round 18294051
141
- ╭─── Round 18294051 ────╮
142
- │ Date: 2025-09-14
143
- │ Course: Torrey Pines
165
+ $ arccos round 12345678
166
+ ╭─── Round 12345678 ────╮
167
+ │ Date: 2026-01-15
168
+ │ Course: Example GC
144
169
  │ Score: 79 (+7) │
145
170
  │ Holes: 18 │
146
171
  ╰───────────────────────╯
@@ -175,9 +200,9 @@ Pace of Play — 10 rounds across 6 courses Overall avg: 4h 22m
175
200
  ╭───┬──────────┬───────────────────────┬────────╮
176
201
  │ │ Avg Time │ Course │ Rounds │
177
202
  ├───┼──────────┼───────────────────────┼────────┤
178
- │ ● │ 5h 10m │ Bethpage Black │ 2 │
179
- │ ● │ 4h 45m │ Torrey Pines │ 3 │
180
- │ ● │ 3h 50m │ Bandon Dunes │ 2 │
203
+ │ ● │ 5h 10m │ Example Golf Club A │ 2 │
204
+ │ ● │ 4h 45m │ Example Golf Club B │ 3 │
205
+ │ ● │ 3h 50m │ Example Golf Club C │ 2 │
181
206
  │ ● │ 3h 35m │ Chambers Bay │ 1 │
182
207
  ╰───┴──────────┴───────────────────────┴────────╯
183
208
  ```
@@ -230,12 +255,24 @@ for club in client.clubs.smart_distances():
230
255
  print(f" Club {club['clubId']}: {dist:.0f}y")
231
256
 
232
257
  # Bag configuration (maps clubId → clubType + make/model)
233
- profile = client._http.get(f"/users/{client.user_id}")
258
+ profile = client.profile()
234
259
  bag = client.clubs.bag(str(profile["bagId"]))
235
260
  for club in bag["clubs"]:
236
261
  if club.get("isDeleted") != "T":
237
262
  print(f" {club['clubMakeOther']} {club['clubModelOther']}")
238
263
 
264
+ # Current dashboard club/SGA analytics endpoints
265
+ club_inventory = client.clubs.list()
266
+ shot_dispersion = client.clubs.shot_dispersion()
267
+ dashboard_sga = client.stats.dashboard_analysis(goal_hcp=5, no_of_rounds=20)
268
+ player_profile = client.stats.player_profile()
269
+ tour_summary = client.stats.tour_analytics_summary()
270
+
271
+ # Course / hole imagery metadata
272
+ hole_features = client.courses.hole_features(10769, 7, version=11)
273
+ image_url = client.courses.hole_image_url(10769, 7, 11, "2026-07")
274
+ metadata_url = client.courses.hole_metadata_url(10769, 7, 11, "2026-07")
275
+
239
276
  # Courses played
240
277
  for course in client.courses.played():
241
278
  print(f" {course.get('name', course['courseId'])}")
@@ -254,7 +291,7 @@ for r in client.rounds.iter_all():
254
291
  print(r["roundId"], r["noOfShots"])
255
292
 
256
293
  # Course search
257
- results = client.courses.search("Torrey Pines")
294
+ results = client.courses.search("Example Golf Club")
258
295
  ```
259
296
 
260
297
  ## API Reference
@@ -265,6 +302,8 @@ Full OpenAPI 3.1 spec: [`docs/openapi.yaml`](docs/openapi.yaml)
265
302
 
266
303
  All auth calls go to `https://authentication.arccosgolf.com`.
267
304
 
305
+ 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.
306
+
268
307
  | Step | Endpoint | Body | Returns |
269
308
  |------|----------|------|---------|
270
309
  | 1. Get access key | `POST /accessKeys` | `{"email", "password", "signedInByFacebook": "F"}` | `{"userId", "accessKey", "secret"}` |
@@ -276,19 +315,31 @@ The JWT expires in ~3 hours. The `accessKey` is valid for ~180 days. Refresh by
276
315
 
277
316
  All data calls go to `https://api.arccosgolf.com` with `Authorization: Bearer <token>`.
278
317
 
318
+ The data client enforces `GET` only and exposes no `POST`, `PUT`, `PATCH`, or `DELETE` methods.
319
+
279
320
  | Resource | Method | Path |
280
321
  |----------|--------|------|
281
322
  | User profile | GET | `/users/{userId}` (includes `bagId`, `bags[]`) |
323
+ | User details | GET | `/users/{userId}/details` |
282
324
  | Bag / clubs | GET | `/users/{userId}/bags/{bagId}` (club config with make/model) |
325
+ | Club inventory | GET | `/v6/users/{userId}/clubs` |
283
326
  | Rounds | GET | `/users/{userId}/rounds?offSet=0&limit=200&roundType=flagship` |
284
327
  | Round detail | GET | `/users/{userId}/rounds/{roundId}` (includes `holes[]` with `shots[]`) |
285
328
  | Handicap | GET | `/users/{userId}/handicaps/latest` |
286
329
  | Handicap history | GET | `/users/{userId}/handicaps?rounds=20` |
287
330
  | Smart distances | GET | `/v4/clubs/user/{userId}/smart-distances` |
331
+ | Shot dispersion | GET | `/v3/user/{userId}/clubs/dispersion` |
332
+ | Club shot dispersion | GET | `/v3/user/{userId}/clubs/{clubId}/dispersion` |
288
333
  | Courses played | GET | `/users/{userId}/coursesPlayed` |
289
334
  | Course metadata | GET | `/courses/{courseId}?courseVersion=1` |
335
+ | Hole feature metadata | GET | `/courses/{courseId}/holes/{holeId}?courseVersion=1` |
290
336
  | Course search | GET | `/v2/courses?search={query}` |
291
337
  | Club shots | GET | `/users/{userId}/bags/{bagId}/clubs/{clubId}/shots` |
338
+ | Dashboard SGA | GET | `/sga/getDashboardAnalysis/{userId}` |
339
+ | SGA player profile | GET | `/sga/playerProfile/{userId}` |
340
+ | Tour analytics | GET | `/users/{userId}/tourAnalytics` |
341
+ | Tour analytics summary | GET | `/users/{userId}/tourAnalyticsSummary` |
342
+ | Tour analytics ranked | GET | `/users/{userId}/tourAnalyticsRanked` |
292
343
  | Personal bests | GET | `/users/{userId}/personalBests?tags=allTimeBest` |
293
344
  | Strokes to get down | GET | `/v2/sga/strokes-to-get-down` |
294
345
 
@@ -297,11 +348,11 @@ All data calls go to `https://api.arccosgolf.com` with `Authorization: Bearer <t
297
348
  **Round** (from `GET /users/{id}/rounds/{roundId}`):
298
349
  ```json
299
350
  {
300
- "roundId": 18294051,
351
+ "roundId": 12345678,
301
352
  "courseId": 12450,
302
353
  "courseVersion": 4,
303
- "startTime": "2025-09-14T15:30:00.000000Z",
304
- "endTime": "2025-09-14T20:15:00.000000Z",
354
+ "startTime": "2026-01-15T15:30:00.000000Z",
355
+ "endTime": "2026-01-15T20:15:00.000000Z",
305
356
  "noOfShots": 79,
306
357
  "noOfHoles": 18,
307
358
  "holes": [
@@ -329,7 +380,7 @@ All data calls go to `https://api.arccosgolf.com` with `Authorization: Bearer <t
329
380
  ```json
330
381
  {
331
382
  "clubId": 1, "clubType": 1,
332
- "clubMakeOther": "TaylorMade", "clubModelOther": "Qi10",
383
+ "clubMakeOther": "Example", "clubModelOther": "Model 1",
333
384
  "isDeleted": "F"
334
385
  }
335
386
  ```
@@ -343,19 +394,22 @@ python3 -m venv .venv
343
394
  source .venv/bin/activate
344
395
  pip install -e ".[dev]"
345
396
 
346
- pytest # tests with coverage (87%+, 139 tests)
347
- ruff check arccos/ # lint
397
+ pytest # tests with coverage (87%+, 173 tests)
398
+ ruff check . # lint
348
399
  mypy arccos/ # type check
349
400
  ```
350
401
 
351
402
  Coverage report: `htmlcov/index.html`. Minimum threshold: 80% (configured in `pyproject.toml`).
403
+ Current suite: 173 tests.
352
404
 
353
405
  ## Security
354
406
 
407
+ - 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.
355
408
  - Credentials are cached in `~/.arccos_creds.json` (mode `0600` — owner-only read/write). The file contains your access key (~180-day validity) and JWT token. Keep it safe.
356
409
  - All API communication uses HTTPS with TLS certificate verification.
357
410
  - `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.
358
411
  - The export command restricts output paths to your home directory or current working directory.
412
+ - Security issues should be reported privately as described in [`SECURITY.md`](SECURITY.md).
359
413
 
360
414
  ## Disclaimer
361
415
 
@@ -50,7 +50,8 @@ class _SensitiveFilter(_logging.Filter):
50
50
 
51
51
 
52
52
  _logger = _logging.getLogger("arccos")
53
- _logger.addFilter(_SensitiveFilter())
53
+ _sensitive_filter = _SensitiveFilter()
54
+ _logger.addFilter(_sensitive_filter)
54
55
 
55
56
  # Allow ARCCOS_LOG_LEVEL=DEBUG (or INFO, WARNING, etc.) to enable logging
56
57
  _log_level = _os.environ.get("ARCCOS_LOG_LEVEL")
@@ -59,6 +60,8 @@ if _log_level:
59
60
  if not _logger.handlers:
60
61
  _handler = _logging.StreamHandler()
61
62
  _handler.setFormatter(_logging.Formatter("%(name)s %(levelname)s: %(message)s"))
63
+ # Handler filters also cover records propagated by child loggers.
64
+ _handler.addFilter(_sensitive_filter)
62
65
  _logger.addHandler(_handler)
63
66
 
64
67
  __version__ = _pkg_version("arccos-api")