arccos-api 0.1.4__tar.gz → 0.2.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.1.4 → arccos_api-0.2.0}/PKG-INFO +17 -8
  2. {arccos_api-0.1.4 → arccos_api-0.2.0}/README.md +16 -7
  3. {arccos_api-0.1.4 → arccos_api-0.2.0}/arccos/__init__.py +3 -1
  4. {arccos_api-0.1.4 → arccos_api-0.2.0}/arccos/_http.py +49 -8
  5. arccos_api-0.2.0/arccos/cli/__init__.py +54 -0
  6. arccos_api-0.2.0/arccos/cli/_helpers.py +81 -0
  7. arccos_api-0.2.0/arccos/cli/auth.py +55 -0
  8. arccos_api-0.2.0/arccos/cli/rounds.py +441 -0
  9. arccos_api-0.2.0/arccos/cli/stats.py +504 -0
  10. {arccos_api-0.1.4 → arccos_api-0.2.0}/arccos/resources/rounds.py +44 -0
  11. arccos_api-0.2.0/arccos/types.py +143 -0
  12. {arccos_api-0.1.4 → arccos_api-0.2.0}/arccos_api.egg-info/PKG-INFO +17 -8
  13. {arccos_api-0.1.4 → arccos_api-0.2.0}/arccos_api.egg-info/SOURCES.txt +6 -1
  14. {arccos_api-0.1.4 → arccos_api-0.2.0}/pyproject.toml +2 -2
  15. {arccos_api-0.1.4 → arccos_api-0.2.0}/tests/test_cli.py +58 -46
  16. {arccos_api-0.1.4 → arccos_api-0.2.0}/tests/test_http.py +120 -1
  17. {arccos_api-0.1.4 → arccos_api-0.2.0}/tests/test_rounds.py +24 -0
  18. {arccos_api-0.1.4 → arccos_api-0.2.0}/tests/test_security.py +1 -1
  19. arccos_api-0.1.4/arccos/cli.py +0 -1079
  20. {arccos_api-0.1.4 → arccos_api-0.2.0}/MANIFEST.in +0 -0
  21. {arccos_api-0.1.4 → arccos_api-0.2.0}/arccos/__main__.py +0 -0
  22. {arccos_api-0.1.4 → arccos_api-0.2.0}/arccos/auth.py +0 -0
  23. {arccos_api-0.1.4 → arccos_api-0.2.0}/arccos/client.py +0 -0
  24. {arccos_api-0.1.4 → arccos_api-0.2.0}/arccos/exceptions.py +0 -0
  25. {arccos_api-0.1.4 → arccos_api-0.2.0}/arccos/resources/__init__.py +0 -0
  26. {arccos_api-0.1.4 → arccos_api-0.2.0}/arccos/resources/clubs.py +0 -0
  27. {arccos_api-0.1.4 → arccos_api-0.2.0}/arccos/resources/courses.py +0 -0
  28. {arccos_api-0.1.4 → arccos_api-0.2.0}/arccos/resources/handicap.py +0 -0
  29. {arccos_api-0.1.4 → arccos_api-0.2.0}/arccos/resources/stats.py +0 -0
  30. {arccos_api-0.1.4 → arccos_api-0.2.0}/arccos_api.egg-info/dependency_links.txt +0 -0
  31. {arccos_api-0.1.4 → arccos_api-0.2.0}/arccos_api.egg-info/entry_points.txt +0 -0
  32. {arccos_api-0.1.4 → arccos_api-0.2.0}/arccos_api.egg-info/requires.txt +0 -0
  33. {arccos_api-0.1.4 → arccos_api-0.2.0}/arccos_api.egg-info/top_level.txt +0 -0
  34. {arccos_api-0.1.4 → arccos_api-0.2.0}/setup.cfg +0 -0
  35. {arccos_api-0.1.4 → arccos_api-0.2.0}/tests/test_auth.py +0 -0
  36. {arccos_api-0.1.4 → arccos_api-0.2.0}/tests/test_client.py +0 -0
  37. {arccos_api-0.1.4 → arccos_api-0.2.0}/tests/test_exceptions.py +0 -0
  38. {arccos_api-0.1.4 → arccos_api-0.2.0}/tests/test_resources.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arccos-api
