arccos-api 0.2.1__tar.gz → 0.3.1__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.
- {arccos_api-0.2.1 → arccos_api-0.3.1}/PKG-INFO +28 -4
- {arccos_api-0.2.1 → arccos_api-0.3.1}/README.md +26 -3
- {arccos_api-0.2.1 → arccos_api-0.3.1}/arccos/__init__.py +12 -1
- {arccos_api-0.2.1 → arccos_api-0.3.1}/arccos/cli/__init__.py +2 -0
- {arccos_api-0.2.1 → arccos_api-0.3.1}/arccos/cli/stats.py +132 -2
- {arccos_api-0.2.1 → arccos_api-0.3.1}/arccos/exceptions.py +1 -1
- {arccos_api-0.2.1 → arccos_api-0.3.1}/arccos/resources/courses.py +1 -1
- {arccos_api-0.2.1 → arccos_api-0.3.1}/arccos/resources/rounds.py +6 -2
- {arccos_api-0.2.1 → arccos_api-0.3.1}/arccos_api.egg-info/PKG-INFO +28 -4
- {arccos_api-0.2.1 → arccos_api-0.3.1}/pyproject.toml +2 -1
- {arccos_api-0.2.1 → arccos_api-0.3.1}/tests/test_cli.py +115 -0
- {arccos_api-0.2.1 → arccos_api-0.3.1}/MANIFEST.in +0 -0
- {arccos_api-0.2.1 → arccos_api-0.3.1}/arccos/__main__.py +0 -0
- {arccos_api-0.2.1 → arccos_api-0.3.1}/arccos/_http.py +0 -0
- {arccos_api-0.2.1 → arccos_api-0.3.1}/arccos/auth.py +0 -0
- {arccos_api-0.2.1 → arccos_api-0.3.1}/arccos/cli/_helpers.py +0 -0
- {arccos_api-0.2.1 → arccos_api-0.3.1}/arccos/cli/auth.py +0 -0
- {arccos_api-0.2.1 → arccos_api-0.3.1}/arccos/cli/rounds.py +0 -0
- {arccos_api-0.2.1 → arccos_api-0.3.1}/arccos/client.py +0 -0
- {arccos_api-0.2.1 → arccos_api-0.3.1}/arccos/resources/__init__.py +0 -0
- {arccos_api-0.2.1 → arccos_api-0.3.1}/arccos/resources/clubs.py +0 -0
- {arccos_api-0.2.1 → arccos_api-0.3.1}/arccos/resources/handicap.py +0 -0
- {arccos_api-0.2.1 → arccos_api-0.3.1}/arccos/resources/stats.py +0 -0
- {arccos_api-0.2.1 → arccos_api-0.3.1}/arccos/types.py +0 -0
- {arccos_api-0.2.1 → arccos_api-0.3.1}/arccos_api.egg-info/SOURCES.txt +0 -0
- {arccos_api-0.2.1 → arccos_api-0.3.1}/arccos_api.egg-info/dependency_links.txt +0 -0
- {arccos_api-0.2.1 → arccos_api-0.3.1}/arccos_api.egg-info/entry_points.txt +0 -0
- {arccos_api-0.2.1 → arccos_api-0.3.1}/arccos_api.egg-info/requires.txt +0 -0
- {arccos_api-0.2.1 → arccos_api-0.3.1}/arccos_api.egg-info/top_level.txt +0 -0
- {arccos_api-0.2.1 → arccos_api-0.3.1}/setup.cfg +0 -0
- {arccos_api-0.2.1 → arccos_api-0.3.1}/tests/test_auth.py +0 -0
- {arccos_api-0.2.1 → arccos_api-0.3.1}/tests/test_client.py +0 -0
- {arccos_api-0.2.1 → arccos_api-0.3.1}/tests/test_exceptions.py +0 -0
- {arccos_api-0.2.1 → arccos_api-0.3.1}/tests/test_http.py +0 -0
- {arccos_api-0.2.1 → arccos_api-0.3.1}/tests/test_resources.py +0 -0
- {arccos_api-0.2.1 → arccos_api-0.3.1}/tests/test_rounds.py +0 -0
- {arccos_api-0.2.1 → arccos_api-0.3.1}/tests/test_security.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: arccos-api
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Unofficial Python CLI and client library for the Arccos Golf API
|
|
5
5
|
Author: Paul Frederiksen
|
|
6
6
|
License: MIT
|
|
@@ -17,6 +17,7 @@ Classifier: Programming Language :: Python :: 3
|
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.11
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.12
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
21
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
22
|
Requires-Python: >=3.11
|
|
22
23
|
Description-Content-Type: text/markdown
|
|
@@ -83,9 +84,11 @@ arccos clubs # smart club distances
|
|
|
83
84
|
| `arccos bests` | All-time personal bests (lowest score, longest drive, etc.) |
|
|
84
85
|
| `arccos overview` | Performance summary — scoring avg + handicap breakdown |
|
|
85
86
|
| `arccos scoring` | Scoring trend with visual bar chart |
|
|
86
|
-
| `arccos courses` | List
|
|
87
|
+
| `arccos courses` | List courses you've played (or `--search` any course) |
|
|
87
88
|
| `arccos pace` | Pace of play analysis by course (color-coded) |
|
|
88
|
-
| `arccos stats
|
|
89
|
+
| `arccos stats` | Strokes gained analysis (defaults to latest round) |
|
|
90
|
+
| `arccos club-shots <id>` | Shot history for a specific club |
|
|
91
|
+
| `arccos up-and-down` | Scramble / strokes-to-get-down stats |
|
|
89
92
|
| `arccos export` | Export rounds to JSON, CSV, or NDJSON (`--detail` for hole data) |
|
|
90
93
|
| `arccos logout` | Clear cached credentials |
|
|
91
94
|
|
|
@@ -116,9 +119,14 @@ arccos clubs --after 2025-01-01 # distances from this year only
|
|
|
116
119
|
arccos bests # all-time personal bests
|
|
117
120
|
arccos overview # scoring avg + handicap breakdown
|
|
118
121
|
arccos scoring # scoring trend with bar chart
|
|
122
|
+
arccos up-and-down # scramble stats
|
|
119
123
|
|
|
120
124
|
# Courses
|
|
121
125
|
arccos courses # all courses played
|
|
126
|
+
arccos courses --search "torrey" # search any course by name
|
|
127
|
+
|
|
128
|
+
# Club detail
|
|
129
|
+
arccos club-shots 1 # shot history for club ID 1
|
|
122
130
|
|
|
123
131
|
# Pace of play
|
|
124
132
|
arccos pace # all rounds, slowest courses first
|
|
@@ -208,6 +216,13 @@ export ARCCOS_PASSWORD="your_password"
|
|
|
208
216
|
arccos rounds
|
|
209
217
|
```
|
|
210
218
|
|
|
219
|
+
Other environment variables:
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
export ARCCOS_TIMEOUT=30 # request timeout in seconds (default: 15)
|
|
223
|
+
export ARCCOS_LOG_LEVEL=DEBUG # enable debug logging (DEBUG, INFO, WARNING)
|
|
224
|
+
```
|
|
225
|
+
|
|
211
226
|
> **Security note:** Environment variables may be visible in process listings (`ps`) and shell history. Prefer `arccos login` for interactive use.
|
|
212
227
|
|
|
213
228
|
## Python Library
|
|
@@ -257,6 +272,13 @@ for c in pace["course_averages"][:5]:
|
|
|
257
272
|
|
|
258
273
|
# Personal bests
|
|
259
274
|
bests = client.stats.personal_bests()
|
|
275
|
+
|
|
276
|
+
# Iterate ALL rounds (auto-pagination)
|
|
277
|
+
for r in client.rounds.iter_all():
|
|
278
|
+
print(r["roundId"], r["noOfShots"])
|
|
279
|
+
|
|
280
|
+
# Course search
|
|
281
|
+
results = client.courses.search("Torrey Pines")
|
|
260
282
|
```
|
|
261
283
|
|
|
262
284
|
## API Reference
|
|
@@ -290,7 +312,9 @@ All data calls go to `https://api.arccosgolf.com` with `Authorization: Bearer <t
|
|
|
290
312
|
| Courses played | GET | `/users/{userId}/coursesPlayed` |
|
|
291
313
|
| Course metadata | GET | `/courses/{courseId}?courseVersion=1` |
|
|
292
314
|
| Course search | GET | `/v2/courses?search={query}` |
|
|
315
|
+
| Club shots | GET | `/users/{userId}/bags/{bagId}/clubs/{clubId}/shots` |
|
|
293
316
|
| Personal bests | GET | `/users/{userId}/personalBests?tags=allTimeBest` |
|
|
317
|
+
| Strokes to get down | GET | `/v2/sga/strokes-to-get-down` |
|
|
294
318
|
|
|
295
319
|
### Key data structures
|
|
296
320
|
|
|
@@ -343,7 +367,7 @@ python3 -m venv .venv
|
|
|
343
367
|
source .venv/bin/activate
|
|
344
368
|
pip install -e ".[dev]"
|
|
345
369
|
|
|
346
|
-
pytest # tests with coverage (
|
|
370
|
+
pytest # tests with coverage (87%+, 139 tests)
|
|
347
371
|
ruff check arccos/ # lint
|
|
348
372
|
mypy arccos/ # type check
|
|
349
373
|
```
|
|
@@ -49,9 +49,11 @@ arccos clubs # smart club distances
|
|
|
49
49
|
| `arccos bests` | All-time personal bests (lowest score, longest drive, etc.) |
|
|
50
50
|
| `arccos overview` | Performance summary — scoring avg + handicap breakdown |
|
|
51
51
|
| `arccos scoring` | Scoring trend with visual bar chart |
|
|
52
|
-
| `arccos courses` | List
|
|
52
|
+
| `arccos courses` | List courses you've played (or `--search` any course) |
|
|
53
53
|
| `arccos pace` | Pace of play analysis by course (color-coded) |
|
|
54
|
-
| `arccos stats
|
|
54
|
+
| `arccos stats` | Strokes gained analysis (defaults to latest round) |
|
|
55
|
+
| `arccos club-shots <id>` | Shot history for a specific club |
|
|
56
|
+
| `arccos up-and-down` | Scramble / strokes-to-get-down stats |
|
|
55
57
|
| `arccos export` | Export rounds to JSON, CSV, or NDJSON (`--detail` for hole data) |
|
|
56
58
|
| `arccos logout` | Clear cached credentials |
|
|
57
59
|
|
|
@@ -82,9 +84,14 @@ arccos clubs --after 2025-01-01 # distances from this year only
|
|
|
82
84
|
arccos bests # all-time personal bests
|
|
83
85
|
arccos overview # scoring avg + handicap breakdown
|
|
84
86
|
arccos scoring # scoring trend with bar chart
|
|
87
|
+
arccos up-and-down # scramble stats
|
|
85
88
|
|
|
86
89
|
# Courses
|
|
87
90
|
arccos courses # all courses played
|
|
91
|
+
arccos courses --search "torrey" # search any course by name
|
|
92
|
+
|
|
93
|
+
# Club detail
|
|
94
|
+
arccos club-shots 1 # shot history for club ID 1
|
|
88
95
|
|
|
89
96
|
# Pace of play
|
|
90
97
|
arccos pace # all rounds, slowest courses first
|
|
@@ -174,6 +181,13 @@ export ARCCOS_PASSWORD="your_password"
|
|
|
174
181
|
arccos rounds
|
|
175
182
|
```
|
|
176
183
|
|
|
184
|
+
Other environment variables:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
export ARCCOS_TIMEOUT=30 # request timeout in seconds (default: 15)
|
|
188
|
+
export ARCCOS_LOG_LEVEL=DEBUG # enable debug logging (DEBUG, INFO, WARNING)
|
|
189
|
+
```
|
|
190
|
+
|
|
177
191
|
> **Security note:** Environment variables may be visible in process listings (`ps`) and shell history. Prefer `arccos login` for interactive use.
|
|
178
192
|
|
|
179
193
|
## Python Library
|
|
@@ -223,6 +237,13 @@ for c in pace["course_averages"][:5]:
|
|
|
223
237
|
|
|
224
238
|
# Personal bests
|
|
225
239
|
bests = client.stats.personal_bests()
|
|
240
|
+
|
|
241
|
+
# Iterate ALL rounds (auto-pagination)
|
|
242
|
+
for r in client.rounds.iter_all():
|
|
243
|
+
print(r["roundId"], r["noOfShots"])
|
|
244
|
+
|
|
245
|
+
# Course search
|
|
246
|
+
results = client.courses.search("Torrey Pines")
|
|
226
247
|
```
|
|
227
248
|
|
|
228
249
|
## API Reference
|
|
@@ -256,7 +277,9 @@ All data calls go to `https://api.arccosgolf.com` with `Authorization: Bearer <t
|
|
|
256
277
|
| Courses played | GET | `/users/{userId}/coursesPlayed` |
|
|
257
278
|
| Course metadata | GET | `/courses/{courseId}?courseVersion=1` |
|
|
258
279
|
| Course search | GET | `/v2/courses?search={query}` |
|
|
280
|
+
| Club shots | GET | `/users/{userId}/bags/{bagId}/clubs/{clubId}/shots` |
|
|
259
281
|
| Personal bests | GET | `/users/{userId}/personalBests?tags=allTimeBest` |
|
|
282
|
+
| Strokes to get down | GET | `/v2/sga/strokes-to-get-down` |
|
|
260
283
|
|
|
261
284
|
### Key data structures
|
|
262
285
|
|
|
@@ -309,7 +332,7 @@ python3 -m venv .venv
|
|
|
309
332
|
source .venv/bin/activate
|
|
310
333
|
pip install -e ".[dev]"
|
|
311
334
|
|
|
312
|
-
pytest # tests with coverage (
|
|
335
|
+
pytest # tests with coverage (87%+, 139 tests)
|
|
313
336
|
ruff check arccos/ # lint
|
|
314
337
|
mypy arccos/ # type check
|
|
315
338
|
```
|
|
@@ -10,6 +10,7 @@ Quick start:
|
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
import logging as _logging
|
|
13
|
+
import os as _os
|
|
13
14
|
import re as _re
|
|
14
15
|
from importlib.metadata import version as _pkg_version
|
|
15
16
|
|
|
@@ -40,7 +41,17 @@ class _SensitiveFilter(_logging.Filter):
|
|
|
40
41
|
return True
|
|
41
42
|
|
|
42
43
|
|
|
43
|
-
_logging.getLogger("arccos")
|
|
44
|
+
_logger = _logging.getLogger("arccos")
|
|
45
|
+
_logger.addFilter(_SensitiveFilter())
|
|
46
|
+
|
|
47
|
+
# Allow ARCCOS_LOG_LEVEL=DEBUG (or INFO, WARNING, etc.) to enable logging
|
|
48
|
+
_log_level = _os.environ.get("ARCCOS_LOG_LEVEL")
|
|
49
|
+
if _log_level:
|
|
50
|
+
_logger.setLevel(getattr(_logging, _log_level.upper(), _logging.WARNING))
|
|
51
|
+
if not _logger.handlers:
|
|
52
|
+
_handler = _logging.StreamHandler()
|
|
53
|
+
_handler.setFormatter(_logging.Formatter("%(name)s %(levelname)s: %(message)s"))
|
|
54
|
+
_logger.addHandler(_handler)
|
|
44
55
|
|
|
45
56
|
__version__ = _pkg_version("arccos-api")
|
|
46
57
|
__all__ = [
|
|
@@ -31,6 +31,8 @@ def cli():
|
|
|
31
31
|
arccos bests Personal bests (all-time records)
|
|
32
32
|
arccos overview Overall stats summary
|
|
33
33
|
arccos scoring Scoring trend over recent rounds
|
|
34
|
+
arccos club-shots <id> Shot history for a specific club
|
|
35
|
+
arccos up-and-down Scramble / up-and-down stats
|
|
34
36
|
arccos export Export rounds to JSON/CSV
|
|
35
37
|
arccos logout Clear cached credentials
|
|
36
38
|
|
|
@@ -16,10 +16,12 @@ from ._helpers import _get_client, _output_json, console, err_console
|
|
|
16
16
|
def register(cli: click.Group) -> None:
|
|
17
17
|
cli.add_command(handicap)
|
|
18
18
|
cli.add_command(clubs)
|
|
19
|
+
cli.add_command(club_shots)
|
|
19
20
|
cli.add_command(stats)
|
|
20
21
|
cli.add_command(bests)
|
|
21
22
|
cli.add_command(overview)
|
|
22
23
|
cli.add_command(courses)
|
|
24
|
+
cli.add_command(up_and_down)
|
|
23
25
|
|
|
24
26
|
|
|
25
27
|
# ---------------------------------------------------------------------------
|
|
@@ -462,14 +464,23 @@ def overview(limit: int, as_json: bool):
|
|
|
462
464
|
# ---------------------------------------------------------------------------
|
|
463
465
|
|
|
464
466
|
@click.command()
|
|
467
|
+
@click.option("--search", "-s", default=None, help="Search for a course by name.")
|
|
465
468
|
@click.option("--json", "as_json", is_flag=True, help="Output raw JSON.")
|
|
466
|
-
def courses(as_json: bool):
|
|
467
|
-
"""List courses you have played."""
|
|
469
|
+
def courses(search: str, as_json: bool):
|
|
470
|
+
"""List courses you have played, or search for any course."""
|
|
468
471
|
client = _get_client()
|
|
469
472
|
|
|
470
473
|
with console.status("Fetching courses\u2026"):
|
|
471
474
|
data = client.courses.played()
|
|
472
475
|
|
|
476
|
+
if search:
|
|
477
|
+
needle = search.lower()
|
|
478
|
+
data = [
|
|
479
|
+
c for c in data
|
|
480
|
+
if needle in (c.get("name") or c.get("courseName") or "").lower()
|
|
481
|
+
or needle in (c.get("city") or "").lower()
|
|
482
|
+
]
|
|
483
|
+
|
|
473
484
|
if as_json:
|
|
474
485
|
_output_json(data)
|
|
475
486
|
return
|
|
@@ -502,3 +513,122 @@ def courses(as_json: bool):
|
|
|
502
513
|
table.add_row(name, city, state, played, cid)
|
|
503
514
|
|
|
504
515
|
console.print(table)
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
# ---------------------------------------------------------------------------
|
|
519
|
+
# club-shots
|
|
520
|
+
# ---------------------------------------------------------------------------
|
|
521
|
+
|
|
522
|
+
@click.command(name="club-shots")
|
|
523
|
+
@click.argument("club_id", type=int)
|
|
524
|
+
@click.option("--limit", "-n", default=20, show_default=True, help="Number of shots to show.")
|
|
525
|
+
@click.option("--json", "as_json", is_flag=True, help="Output raw JSON.")
|
|
526
|
+
def club_shots(club_id: int, limit: int, as_json: bool):
|
|
527
|
+
"""Show individual shot history for a specific club.
|
|
528
|
+
|
|
529
|
+
CLUB_ID is the bag-specific club ID (shown in `arccos clubs --json`).
|
|
530
|
+
"""
|
|
531
|
+
client = _get_client()
|
|
532
|
+
|
|
533
|
+
with console.status("Fetching shots\u2026"):
|
|
534
|
+
profile = client._http.get(f"/users/{client.user_id}")
|
|
535
|
+
bag_id = profile.get("bagId")
|
|
536
|
+
if not bag_id:
|
|
537
|
+
err_console.print("[red]Could not determine bag ID.[/red]")
|
|
538
|
+
sys.exit(1)
|
|
539
|
+
data = client.clubs.club_shots(str(bag_id), club_id, limit=limit)
|
|
540
|
+
|
|
541
|
+
if as_json:
|
|
542
|
+
_output_json(data)
|
|
543
|
+
return
|
|
544
|
+
|
|
545
|
+
if not data:
|
|
546
|
+
console.print("[dim]No shots found for this club.[/dim]")
|
|
547
|
+
return
|
|
548
|
+
|
|
549
|
+
from arccos.resources.clubs import CLUB_TYPE_NAMES
|
|
550
|
+
|
|
551
|
+
# Try to resolve club name
|
|
552
|
+
try:
|
|
553
|
+
bag = client.clubs.bag(str(bag_id))
|
|
554
|
+
bag_club: dict = next(
|
|
555
|
+
(c for c in bag.get("clubs", []) if c["clubId"] == club_id), {}
|
|
556
|
+
)
|
|
557
|
+
club_type = bag_club.get("clubType", club_id)
|
|
558
|
+
club_name = CLUB_TYPE_NAMES.get(club_type, f"Club {club_id}")
|
|
559
|
+
except Exception:
|
|
560
|
+
club_name = f"Club {club_id}"
|
|
561
|
+
|
|
562
|
+
table = Table(
|
|
563
|
+
box=box.ROUNDED,
|
|
564
|
+
header_style="bold cyan",
|
|
565
|
+
title=f"[bold]Shot History — {club_name}[/bold] [dim]({len(data)} shots)[/dim]",
|
|
566
|
+
)
|
|
567
|
+
table.add_column("Date", style="white", no_wrap=True)
|
|
568
|
+
table.add_column("Distance", justify="right", style="green bold")
|
|
569
|
+
table.add_column("Terrain", style="dim")
|
|
570
|
+
|
|
571
|
+
for shot in data:
|
|
572
|
+
date = shot.get("shotTime", shot.get("startTime", ""))[:10]
|
|
573
|
+
dist = shot.get("distance")
|
|
574
|
+
dist_s = f"{dist:.0f}y" if dist else "\u2014"
|
|
575
|
+
terrain = str(shot.get("userStartTerrainOverride", ""))
|
|
576
|
+
table.add_row(date, dist_s, terrain)
|
|
577
|
+
|
|
578
|
+
console.print(table)
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
# ---------------------------------------------------------------------------
|
|
582
|
+
# up-and-down
|
|
583
|
+
# ---------------------------------------------------------------------------
|
|
584
|
+
|
|
585
|
+
@click.command(name="up-and-down")
|
|
586
|
+
@click.option("--json", "as_json", is_flag=True, help="Output raw JSON.")
|
|
587
|
+
def up_and_down(as_json: bool):
|
|
588
|
+
"""Show strokes-to-get-down (up-and-down / scramble) stats."""
|
|
589
|
+
client = _get_client()
|
|
590
|
+
|
|
591
|
+
from arccos.exceptions import ArccosError
|
|
592
|
+
|
|
593
|
+
try:
|
|
594
|
+
with console.status("Fetching up-and-down stats\u2026"):
|
|
595
|
+
data = client.stats.strokes_to_get_down()
|
|
596
|
+
except ArccosError as e:
|
|
597
|
+
err_console.print(f"[red]Failed:[/red] {e}")
|
|
598
|
+
err_console.print(
|
|
599
|
+
"[dim]This endpoint may not be available for all accounts.[/dim]"
|
|
600
|
+
)
|
|
601
|
+
sys.exit(1)
|
|
602
|
+
|
|
603
|
+
if as_json:
|
|
604
|
+
_output_json(data)
|
|
605
|
+
return
|
|
606
|
+
|
|
607
|
+
if not data:
|
|
608
|
+
console.print("[dim]No up-and-down data available.[/dim]")
|
|
609
|
+
return
|
|
610
|
+
|
|
611
|
+
# Display whatever the API returns as a key-value table
|
|
612
|
+
table = Table(
|
|
613
|
+
box=box.ROUNDED,
|
|
614
|
+
header_style="bold cyan",
|
|
615
|
+
title="[bold]Strokes to Get Down[/bold]",
|
|
616
|
+
)
|
|
617
|
+
table.add_column("Stat", style="white bold")
|
|
618
|
+
table.add_column("Value", justify="right", style="bold")
|
|
619
|
+
|
|
620
|
+
for key, val in data.items():
|
|
621
|
+
if isinstance(val, (int, float)):
|
|
622
|
+
display = f"{val:.1f}" if isinstance(val, float) else str(val)
|
|
623
|
+
table.add_row(key, display)
|
|
624
|
+
|
|
625
|
+
if table.row_count == 0:
|
|
626
|
+
# Nested structure — try one level deeper
|
|
627
|
+
for section, vals in data.items():
|
|
628
|
+
if isinstance(vals, dict):
|
|
629
|
+
for k, v in vals.items():
|
|
630
|
+
if isinstance(v, (int, float)):
|
|
631
|
+
display = f"{v:.1f}" if isinstance(v, float) else str(v)
|
|
632
|
+
table.add_row(f"{section}.{k}", display)
|
|
633
|
+
|
|
634
|
+
console.print(table)
|
|
@@ -6,7 +6,7 @@ Custom exceptions for the Arccos API client.
|
|
|
6
6
|
class ArccosError(Exception):
|
|
7
7
|
"""Base exception for all Arccos API errors."""
|
|
8
8
|
|
|
9
|
-
def __init__(self, message: str, status_code: int = None, error_code: int = None):
|
|
9
|
+
def __init__(self, message: str, status_code: int | None = None, error_code: int | None = None):
|
|
10
10
|
super().__init__(message)
|
|
11
11
|
self.status_code = status_code
|
|
12
12
|
self.error_code = error_code
|
|
@@ -6,11 +6,15 @@ Endpoint prefix: /users/{userId}/rounds
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
+
import builtins
|
|
9
10
|
from collections.abc import Iterator
|
|
10
11
|
from datetime import datetime
|
|
11
12
|
|
|
12
13
|
from .._http import HttpClient
|
|
13
14
|
|
|
15
|
+
# Alias to avoid shadowing by the RoundsResource.list method
|
|
16
|
+
_list = builtins.list
|
|
17
|
+
|
|
14
18
|
|
|
15
19
|
class RoundsResource:
|
|
16
20
|
"""
|
|
@@ -127,7 +131,7 @@ class RoundsResource:
|
|
|
127
131
|
"""
|
|
128
132
|
return self._http.get(f"/users/{self._user_id}/rounds/{round_id}")
|
|
129
133
|
|
|
130
|
-
def holes(self, round_id: int | str) ->
|
|
134
|
+
def holes(self, round_id: int | str) -> _list[dict]:
|
|
131
135
|
"""
|
|
132
136
|
Fetch hole-by-hole data for a round.
|
|
133
137
|
|
|
@@ -143,7 +147,7 @@ class RoundsResource:
|
|
|
143
147
|
# Analysis helpers
|
|
144
148
|
# ------------------------------------------------------------------
|
|
145
149
|
|
|
146
|
-
def pace_of_play(self, rounds:
|
|
150
|
+
def pace_of_play(self, rounds: _list[dict] | None = None) -> dict:
|
|
147
151
|
"""
|
|
148
152
|
Compute pace of play (round duration) per course.
|
|
149
153
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: arccos-api
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Unofficial Python CLI and client library for the Arccos Golf API
|
|
5
5
|
Author: Paul Frederiksen
|
|
6
6
|
License: MIT
|
|
@@ -17,6 +17,7 @@ Classifier: Programming Language :: Python :: 3
|
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.11
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.12
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
21
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
22
|
Requires-Python: >=3.11
|
|
22
23
|
Description-Content-Type: text/markdown
|
|
@@ -83,9 +84,11 @@ arccos clubs # smart club distances
|
|
|
83
84
|
| `arccos bests` | All-time personal bests (lowest score, longest drive, etc.) |
|
|
84
85
|
| `arccos overview` | Performance summary — scoring avg + handicap breakdown |
|
|
85
86
|
| `arccos scoring` | Scoring trend with visual bar chart |
|
|
86
|
-
| `arccos courses` | List
|
|
87
|
+
| `arccos courses` | List courses you've played (or `--search` any course) |
|
|
87
88
|
| `arccos pace` | Pace of play analysis by course (color-coded) |
|
|
88
|
-
| `arccos stats
|
|
89
|
+
| `arccos stats` | Strokes gained analysis (defaults to latest round) |
|
|
90
|
+
| `arccos club-shots <id>` | Shot history for a specific club |
|
|
91
|
+
| `arccos up-and-down` | Scramble / strokes-to-get-down stats |
|
|
89
92
|
| `arccos export` | Export rounds to JSON, CSV, or NDJSON (`--detail` for hole data) |
|
|
90
93
|
| `arccos logout` | Clear cached credentials |
|
|
91
94
|
|
|
@@ -116,9 +119,14 @@ arccos clubs --after 2025-01-01 # distances from this year only
|
|
|
116
119
|
arccos bests # all-time personal bests
|
|
117
120
|
arccos overview # scoring avg + handicap breakdown
|
|
118
121
|
arccos scoring # scoring trend with bar chart
|
|
122
|
+
arccos up-and-down # scramble stats
|
|
119
123
|
|
|
120
124
|
# Courses
|
|
121
125
|
arccos courses # all courses played
|
|
126
|
+
arccos courses --search "torrey" # search any course by name
|
|
127
|
+
|
|
128
|
+
# Club detail
|
|
129
|
+
arccos club-shots 1 # shot history for club ID 1
|
|
122
130
|
|
|
123
131
|
# Pace of play
|
|
124
132
|
arccos pace # all rounds, slowest courses first
|
|
@@ -208,6 +216,13 @@ export ARCCOS_PASSWORD="your_password"
|
|
|
208
216
|
arccos rounds
|
|
209
217
|
```
|
|
210
218
|
|
|
219
|
+
Other environment variables:
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
export ARCCOS_TIMEOUT=30 # request timeout in seconds (default: 15)
|
|
223
|
+
export ARCCOS_LOG_LEVEL=DEBUG # enable debug logging (DEBUG, INFO, WARNING)
|
|
224
|
+
```
|
|
225
|
+
|
|
211
226
|
> **Security note:** Environment variables may be visible in process listings (`ps`) and shell history. Prefer `arccos login` for interactive use.
|
|
212
227
|
|
|
213
228
|
## Python Library
|
|
@@ -257,6 +272,13 @@ for c in pace["course_averages"][:5]:
|
|
|
257
272
|
|
|
258
273
|
# Personal bests
|
|
259
274
|
bests = client.stats.personal_bests()
|
|
275
|
+
|
|
276
|
+
# Iterate ALL rounds (auto-pagination)
|
|
277
|
+
for r in client.rounds.iter_all():
|
|
278
|
+
print(r["roundId"], r["noOfShots"])
|
|
279
|
+
|
|
280
|
+
# Course search
|
|
281
|
+
results = client.courses.search("Torrey Pines")
|
|
260
282
|
```
|
|
261
283
|
|
|
262
284
|
## API Reference
|
|
@@ -290,7 +312,9 @@ All data calls go to `https://api.arccosgolf.com` with `Authorization: Bearer <t
|
|
|
290
312
|
| Courses played | GET | `/users/{userId}/coursesPlayed` |
|
|
291
313
|
| Course metadata | GET | `/courses/{courseId}?courseVersion=1` |
|
|
292
314
|
| Course search | GET | `/v2/courses?search={query}` |
|
|
315
|
+
| Club shots | GET | `/users/{userId}/bags/{bagId}/clubs/{clubId}/shots` |
|
|
293
316
|
| Personal bests | GET | `/users/{userId}/personalBests?tags=allTimeBest` |
|
|
317
|
+
| Strokes to get down | GET | `/v2/sga/strokes-to-get-down` |
|
|
294
318
|
|
|
295
319
|
### Key data structures
|
|
296
320
|
|
|
@@ -343,7 +367,7 @@ python3 -m venv .venv
|
|
|
343
367
|
source .venv/bin/activate
|
|
344
368
|
pip install -e ".[dev]"
|
|
345
369
|
|
|
346
|
-
pytest # tests with coverage (
|
|
370
|
+
pytest # tests with coverage (87%+, 139 tests)
|
|
347
371
|
ruff check arccos/ # lint
|
|
348
372
|
mypy arccos/ # type check
|
|
349
373
|
```
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "arccos-api"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.1"
|
|
8
8
|
description = "Unofficial Python CLI and client library for the Arccos Golf API"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -21,6 +21,7 @@ classifiers = [
|
|
|
21
21
|
"Programming Language :: Python :: 3.11",
|
|
22
22
|
"Programming Language :: Python :: 3.12",
|
|
23
23
|
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Programming Language :: Python :: 3.14",
|
|
24
25
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
25
26
|
]
|
|
26
27
|
dependencies = [
|
|
@@ -726,3 +726,118 @@ class TestExportDetailFlag:
|
|
|
726
726
|
data = json.loads(output[json_start:])
|
|
727
727
|
assert "holes" in data[0]
|
|
728
728
|
assert data[0]["holes"][0]["holeId"] == 1
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
# ---------------------------------------------------------------------------
|
|
732
|
+
# course search
|
|
733
|
+
# ---------------------------------------------------------------------------
|
|
734
|
+
|
|
735
|
+
class TestCourseSearchCommand:
|
|
736
|
+
@patch(_STATS_GET_CLIENT)
|
|
737
|
+
def test_courses_search(self, mock_get, runner):
|
|
738
|
+
client = _mock_client()
|
|
739
|
+
client.courses.played.return_value = [
|
|
740
|
+
{"courseId": 100, "name": "Torrey Pines South", "city": "San Diego",
|
|
741
|
+
"state": "CA"},
|
|
742
|
+
{"courseId": 101, "name": "Torrey Pines North", "city": "San Diego",
|
|
743
|
+
"state": "CA"},
|
|
744
|
+
{"courseId": 200, "name": "Augusta National", "city": "Augusta",
|
|
745
|
+
"state": "GA"},
|
|
746
|
+
]
|
|
747
|
+
mock_get.return_value = client
|
|
748
|
+
|
|
749
|
+
result = runner.invoke(cli, ["courses", "--search", "torrey"])
|
|
750
|
+
assert result.exit_code == 0
|
|
751
|
+
assert "Torrey Pines South" in result.output
|
|
752
|
+
assert "Torrey Pines North" in result.output
|
|
753
|
+
assert "Augusta" not in result.output
|
|
754
|
+
|
|
755
|
+
@patch(_STATS_GET_CLIENT)
|
|
756
|
+
def test_courses_search_empty(self, mock_get, runner):
|
|
757
|
+
client = _mock_client()
|
|
758
|
+
client.courses.played.return_value = [
|
|
759
|
+
{"courseId": 100, "name": "Augusta National"},
|
|
760
|
+
]
|
|
761
|
+
mock_get.return_value = client
|
|
762
|
+
|
|
763
|
+
result = runner.invoke(cli, ["courses", "--search", "xyznotfound"])
|
|
764
|
+
assert result.exit_code == 0
|
|
765
|
+
assert "No courses found" in result.output
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
# ---------------------------------------------------------------------------
|
|
769
|
+
# club-shots
|
|
770
|
+
# ---------------------------------------------------------------------------
|
|
771
|
+
|
|
772
|
+
class TestClubShotsCommand:
|
|
773
|
+
@patch(_STATS_GET_CLIENT)
|
|
774
|
+
def test_club_shots_table(self, mock_get, runner):
|
|
775
|
+
client = _mock_client()
|
|
776
|
+
client._http.get.return_value = {"bagId": "bag-1"}
|
|
777
|
+
client.clubs.club_shots.return_value = [
|
|
778
|
+
{"shotTime": "2025-06-01T12:00:00Z", "distance": 255.3,
|
|
779
|
+
"userStartTerrainOverride": 0},
|
|
780
|
+
{"shotTime": "2025-06-02T14:00:00Z", "distance": 248.1,
|
|
781
|
+
"userStartTerrainOverride": 0},
|
|
782
|
+
]
|
|
783
|
+
client.clubs.bag.return_value = {
|
|
784
|
+
"clubs": [{"clubId": 1, "clubType": 1, "isDeleted": "F"}],
|
|
785
|
+
}
|
|
786
|
+
mock_get.return_value = client
|
|
787
|
+
|
|
788
|
+
result = runner.invoke(cli, ["club-shots", "1"])
|
|
789
|
+
assert result.exit_code == 0
|
|
790
|
+
assert "255y" in result.output
|
|
791
|
+
assert "248y" in result.output
|
|
792
|
+
|
|
793
|
+
@patch(_STATS_GET_CLIENT)
|
|
794
|
+
def test_club_shots_json(self, mock_get, runner):
|
|
795
|
+
client = _mock_client()
|
|
796
|
+
client._http.get.return_value = {"bagId": "bag-1"}
|
|
797
|
+
client.clubs.club_shots.return_value = [{"distance": 255.3}]
|
|
798
|
+
mock_get.return_value = client
|
|
799
|
+
|
|
800
|
+
result = runner.invoke(cli, ["club-shots", "--json", "1"])
|
|
801
|
+
data = json.loads(result.output)
|
|
802
|
+
assert data[0]["distance"] == 255.3
|
|
803
|
+
|
|
804
|
+
@patch(_STATS_GET_CLIENT)
|
|
805
|
+
def test_club_shots_empty(self, mock_get, runner):
|
|
806
|
+
client = _mock_client()
|
|
807
|
+
client._http.get.return_value = {"bagId": "bag-1"}
|
|
808
|
+
client.clubs.club_shots.return_value = []
|
|
809
|
+
mock_get.return_value = client
|
|
810
|
+
|
|
811
|
+
result = runner.invoke(cli, ["club-shots", "99"])
|
|
812
|
+
assert result.exit_code == 0
|
|
813
|
+
assert "No shots found" in result.output
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
# ---------------------------------------------------------------------------
|
|
817
|
+
# up-and-down
|
|
818
|
+
# ---------------------------------------------------------------------------
|
|
819
|
+
|
|
820
|
+
class TestUpAndDownCommand:
|
|
821
|
+
@patch(_STATS_GET_CLIENT)
|
|
822
|
+
def test_up_and_down_table(self, mock_get, runner):
|
|
823
|
+
client = _mock_client()
|
|
824
|
+
client.stats.strokes_to_get_down.return_value = {
|
|
825
|
+
"totalAttempts": 50,
|
|
826
|
+
"successRate": 0.42,
|
|
827
|
+
"avgProximity": 15.3,
|
|
828
|
+
}
|
|
829
|
+
mock_get.return_value = client
|
|
830
|
+
|
|
831
|
+
result = runner.invoke(cli, ["up-and-down"])
|
|
832
|
+
assert result.exit_code == 0
|
|
833
|
+
assert "50" in result.output
|
|
834
|
+
|
|
835
|
+
@patch(_STATS_GET_CLIENT)
|
|
836
|
+
def test_up_and_down_json(self, mock_get, runner):
|
|
837
|
+
client = _mock_client()
|
|
838
|
+
client.stats.strokes_to_get_down.return_value = {"totalAttempts": 50}
|
|
839
|
+
mock_get.return_value = client
|
|
840
|
+
|
|
841
|
+
result = runner.invoke(cli, ["up-and-down", "--json"])
|
|
842
|
+
data = json.loads(result.output)
|
|
843
|
+
assert data["totalAttempts"] == 50
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|