3
- Version: 0.1.4
3
+ Version: 0.2.0
4
4
  Summary: Unofficial Python CLI and client library for the Arccos Golf API
5
5
  Author: Paul Frederiksen
6
6
  License: MIT
@@ -76,14 +76,17 @@ arccos clubs # smart club distances
76
76
  | Command | Description |
77
77
  |---------|-------------|
78
78
  | `arccos login` | Authenticate and cache credentials to `~/.arccos_creds.json` |
79
- | `arccos rounds` | List recent rounds with date, score, holes, course |
79
+ | `arccos rounds` | List recent rounds with date, score, +/-, course name |
80
80
  | `arccos round <id>` | Hole-by-hole detail (score, putts, FIR, GIR) with totals |
81
81
  | `arccos handicap` | Handicap breakdown by category (overall, driving, approach, etc.) |
82
82
  | `arccos clubs` | Smart club distances with make/model, range, and shot count |
83
+ | `arccos bests` | All-time personal bests (lowest score, longest drive, etc.) |
84
+ | `arccos overview` | Performance summary — scoring avg + handicap breakdown |
85
+ | `arccos scoring` | Scoring trend with visual bar chart |
83
86
  | `arccos courses` | List all courses you've played |
84
87
  | `arccos pace` | Pace of play analysis by course (color-coded) |
85
88
  | `arccos stats <id>` | Strokes gained analysis for a round |
86
- | `arccos export` | Export rounds to JSON, CSV, or NDJSON |
89
+ | `arccos export` | Export rounds to JSON, CSV, or NDJSON (`--detail` for hole data) |
87
90
  | `arccos logout` | Clear cached credentials |
88
91
 
89
92
  Every command supports `--json` for raw JSON output and `--help` for usage info.
@@ -92,9 +95,10 @@ Every command supports `--json` for raw JSON output and `--help` for usage info.
92
95
 
93
96
  ```bash
94
97
  # Rounds
95
- arccos rounds # last 20 rounds
98
+ arccos rounds # last 20 rounds (with +/- and course names)
96
99
  arccos rounds -n 50 # last 50
97
100
  arccos rounds --after 2025-01-01 # filter by date
101
+ arccos rounds --course "pebble" # filter by course name (substring)
98
102
  arccos rounds --json # raw JSON
99
103
 
100
104
  # Round detail
@@ -108,6 +112,11 @@ arccos handicap --history # revision history
108
112
  arccos clubs # active clubs with make/model
109
113
  arccos clubs --after 2025-01-01 # distances from this year only
110
114
 
115
+ # Performance
116
+ arccos bests # all-time personal bests
117
+ arccos overview # scoring avg + handicap breakdown
118
+ arccos scoring # scoring trend with bar chart
119
+
111
120
  # Courses
112
121
  arccos courses # all courses played
113
122
 
@@ -117,6 +126,7 @@ arccos pace -n 20 # last 20 rounds only
117
126
 
118
127
  # Export
119
128
  arccos export -f csv -o rounds.csv # export to CSV file
129
+ arccos export --detail # include hole-by-hole data
120
130
  arccos export -f json # dump JSON to stdout
121
131
  arccos export -f ndjson # newline-delimited JSON
122
132
  ```
@@ -237,7 +247,7 @@ for club in bag["clubs"]:
237
247
 
238
248
  # Courses played
239
249
  for course in client.courses.played():
240
- print(f" {course.get('courseName', course['courseId'])}")
250
+ print(f" {course.get('name', course['courseId'])}")
241
251
 
242
252
  # Pace of play analysis
243
253
  pace = client.rounds.pace_of_play()
@@ -289,12 +299,11 @@ All data calls go to `https://api.arccosgolf.com` with `Authorization: Bearer <t
289
299
  {
290
300
  "roundId": 18294051,
291
301
  "courseId": 12450,
292
- "courseName": "Torrey Pines",
302
+ "courseVersion": 4,
293
303
  "startTime": "2025-09-14T15:30:00.000000Z",
294
304
  "endTime": "2025-09-14T20:15:00.000000Z",
295
305
  "noOfShots": 79,
296
306
  "noOfHoles": 18,
297
- "overUnder": 7,
298
307
  "holes": [
299
308
  {
300
309
  "holeId": 1, "noOfShots": 4, "putts": 2,
@@ -334,7 +343,7 @@ python3 -m venv .venv
334
343
  source .venv/bin/activate
335
344
  pip install -e ".[dev]"
336
345
 
337
- pytest # tests with coverage (90%+, 96 tests)
346
+ pytest # tests with coverage (88%+, 132 tests)
338
347
  ruff check arccos/ # lint
339
348
  mypy arccos/ # type check
340
349
  ```
@@ -42,14 +42,17 @@ arccos clubs # smart club distances
42
42
  | Command | Description |
43
43
  |---------|-------------|
44
44
  | `arccos login` | Authenticate and cache credentials to `~/.arccos_creds.json` |
45
- | `arccos rounds` | List recent rounds with date, score, holes, course |
45
+ | `arccos rounds` | List recent rounds with date, score, +/-, course name |
46
46
  | `arccos round <id>` | Hole-by-hole detail (score, putts, FIR, GIR) with totals |
47
47
  | `arccos handicap` | Handicap breakdown by category (overall, driving, approach, etc.) |
48
48
  | `arccos clubs` | Smart club distances with make/model, range, and shot count |
49
+ | `arccos bests` | All-time personal bests (lowest score, longest drive, etc.) |
50
+ | `arccos overview` | Performance summary — scoring avg + handicap breakdown |
51
+ | `arccos scoring` | Scoring trend with visual bar chart |
49
52
  | `arccos courses` | List all courses you've played |
50
53
  | `arccos pace` | Pace of play analysis by course (color-coded) |
51
54
  | `arccos stats <id>` | Strokes gained analysis for a round |
52
- | `arccos export` | Export rounds to JSON, CSV, or NDJSON |
55
+ | `arccos export` | Export rounds to JSON, CSV, or NDJSON (`--detail` for hole data) |
53
56
  | `arccos logout` | Clear cached credentials |
54
57
 
55
58
  Every command supports `--json` for raw JSON output and `--help` for usage info.
@@ -58,9 +61,10 @@ Every command supports `--json` for raw JSON output and `--help` for usage info.
58
61
 
59
62
  ```bash
60
63
  # Rounds
61
- arccos rounds # last 20 rounds
64
+ arccos rounds # last 20 rounds (with +/- and course names)
62
65
  arccos rounds -n 50 # last 50
63
66
  arccos rounds --after 2025-01-01 # filter by date
67
+ arccos rounds --course "pebble" # filter by course name (substring)
64
68
  arccos rounds --json # raw JSON
65
69
 
66
70
  # Round detail
@@ -74,6 +78,11 @@ arccos handicap --history # revision history
74
78
  arccos clubs # active clubs with make/model
75
79
  arccos clubs --after 2025-01-01 # distances from this year only
76
80
 
81
+ # Performance
82
+ arccos bests # all-time personal bests
83
+ arccos overview # scoring avg + handicap breakdown
84
+ arccos scoring # scoring trend with bar chart
85
+
77
86
  # Courses
78
87
  arccos courses # all courses played
79
88
 
@@ -83,6 +92,7 @@ arccos pace -n 20 # last 20 rounds only
83
92
 
84
93
  # Export
85
94
  arccos export -f csv -o rounds.csv # export to CSV file
95
+ arccos export --detail # include hole-by-hole data
86
96
  arccos export -f json # dump JSON to stdout
87
97
  arccos export -f ndjson # newline-delimited JSON
88
98
  ```
@@ -203,7 +213,7 @@ for club in bag["clubs"]:
203
213
 
204
214
  # Courses played
205
215
  for course in client.courses.played():
206
- print(f" {course.get('courseName', course['courseId'])}")
216
+ print(f" {course.get('name', course['courseId'])}")
207
217
 
208
218
  # Pace of play analysis
209
219
  pace = client.rounds.pace_of_play()
@@ -255,12 +265,11 @@ All data calls go to `https://api.arccosgolf.com` with `Authorization: Bearer <t
255
265
  {
256
266
  "roundId": 18294051,
257
267
  "courseId": 12450,
258
- "courseName": "Torrey Pines",
268
+ "courseVersion": 4,
259
269
  "startTime": "2025-09-14T15:30:00.000000Z",
260
270
  "endTime": "2025-09-14T20:15:00.000000Z",
261
271
  "noOfShots": 79,
262
272
  "noOfHoles": 18,
263
- "overUnder": 7,
264
273
  "holes": [
265
274
  {
266
275
  "holeId": 1, "noOfShots": 4, "putts": 2,
@@ -300,7 +309,7 @@ python3 -m venv .venv
300
309
  source .venv/bin/activate
301
310
  pip install -e ".[dev]"
302
311
 
303
- pytest # tests with coverage (90%+, 96 tests)
312
+ pytest # tests with coverage (88%+, 132 tests)
304
313
  ruff check arccos/ # lint
305
314
  mypy arccos/ # type check
306
315
  ```
@@ -9,6 +9,8 @@ Quick start:
9
9
  print(client.handicap.current())
10
10
  """
11
11
 
12
+ from importlib.metadata import version as _pkg_version
13
+
12
14
  from .auth import ArccosAuth
13
15
  from .client import ArccosClient
14
16
  from .exceptions import (
@@ -19,7 +21,7 @@ from .exceptions import (
19
21
  ArccosRateLimitError,
20
22
  )
21
23
 
22
- __version__ = "0.1.0"
24
+ __version__ = _pkg_version("arccos-api")
23
25
  __all__ = [
24
26
  "ArccosClient",
25
27
  "ArccosAuth",
@@ -2,11 +2,14 @@
2
2
  Internal HTTP client for the Arccos API.
3
3
 
4
4
  All API calls share a single requests.Session with automatic token refresh.
5
+ Retries transient failures (429, 5xx, timeouts) with exponential backoff.
5
6
  """
6
7
 
7
8
  from __future__ import annotations
8
9
 
9
10
  import logging
11
+ import os
12
+ import time
10
13
  from typing import Any
11
14
 
12
15
  import requests
@@ -17,6 +20,9 @@ from .exceptions import raise_for_status
17
20
  logger = logging.getLogger(__name__)
18
21
 
19
22
  API_BASE = "https://api.arccosgolf.com"
23
+ DEFAULT_TIMEOUT = 15
24
+ MAX_RETRIES = 3
25
+ RETRY_BACKOFF = 1.0 # seconds; doubles each retry
20
26
 
21
27
 
22
28
  class HttpClient:
@@ -26,6 +32,7 @@ class HttpClient:
26
32
  - Prepends the API base URL
27
33
  - Injects the ``Authorization: Bearer`` header on every request
28
34
  - Auto-refreshes the JWT when expired
35
+ - Retries on transient failures (429, 5xx, timeouts)
29
36
  - Raises typed :mod:`arccos.exceptions` for non-2xx responses
30
37
  """
31
38
 
@@ -37,6 +44,7 @@ class HttpClient:
37
44
  "Content-Type": "application/json",
38
45
  "Accept": "application/json",
39
46
  })
47
+ self._timeout = int(os.environ.get("ARCCOS_TIMEOUT", DEFAULT_TIMEOUT))
40
48
 
41
49
  # ------------------------------------------------------------------
42
50
  # Public interface
@@ -90,11 +98,44 @@ class HttpClient:
90
98
  url = f"{API_BASE}{path}"
91
99
  logger.debug("%s %s", method, url)
92
100
 
93
- resp = self._session.request(
94
- method, url, headers=headers, timeout=15, verify=True, **kwargs
95
- )
96
- raise_for_status(resp)
97
-
98
- if resp.content:
99
- return resp.json()
100
- return {}
101
+ last_exc: Exception | None = None
102
+ for attempt in range(MAX_RETRIES):
103
+ try:
104
+ resp = self._session.request(
105
+ method, url, headers=headers,
106
+ timeout=self._timeout, verify=True, **kwargs,
107
+ )
108
+ except requests.exceptions.Timeout as exc:
109
+ last_exc = exc
110
+ self._backoff(attempt)
111
+ continue
112
+ except requests.exceptions.ConnectionError as exc:
113
+ last_exc = exc
114
+ self._backoff(attempt)
115
+ continue
116
+
117
+ if resp.ok:
118
+ if resp.content:
119
+ return resp.json()
120
+ return {}
121
+
122
+ # Retry on 429 and 5xx; raise immediately on other errors
123
+ if resp.status_code == 429 or resp.status_code >= 500:
124
+ try:
125
+ raise_for_status(resp)
126
+ except Exception as exc:
127
+ last_exc = exc
128
+ self._backoff(attempt)
129
+ continue
130
+
131
+ # Non-retryable error (4xx other than 429)
132
+ raise_for_status(resp)
133
+
134
+ # Exhausted retries
135
+ raise last_exc # type: ignore[misc]
136
+
137
+ @staticmethod
138
+ def _backoff(attempt: int) -> None:
139
+ delay = RETRY_BACKOFF * (2 ** attempt)
140
+ logger.warning("Retrying in %.1fs (attempt %d/%d)", delay, attempt + 1, MAX_RETRIES)
141
+ time.sleep(delay)
@@ -0,0 +1,54 @@
1
+ """
2
+ arccos CLI — pull your Arccos Golf data from the terminal.
3
+
4
+ Uses the reverse-engineered Arccos Golf API. Not affiliated with Arccos Golf LLC.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import click
10
+
11
+ from . import auth, rounds, stats
12
+
13
+
14
+ @click.group()
15
+ @click.version_option(package_name="arccos-api", prog_name="arccos")
16
+ def cli():
17
+ """
18
+ \b
19
+ arccos — Your Arccos Golf data in the terminal.
20
+
21
+ \b
22
+ Quick start:
23
+ arccos login Authenticate and save credentials
24
+ arccos rounds Your recent rounds
25
+ arccos round <id> Hole-by-hole round detail
26
+ arccos handicap Current handicap index
27
+ arccos clubs Smart club distances
28
+ arccos courses Courses you've played
29
+ arccos pace Pace of play by course
30
+ arccos stats Strokes gained analysis
31
+ arccos bests Personal bests (all-time records)
32
+ arccos overview Overall stats summary
33
+ arccos scoring Scoring trend over recent rounds
34
+ arccos export Export rounds to JSON/CSV
35
+ arccos logout Clear cached credentials
36
+
37
+ \b
38
+ Credentials are cached in ~/.arccos_creds.json after first login.
39
+ Set ARCCOS_EMAIL and ARCCOS_PASSWORD to skip the prompts.
40
+ """
41
+
42
+
43
+ # Register all command submodules
44
+ auth.register(cli)
45
+ rounds.register(cli)
46
+ stats.register(cli)
47
+
48
+
49
+ def main():
50
+ cli()
51
+
52
+
53
+ if __name__ == "__main__":
54
+ main()
@@ -0,0 +1,81 @@
1
+ """Shared helpers for CLI commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import sys
8
+
9
+ import click
10
+ from rich.console import Console
11
+
12
+ console = Console()
13
+ err_console = Console(stderr=True)
14
+
15
+
16
+ def _get_client():
17
+ """Build an ArccosClient from env vars or cached creds, with a friendly error."""
18
+ from arccos import ArccosClient
19
+ from arccos.exceptions import ArccosAuthError
20
+
21
+ email = os.environ.get("ARCCOS_EMAIL")
22
+ password = os.environ.get("ARCCOS_PASSWORD")
23
+
24
+ try:
25
+ return ArccosClient(email=email, password=password)
26
+ except ArccosAuthError as e:
27
+ err_console.print(f"[red]Auth failed:[/red] {e}")
28
+ err_console.print("Run [bold]arccos login[/bold] or set ARCCOS_EMAIL and ARCCOS_PASSWORD.")
29
+ sys.exit(1)
30
+ except ValueError:
31
+ err_console.print(
32
+ "[red]Not logged in.[/red] Run [bold]arccos login[/bold] first, or set "
33
+ "ARCCOS_EMAIL and ARCCOS_PASSWORD environment variables."
34
+ )
35
+ sys.exit(1)
36
+
37
+
38
+ def _output_json(data):
39
+ click.echo(json.dumps(data, indent=2))
40
+
41
+
42
+ def _flag(minutes: int) -> str:
43
+ if minutes > 300:
44
+ return "\U0001f534"
45
+ if minutes > 270:
46
+ return "\U0001f7e1"
47
+ return "\U0001f7e2"
48
+
49
+
50
+ def _build_course_map(client) -> tuple[dict[int, str], dict[int, int]]:
51
+ """Build courseId → courseName and courseId → par lookups from played courses."""
52
+ try:
53
+ played = client.courses.played()
54
+ names = {
55
+ c["courseId"]: c.get("courseName") or c.get("name") or f"Course {c['courseId']}"
56
+ for c in played
57
+ }
58
+ pars = {
59
+ c["courseId"]: c["mensPar"]
60
+ for c in played
61
+ if c.get("mensPar")
62
+ }
63
+ return names, pars
64
+ except Exception:
65
+ return {}, {}
66
+
67
+
68
+ def _score_color(score: int, par: int | None) -> str:
69
+ """Return a Rich color string based on score relative to par."""
70
+ if par is None:
71
+ return "bold"
72
+ diff = score - par
73
+ if diff <= -2:
74
+ return "bold yellow" # eagle or better
75
+ if diff == -1:
76
+ return "bold green" # birdie
77
+ if diff == 0:
78
+ return "white" # par
79
+ if diff == 1:
80
+ return "red" # bogey
81
+ return "bold red" # double+
@@ -0,0 +1,55 @@
1
+ """Login and logout CLI commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+ import click
8
+
9
+ from ._helpers import console, err_console
10
+
11
+
12
+ def register(cli: click.Group) -> None:
13
+ cli.add_command(login)
14
+ cli.add_command(logout)
15
+
16
+
17
+ @click.command()
18
+ @click.option("--email", envvar="ARCCOS_EMAIL", prompt="Arccos email",
19
+ help="Your Arccos account email.")
20
+ @click.option("--password", envvar="ARCCOS_PASSWORD", prompt="Password",
21
+ hide_input=True, help="Your Arccos password.")
22
+ def login(email: str, password: str):
23
+ """Authenticate and save credentials to ~/.arccos_creds.json."""
24
+ from arccos import ArccosClient
25
+ from arccos.exceptions import ArccosAuthError
26
+
27
+ with console.status("Logging in\u2026"):
28
+ try:
29
+ client = ArccosClient(email=email, password=password)
30
+ except ArccosAuthError as e:
31
+ err_console.print(f"[red]Login failed:[/red] {e}")
32
+ sys.exit(1)
33
+
34
+ console.print(f"[green]\u2713[/green] Logged in as [bold]{client.email}[/bold]")
35
+ console.print(f" User ID: [dim]{client.user_id}[/dim]")
36
+ console.print(" Credentials cached at [dim]~/.arccos_creds.json[/dim]")
37
+
38
+
39
+ @click.command()
40
+ def logout():
41
+ """Clear cached credentials (~/.arccos_creds.json)."""
42
+ from arccos.auth import DEFAULT_CREDS_PATH
43
+
44
+ if DEFAULT_CREDS_PATH.exists():
45
+ DEFAULT_CREDS_PATH.unlink()
46
+ console.print(
47
+ "[green]\u2713[/green] Credentials removed from "
48
+ "[bold]~/.arccos_creds.json[/bold]"
49
+ )
50
+ console.print(
51
+ "[dim]Note: Server-side tokens are not revoked. "
52
+ "The access key remains valid until it expires (~180 days).[/dim]"
53
+ )
54
+ else:
55
+ console.print("[dim]No cached credentials found.[/dim]")